diff --git a/.vscode/launch.json b/.vscode/launch.json index 360f2e3c..112e9c56 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -4,6 +4,15 @@ // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 "version": "0.2.0", "configurations": [ + { + "type": "firefox", + "request": "launch", + "reAttach": true, + "name": "Launch Firefox against localhost", + "url": "http://localhost:4200", + "webRoot": "${workspaceFolder}", + "profile": "default" + }, { "type": "chrome", "request": "launch", diff --git a/README.md b/README.md index 9e72cc17..862793f4 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,6 @@ -![KahlaLogo](./src/assets/144x144.png) +

+ +

# Kahla @@ -6,11 +8,14 @@ Welcome to Kahla. Kahla is a cross-platform business messaging app. Kahla also achieved one target to use one copy of code and target all platforms. -screenshot +

+screenshot +

Try it here: [web.kahla.app](https://web.kahla.app) -Or Get it on Google Play +Get it on Google Play +English badge Kahla currently targets Windows, Linux, macOS, Android, and iOS. diff --git a/package-lock.json b/package-lock.json index 32f065bc..4003e80c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "kahla", - "version": "3.8.1", + "version": "3.8.2", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -6099,9 +6099,9 @@ "dev": true }, "https-proxy-agent": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.2.tgz", - "integrity": "sha512-c8Ndjc9Bkpfx/vCJueCPy0jlP4ccCCSNDp8xwCZzPjKJUm+B+u9WX2x98Qx4n1PiMNTWo3D7KK5ifNV/yJyRzg==", + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-2.2.3.tgz", + "integrity": "sha512-Ytgnz23gm2DVftnzqRRz2dOXZbGd2uiajSw/95bPp6v53zPRspQjLm/AfBgqbJ2qfeRXWIOMVLpp86+/5yX39Q==", "dev": true, "requires": { "agent-base": "^4.3.0", diff --git a/package.json b/package.json index de8f5c0a..54b50ddc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kahla", - "version": "3.8.1", + "version": "3.8.2", "description": "Kahla is a cross-platform business messaging app.", "author": "Aiursoft (https://www.aiursoft.com/)", "build": { diff --git a/src/app/Controllers/add-friend.component.ts b/src/app/Controllers/add-friend.component.ts index 4fb10376..d93a716e 100644 --- a/src/app/Controllers/add-friend.component.ts +++ b/src/app/Controllers/add-friend.component.ts @@ -3,6 +3,7 @@ import { FriendsApiService } from '../Services/FriendsApiService'; import { Values } from '../values'; import { SearchResult } from '../Models/SearchResult'; import { FriendshipService } from '../Services/FriendshipService'; +import { ProbeService } from '../Services/ProbeService'; @Component({ templateUrl: '../Views/add-friend.html', @@ -21,7 +22,8 @@ export class AddFriendComponent implements OnInit { constructor( private friendsApiService: FriendsApiService, - public friendshipService: FriendshipService + public friendshipService: FriendshipService, + private probeService: ProbeService, ) { } @@ -47,10 +49,10 @@ export class AddFriendComponent implements OnInit { this.friendsApiService.SearchEverything(term.trim(), this.searchNumbers).subscribe(result => { if (result.code === 0) { result.users.forEach(user => { - user.avatarURL = Values.fileAddress + user.iconFilePath; + user.avatarURL = this.probeService.encodeProbeFileUrl(user.iconFilePath); }); result.groups.forEach(group => { - group.avatarURL = Values.fileAddress + group.imagePath; + group.avatarURL = this.probeService.encodeProbeFileUrl(group.imagePath); }); this.results = result; if (this.showUsers && result.usersCount === 0 && result.groupsCount !== 0) { diff --git a/src/app/Controllers/advanced-setting.component.ts b/src/app/Controllers/advanced-setting.component.ts index c7ef18b4..379bef48 100644 --- a/src/app/Controllers/advanced-setting.component.ts +++ b/src/app/Controllers/advanced-setting.component.ts @@ -3,8 +3,8 @@ import { AuthApiService } from '../Services/AuthApiService'; import { KahlaUser } from '../Models/KahlaUser'; import Swal from 'sweetalert2'; import { DevicesApiService } from '../Services/DevicesApiService'; -import { Values } from '../values'; import { CacheService } from '../Services/CacheService'; +import { ProbeService } from '../Services/ProbeService'; @Component({ templateUrl: '../Views/advanced-settings.html', @@ -20,7 +20,8 @@ export class AdvancedSettingComponent implements OnInit { constructor( private authApiService: AuthApiService, private devicesApiService: DevicesApiService, - private cacheService: CacheService + private cacheService: CacheService, + private probeService: ProbeService, ) { } @@ -30,7 +31,7 @@ export class AdvancedSettingComponent implements OnInit { } else { this.authApiService.Me().subscribe(p => { this.me = p.value; - this.me.avatarURL = Values.fileAddress + this.me.iconFilePath; + this.me.avatarURL = this.probeService.encodeProbeFileUrl(this.me.iconFilePath); }); } } diff --git a/src/app/Controllers/discover.component.ts b/src/app/Controllers/discover.component.ts index 5532bc45..d5a990e7 100644 --- a/src/app/Controllers/discover.component.ts +++ b/src/app/Controllers/discover.component.ts @@ -3,6 +3,7 @@ import { FriendsApiService } from '../Services/FriendsApiService'; import { DiscoverUser } from '../Models/DiscoverUser'; import { Values } from '../values'; import Swal from 'sweetalert2'; +import { ProbeService } from '../Services/ProbeService'; @Component({ templateUrl: '../Views/discover.html', @@ -17,7 +18,9 @@ export class DiscoverComponent implements OnInit { public loadingImgURL = Values.loadingImgURL; constructor( - private friendsApiService: FriendsApiService) { + private friendsApiService: FriendsApiService, + private probeService: ProbeService, + ) { } public ngOnInit(): void { @@ -28,7 +31,7 @@ export class DiscoverComponent implements OnInit { this.loading = true; this.friendsApiService.Discover(this.amount).subscribe(users => { users.items.forEach(item => { - item.targetUser.avatarURL = Values.fileAddress + item.targetUser.iconFilePath; + item.targetUser.avatarURL = this.probeService.encodeProbeFileUrl(item.targetUser.iconFilePath); }); const top = window.scrollY; this.users = users.items; diff --git a/src/app/Controllers/group.component.ts b/src/app/Controllers/group.component.ts index ae15efdd..8d5b692a 100644 --- a/src/app/Controllers/group.component.ts +++ b/src/app/Controllers/group.component.ts @@ -8,6 +8,7 @@ import { Values } from '../values'; import { GroupConversation } from '../Models/GroupConversation'; import { ConversationApiService } from '../Services/ConversationApiService'; import { MessageService } from '../Services/MessageService'; +import { ProbeService } from '../Services/ProbeService'; @Component({ templateUrl: '../Views/group.html', @@ -30,7 +31,9 @@ export class GroupComponent implements OnInit { private conversationApiService: ConversationApiService, private router: Router, private cacheService: CacheService, - public messageService: MessageService) { + public messageService: MessageService, + private probeService: ProbeService, + ) { } public ngOnInit(): void { @@ -44,9 +47,9 @@ export class GroupComponent implements OnInit { this.messageService.conversation = conversation; this.conversation = conversation; this.groupMembers = conversation.users.length; - this.conversation.avatarURL = Values.fileAddress + (this.conversation).groupImagePath; + this.conversation.avatarURL = this.probeService.encodeProbeFileUrl((this.conversation).groupImagePath); this.conversation.users.forEach(user => { - user.user.avatarURL = Values.fileAddress + user.user.iconFilePath; + user.user.avatarURL = this.probeService.encodeProbeFileUrl(user.user.iconFilePath); try { if (user.userId === this.cacheService.cachedData.me.id) { this.muted = user.muted; diff --git a/src/app/Controllers/manageGroup.component.ts b/src/app/Controllers/manageGroup.component.ts index 1a59d10a..206df3f8 100644 --- a/src/app/Controllers/manageGroup.component.ts +++ b/src/app/Controllers/manageGroup.component.ts @@ -7,11 +7,11 @@ import { GroupConversation } from '../Models/GroupConversation'; import { ConversationApiService } from '../Services/ConversationApiService'; import Swal from 'sweetalert2'; import { TimerService } from '../Services/TimerService'; -import { Values } from '../values'; import { UploadService } from '../Services/UploadService'; import { ElectronService } from 'ngx-electron'; import { AiurCollection } from '../Models/AiurCollection'; import { CacheService } from '../Services/CacheService'; +import { ProbeService } from '../Services/ProbeService'; @Component({ templateUrl: '../Views/manageGroup.html', @@ -34,7 +34,8 @@ export class ManageGroupComponent implements OnInit { private router: Router, public timerService: TimerService, public uploadService: UploadService, - public _electronService: ElectronService + public _electronService: ElectronService, + private probeService: ProbeService, ) { } @@ -50,12 +51,12 @@ export class ManageGroupComponent implements OnInit { .subscribe(conversation => { this.messageService.conversation = conversation; this.conversation = conversation; - this.conversation.avatarURL = Values.fileAddress + this.conversation.groupImagePath; + this.conversation.avatarURL = this.probeService.encodeProbeFileUrl(this.conversation.groupImagePath); this.newGroupName = this.conversation.groupName; }); } else { this.conversation = this.messageService.conversation; - this.conversation.avatarURL = Values.fileAddress + this.conversation.groupImagePath; + this.conversation.avatarURL = this.probeService.encodeProbeFileUrl(this.conversation.groupImagePath); this.newGroupName = this.conversation.groupName; } } diff --git a/src/app/Controllers/settings.component.ts b/src/app/Controllers/settings.component.ts index fd748220..45a7af7c 100644 --- a/src/app/Controllers/settings.component.ts +++ b/src/app/Controllers/settings.component.ts @@ -8,6 +8,7 @@ import Swal from 'sweetalert2'; import { ElectronService } from 'ngx-electron'; import { CacheService } from '../Services/CacheService'; import { HomeService } from '../Services/HomeService'; +import { ProbeService } from '../Services/ProbeService'; @Component({ selector: 'app-settings', @@ -25,7 +26,9 @@ export class SettingsComponent implements OnInit { public messageService: MessageService, public cacheService: CacheService, private _electronService: ElectronService, - public homeService: HomeService) { + public homeService: HomeService, + private probeService: ProbeService, + ) { } public ngOnInit(): void { @@ -33,7 +36,7 @@ export class SettingsComponent implements OnInit { this.authApiService.Me().subscribe(p => { if (p.code === 0) { this.cacheService.cachedData.me = p.value; - this.cacheService.cachedData.me.avatarURL = Values.fileAddress + p.value.iconFilePath; + this.cacheService.cachedData.me.avatarURL = this.probeService.encodeProbeFileUrl(p.value.iconFilePath); this.cacheService.saveCache(); } }); diff --git a/src/app/Controllers/talking.component.ts b/src/app/Controllers/talking.component.ts index 75b21668..a14d350f 100644 --- a/src/app/Controllers/talking.component.ts +++ b/src/app/Controllers/talking.component.ts @@ -19,6 +19,7 @@ import { FriendshipService } from '../Services/FriendshipService'; import { CacheService } from '../Services/CacheService'; import { Conversation } from '../Models/Conversation'; import { FileType } from '../Models/FileType'; +import { ProbeService } from '../Services/ProbeService'; declare var MediaRecorder: any; @@ -37,7 +38,6 @@ export class TalkingComponent implements OnInit, OnDestroy { private windowInnerHeight = 0; private formerWindowInnerHeight = 0; private keyBoardHeight = 0; - public fileAddress = Values.fileAddress; private conversationID = 0; public autoSaveInterval; public recording = false; @@ -52,10 +52,10 @@ export class TalkingComponent implements OnInit, OnDestroy { public showUserList = false; public matchedUsers: Array = []; - @ViewChild('imageInput', {static: false}) public imageInput; - @ViewChild('videoInput', {static: false}) public videoInput; - @ViewChild('fileInput', {static: false}) public fileInput; - @ViewChild('header', {static: true}) public header: HeaderComponent; + @ViewChild('imageInput', { static: false }) public imageInput; + @ViewChild('videoInput', { static: false }) public videoInput; + @ViewChild('fileInput', { static: false }) public fileInput; + @ViewChild('header', { static: true }) public header: HeaderComponent; constructor( private route: ActivatedRoute, @@ -67,6 +67,7 @@ export class TalkingComponent implements OnInit, OnDestroy { public timerService: TimerService, private friendshipService: FriendshipService, public _electronService: ElectronService, + public probeService: ProbeService, ) { } @@ -165,7 +166,7 @@ export class TalkingComponent implements OnInit, OnDestroy { if (this.cacheService.cachedData.conversationDetail[this.conversationID]) { this.updateConversation(this.cacheService.cachedData.conversationDetail[this.conversationID]); this.messageService.initMessage(this.conversationID); - this.messageService.getMessages(this.unread, this.conversationID, -1, this.load); + this.messageService.getMessages(this.unread, this.conversationID, -1, this.load, false); } else { const listItem = this.cacheService.cachedData.conversations.find(t => t.conversationId === this.conversationID); if (listItem) { @@ -199,11 +200,12 @@ export class TalkingComponent implements OnInit, OnDestroy { this.updateConversation(conversation); if (!this.cacheService.cachedData.conversationDetail[this.conversationID]) { this.messageService.initMessage(this.conversationID); - this.messageService.getMessages(this.unread, this.conversationID, -1, this.load); + this.messageService.getMessages(this.unread, this.conversationID, -1, this.load, false); } this.messageService.cleanMessageByTimer(); this.cacheService.cachedData.conversationDetail[this.conversationID] = conversation; this.cacheService.saveCache(); + this.messageService.showFailedMessages(); }); this.windowInnerHeight = window.innerHeight; } @@ -270,10 +272,10 @@ export class TalkingComponent implements OnInit, OnDestroy { if (unsentMessages.get(_this.conversationID) && (>unsentMessages.get(_this.conversationID)).length > 0) { const tempArray = >unsentMessages.get(_this.conversationID); - tempArray.push(encryptedMessage); + tempArray.push(he.decode(tempMessage.content)); unsentMessages.set(_this.conversationID, tempArray); } else { - unsentMessages.set(_this.conversationID, [encryptedMessage]); + unsentMessages.set(_this.conversationID, [he.decode(tempMessage.content)]); } localStorage.setItem('unsentMessages', JSON.stringify(Array.from(unsentMessages))); } @@ -285,6 +287,32 @@ export class TalkingComponent implements OnInit, OnDestroy { inputElement.style.height = 34 + 'px'; } + public resend(content: string): void { + const messageIDArry = this.messageService.getAtIDs(content); + const encryptedMessage = AES.encrypt(content, this.messageService.conversation.aesKey).toString(); + this.conversationApiService.SendMessage(this.messageService.conversation.id, encryptedMessage, messageIDArry.slice(1)) + .subscribe(result => { + if (result.code === 0) { + this.delete(content); + } + }); + } + + public delete(content: string): void { + for (let i = 0; i < this.messageService.localMessages.length; i++) { + if (this.messageService.localMessages[i].resend && this.messageService.localMessages[i].content === content) { + this.messageService.localMessages.splice(i, 1); + break; + } + } + const unsentMessages = new Map(JSON.parse(localStorage.getItem('unsentMessages'))); + const tempArray = >unsentMessages.get(this.conversationID); + const index = tempArray.indexOf(content); + tempArray.splice(index, 1); + unsentMessages.set(this.conversationID, tempArray); + localStorage.setItem('unsentMessages', JSON.stringify(Array.from(unsentMessages))); + } + public startInput(): void { if (this.showPanel) { this.showPanel = false; @@ -411,7 +439,7 @@ export class TalkingComponent implements OnInit, OnDestroy { if (this.recording) { this.mediaRecorder.stop(); } else { - navigator.mediaDevices.getUserMedia({audio: true}) + navigator.mediaDevices.getUserMedia({ audio: true }) .then(stream => { this.recording = true; this.mediaRecorder = new MediaRecorder(stream); @@ -471,7 +499,7 @@ export class TalkingComponent implements OnInit, OnDestroy { public shareToOther(message: string): void { - this.router.navigate(['share-target', {message: message}]); + this.router.navigate(['share-target', { message: message }]); } public getAtListMaxHeight(): number { diff --git a/src/app/Controllers/user.component.ts b/src/app/Controllers/user.component.ts index 3b1df6df..711d06ce 100644 --- a/src/app/Controllers/user.component.ts +++ b/src/app/Controllers/user.component.ts @@ -1,20 +1,20 @@ import { Component, OnInit } from '@angular/core'; -import { ActivatedRoute, Params, Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { FriendsApiService } from '../Services/FriendsApiService'; import { KahlaUser } from '../Models/KahlaUser'; import { CacheService } from '../Services/CacheService'; -import { switchMap, } from 'rxjs/operators'; import Swal from 'sweetalert2'; import { Values } from '../values'; import { MessageService } from '../Services/MessageService'; import { TimerService } from '../Services/TimerService'; import { Request } from '../Models/Request'; +import { ProbeService } from '../Services/ProbeService'; @Component({ templateUrl: '../Views/user.html', styleUrls: ['../Styles/menu.scss', - '../Styles/button.scss', - '../Styles/badge.scss'] + '../Styles/button.scss', + '../Styles/badge.scss'] }) export class UserComponent implements OnInit { @@ -32,20 +32,25 @@ export class UserComponent implements OnInit { public cacheService: CacheService, public messageService: MessageService, public timerService: TimerService, + private probeService: ProbeService, ) { } public ngOnInit(): void { - this.route.params - .pipe(switchMap((params: Params) => this.friendsApiService.UserDetail(params['id']))) - .subscribe(response => { - this.info = response.user; - this.areFriends = response.areFriends; - this.conversationId = response.conversationId; - this.sentRequest = response.sentRequest; - this.pendingRequest = response.pendingRequest; - this.info.avatarURL = Values.fileAddress + this.info.iconFilePath; - }); + this.route.params.subscribe(t => { + this.updateFriendInfo(t.id); + }); + } + + public updateFriendInfo(userId: string) { + this.friendsApiService.UserDetail(userId).subscribe(response => { + this.info = response.user; + this.areFriends = response.areFriends; + this.conversationId = response.conversationId; + this.sentRequest = response.sentRequest; + this.pendingRequest = response.pendingRequest; + this.info.avatarURL = this.probeService.encodeProbeFileUrl(this.info.iconFilePath); + }); } public delete(id: string): void { @@ -89,7 +94,7 @@ export class UserComponent implements OnInit { confirmButtonColor: 'red', showCancelButton: true, confirmButtonText: 'Report' - }).then((result) => { + }).then((result) => { if (result.value) { if (result.value.length >= 5) { this.friendsApiService.Report(this.info.id, result.value).subscribe(response => { @@ -105,13 +110,14 @@ export class UserComponent implements OnInit { Swal.fire('Error', 'The reason\'s length should between five and two hundreds.', 'error'); } } - }); + }); } public accept(id: number): void { this.friendsApiService.CompleteRequest(id, true) .subscribe(r => { Swal.fire('Success', r.message, 'success'); + this.updateFriendInfo(this.info.id); this.cacheService.updateRequests(); this.cacheService.updateFriends(); this.areFriends = true; diff --git a/src/app/Controllers/userDetail.component.ts b/src/app/Controllers/userDetail.component.ts index bee91acd..bc9d3359 100644 --- a/src/app/Controllers/userDetail.component.ts +++ b/src/app/Controllers/userDetail.component.ts @@ -10,6 +10,7 @@ import { Values } from '../values'; import { MessageService } from '../Services/MessageService'; import { ElectronService } from 'ngx-electron'; import { CacheService } from '../Services/CacheService'; +import { ProbeService } from '../Services/ProbeService'; @Component({ templateUrl: '../Views/userDetail.html', @@ -31,7 +32,8 @@ export class UserDetailComponent implements OnInit { public uploadService: UploadService, public messageService: MessageService, public cacheService: CacheService, - public _electronService: ElectronService + public _electronService: ElectronService, + private probeService: ProbeService, ) { } @@ -39,7 +41,7 @@ export class UserDetailComponent implements OnInit { if (!this.cacheService.cachedData.me) { this.authApiService.Me().subscribe(p => { this.user = p.value; - this.user.avatarURL = Values.fileAddress + this.user.iconFilePath; + this.user.avatarURL = this.probeService.encodeProbeFileUrl(this.user.iconFilePath); }); } else { this.user = Object.assign({}, this.cacheService.cachedData.me); diff --git a/src/app/Models/Message.ts b/src/app/Models/Message.ts index cde9d52f..3960711c 100644 --- a/src/app/Models/Message.ts +++ b/src/app/Models/Message.ts @@ -7,7 +7,7 @@ export class Message { public sender: KahlaUser; public sendTime: string; public content: string; - + public resend: boolean; public contentRaw: string; public isEmoji = false; public read: boolean; diff --git a/src/app/Services/CacheService.ts b/src/app/Services/CacheService.ts index 02230d41..eb340b04 100644 --- a/src/app/Services/CacheService.ts +++ b/src/app/Services/CacheService.ts @@ -2,11 +2,11 @@ import { Injectable } from '@angular/core'; import { CacheModel } from '../Models/CacheModel'; import { FriendsApiService } from './FriendsApiService'; import { Request } from '../Models/Request'; -import { Values } from '../values'; import { map } from 'rxjs/operators'; import { AES, enc } from 'crypto-js'; import { DevicesApiService } from './DevicesApiService'; import { ConversationApiService } from './ConversationApiService'; +import { ProbeService } from './ProbeService'; @Injectable() export class CacheService { @@ -17,7 +17,8 @@ export class CacheService { constructor( private friendsApiService: FriendsApiService, private devicesApiService: DevicesApiService, - private conversationApiService: ConversationApiService + private conversationApiService: ConversationApiService, + private probeService: ProbeService, ) { } public reset() { @@ -42,7 +43,7 @@ export class CacheService { } e.latestMessage = this.modifyMessage(e.latestMessage); } - e.avatarURL = Values.fileAddress + e.displayImagePath; + e.avatarURL = this.probeService.encodeProbeFileUrl(e.displayImagePath); }); this.cachedData.conversations = info; this.updateTotalUnread(); @@ -55,10 +56,10 @@ export class CacheService { .subscribe(result => { if (result.code === 0) { result.users.forEach(user => { - user.avatarURL = Values.fileAddress + user.iconFilePath; + user.avatarURL = this.probeService.encodeProbeFileUrl(user.iconFilePath); }); result.groups.forEach(group => { - group.avatarURL = Values.fileAddress + group.imagePath; + group.avatarURL = this.probeService.encodeProbeFileUrl(group.imagePath); }); this.cachedData.friends = result; @@ -71,7 +72,7 @@ export class CacheService { this.friendsApiService.MyRequests().subscribe(response => { this.cachedData.requests = response.items; response.items.forEach(item => { - item.creator.avatarURL = Values.fileAddress + item.creator.iconFilePath; + item.creator.avatarURL = this.probeService.encodeProbeFileUrl(item.creator.iconFilePath); }); this.totalRequests = response.items.filter(t => !t.completed).length; this.saveCache(); @@ -109,6 +110,8 @@ export class CacheService { deviceName.push('Seamonkey'); } else if (item.name.includes('Edge')) { deviceName.push('Microsoft Edge'); + } else if (item.name.includes('Edg')) { + deviceName.push('Edge Chromium'); } else if (item.name.includes('Chrome') && !item.name.includes('Chromium')) { deviceName.push('Chrome'); } else if (item.name.includes('Chromium')) { diff --git a/src/app/Services/InitService.ts b/src/app/Services/InitService.ts index 4b527b38..52a505a6 100644 --- a/src/app/Services/InitService.ts +++ b/src/app/Services/InitService.ts @@ -3,14 +3,13 @@ import { CheckService } from './CheckService'; import { AuthApiService } from './AuthApiService'; import { Router } from '@angular/router'; import { MessageService } from './MessageService'; -import { Values } from '../values'; import { CacheService } from './CacheService'; -import { ConversationApiService } from './ConversationApiService'; 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'; @Injectable({ providedIn: 'root' @@ -21,8 +20,8 @@ export class InitService { private timeoutNumber = 1000; private interval; private timeout; - private online; - private errorOrClose; + public online: boolean; + private errorOrClose: boolean; private closeWebSocket = false; private options = { userVisibleOnly: true, @@ -35,10 +34,11 @@ export class InitService { private router: Router, private messageService: MessageService, private cacheService: CacheService, - private conversationApiService: ConversationApiService, private _electronService: ElectronService, private themeService: ThemeService, - private devicesApiService: DevicesApiService) { + private devicesApiService: DevicesApiService, + private probeService: ProbeService, + ) { } public init(): void { @@ -57,12 +57,12 @@ export class InitService { this.cacheService.initCache(); this.authApiService.SignInStatus().subscribe(signInStatus => { if (signInStatus.value === false) { - this.router.navigate(['/signin'], {replaceUrl: true}); + 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 = Values.fileAddress + p.value.iconFilePath; + this.cacheService.cachedData.me.avatarURL = this.probeService.encodeProbeFileUrl(p.value.iconFilePath); this.themeService.ApplyThemeFromRemote(p.value); if (!this._electronService.isElectronApp && navigator.serviceWorker) { this.subscribeUser(); @@ -95,18 +95,21 @@ export class InitService { this.interval = setInterval(this.checkNetwork.bind(this), 3000); }; this.ws.onmessage = evt => this.messageService.OnMessage(evt); - this.ws.onerror = () => this.errorOrClosedFunc(); + this.ws.onerror = () => { + this.errorOrClosedFunc(); + this.fireNetworkAlert(); + }; this.ws.onclose = () => this.errorOrClosedFunc(); - this.resend(); if (reconnect) { this.cacheService.updateConversation(); this.cacheService.updateFriends(); - } - if (this.messageService.conversation && reconnect) { - this.messageService.getMessages(0, this.messageService.conversation.id, -1, 15); + if (this.messageService.conversation) { + this.messageService.getMessages(0, this.messageService.conversation.id, -1, 15, true); + } } }, () => { - this.errorOrClosedFunc(); + this.fireNetworkAlert(); + this.errorOrClosedFunc(); }); } @@ -120,6 +123,11 @@ export class InitService { } } + public fireNetworkAlert(): void { + Swal.fire('Failed to connect to stargate channel.', 'This might caused by the bad network you connected.
' + + 'We will try to reconnect later, but before that, your message might no be the latest.', 'error'); + } + private checkNetwork(): void { if (navigator.onLine && !this.connecting && (!this.online || this.errorOrClose)) { this.autoReconnect(); @@ -150,41 +158,17 @@ export class InitService { }, this.timeoutNumber); } - private resend(): void { - if (navigator.onLine) { - const unsentMessages = new Map(JSON.parse(localStorage.getItem('unsentMessages'))); - unsentMessages.forEach((messages, id) => { - const sendFailMessages = []; - for (let i = 0; i < (>messages).length; i++) { - setTimeout(() => { - const message = (>messages)[i]; - this.conversationApiService.SendMessage(Number(id), message, null) - .subscribe({ - error(e) { - if (e.status === 0 || e.status === 503) { - sendFailMessages.push(message); - } - } - }); - }, 500); - } - unsentMessages.set(id, sendFailMessages); - localStorage.setItem('unsentMessages', JSON.stringify(Array.from(unsentMessages))); - }); - } - } - private subscribeUser(): void { 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(function (registration) { + return registration.pushManager.getSubscription().then(function (sub) { if (sub === null) { return registration.pushManager.subscribe(_this.options) - .then(function(pushSubscription) { + .then(function (pushSubscription) { return _this.devicesApiService.AddDevice(navigator.userAgent, pushSubscription.endpoint, pushSubscription.toJSON().keys.p256dh, pushSubscription.toJSON().keys.auth) - .subscribe(function(result) { + .subscribe(function (result) { localStorage.setItem('deviceID', result.value.toString()); }); }); @@ -197,10 +181,10 @@ export class InitService { 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(function (registration) { + return navigator.serviceWorker.addEventListener('pushsubscriptionchange', function () { registration.pushManager.subscribe(_this.options) - .then(function(pushSubscription) { + .then(function (pushSubscription) { return _this.devicesApiService.UpdateDevice(Number(localStorage.getItem('deviceID')), navigator.userAgent, pushSubscription.endpoint, pushSubscription.toJSON().keys.p256dh, pushSubscription.toJSON().keys.auth) .subscribe(); diff --git a/src/app/Services/MessageService.ts b/src/app/Services/MessageService.ts index e45eedc8..d55a5690 100644 --- a/src/app/Services/MessageService.ts +++ b/src/app/Services/MessageService.ts @@ -29,6 +29,7 @@ import { DissolveEvent } from '../Models/DissolveEvent'; import { HomeService } from './HomeService'; import { GroupsApiService } from './GroupsApiService'; import { FriendsApiService } from './FriendsApiService'; +import { ProbeService } from './ProbeService'; @Injectable({ providedIn: 'root' @@ -61,7 +62,8 @@ export class MessageService { private router: Router, private homeService: HomeService, private groupsApiService: GroupsApiService, - private friendsApiService: FriendsApiService + private friendsApiService: FriendsApiService, + private probeService: ProbeService, ) { } public OnMessage(data: MessageEvent) { @@ -95,22 +97,21 @@ export class MessageService { } } if (this.conversation && this.conversation.id === evt.message.conversationId) { - // this.getMessages(0, this.conversation.id, -1, 15); this.rawMessages.push(evt.message); this.localMessages.push(this.modifyMessage(Object.assign({}, evt.message))); this.reorderLocalMessages(); - this.localMessages = this.localMessages.filter((t => !t.local)); + this.localMessages = this.localMessages.filter(t => !t.local && !t.resend); this.updateAtLink(); if (this.belowWindowPercent <= 0.2) { setTimeout(() => { this.uploadService.scrollBottom(true); }, 0); } - this.conversationApiService.GetMessage(this.conversation.id, -1, 0).subscribe(); if (!document.hasFocus()) { this.showNotification(evt); } this.saveMessage(); + this.showFailedMessages(); } else { this.showNotification(evt); } @@ -207,7 +208,7 @@ export class MessageService { } } - public getMessages(unread: number, id: number, skipTill: number, take: number) { + public getMessages(unread: number, id: number, skipTill: number, take: number, reconnect: boolean) { this.messageLoading = true; this.conversationApiService.GetMessage(id, skipTill, take) .pipe( @@ -280,6 +281,9 @@ export class MessageService { } this.cacheService.updateTotalUnread(); this.messageLoading = false; + if (reconnect) { + this.showFailedMessages(); + } }); } @@ -292,7 +296,7 @@ export class MessageService { if (!this.noMoreMessages) { this.loadingMore = true; this.oldScrollHeight = document.documentElement.scrollHeight; - this.getMessages(-1, this.conversation.id, this.localMessages[0].id, 15); + this.getMessages(-1, this.conversation.id, this.localMessages[0].id, 15, false); } } @@ -325,7 +329,7 @@ export class MessageService { event.message.content = this.cacheService.modifyMessage(event.message.content); const notify = new Notification(event.message.sender.nickName, { body: event.message.content, - icon: Values.fileAddress + event.message.sender.iconFilePath + icon: this.probeService.encodeProbeFileUrl(event.message.sender.iconFilePath) }); notify.onclick = function (clickEvent) { clickEvent.preventDefault(); @@ -342,19 +346,22 @@ export class MessageService { } public searchUser(nickName: string, getMessage: boolean): Array { - if (nickName.length === 0 && !getMessage) { - return this.conversation.users.map(x => x.user); - } else { - const matchedUsers = []; - this.conversation.users.forEach((value: UserGroupRelation) => { - if (!getMessage && value.user.nickName.toLowerCase().replace(/ /g, '').includes(nickName.toLowerCase())) { - matchedUsers.push(value.user); - } else if (getMessage && value.user.nickName.toLowerCase().replace(/ /g, '') === nickName.toLowerCase()) { - matchedUsers.push(value.user); - } - }); - return matchedUsers; + if (typeof this.conversation.users !== 'undefined') { + if (nickName.length === 0 && !getMessage) { + return this.conversation.users.map(x => x.user); + } else { + const matchedUsers = []; + this.conversation.users.forEach((value: UserGroupRelation) => { + if (!getMessage && value.user.nickName.toLowerCase().replace(/ /g, '').includes(nickName.toLowerCase())) { + matchedUsers.push(value.user); + } else if (getMessage && value.user.nickName.toLowerCase().replace(/ /g, '') === nickName.toLowerCase()) { + matchedUsers.push(value.user); + } + }); + return matchedUsers; + } } + return []; } public getAtIDs(message: string): Array { @@ -412,9 +419,7 @@ export class MessageService { imageWidth = realMaxWidth; imageHeight = Math.floor(realMaxWidth * ratio); } - t.content = - `[img]${Values.fileAddress}${encodeURIComponent(t.content.substring(5).split('|')[0]) - .replace(/%2F/g, '/')}|${imageWidth}|${imageHeight}`; + t.content = `[img]${this.probeService.encodeProbeFileUrl(t.content.substring(5).split('|')[0])}|${imageWidth}|${imageHeight}`; } } else if (t.content.startsWith('[group]')) { const groupId = Number(t.content.substring(7)); @@ -422,7 +427,7 @@ export class MessageService { this.groupsApiService.GroupSummary(groupId).subscribe(p => { if (p.value) { t.content = `[share]${p.value.id}|${p.value.name.replace(/\|/g, '')}|` + - `${p.value.hasPassword ? 'Private' : 'Public'}|${Values.fileAddress}${p.value.imagePath}`; + `${p.value.hasPassword ? 'Private' : 'Public'}|${this.probeService.encodeProbeFileUrl(p.value.imagePath)}`; t.relatedData = p.value; } else { t.content = 'Invalid Group'; @@ -435,7 +440,7 @@ export class MessageService { this.friendsApiService.UserDetail(userId).subscribe(p => { if (p.user) { t.content = `[share]${p.user.id}|${p.user.nickName.replace(/\|/g, '')}|` + - `${p.user.bio ? p.user.bio.replace(/\|/g, ' ') : ' '}|${Values.fileAddress}${p.user.iconFilePath}`; + `${p.user.bio ? p.user.bio.replace(/\|/g, ' ') : ' '}|${this.probeService.encodeProbeFileUrl(p.user.iconFilePath)}`; t.relatedData = p.user; } else { t.content = 'Invalid User'; @@ -505,4 +510,20 @@ export class MessageService { } this.saveMessage(); } + + public showFailedMessages(): void { + const unsentMessages = new Map(JSON.parse(localStorage.getItem('unsentMessages'))); + this.localMessages = this.localMessages.filter(m => !m.local); + if (unsentMessages.has(this.conversation.id)) { + (>unsentMessages.get(this.conversation.id)).forEach(content => { + const message = new Message(); + message.content = content; + message.resend = true; + message.senderId = this.cacheService.cachedData.me.id; + message.sender = this.cacheService.cachedData.me; + message.local = true; + this.localMessages.push(message); + }, this); + } + } } diff --git a/src/app/Services/ProbeService.ts b/src/app/Services/ProbeService.ts new file mode 100644 index 00000000..e4d222f1 --- /dev/null +++ b/src/app/Services/ProbeService.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { Values } from '../values'; + +@Injectable({ + providedIn: 'root', +}) +export class ProbeService { + public encodeProbeFileUrl(filePath: string) { + const encoded = filePath = encodeURIComponent(filePath).replace(/%2F/g, '/'); + const index = encoded.indexOf('/'); + return Values.fileCompatAddress.replace('{site}', encoded.substring(0, index)) + encoded.substring(index + 1); + } +} diff --git a/src/app/Services/TimerService.ts b/src/app/Services/TimerService.ts index 92ae3ed7..41fccb2c 100644 --- a/src/app/Services/TimerService.ts +++ b/src/app/Services/TimerService.ts @@ -53,6 +53,6 @@ export class TimerService { private getDestructTime(time: number): string { time = Number(time); - return Timers[time]; + return Timers[time] ? Timers[time] : 'off'; } } diff --git a/src/app/Services/UploadService.ts b/src/app/Services/UploadService.ts index 4966dc01..1e668e78 100644 --- a/src/app/Services/UploadService.ts +++ b/src/app/Services/UploadService.ts @@ -9,6 +9,7 @@ import * as loadImage from 'blueimp-load-image'; import { GroupConversation } from '../Models/GroupConversation'; import { Values } from '../values'; import { FileType } from '../Models/FileType'; +import { ProbeService } from './ProbeService'; @Injectable({ providedIn: 'root' @@ -19,6 +20,7 @@ export class UploadService { constructor( private filesApiService: FilesApiService, private conversationApiService: ConversationApiService, + private probeService: ProbeService, ) {} public upload(file: File, conversationID: number, aesKey: string, fileType: FileType): void { @@ -196,7 +198,7 @@ export class UploadService { } else if (res != null && (res).code === 0) { Swal.close(); user.iconFilePath = (res).filePath; - user.avatarURL = Values.fileAddress + user.iconFilePath; + user.avatarURL = this.probeService.encodeProbeFileUrl(user.iconFilePath); } }); alert.then(result => { @@ -225,7 +227,7 @@ export class UploadService { } else if (res != null && (res).code === 0) { Swal.close(); group.groupImagePath = (res).filePath; - group.avatarURL = Values.fileAddress + group.groupImagePath; + group.avatarURL = this.probeService.encodeProbeFileUrl(group.groupImagePath); } }); alert.then(result => { @@ -272,7 +274,7 @@ export class UploadService { target.style.display = 'none'; const audioElement = document.createElement('audio'); audioElement.style.maxWidth = '100%'; - audioElement.src = Values.fileAddress + encodeURIComponent(message.substring(7).split('|')[0]).replace(/%2F/g, '/'); + audioElement.src = this.probeService.encodeProbeFileUrl(message.substring(7).split('|')[0]); audioElement.controls = true; target.parentElement.appendChild(audioElement); audioElement.play(); diff --git a/src/app/Styles/signin.scss b/src/app/Styles/signin.scss index 86432e80..72fee272 100644 --- a/src/app/Styles/signin.scss +++ b/src/app/Styles/signin.scss @@ -21,6 +21,7 @@ .auth-header { img { width: 90px; + height: 90px; overflow: hidden; } diff --git a/src/app/Styles/talking.scss b/src/app/Styles/talking.scss index be22e56d..805c077f 100644 --- a/src/app/Styles/talking.scss +++ b/src/app/Styles/talking.scss @@ -53,6 +53,15 @@ width: 70%; margin-bottom: 1.5%; + * { + vertical-align: middle; + } + + .button { + margin-right: 0.5rem; + background-color: red; + } + .share { cursor: pointer; } @@ -142,7 +151,7 @@ } .right .chat-content { - background-color: var(--primary-color-depth3); + background: linear-gradient(120deg, var(--primary-color-depth3) 0%, var(--primary-color-depth2) 100%); &::after { border-left-color: var(--primary-color-depth3); diff --git a/src/app/Styles/themes/_dark.scss b/src/app/Styles/themes/_dark.scss index 3d6cc1c7..866f64d0 100644 --- a/src/app/Styles/themes/_dark.scss +++ b/src/app/Styles/themes/_dark.scss @@ -2,7 +2,7 @@ // Dark Theme // $primary-color-depth1: #83b6d3; -$primary-color-depth2: #43b1f3; +$primary-color-depth2: #18bbf9; $primary-color-depth3: #18a4f9; body.theme-dark { diff --git a/src/app/Styles/themes/_light.scss b/src/app/Styles/themes/_light.scss index 223a4e5e..adc03065 100644 --- a/src/app/Styles/themes/_light.scss +++ b/src/app/Styles/themes/_light.scss @@ -2,7 +2,7 @@ // Light Theme // $primary-color-depth1: #83b6d3; -$primary-color-depth2: #43b1f3; +$primary-color-depth2: #38cbf0; $primary-color-depth3: #18a4f9; body.theme-light { diff --git a/src/app/Views/devices.html b/src/app/Views/devices.html index dd1ec387..fe097383 100644 --- a/src/app/Views/devices.html +++ b/src/app/Views/devices.html @@ -11,7 +11,8 @@
- diff --git a/src/app/Views/header.html b/src/app/Views/header.html index f9a96b64..aaac7f38 100644 --- a/src/app/Views/header.html +++ b/src/app/Views/header.html @@ -33,3 +33,11 @@ Connecting...
+ +
+
+ + Disconnected +
+
diff --git a/src/app/Views/talking.html b/src/app/Views/talking.html index 302ef9d6..687e71d9 100644 --- a/src/app/Views/talking.html +++ b/src/app/Views/talking.html @@ -1,40 +1,50 @@ - + +
    - +
  • --- LAST READ ---
    -
    +
    - +
  • - @@ -123,15 +138,14 @@
    {{message.content.split('|')[1]}}
    - + @@ -139,7 +153,7 @@
    {{message.content.split('|')[1]}}
    + accept="image/png, image/jpeg, image/bmp, image/gif, image/svg+xml" />
    \ No newline at end of file diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 0ae88c25..811cd7b1 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -46,6 +46,7 @@ import { ThemeService } from './Services/ThemeService'; import { TimerService } from './Services/TimerService'; import { HomeService } from './Services/HomeService'; import { FriendshipService } from './Services/FriendshipService'; +import { ProbeService } from './Services/ProbeService'; @NgModule({ imports: [ @@ -96,7 +97,8 @@ import { FriendshipService } from './Services/FriendshipService'; ThemeService, TimerService, HomeService, - FriendshipService + FriendshipService, + ProbeService ], bootstrap: [AppComponent] }) diff --git a/src/app/values.ts b/src/app/values.ts index ac93c305..d3168b64 100644 --- a/src/app/values.ts +++ b/src/app/values.ts @@ -1,4 +1,5 @@ export class Values { + public static fileCompatAddress = 'https://{site}.aiursoft.io/'; public static fileAddress = 'https://probe.aiursoft.com/Download/Open/'; public static fileDownloadAddress = 'https://probe.aiursoft.com/Download/File/'; public static loadingImgURL = 'https://ui.cdn.aiursoft.com/images/loading.gif'; diff --git a/src/sw.js b/src/sw.js index 0081e713..773a15e0 100644 --- a/src/sw.js +++ b/src/sw.js @@ -1,6 +1,6 @@ importScripts('https://cdnjs.cloudflare.com/ajax/libs/crypto-js/3.1.9-1/crypto-js.min.js'); -const CACHE = 'v3'; +const CACHE = 'v4'; self.addEventListener('install', function (event) { event.waitUntil( @@ -16,6 +16,7 @@ self.addEventListener('install', function (event) { '/favicon.ico', '/fa-solid-900.woff2', '/fa-regular-400.woff2', + '/fa-brands-400.woff2', '/assets/144x144.png' ]); }) diff --git a/tslint.json b/tslint.json index 56bd94ee..6561d27a 100644 --- a/tslint.json +++ b/tslint.json @@ -73,7 +73,6 @@ "no-trailing-whitespace": true, "no-unnecessary-initializer": true, "no-unused-expression": true, - "no-use-before-declare": true, "no-var-keyword": true, "object-literal-sort-keys": false, "one-line": [ @@ -135,6 +134,6 @@ "use-life-cycle-interface": true, "use-pipe-transform-interface": true, "component-class-suffix": true, - "directive-class-suffix": true + "directive-class-suffix": true } -} +} \ No newline at end of file