diff --git a/package-lock.json b/package-lock.json index 3283bede..0c83be39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "kahla", - "version": "3.9.3", + "version": "4.0.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -122,6 +122,12 @@ "worker-plugin": "3.2.0" }, "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + }, "cacache": { "version": "13.0.1", "resolved": "https://registry.npmjs.org/cacache/-/cacache-13.0.1.tgz", @@ -2519,9 +2525,9 @@ } }, "acorn": { - "version": "6.4.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.0.tgz", - "integrity": "sha512-gac8OEcQ2Li1dxIEWGZzsp2BitJxwkwcOm0zHAJLcPJaVvm58FRnk6RkuLRpU1EujipU2ZFODv2P9DLMfnV8mw==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.1.tgz", + "integrity": "sha512-add7dgA5ppRPxCFJoAGfMDi7PIBXq1RtGo7BhbLaxwrXPOmw8gq48Y9ozT01hUKy9byMjlR20EJhu5zlkErEkg==", "dev": true }, "agent-base": { @@ -10351,14 +10357,6 @@ "@types/estree": "*", "@types/node": "*", "acorn": "^7.1.0" - }, - "dependencies": { - "acorn": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.1.0.tgz", - "integrity": "sha512-kL5CuoXA/dgxlBbVrflsflzQ3PAas7RYZB52NOm/6839iVYJgKMJ3cQJD+t2i5+qFa8h3MDpEOJiS64E8JLnSQ==", - "dev": true - } } }, "run-async": { @@ -13031,6 +13029,14 @@ "terser-webpack-plugin": "^1.4.3", "watchpack": "^1.6.0", "webpack-sources": "^1.4.1" + }, + "dependencies": { + "acorn": { + "version": "6.4.1", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.1.tgz", + "integrity": "sha512-ZVA9k326Nwrj3Cj9jlh3wGFutC2ZornPNARZwsNYqQYgN0EsV2d53w5RN/co65Ohn4sUAUtb1rSUAOD6XN9idA==", + "dev": true + } } }, "webpack-dev-middleware": { diff --git a/package.json b/package.json index 6b1942a0..0f185589 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kahla", - "version": "3.9.3", + "version": "4.0.0", "description": "Kahla is a cross-platform business messaging app.", "author": "Aiursoft (https://www.aiursoft.com/)", "build": { diff --git a/src/app/Controllers/about.component.ts b/src/app/Controllers/about.component.ts index d80ba951..125cf1e5 100644 --- a/src/app/Controllers/about.component.ts +++ b/src/app/Controllers/about.component.ts @@ -1,7 +1,9 @@ import { Component } from '@angular/core'; import { CheckService } from '../Services/CheckService'; import { Values } from '../values'; +import { ApiService } from '../Services/ApiService'; import { ElectronService } from 'ngx-electron'; +import { environment } from '../../environments/environment'; @Component({ templateUrl: '../Views/about.html', @@ -13,13 +15,20 @@ import { ElectronService } from 'ngx-electron'; export class AboutComponent { public sourceCodeURL = Values.sourceCodeURL; + public website = environment.officialServerList; + constructor( public checkService: CheckService, - public electronService: ElectronService + public electronService: ElectronService, + public apiService: ApiService ) { } public check(): void { this.checkService.checkVersion(true); } + + public getCurrentYear(): number { + return new Date().getFullYear(); + } } diff --git a/src/app/Controllers/add-friend.component.ts b/src/app/Controllers/add-friend.component.ts index d93a716e..7e14cbab 100644 --- a/src/app/Controllers/add-friend.component.ts +++ b/src/app/Controllers/add-friend.component.ts @@ -68,10 +68,10 @@ export class AddFriendComponent implements OnInit { this.showUsers = selectUsers; } - SearchBoxKeyUp(event: KeyboardEvent, element: HTMLInputElement) { + SearchBoxKeyUp(event: KeyboardEvent, value: string) { if (event.key === 'Enter') { event.preventDefault(); - this.search(element.value, 0); + this.search(value, 0); } } } diff --git a/src/app/Controllers/advanced-setting.component.ts b/src/app/Controllers/advanced-setting.component.ts index a3bb5d73..c9c934be 100644 --- a/src/app/Controllers/advanced-setting.component.ts +++ b/src/app/Controllers/advanced-setting.component.ts @@ -2,7 +2,6 @@ import { Component, OnInit } from '@angular/core'; import { AuthApiService } from '../Services/AuthApiService'; import { KahlaUser } from '../Models/KahlaUser'; import Swal from 'sweetalert2'; -import { DevicesApiService } from '../Services/DevicesApiService'; import { CacheService } from '../Services/CacheService'; import { ProbeService } from '../Services/ProbeService'; import { Subscription } from 'rxjs'; @@ -21,7 +20,6 @@ export class AdvancedSettingComponent implements OnInit { constructor( private authApiService: AuthApiService, - private devicesApiService: DevicesApiService, private cacheService: CacheService, private probeService: ProbeService, ) { @@ -61,18 +59,6 @@ export class AdvancedSettingComponent implements OnInit { }); } - public testPush(): void { - this.devicesApiService.PushTestMessage().subscribe(t => { - if (t.code === 0) { - Swal.fire( - 'Successfully sent!', - t.message, - 'info' - ); - } - }); - } - public todo(): void { Swal.fire('Under development', 'This features is still under development ^_^.', 'info'); } diff --git a/src/app/Controllers/changePassword.component.ts b/src/app/Controllers/changePassword.component.ts index 23ee2a25..4f3b329e 100644 --- a/src/app/Controllers/changePassword.component.ts +++ b/src/app/Controllers/changePassword.component.ts @@ -6,8 +6,8 @@ import { catchError } from 'rxjs/operators'; @Component({ templateUrl: '../Views/changePassword.html', styleUrls: [ - '../Styles/userDetail.scss', - '../Styles/button.scss' + '../Styles/userDetail.scss', + '../Styles/button.scss' ] }) export class ChangePasswordComponent { @@ -24,9 +24,8 @@ export class ChangePasswordComponent { public checkValid(): void { this.samePassword = this.newPassword === this.confirmPassword; - if (this.oldPassword.length >= 6 && this.oldPassword.length <= 32 && this.newPassword.length >= 6 && - this.newPassword.length <= 32 && this.samePassword) { - this.valid = true; + if (/^.{6,32}$/.test(this.oldPassword) && /^.{6,32}$/.test(this.newPassword)) { + this.valid = true; } } @@ -34,12 +33,13 @@ export class ChangePasswordComponent { this.checkValid(); if (!this.samePassword) { Swal.fire('Passwords are not same!', 'error'); + return; } - if (!this.valid && this.samePassword) { - Swal.fire('Password length should between six and thirty-two'); + if (!this.valid) { + Swal.fire('Password length should between 6 and 32.'); + return; } - if (this.valid) { - this.authApiServer.ChangePassword(this.oldPassword, this.newPassword, this.confirmPassword) + this.authApiServer.ChangePassword(this.oldPassword, this.newPassword, this.confirmPassword) .pipe(catchError(error => { Swal.fire('Network issue', 'Could not connect to Kahla server.', 'error'); return Promise.reject(error.message || error); @@ -51,6 +51,5 @@ export class ChangePasswordComponent { Swal.fire('Try again', result.message, 'error'); } }); - } } } diff --git a/src/app/Controllers/conversations.component.ts b/src/app/Controllers/conversations.component.ts index a75f3442..57686697 100644 --- a/src/app/Controllers/conversations.component.ts +++ b/src/app/Controllers/conversations.component.ts @@ -16,6 +16,7 @@ import { HomeService } from '../Services/HomeService'; }) export class ConversationsComponent implements OnInit, OnDestroy { public loadingImgURL = Values.loadingImgURL; + constructor( private router: Router, public cacheService: CacheService, diff --git a/src/app/Controllers/devices.component.ts b/src/app/Controllers/devices.component.ts index 326eae40..4a1e03e2 100644 --- a/src/app/Controllers/devices.component.ts +++ b/src/app/Controllers/devices.component.ts @@ -2,19 +2,32 @@ import { Component, OnInit } from '@angular/core'; import { CacheService } from '../Services/CacheService'; import Swal from 'sweetalert2'; import { Device } from '../Models/Device'; +import { ElectronService } from 'ngx-electron'; +import { DevicesApiService } from '../Services/DevicesApiService'; +import { PushSubscriptionSetting } from '../Models/PushSubscriptionSetting'; +import { InitService } from '../Services/InitService'; @Component({ templateUrl: '../Views/devices.html', - styleUrls: ['../Styles/menu.scss'] + styleUrls: ['../Styles/menu.scss', + '../Styles/toggleButton.scss'] }) export class DevicesComponent implements OnInit { constructor( public cacheService: CacheService, + public electronService: ElectronService, + public devicesApiService: DevicesApiService, + public initService: InitService, ) { } + public webPushEnabled: boolean; + public ngOnInit(): void { - this.cacheService.updateDevice(); + this.cacheService.updateDevice(); + if (this.webpushSupported()) { + this.webPushEnabled = this.getWebPushStatus(); + } } public detail(device: Device): void { @@ -27,4 +40,49 @@ export class DevicesComponent implements OnInit { }); } } + + public webpushSupported(): boolean { + return !this.electronService.isElectronApp && 'Notification' in window && 'serviceWorker' in navigator; + } + + public testPush(): void { + this.devicesApiService.PushTestMessage().subscribe(t => { + if (t.code === 0) { + Swal.fire( + 'Successfully sent!', + t.message, + 'info' + ); + } + }); + } + + public getWebPushStatus(): boolean { + if (!localStorage.getItem('setting-pushSubscription')) { + return true; + } + const status: PushSubscriptionSetting = JSON.parse(localStorage.getItem('setting-pushSubscription')); + return status.enabled; + } + + public setWebPushStatus(value: boolean) { + const status: PushSubscriptionSetting = localStorage.getItem('setting-pushSubscription') ? + JSON.parse(localStorage.getItem('setting-pushSubscription')) : + { + enabled: value, + deviceId: 0 + }; + status.enabled = value; + localStorage.setItem('setting-pushSubscription', JSON.stringify(status)); + this.webPushEnabled = value; + this.initService.subscribeUser(); + } + + public getElectronNotify(): boolean { + return localStorage.getItem('setting-electronNotify') !== 'false'; + } + + public setElectronNotify(value: boolean) { + localStorage.setItem('setting-electronNotify', value ? 'true' : 'false'); + } } diff --git a/src/app/Controllers/file-history.component.ts b/src/app/Controllers/file-history.component.ts index 2efc6e9a..0f2d80e5 100644 --- a/src/app/Controllers/file-history.component.ts +++ b/src/app/Controllers/file-history.component.ts @@ -1,5 +1,5 @@ import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { FileHistoryApiModel } from '../Models/ApiModels/FileHistoryApiModel'; import { switchMap } from 'rxjs/operators'; import { ConversationApiService } from '../Services/ConversationApiService'; @@ -22,6 +22,7 @@ export class FileHistoryComponent implements OnInit { private route: ActivatedRoute, private conversationApiService: ConversationApiService, public probeService: ProbeService, + public router: Router, ) { } @@ -58,4 +59,11 @@ export class FileHistoryComponent implements OnInit { }); return `${count} files. Total Size:${this.probeService.getFileSizeText(totalSize)}`; } + + public share(file: ProbeFile, dir: ProbeFolder) { + this.router.navigate(['share-target', { + message: `[file]${this.files.siteName}/${this.files.rootPath}/${ + dir.folderName}/${file.fileName}|${file.fileName}|${this.probeService.getFileSizeText(file.fileSize)}` + }]); + } } diff --git a/src/app/Controllers/manageGroup.component.ts b/src/app/Controllers/manageGroup.component.ts index 51d0507b..34e3715a 100644 --- a/src/app/Controllers/manageGroup.component.ts +++ b/src/app/Controllers/manageGroup.component.ts @@ -17,7 +17,8 @@ import { ProbeService } from '../Services/ProbeService'; templateUrl: '../Views/manageGroup.html', styleUrls: ['../Styles/menu.scss', '../Styles/userDetail.scss', - '../Styles/button.scss' + '../Styles/button.scss', + '../Styles/toggleButton.scss' ] }) export class ManageGroupComponent implements OnInit { @@ -135,17 +136,18 @@ export class ManageGroupComponent implements OnInit { } public saveInfo() { - this.groupsApiService.UpdateGroupInfo(this.conversation.groupName, this.conversation.groupImagePath, this.newGroupName) + this.groupsApiService.UpdateGroupInfo(this.conversation.groupName, this.conversation.listInSearchResult, + this.conversation.groupImagePath, this.newGroupName) .subscribe(res => { - if (res.code === 0) { - Swal.fire('Success', res.message, 'success'); - this.conversation.groupName = this.newGroupName; - } else if (res.code === -10 && (res as AiurCollection).items) { - Swal.fire('Error', (res as AiurCollection).items.join('
'), 'error'); - } else { - Swal.fire('Error', res.message, 'error'); - } - }); + if (res.code === 0) { + Swal.fire('Success', res.message, 'success'); + this.conversation.groupName = this.newGroupName; + } else if (res.code === -10 && (res as AiurCollection).items) { + Swal.fire('Error', (res as AiurCollection).items.join('
'), 'error'); + } else { + Swal.fire('Error', res.message, 'error'); + } + }); } public kickMember() { diff --git a/src/app/Controllers/signin.component.ts b/src/app/Controllers/signin.component.ts index 10b885e8..401b55f4 100644 --- a/src/app/Controllers/signin.component.ts +++ b/src/app/Controllers/signin.component.ts @@ -1,18 +1,105 @@ -import { Component } from '@angular/core'; -import { ApiService } from '../Services/ApiService'; +import { Component, OnInit } from '@angular/core'; import { ElectronService } from 'ngx-electron'; +import { ApiService } from '../Services/ApiService'; +import { InitService } from '../Services/InitService'; +import Swal from 'sweetalert2'; +import { HttpClient } from '@angular/common/http'; +import { ServerListApiService } from '../Services/ServerListApiService'; @Component({ templateUrl: '../Views/signin.html', styleUrls: ['../Styles/signin.scss', - '../Styles/button.scss'] + '../Styles/button.scss'] }) -export class SignInComponent { - public OAuthURL: string; +export class SignInComponent implements OnInit { + + public changingServer = false; + public serverAddr; constructor( public _electronService: ElectronService, - ) { - this.OAuthURL = ApiService.serverAddress; + public apiService: ApiService, + public serverListApiService: ServerListApiService, + public initService: InitService, + public http: HttpClient, + ) { + } + + public clearCommunityServerData() { + localStorage.removeItem('serverConfig'); + this.changingServer = false; + this.initService.init(); + } + + public connectCommunity() { + if (!this.serverAddr) { + Swal.fire('Please input an valid server url!', '', 'error'); + return; + } + if (!this.serverAddr.match(/^https?:\/\/.+/g)) { + this.serverAddr = 'https://' + this.serverAddr; + } + Swal.fire({ + icon: 'info', + title: 'Fetching manifest from your community server...', + text: this.serverAddr, + showConfirmButton: false, + showCancelButton: false + }); + Swal.showLoading(); + const fireFailed = () => Swal.fire('Failed to fetch manifest from server.', 'Check syntax, then contract the server\'s owner.', 'error'); + this.serverListApiService.getServerConfig(this.serverAddr).subscribe({ + next: serverConfig => { + if (serverConfig.code !== 0 || serverConfig.domain.server !== this.serverAddr) { + Swal.close(); + fireFailed(); + return; + } + this.serverListApiService.Servers().subscribe(officialServer => { + Swal.close(); + const last = () => { + localStorage.setItem('serverConfig', JSON.stringify(serverConfig)); + this.changingServer = false; + this.initService.init(); + }; + if (officialServer.map(t => t.domain.server).includes(serverConfig.domain.server)) { + // an official server + serverConfig.officialServer = true; + last(); + } else { + Swal.fire({ + title: 'Connecting to a community server...', + text: 'Aiursoft CANNOT prove the community server is secure.\n' + + ' You should NEVER connect to a server you don\'t trust.\n' + + 'Chat data in community server will never be synced with one in official server.', + icon: 'warning', + showCancelButton: true, + confirmButtonText: 'Continue' + }).then(res => { + if (res.dismiss) { + return; + } + serverConfig.officialServer = false; + last(); + }); + } + }); + }, + error: _t => fireFailed() + }); + } + + ngOnInit(): void { + if (this.apiService.serverConfig && !this.apiService.serverConfig.officialServer) { + this.serverAddr = this.apiService.serverConfig.domain.server; + } + } + + public goLogin(url: string) { + if (this._electronService.isElectronApp) { + this._electronService.ipcRenderer.send('oauth', url); + } else { + document.location.href = url; } + } } diff --git a/src/app/Controllers/theme.component.ts b/src/app/Controllers/theme.component.ts index 1b08a749..40480df0 100644 --- a/src/app/Controllers/theme.component.ts +++ b/src/app/Controllers/theme.component.ts @@ -12,18 +12,18 @@ export class ThemeComponent implements OnInit { ) { } - public currentTheme: Themes = Themes.kahlaLight; + public currentTheme: Themes = Themes.kahlaAuto; public primaryColor: number; public accentColor: number; ngOnInit(): void { this.currentTheme = this.themeService.LocalThemeSetting; - this.primaryColor = Math.floor(this.currentTheme / 2); - this.accentColor = this.currentTheme % 2; + this.primaryColor = Math.floor(this.currentTheme / 3); + this.accentColor = this.currentTheme % 3; } public applyTheme() { - this.changeTheme(this.primaryColor * 2 + this.accentColor); + this.changeTheme(this.primaryColor * 3 + this.accentColor); } public changeTheme(theme: Themes) { diff --git a/src/app/Models/CacheModel.ts b/src/app/Models/CacheModel.ts index 39376625..207ccffe 100644 --- a/src/app/Models/CacheModel.ts +++ b/src/app/Models/CacheModel.ts @@ -6,7 +6,7 @@ import { KahlaUser } from './KahlaUser'; import { Conversation } from './Conversation'; export class CacheModel { - public static readonly VERSION = 2; + public static readonly VERSION = 3; public version = CacheModel.VERSION; public me: KahlaUser; public conversations: ContactInfo[]; diff --git a/src/app/Models/ContactInfo.ts b/src/app/Models/ContactInfo.ts index 1ac61422..fc76fb98 100644 --- a/src/app/Models/ContactInfo.ts +++ b/src/app/Models/ContactInfo.ts @@ -1,12 +1,12 @@ - +import { Message } from './Message'; + export class ContactInfo { public displayName: string; public displayImagePath: string; - public latestMessage: string; - public latestMessageTime: Date; + public latestMessage: Message; public unReadAmount: number; public conversationId: number; - public discriminator: string; + public discriminator: 'GroupConversation' | 'PrivateConversation'; public userId: string; public aesKey: string; public avatarURL: string; diff --git a/src/app/Models/AiurEvent.ts b/src/app/Models/Events/AiurEvent.ts similarity index 100% rename from src/app/Models/AiurEvent.ts rename to src/app/Models/Events/AiurEvent.ts diff --git a/src/app/Models/DissolveEvent.ts b/src/app/Models/Events/DissolveEvent.ts similarity index 100% rename from src/app/Models/DissolveEvent.ts rename to src/app/Models/Events/DissolveEvent.ts diff --git a/src/app/Models/EventType.ts b/src/app/Models/Events/EventType.ts similarity index 57% rename from src/app/Models/EventType.ts rename to src/app/Models/Events/EventType.ts index c93d08a1..5a6499cd 100644 --- a/src/app/Models/EventType.ts +++ b/src/app/Models/Events/EventType.ts @@ -1,10 +1,11 @@ export enum EventType { NewMessage = 0, NewFriendRequest = 1, - WereDeletedEvent = 2, - FriendAcceptedEvent = 3, + FriendDeletedEvent = 2, + FriendsChangedEvent = 3, TimerUpdatedEvent = 4, NewMemberEvent = 5, - SomeoneLeftLevent = 6, + SomeoneLeftEvent = 6, DissolveEvent = 7, + GroupJoinedEvent = 8, } diff --git a/src/app/Models/Events/FriendDeletedEvent.ts b/src/app/Models/Events/FriendDeletedEvent.ts new file mode 100644 index 00000000..92f17b74 --- /dev/null +++ b/src/app/Models/Events/FriendDeletedEvent.ts @@ -0,0 +1,7 @@ +import { AiurEvent } from './AiurEvent'; +import { KahlaUser } from '../KahlaUser'; + +export class FriendDeletedEvent extends AiurEvent { + public trigger: KahlaUser; + public conversationId: number; +} diff --git a/src/app/Models/Events/FriendsChangedEvent.ts b/src/app/Models/Events/FriendsChangedEvent.ts new file mode 100644 index 00000000..63b7c0d9 --- /dev/null +++ b/src/app/Models/Events/FriendsChangedEvent.ts @@ -0,0 +1,9 @@ +import { AiurEvent } from './AiurEvent'; +import { Request } from '../Request'; +import { Conversation } from '../Conversation'; + +export class FriendsChangedEvent extends AiurEvent { + public request: Request; + public result: boolean; + public createdConversation: Conversation; +} diff --git a/src/app/Models/Events/GroupJoinedEvent.ts b/src/app/Models/Events/GroupJoinedEvent.ts new file mode 100644 index 00000000..9c1d2fd1 --- /dev/null +++ b/src/app/Models/Events/GroupJoinedEvent.ts @@ -0,0 +1,9 @@ +import { AiurEvent } from './AiurEvent'; +import { Conversation } from '../Conversation'; +import { Message } from '../Message'; + +export class GroupJoinedEvent extends AiurEvent { + public createdConversation: Conversation; + public messageCount: number; + public latestMessage: Message; +} diff --git a/src/app/Models/NewFriendRequestEvent.ts b/src/app/Models/Events/NewFriendRequestEvent.ts similarity index 57% rename from src/app/Models/NewFriendRequestEvent.ts rename to src/app/Models/Events/NewFriendRequestEvent.ts index 1cdd766b..e206163f 100644 --- a/src/app/Models/NewFriendRequestEvent.ts +++ b/src/app/Models/Events/NewFriendRequestEvent.ts @@ -1,6 +1,6 @@ import { AiurEvent } from './AiurEvent'; -import { KahlaUser } from './KahlaUser'; +import { Request } from '../Request'; export class NewFriendRequestEvent extends AiurEvent { - public requester: KahlaUser; + public request: Request; } diff --git a/src/app/Models/NewMemberEvent.ts b/src/app/Models/Events/NewMemberEvent.ts similarity index 79% rename from src/app/Models/NewMemberEvent.ts rename to src/app/Models/Events/NewMemberEvent.ts index 2e34709e..4525a5a1 100644 --- a/src/app/Models/NewMemberEvent.ts +++ b/src/app/Models/Events/NewMemberEvent.ts @@ -1,5 +1,5 @@ import { AiurEvent } from './AiurEvent'; -import { KahlaUser } from './KahlaUser'; +import { KahlaUser } from '../KahlaUser'; export class NewMemberEvent extends AiurEvent { public newMember: KahlaUser; diff --git a/src/app/Models/NewMessageEvent.ts b/src/app/Models/Events/NewMessageEvent.ts similarity index 73% rename from src/app/Models/NewMessageEvent.ts rename to src/app/Models/Events/NewMessageEvent.ts index 79c70e22..7942a0e5 100644 --- a/src/app/Models/NewMessageEvent.ts +++ b/src/app/Models/Events/NewMessageEvent.ts @@ -1,8 +1,9 @@ import { AiurEvent } from './AiurEvent'; -import { Message } from './Message'; +import { Message } from '../Message'; export class NewMessageEvent extends AiurEvent { public message: Message; + public previousMessageId: string; public aesKey: string; public muted: boolean; public mentioned: boolean; diff --git a/src/app/Models/SomeoneLeftEvent.ts b/src/app/Models/Events/SomeoneLeftEvent.ts similarity index 79% rename from src/app/Models/SomeoneLeftEvent.ts rename to src/app/Models/Events/SomeoneLeftEvent.ts index a2a83796..ef486751 100644 --- a/src/app/Models/SomeoneLeftEvent.ts +++ b/src/app/Models/Events/SomeoneLeftEvent.ts @@ -1,5 +1,5 @@ import { AiurEvent } from './AiurEvent'; -import { KahlaUser } from './KahlaUser'; +import { KahlaUser } from '../KahlaUser'; export class SomeoneLeftEvent extends AiurEvent { public leftUser: KahlaUser; diff --git a/src/app/Models/TimerUpdatedEvent.ts b/src/app/Models/Events/TimerUpdatedEvent.ts similarity index 100% rename from src/app/Models/TimerUpdatedEvent.ts rename to src/app/Models/Events/TimerUpdatedEvent.ts diff --git a/src/app/Models/FriendAcceptedEvent.ts b/src/app/Models/FriendAcceptedEvent.ts deleted file mode 100644 index 5c45b0db..00000000 --- a/src/app/Models/FriendAcceptedEvent.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AiurEvent } from './AiurEvent'; -import { KahlaUser } from './KahlaUser'; - -export class FriendAcceptedEvent extends AiurEvent { - public target: KahlaUser; -} diff --git a/src/app/Models/GroupConversation.ts b/src/app/Models/GroupConversation.ts index 5659d921..ecd93151 100644 --- a/src/app/Models/GroupConversation.ts +++ b/src/app/Models/GroupConversation.ts @@ -5,4 +5,5 @@ export class GroupConversation extends Conversation { public groupImagePath: string; public hasPassword: boolean; public ownerId: string; + public listInSearchResult: boolean; } diff --git a/src/app/Models/NewFriendRequest.ts b/src/app/Models/NewFriendRequest.ts deleted file mode 100644 index 07b30adb..00000000 --- a/src/app/Models/NewFriendRequest.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { AiurEvent } from './AiurEvent'; - -export class NewFriendRequest extends AiurEvent { - public requesterId: string; -} diff --git a/src/app/Models/PushSubscriptionSetting.ts b/src/app/Models/PushSubscriptionSetting.ts new file mode 100644 index 00000000..0a4a1069 --- /dev/null +++ b/src/app/Models/PushSubscriptionSetting.ts @@ -0,0 +1,4 @@ +export class PushSubscriptionSetting { + public deviceId: number; + public enabled: boolean; +} diff --git a/src/app/Models/Request.ts b/src/app/Models/Request.ts index b45970d7..bae73a2d 100644 --- a/src/app/Models/Request.ts +++ b/src/app/Models/Request.ts @@ -5,6 +5,7 @@ export class Request { public creatorId: string; public creator: KahlaUser; public targetId: string; + public target: KahlaUser; public createTime: Date; public completed: boolean; } diff --git a/src/app/Models/ServerConfig.ts b/src/app/Models/ServerConfig.ts new file mode 100644 index 00000000..489b4067 --- /dev/null +++ b/src/app/Models/ServerConfig.ts @@ -0,0 +1,13 @@ +import { AiurProtocal } from './AiurProtocal'; + +export class ServerConfig extends AiurProtocal { + public apiVersion: string; + public vapidPublicKey: string; + public serverName: string; + public mode: string; + public officialServer: boolean; + public domain: { + server: string, + client: string, + }; +} diff --git a/src/app/Models/Themes.ts b/src/app/Models/Themes.ts index 602515f3..b578140d 100644 --- a/src/app/Models/Themes.ts +++ b/src/app/Models/Themes.ts @@ -1,12 +1,17 @@ export enum Themes { + kahlaAuto, kahlaLight, kahlaDark, + sakuraAuto, sakuraLight, sakuraDark, + violetAuto, violetLight, violetDark, + communistAuto, communistLight, communistDark, + grassAuto, grassLight, grassDark } diff --git a/src/app/Models/VersionViewModel.ts b/src/app/Models/VersionViewModel.ts index 77fe4d0e..a6466dd2 100644 --- a/src/app/Models/VersionViewModel.ts +++ b/src/app/Models/VersionViewModel.ts @@ -2,7 +2,5 @@ import { AiurProtocal } from './AiurProtocal'; export class VersionViewModel extends AiurProtocal { public latestVersion: string; - public oldestSupportedVersion: string; - public apiVersion: string; public downloadAddress: string; } diff --git a/src/app/Models/WereDeletedEvent.ts b/src/app/Models/WereDeletedEvent.ts deleted file mode 100644 index 777238bd..00000000 --- a/src/app/Models/WereDeletedEvent.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { AiurEvent } from './AiurEvent'; -import { KahlaUser } from './KahlaUser'; - -export class WereDeletedEvent extends AiurEvent { - public trigger: KahlaUser; -} diff --git a/src/app/Services/ApiService.ts b/src/app/Services/ApiService.ts index bfcfd205..7d6e5da1 100644 --- a/src/app/Services/ApiService.ts +++ b/src/app/Services/ApiService.ts @@ -3,10 +3,14 @@ import { Observable } from 'rxjs/'; import { ParamService } from './ParamService'; import { HttpClient, HttpHeaders } from '@angular/common/http'; import { catchError } from 'rxjs/operators'; +import { ServerConfig } from '../Models/ServerConfig'; -@Injectable() +@Injectable({ + providedIn: 'root' +}) export class ApiService { - public static serverAddress; + public readonly STORAGE_SERVER_CONFIG = 'serverConfig'; + public serverConfig: ServerConfig; private _headers: HttpHeaders = new HttpHeaders({ @@ -19,14 +23,18 @@ export class ApiService { } public Get(address: string): Observable { - return this.http.get(`${ApiService.serverAddress}${address}`, { + return this.GetByFullUrl(`${this.serverConfig.domain.server}${address}`); + } + + public GetByFullUrl(address: string, withCredentials = true): Observable { + return this.http.get(address, { headers: this._headers, - withCredentials: true + withCredentials: withCredentials }).pipe(catchError(this.handleError)); } public Post(address: string, data: any): Observable { - return this.http.post(`${ApiService.serverAddress}${address}`, this.paramTool.param(data), { + return this.http.post(`${this.serverConfig.domain.server}${address}`, this.paramTool.param(data), { headers: this._headers, withCredentials: true }).pipe(catchError(this.handleError)); diff --git a/src/app/Services/AuthApiService.ts b/src/app/Services/AuthApiService.ts index c4430b09..c3da41cb 100644 --- a/src/app/Services/AuthApiService.ts +++ b/src/app/Services/AuthApiService.ts @@ -4,7 +4,6 @@ import { KahlaUser } from '../Models/KahlaUser'; import { Observable } from 'rxjs/'; import { AiurProtocal } from '../Models/AiurProtocal'; import { InitPusherViewModel } from '../Models/ApiModels/InitPusherViewModel'; -import { VersionViewModel } from '../Models/VersionViewModel'; import { ApiService } from './ApiService'; @Injectable() @@ -15,10 +14,6 @@ export class AuthApiService { private apiService: ApiService ) {} - public Version(): Observable { - return this.apiService.Get(AuthApiService.serverPath + '/Version'); - } - public SignInStatus(): Observable> { return this.apiService.Get(AuthApiService.serverPath + '/SignInStatus'); } diff --git a/src/app/Services/CacheService.ts b/src/app/Services/CacheService.ts index 52911f0f..01a77ddd 100644 --- a/src/app/Services/CacheService.ts +++ b/src/app/Services/CacheService.ts @@ -6,6 +6,7 @@ import { AES, enc } from 'crypto-js'; import { DevicesApiService } from './DevicesApiService'; import { ConversationApiService } from './ConversationApiService'; import { ProbeService } from './ProbeService'; +import { PushSubscriptionSetting } from '../Models/PushSubscriptionSetting'; @Injectable() export class CacheService { @@ -31,11 +32,11 @@ export class CacheService { info.forEach(e => { if (e.latestMessage != null) { try { - e.latestMessage = AES.decrypt(e.latestMessage, e.aesKey).toString(enc.Utf8); + e.latestMessage.content = AES.decrypt(e.latestMessage.content, e.aesKey).toString(enc.Utf8); } catch (error) { - e.latestMessage = ''; + e.latestMessage.content = ''; } - e.latestMessage = this.modifyMessage(e.latestMessage); + e.latestMessage.content = this.modifyMessage(e.latestMessage.content); } e.avatarURL = this.probeService.encodeProbeFileUrl(e.displayImagePath); }); @@ -75,6 +76,10 @@ export class CacheService { public updateDevice(): void { this.devicesApiService.MyDevices().subscribe(response => { + let currentId = 0; + if (localStorage.getItem('setting-pushSubscription')) { + currentId = (JSON.parse(localStorage.getItem('setting-pushSubscription'))).deviceId; + } response.items.forEach(item => { if (item.name !== null && item.name.length >= 0) { const deviceName = []; @@ -93,7 +98,7 @@ export class CacheService { deviceName.push('Unknown OS'); } - if (item.id === Number(localStorage.getItem('deviceID'))) { + if (item.id === currentId) { deviceName[0] += '(Current device)'; } diff --git a/src/app/Services/CheckService.ts b/src/app/Services/CheckService.ts index 2fc5a368..988e2a87 100644 --- a/src/app/Services/CheckService.ts +++ b/src/app/Services/CheckService.ts @@ -1,8 +1,9 @@ import { Injectable } from '@angular/core'; -import { AuthApiService } from './AuthApiService'; import Swal from 'sweetalert2'; import { versions } from '../../environments/versions'; import { ElectronService } from 'ngx-electron'; +import { ServerListApiService } from './ServerListApiService'; +import { ApiService } from './ApiService'; @Injectable({ providedIn: 'root' @@ -15,8 +16,9 @@ export class CheckService { public buildTime = versions.buildTime; constructor( - private authApiService: AuthApiService, - private _electronService: ElectronService + private _electronService: ElectronService, + private serverListApiService: ServerListApiService, + private apiService: ApiService, ) { if (this.checkSwCache()) { navigator.serviceWorker.addEventListener('message', (t: MessageEvent) => { @@ -41,21 +43,10 @@ export class CheckService { public checkVersion(showAlert: boolean): void { this.checking = true; - this.authApiService.Version() + this.serverListApiService.Version() .subscribe(t => { - const latestVersion: Array = t.latestVersion.split('.'); - const latestAPIVersion: Array = t.apiVersion.split('.'); - const currentVersion: Array = versions.version.split('.'); - const downloadAddress: string = t.downloadAddress; - if (latestVersion[0] > currentVersion[0] || - latestVersion[1] > currentVersion[1] || - latestVersion[2] > currentVersion[2]) { - this.redirectToDownload(downloadAddress, showAlert); - } else if ( - latestAPIVersion[0] > currentVersion[0] || - latestAPIVersion[1] > currentVersion[1]) { - Swal.fire('API version mismatch', 'API level is too far from client! You have to upgrade now!', 'warning'); - this.redirectToDownload(downloadAddress, true); + if (this.compareVersion(t.latestVersion, versions.version) > 0) { + this.redirectToDownload(t.downloadAddress, showAlert); } else if (showAlert) { Swal.fire('Success', 'You are running the latest version of Kahla!', 'success'); } @@ -63,6 +54,32 @@ export class CheckService { }); } + public checkApiVersion(): void { + this.serverListApiService.getServerConfig(this.apiService.serverConfig.domain.server).subscribe(t => { + const delta = this.compareVersion(t.apiVersion, versions.version); + if (delta === 1 || delta === 2) { + Swal.fire('Outdated client.', 'Your Kahla App is too far from the version of the server connected.\n' + + 'Kahla might not work properly if you don\'t upgrade.', 'warning'); + } else if (delta < 0 && !this.apiService.serverConfig.officialServer) { + Swal.fire('Community server outdated!', 'The Client version is newer then the Server version.\n' + + 'Consider contact the host of the server for updating the kahla.server version to latest.', 'warning'); + } + }); + } + + public compareVersion(a: string, b: string): number { + const verA = a.split('.').map(Number); + const verB = b.split('.').map(Number); + + for (let i = 0; i < 3; i++) { + if (verA[i] === verB[i]) { + continue; + } + return Math.sign(verA[i] - verB[i]) * (i + 1); + } + return 0; + } + private redirectToDownload(downloadAddress: string, showAlert: boolean = false): void { if (this._electronService.isElectronApp) { Swal.fire({ diff --git a/src/app/Services/DevicesApiService.ts b/src/app/Services/DevicesApiService.ts index d213cd53..190e6717 100644 --- a/src/app/Services/DevicesApiService.ts +++ b/src/app/Services/DevicesApiService.ts @@ -24,7 +24,7 @@ export class DevicesApiService { } public UpdateDevice(deviceID: number, userAgent: string, PushEndpoint: string, - PushP256DH: string, PushAuth: string): Observable> { + PushP256DH: string, PushAuth: string): Observable> { return this.apiService.Post(DevicesApiService.serverPath + '/UpdateDevice', { DeviceId: deviceID, Name: userAgent, @@ -34,6 +34,12 @@ export class DevicesApiService { }); } + public DropDevice(deviceId: number): Observable { + return this.apiService.Post(DevicesApiService.serverPath + '/DropDevice', { + id: deviceId + }); + } + public MyDevices(): Observable> { return this.apiService.Get(DevicesApiService.serverPath + '/MyDevices'); } diff --git a/src/app/Services/GroupsApiService.ts b/src/app/Services/GroupsApiService.ts index 4cd78fc5..6d301591 100644 --- a/src/app/Services/GroupsApiService.ts +++ b/src/app/Services/GroupsApiService.ts @@ -42,16 +42,18 @@ export class GroupsApiService { public TransferOwner(groupName: string, targetUserId: string): Observable { return this.apiService.Post(GroupsApiService.serverPath + '/TransferGroupOwner', { - groupName: groupName, + groupName: groupName, targetUserId: targetUserId }); } - public UpdateGroupInfo(groupName: string, avatarPath?: string, newName?: string): Observable { + public UpdateGroupInfo(groupName: string, listInSearchResult: boolean, + avatarPath?: string, newName?: string): Observable { return this.apiService.Post(GroupsApiService.serverPath + '/UpdateGroupInfo', { GroupName: groupName, AvatarPath: avatarPath, - NewName: newName + NewName: newName, + ListInSearchResult: listInSearchResult, }); } diff --git a/src/app/Services/InitService.ts b/src/app/Services/InitService.ts index 1d73872f..8639b6cd 100644 --- a/src/app/Services/InitService.ts +++ b/src/app/Services/InitService.ts @@ -4,12 +4,15 @@ import { AuthApiService } from './AuthApiService'; import { Router } from '@angular/router'; import { MessageService } from './MessageService'; import { CacheService } from './CacheService'; -import { environment } from '../../environments/environment'; import { ElectronService } from 'ngx-electron'; import { DevicesApiService } from './DevicesApiService'; import { ThemeService } from './ThemeService'; import Swal from 'sweetalert2'; import { ProbeService } from './ProbeService'; +import { ServerConfig } from '../Models/ServerConfig'; +import { ApiService } from './ApiService'; +import { ServerListApiService } from './ServerListApiService'; +import { PushSubscriptionSetting } from '../Models/PushSubscriptionSetting'; @Injectable({ providedIn: 'root' @@ -25,10 +28,11 @@ export class InitService { private closeWebSocket = false; private options = { userVisibleOnly: true, - applicationServerKey: this.urlBase64ToUint8Array(environment.applicationServerKey) + applicationServerKey: null }; constructor( + private apiService: ApiService, private checkService: CheckService, private authApiService: AuthApiService, private router: Router, @@ -38,13 +42,11 @@ export class InitService { private themeService: ThemeService, private devicesApiService: DevicesApiService, private probeService: ProbeService, + private serverListApiService: ServerListApiService ) { } public init(): void { - this.online = navigator.onLine; - this.closeWebSocket = false; - this.checkService.checkVersion(false); if (navigator.userAgent.match(/MSIE|Trident/)) { Swal.fire( 'Oops, it seems that you are opening Kahla in IE.', @@ -54,28 +56,64 @@ export class InitService { 'or Microsoft Edge.' ); } + this.checkService.checkVersion(false); + // load server config + if (localStorage.getItem(this.apiService.STORAGE_SERVER_CONFIG)) { + this.apiService.serverConfig = JSON.parse(localStorage.getItem(this.apiService.STORAGE_SERVER_CONFIG)) as ServerConfig; + } else { + this.router.navigate(['/signin'], {replaceUrl: true}); + this.serverListApiService.Servers().subscribe(servers => { + let target: ServerConfig; + if (this._electronService.isElectronApp) { + target = servers[0]; + } else { + target = servers.find(t => t.domain.client === window.location.origin); + } + + if (target) { + target.officialServer = true; + this.apiService.serverConfig = target; + localStorage.setItem(this.apiService.STORAGE_SERVER_CONFIG, JSON.stringify(target)); + } + this.init(); + }); + return; + } + + this.online = navigator.onLine; + this.closeWebSocket = false; this.cacheService.initCache(); - this.authApiService.SignInStatus().subscribe(signInStatus => { - if (signInStatus.value === false) { - this.router.navigate(['/signin'], { replaceUrl: true }); - } else { - this.authApiService.Me().subscribe(p => { - if (p.code === 0) { - this.cacheService.cachedData.me = p.value; - this.cacheService.cachedData.me.avatarURL = this.probeService.encodeProbeFileUrl(p.value.iconFilePath); - this.themeService.ApplyThemeFromRemote(p.value); - if (!this._electronService.isElectronApp && navigator.serviceWorker) { - this.subscribeUser(); - this.updateSubscription(); - } - this.loadPusher(false); - this.cacheService.updateConversation(); - this.cacheService.updateFriends(); - this.cacheService.updateRequests(); + + if (this.apiService.serverConfig) { + this.options.applicationServerKey = this.urlBase64ToUint8Array(this.apiService.serverConfig.vapidPublicKey); + this.checkService.checkApiVersion(); + this.authApiService.SignInStatus().subscribe(signInStatus => { + if (signInStatus.value === false) { + this.router.navigate(['/signin'], {replaceUrl: true}); + } else { + if (this.router.isActive('/signin', false)) { + this.router.navigate(['/home'], {replaceUrl: true}); } - }); - } - }); + this.authApiService.Me().subscribe(p => { + if (p.code === 0) { + this.cacheService.cachedData.me = p.value; + this.cacheService.cachedData.me.avatarURL = this.probeService.encodeProbeFileUrl(p.value.iconFilePath); + this.themeService.ApplyThemeFromRemote(p.value); + if (!this._electronService.isElectronApp && navigator.serviceWorker) { + this.subscribeUser(); + this.updateSubscription(); + } + this.loadPusher(false); + this.cacheService.updateConversation(); + this.cacheService.updateFriends(); + this.cacheService.updateRequests(); + } + }); + } + }); + } else { + this.router.navigate(['/signin'], {replaceUrl: true}); + } } private loadPusher(reconnect: boolean): void { @@ -158,39 +196,68 @@ export class InitService { }, this.timeoutNumber); } - private subscribeUser(): void { + public subscribeUser() { if ('Notification' in window && 'serviceWorker' in navigator && Notification.permission === 'granted') { const _this = this; - navigator.serviceWorker.ready.then(function (registration) { - return registration.pushManager.getSubscription().then(function (sub) { + navigator.serviceWorker.ready.then((registration => { + return registration.pushManager.getSubscription().then(sub => { if (sub === null) { return registration.pushManager.subscribe(_this.options) .then(function (pushSubscription) { - return _this.devicesApiService.AddDevice(navigator.userAgent, pushSubscription.endpoint, - pushSubscription.toJSON().keys.p256dh, pushSubscription.toJSON().keys.auth) - .subscribe(function (result) { - localStorage.setItem('deviceID', result.value.toString()); - }); + _this.bindDevice(pushSubscription); }); + } else { + _this.bindDevice(sub); } }); - }.bind(_this)); + })); + } + } + + public bindDevice(pushSubscription: PushSubscription, force: boolean = false) { + let data: PushSubscriptionSetting = JSON.parse(localStorage.getItem('setting-pushSubscription')); + if (!data) { + data = { + enabled: true, + deviceId: 0 + }; + localStorage.setItem('setting-pushSubscription', JSON.stringify(data)); + } + if (!data.enabled && data.deviceId) { + this.devicesApiService.DropDevice(data.deviceId).subscribe(t => { + if (t.code === 0) { + data.deviceId = 0; + localStorage.setItem('setting-pushSubscription', JSON.stringify(data)); + } + }); + } + if (data.enabled) { + if (data.deviceId) { + if (force) { + this.devicesApiService.UpdateDevice(data.deviceId, navigator.userAgent, pushSubscription.endpoint, + pushSubscription.toJSON().keys.p256dh, pushSubscription.toJSON().keys.auth).subscribe(); + } + } else { + this.devicesApiService.AddDevice(navigator.userAgent, pushSubscription.endpoint, + pushSubscription.toJSON().keys.p256dh, pushSubscription.toJSON().keys.auth).subscribe(t => { + data.deviceId = t.value; + localStorage.setItem('setting-pushSubscription', JSON.stringify(data)); + }); + } } + } private updateSubscription(): void { if ('Notification' in window && 'serviceWorker' in navigator && Notification.permission === 'granted') { const _this = this; - navigator.serviceWorker.ready.then(function (registration) { - return navigator.serviceWorker.addEventListener('pushsubscriptionchange', function () { + navigator.serviceWorker.ready.then(registration => + navigator.serviceWorker.addEventListener('pushsubscriptionchange', () => { registration.pushManager.subscribe(_this.options) - .then(function (pushSubscription) { - return _this.devicesApiService.UpdateDevice(Number(localStorage.getItem('deviceID')), navigator.userAgent, - pushSubscription.endpoint, pushSubscription.toJSON().keys.p256dh, pushSubscription.toJSON().keys.auth) - .subscribe(); + .then(pushSubscription => { + _this.bindDevice(pushSubscription, true); }); - }); - }.bind(_this)); + })); } } diff --git a/src/app/Services/MessageService.ts b/src/app/Services/MessageService.ts index f8114a52..9bab6c18 100644 --- a/src/app/Services/MessageService.ts +++ b/src/app/Services/MessageService.ts @@ -1,8 +1,8 @@ import { Injectable } from '@angular/core'; -import { EventType } from '../Models/EventType'; -import { AiurEvent } from '../Models/AiurEvent'; +import { EventType } from '../Models/Events/EventType'; +import { AiurEvent } from '../Models/Events/AiurEvent'; import Swal from 'sweetalert2'; -import { NewMessageEvent } from '../Models/NewMessageEvent'; +import { NewMessageEvent } from '../Models/Events/NewMessageEvent'; import { Conversation } from '../Models/Conversation'; import { Message } from '../Models/Message'; import { ConversationApiService } from './ConversationApiService'; @@ -15,17 +15,17 @@ import * as he from 'he'; import Autolinker from 'autolinker'; import { Values } from '../values'; import { ElectronService } from 'ngx-electron'; -import { TimerUpdatedEvent } from '../Models/TimerUpdatedEvent'; +import { TimerUpdatedEvent } from '../Models/Events/TimerUpdatedEvent'; import { TimerService } from './TimerService'; -import { WereDeletedEvent } from '../Models/WereDeletedEvent'; -import { NewFriendRequestEvent } from '../Models/NewFriendRequestEvent'; -import { FriendAcceptedEvent } from '../Models/FriendAcceptedEvent'; +import { FriendDeletedEvent } from '../Models/Events/FriendDeletedEvent'; +import { NewFriendRequestEvent } from '../Models/Events/NewFriendRequestEvent'; +import { FriendsChangedEvent } from '../Models/Events/FriendsChangedEvent'; import { Router } from '@angular/router'; import { UserGroupRelation } from '../Models/UserGroupRelation'; -import { SomeoneLeftEvent } from '../Models/SomeoneLeftEvent'; -import { NewMemberEvent } from '../Models/NewMemberEvent'; +import { SomeoneLeftEvent } from '../Models/Events/SomeoneLeftEvent'; +import { NewMemberEvent } from '../Models/Events/NewMemberEvent'; import { GroupConversation } from '../Models/GroupConversation'; -import { DissolveEvent } from '../Models/DissolveEvent'; +import { DissolveEvent } from '../Models/Events/DissolveEvent'; import { HomeService } from './HomeService'; import { GroupsApiService } from './GroupsApiService'; import { FriendsApiService } from './FriendsApiService'; @@ -45,9 +45,10 @@ export class MessageService { public newMessages = false; private oldScrollHeight: number; public maxImageWidth = 0; + public videoHeight = 0; private userColors = new Map(); - private colors = ['aqua', 'aquamarine', 'bisque', 'blue', 'blueviolet', 'brown', 'burlywood', 'cadetblue', 'chocolate', - 'coral', 'cornflowerblue', 'darkcyan', 'darkgoldenrod']; + private colors = ['aqua', 'aquamarine', 'bisque', 'blue', 'blueviolet', 'brown', 'burlywood', 'chocolate', + 'coral', 'deepskyblue', 'darkturquoise', 'lightseagreen', 'indigo', 'lavenderblush', 'lawngreen', 'maroon']; public groupConversation = false; public sysNotifyText: string; public sysNotifyShown: boolean; @@ -64,7 +65,8 @@ export class MessageService { private groupsApiService: GroupsApiService, private friendsApiService: FriendsApiService, private probeService: ProbeService, - ) { } + ) { + } public OnMessage(data: MessageEvent) { const ev = JSON.parse(data.data) as AiurEvent; @@ -76,8 +78,10 @@ export class MessageService { .findIndex(x => x.conversationId === evt.message.conversationId); if (conversationCacheIndex !== -1) { const conversationCache = this.cacheService.cachedData.conversations[conversationCacheIndex]; - conversationCache.latestMessage = this.cacheService.modifyMessage( + const latestMsg = Object.assign({}, evt.message); + latestMsg.content = this.cacheService.modifyMessage( AES.decrypt(evt.message.content, evt.aesKey).toString(enc.Utf8)); + conversationCache.latestMessage = latestMsg; if (!this.conversation || this.conversation.id !== evt.message.conversationId) { conversationCache.unReadAmount++; if (evt.mentioned) { @@ -98,11 +102,17 @@ export class MessageService { } if (this.conversation && this.conversation.id === evt.message.conversationId && this.localMessages.findIndex(t => t.id === evt.message.id) === -1) { - this.rawMessages.push(evt.message); - this.localMessages.push(this.modifyMessage(Object.assign({}, evt.message))); - this.reorderLocalMessages(); - this.updateAtLink(); - this.conversationApiService.GetMessage(this.conversation.id, null, 0).subscribe(); + if (evt.previousMessageId === this.rawMessages[this.rawMessages.length - 1].id || + evt.previousMessageId === '00000000-0000-0000-0000-000000000000') { + this.rawMessages.push(evt.message); + this.localMessages.push(this.modifyMessage(Object.assign({}, evt.message))); + this.reorderLocalMessages(); + this.updateAtLink(); + this.conversationApiService.GetMessage(this.conversation.id, null, 0).subscribe(); + this.saveMessage(); + } else { // lost some message. + this.getMessages(0, this.conversation.id, null, 15); + } if (this.belowWindowPercent <= 0.2) { setTimeout(() => { this.uploadService.scrollBottom(true); @@ -111,38 +121,43 @@ export class MessageService { if (!document.hasFocus()) { this.showNotification(evt); } - this.saveMessage(); } else { this.showNotification(evt); } break; } case EventType.NewFriendRequest: { - if (fireAlert) { - Swal.fire('Friend request', 'New friend request from ' + (ev).requester.nickName, 'info'); + if (fireAlert && (ev).request.creatorId !== this.cacheService.cachedData.me.id) { + Swal.fire('Friend request', 'New friend request from ' + (ev).request.creator.nickName, 'info'); } this.cacheService.updateRequests(); break; } - case EventType.WereDeletedEvent: { - if (fireAlert) { - Swal.fire('Were deleted', 'You were deleted by ' + (ev).trigger.nickName, 'info'); + case EventType.FriendDeletedEvent: { + if (fireAlert && (ev).trigger.id !== this.cacheService.cachedData.me.id) { + Swal.fire('Were deleted', 'You were deleted by ' + (ev).trigger.nickName, 'info'); } this.cacheService.updateConversation(); this.cacheService.updateFriends(); break; } - case EventType.FriendAcceptedEvent: { - if (fireAlert) { - Swal.fire('Friend request accepted', 'You and ' + (ev).target.nickName + - ' are now friends!', 'success'); - } - this.cacheService.updateConversation(); - this.cacheService.updateFriends(); - if (this.router.isActive(`/user/${(ev as FriendAcceptedEvent).target.id}`, false)) { - this.friendsApiService.UserDetail((ev as FriendAcceptedEvent).target.id).subscribe(t => { - this.router.navigate(['/talking', t.conversationId]); - }); + case EventType.FriendsChangedEvent: { + const evt = ev; + this.cacheService.updateRequests(); + if (evt.result) { + if (fireAlert && evt.request.creatorId === this.cacheService.cachedData.me.id) { + Swal.fire('Friend request accepted', 'You and ' + evt.createdConversation.displayName + + ' are now friends!', 'success'); + } + this.cacheService.updateConversation(); + this.cacheService.updateFriends(); + if (this.router.isActive(`/user/${evt.request.targetId}`, false)) { + this.router.navigate(['/talking', evt.createdConversation.id]); + } + } else { + if (fireAlert && evt.request.creatorId === this.cacheService.cachedData.me.id) { + Swal.fire('Friend request rejected', `${evt.request.target.nickName} rejected your friend request.`, 'info'); + } } break; } @@ -168,7 +183,7 @@ export class MessageService { } break; } - case EventType.SomeoneLeftLevent: { + case EventType.SomeoneLeftEvent: { const evt = ev as SomeoneLeftEvent; const current = this.conversation && this.conversation.id === evt.conversationId && this.router.isActive('talking', false); if (evt.leftUser.id === this.cacheService.cachedData.me.id) { @@ -198,6 +213,11 @@ export class MessageService { this.cacheService.updateConversation(); break; } + case EventType.GroupJoinedEvent: { + this.cacheService.updateFriends(); + this.cacheService.updateConversation(); + break; + } default: break; } @@ -313,6 +333,7 @@ export class MessageService { public updateMaxImageWidth(): void { this.maxImageWidth = Math.floor((this.homeService.contentWrapper.clientWidth - 40) * 0.7 - 20 - 2); + this.videoHeight = Math.floor(Math.min(this.maxImageWidth * 9 / 21, 400)); } public resetVariables(): void { @@ -330,7 +351,10 @@ export class MessageService { } private showNotification(event: NewMessageEvent): void { - if (!event.muted && event.message.sender.id !== this.cacheService.cachedData.me.id && this._electronService.isElectronApp) { + if (!event.muted && + event.message.sender.id !== this.cacheService.cachedData.me.id && + this._electronService.isElectronApp && + localStorage.getItem('setting-electronNotify') !== 'false') { event.message.content = AES.decrypt(event.message.content, event.aesKey).toString(enc.Utf8); event.message.content = this.cacheService.modifyMessage(event.message.content); const notify = new Notification(event.message.sender.nickName, { @@ -417,12 +441,13 @@ export class MessageService { t.contentRaw = t.content; t.sendTimeDate = new Date(t.sendTime); t.timeStamp = t.sendTimeDate.getTime(); - if (t.content.match(/^\[(video|img)\].*/)) { + if (t.content.match(/^\[(video|img)].*/)) { if (t.content.startsWith('[img]')) { let imageWidth = Number(t.content.split('|')[1]), imageHeight = Number(t.content.split('|')[2]); const ratio = imageHeight / imageWidth; - const realMaxWidth = Math.min(this.maxImageWidth, Math.floor(900 / ratio)); + const realMaxWidth = Math.max(Math.min(this.maxImageWidth, Math.floor(500 / ratio)), + Math.min(this.maxImageWidth, 100)); // for too long image, just cut half of it if (realMaxWidth < imageWidth) { imageWidth = realMaxWidth; @@ -455,7 +480,7 @@ export class MessageService { t.content = 'Invalid User'; } }); - } else if (!t.content.match(/^\[(file|audio)\].*/)) { + } else if (!t.content.match(/^\[(file|audio)].*/)) { t.isEmoji = this.checkEmoji(t.content); t.content = he.encode(t.content); t.content = Autolinker.link(t.content, { diff --git a/src/app/Services/ServerListApiService.ts b/src/app/Services/ServerListApiService.ts new file mode 100644 index 00000000..d52b6c94 --- /dev/null +++ b/src/app/Services/ServerListApiService.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { ApiService } from './ApiService'; +import { Observable } from 'rxjs'; +import { ServerConfig } from '../Models/ServerConfig'; +import { environment } from '../../environments/environment'; +import { VersionViewModel } from '../Models/VersionViewModel'; + +@Injectable({ + providedIn: 'root', +}) +export class ServerListApiService { + + constructor(private apiService: ApiService) { + } + + public Servers(): Observable> { + return this.apiService.GetByFullUrl(`${environment.officialServerList}/servers`, false); + } + + public Version(): Observable { + return this.apiService.GetByFullUrl(`${environment.officialServerList}/version`, false); + } + + public getServerConfig(server: string): Observable { + return this.apiService.GetByFullUrl(server, false); + } +} diff --git a/src/app/Services/ThemeService.ts b/src/app/Services/ThemeService.ts index b52f8e08..5c304508 100644 --- a/src/app/Services/ThemeService.ts +++ b/src/app/Services/ThemeService.ts @@ -3,12 +3,17 @@ import { AuthApiService } from './AuthApiService'; import { Themes } from '../Models/Themes'; import { KahlaUser } from '../Models/KahlaUser'; -@Injectable() +@Injectable({ + providedIn: 'root', +}) export class ThemeService { constructor( private authApiService: AuthApiService, - ) { } + ) { + } + + public mediaListener: MediaQueryList; ApplyThemeFromRemote(remoteInfo: KahlaUser) { if (this.LocalThemeSetting !== remoteInfo.themeId) { @@ -22,7 +27,25 @@ export class ThemeService { } ApplyTheme(theme: Themes) { - switch (theme) { + let themeComputed = theme; + if (theme % 3 === 0) { + if (!this.mediaListener) { + this.mediaListener = matchMedia('(prefers-color-scheme: dark)'); + this.mediaListener.onchange = () => this.ApplyThemeFromLocal(); + } + if (this.mediaListener.matches) { + themeComputed = theme + 2; + } else { + themeComputed = theme + 1; + } + } else { + // make sure all media listener detached + if (this.mediaListener) { + this.mediaListener.onchange = null; + this.mediaListener = null; + } + } + switch (themeComputed) { case Themes.sakuraLight: document.body.className = 'theme-sakura-light'; document.querySelector('meta[name=theme-color]') diff --git a/src/app/Styles/button.scss b/src/app/Styles/button.scss index f8563f50..2138408a 100644 --- a/src/app/Styles/button.scss +++ b/src/app/Styles/button.scss @@ -1,10 +1,6 @@ .buttons { padding: 10px; text-align: center; - - .button { - margin: 0 5px 5px 5px; - } } button, @@ -85,11 +81,11 @@ button, background-color: #f5f5f5; color: black; - &:hover:enabled { + &:hover { background-color: #e9e9e9; } - &:focus:enabled { + &:active { background-color: #bbb; } } diff --git a/src/app/Styles/conversations.scss b/src/app/Styles/conversations.scss index 1607857f..fae6df24 100644 --- a/src/app/Styles/conversations.scss +++ b/src/app/Styles/conversations.scss @@ -42,6 +42,10 @@ text-overflow: ellipsis; white-space: nowrap; width: 65%; + + b { + font-weight: 600; + } } .status-badge { diff --git a/src/app/Styles/file-list.scss b/src/app/Styles/file-list.scss index fe17eb49..206f16ed 100644 --- a/src/app/Styles/file-list.scss +++ b/src/app/Styles/file-list.scss @@ -6,53 +6,79 @@ display: grid; width: 250px; height: 60px; - padding: 4px 12px; - grid-template-columns: 60px 1fr; - grid-template-rows: 1fr 1fr; + grid-template-columns: 1fr 30px; + grid-template-rows: 1fr; margin: 6px; border: 1px solid var(--selectable-border-color); background: var(--selectable-bg); user-select: none; - transition: all 0.1s ease-in-out; - &:hover { - background: var(--selectable-hover-bg); - } + a { + padding: 4px 0 4px 12px; + transition: all 0.1s ease-in-out; + display: grid; + grid-template-columns: 60px 1fr; + grid-template-rows: 1fr 1fr; + overflow: hidden; - &:active { - background: var(--selectable-active-bg); - } + &:hover { + background: var(--default-foreground-20); + } - .icon { - grid-area: 1 / 1 / 3 / 2; - vertical-align: center; - align-self: center; - text-align: center; - font-size: 32px; - overflow: hidden; - justify-self: center; - max-height: 60px; - max-width: 60px; - color: var(--primary-color-depth3); - } + &:active { + background: var(--default-foreground-40); + } - .title, .detail { - text-overflow: ellipsis; - overflow: hidden; - align-self: center; - margin-left: 8px; + .icon { + grid-area: 1 / 1 / 3 / 2; + vertical-align: center; + align-self: center; + text-align: center; + font-size: 32px; + overflow: hidden; + justify-self: center; + max-height: 60px; + max-width: 60px; + color: var(--primary-color-depth3); + } + + .title, .detail { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + align-self: center; + margin-left: 8px; + } + + .title { + grid-area: 1 / 2 / 2 / 3; + font-size: 15px; + color: var(--default-textcolor); + } + + .detail { + grid-area: 2 / 2 / 3 / 3; + font-size: 12px; + color: var(--minor-textcolor); + } } - .title { + .operate-button { grid-area: 1 / 2 / 2 / 3; - font-size: 15px; - color: var(--default-textcolor); - } + font-size: 20px; + display: flex; + justify-content: center; + transition: all 0.1s ease-in-out; + align-items: center; + color: var(--primary-color-depth3); + + &:hover { + background: var(--default-foreground-20); + } - .detail { - grid-area: 2 / 2 / 3 / 3; - font-size: 12px; - color: var(--minor-textcolor); + &:active { + background: var(--default-foreground-40); + } } } } diff --git a/src/app/Styles/menu.scss b/src/app/Styles/menu.scss index 55fb37b4..5270ae0e 100644 --- a/src/app/Styles/menu.scss +++ b/src/app/Styles/menu.scss @@ -113,7 +113,7 @@ color: #d9534f !important; } - &.notification { + &.toggle-setting { grid-template-columns: 15% auto 15%; } } @@ -154,6 +154,8 @@ a { .select-button-group { margin: 4px 10px 4px 10px; + display: flex; + flex-wrap: wrap; } .select-button { @@ -198,4 +200,8 @@ a { display: inline-block; border: white solid 2px; margin-right: 4px; + + &.fas { + border: none; + } } diff --git a/src/app/Styles/signin.scss b/src/app/Styles/signin.scss index 72fee272..bd419643 100644 --- a/src/app/Styles/signin.scss +++ b/src/app/Styles/signin.scss @@ -1,3 +1,12 @@ +%header { + color: #339bf9; + font-weight: 400; + text-transform: uppercase; + font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica, sans-serif; + text-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2); + margin: 0; +} + .signin-back { width: 100%; height: 100%; @@ -7,6 +16,11 @@ overflow-y: auto; text-align: center; } +@media (prefers-color-scheme: dark) { + .signin-back { + background-color: rgb(28, 30, 31)!important; + } +} .auth-page { margin: auto; @@ -25,21 +39,13 @@ overflow: hidden; } - h1, h3 { - color: #339bf9; - font-weight: 400; - text-transform: uppercase; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, Helvetica, sans-serif; - text-shadow: 2px 2px 10px rgba(0, 0, 0, 0.2); - margin: 0; - overflow: hidden; - } - h1 { + @extend %header; font-size: 2em; } h3 { + @extend %header; font-size: 1.5em; } } @@ -55,7 +61,7 @@ color: #2391D3 !important; } -.forgetPassword { +.actionLink { margin: 5% auto; display: block; text-decoration: underline; @@ -64,7 +70,7 @@ } p { - margin: 0 auto 8% auto; + margin: 4px 0 4px 0; } a { @@ -76,3 +82,41 @@ a { padding: 0 !important; margin-top: 8%; } + +.split-bar { + @extend %header; + clear: both; + user-select: none; + margin: 32px 0 32px 0; + position: relative; + display: inline-block; + font-size: 1.5em; + + &::before, &::after { + content: ' '; + display: block; + position: absolute; + top: 50%; + left: -120px; + width: 100px; // 100px line on either side + border-bottom: 2px solid #339bf9; + } + + &::after { + left: auto; + right: -120px; + } +} + +.inputAddr { + border: 1px solid var(--text-input-border-color); + color: var(--default-textcolor); + background-color: var(--text-input-bg); + border-radius: 6px; + height: 35px; + padding: 6px 12px; + font-size: 14px; + box-sizing: border-box; + outline: none; + width: 100%; +} diff --git a/src/app/Styles/talking.scss b/src/app/Styles/talking.scss index 79651a46..18e8f019 100644 --- a/src/app/Styles/talking.scss +++ b/src/app/Styles/talking.scss @@ -266,7 +266,7 @@ .image-container { position: relative; - max-height: 1000px; + max-height: 500px; overflow: hidden; img { @@ -274,10 +274,6 @@ } } -.message-list .message-balloon video { - width: 100%; -} - .chat-action { @extend %full-fixed-width; display: inline-block; diff --git a/src/app/Views/about.html b/src/app/Views/about.html index 2382f3dc..3a519589 100644 --- a/src/app/Views/about.html +++ b/src/app/Views/about.html @@ -41,7 +41,7 @@

Kahla {{checkService.version}}

Update history
- +
@@ -49,10 +49,15 @@

Kahla {{checkService.version}}

{{electronService.isElectronApp ? 'Open Kahla official site' : 'Download Kahla App'}}
- Terms - | - Privacy Policy - | - Feedback -

Copyright © 2018 Aiursoft Corporation

+

Server connected: {{apiService.serverConfig.serverName}} ({{apiService.serverConfig.domain.server}})

+

An aiursoft official server.

+

A community server.

+

+ Terms + | + Privacy Policy + | + Feedback +

+

Copyright © {{getCurrentYear()}} Aiursoft Corporation

diff --git a/src/app/Views/add-friend.html b/src/app/Views/add-friend.html index 03ae1901..2161d31c 100644 --- a/src/app/Views/add-friend.html +++ b/src/app/Views/add-friend.html @@ -1,7 +1,8 @@ 
- +
diff --git a/src/app/Views/advanced-settings.html b/src/app/Views/advanced-settings.html index af20d518..7d62a7c1 100644 --- a/src/app/Views/advanced-settings.html +++ b/src/app/Views/advanced-settings.html @@ -1,4 +1,4 @@ - + @@ -8,7 +8,7 @@
Language
-