diff --git a/angular.json b/angular.json index 233085f8..1e4187b8 100644 --- a/angular.json +++ b/angular.json @@ -26,6 +26,7 @@ "src/assets", "src/manifest.json", "src/favicon.ico", + "src/favicon_notify.ico", "src/web.config", "src/robots.txt", "src/sw.js", diff --git a/package.json b/package.json index 2fd28723..834dd155 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kahla", - "version": "4.1.0", + "version": "4.1.1", "description": "Kahla is a cross-platform business messaging app.", "author": "Aiursoft (https://www.aiursoft.com/)", "build": { diff --git a/src/app/Controllers/share.component.ts b/src/app/Controllers/share.component.ts index ad69ae72..4097bdcc 100644 --- a/src/app/Controllers/share.component.ts +++ b/src/app/Controllers/share.component.ts @@ -101,7 +101,7 @@ export class ShareComponent implements OnInit, DoCheck { if (this.fileRef) { // get file path const filePath = this.relativePath ? this.fileRef.filePath : - this.fileRef.filePath.match(new RegExp(`.+\/conversation-${this.srcConversation}\/(.+)`))[1]; + this.fileRef.filePath.match(new RegExp(`.+\/conversation-${this.srcConversation}\/(.+)`))?.[1]; if (!filePath) { resolve(); Swal.fire('Failed.', 'Sorry, but you can\'t share this file.', 'error'); diff --git a/src/app/Controllers/talking.component.ts b/src/app/Controllers/talking.component.ts index f8108c67..56ec0672 100644 --- a/src/app/Controllers/talking.component.ts +++ b/src/app/Controllers/talking.component.ts @@ -113,9 +113,7 @@ export class TalkingComponent implements OnInit, OnDestroy { if (e.key === 'Enter' && !this.showUserList) { e.preventDefault(); if ((e.altKey || e.ctrlKey || e.shiftKey) === this.cacheService.cachedData.me.enableEnterToSendMessage) { - const input = document.getElementById('chatInput'); - this.content = `${this.content.slice(0, input.selectionStart)}\n${this.content.slice(input.selectionStart)}`; - this.updateInputHeight(); + this.insertToSelection('\n'); this.oldContent = ''; // prevent send message on keyup } else { this.oldContent = this.content; @@ -297,11 +295,7 @@ export class TalkingComponent implements OnInit, OnDestroy { }, next: p => { this.messageService.localMessages.splice(this.messageService.localMessages.indexOf(tempMessage), 1); - this.messageService.rawMessages.push(p.value); - this.messageService.localMessages.push(this.messageService.modifyMessage(Object.assign({}, p.value))); - this.messageService.reorderLocalMessages(); - this.messageService.updateAtLink(); - this.messageService.saveMessage(); + this.messageService.insertMessage(p.value); } }); this.content = ''; @@ -344,10 +338,8 @@ export class TalkingComponent implements OnInit, OnDestroy { public togglePanel(): void { this.showPanel = !this.showPanel; if (this.showPanel) { - document.querySelector('.message-list').classList.add('active-list'); window.scroll(0, window.scrollY + 105); } else { - document.querySelector('.message-list').classList.remove('active-list'); if (this.messageService.belowWindowPercent <= 0.2) { this.uploadService.scrollBottom(false); } else { @@ -373,7 +365,11 @@ export class TalkingComponent implements OnInit, OnDestroy { if (fileType !== FileType.File) { files = this.probeService.renameFile(files, fileType === FileType.Image ? 'img_' : 'video_'); } - this.uploadService.upload(files, this.messageService.conversation.id, this.messageService.conversation.aesKey, fileType); + this.uploadService.upload(files, this.messageService.conversation.id, this.messageService.conversation.aesKey, fileType) + ?.then(t => { + this.messageService.insertMessage(t.value); + setTimeout(() => this.uploadService.scrollBottom(true), 0); + }); } } @@ -393,7 +389,10 @@ export class TalkingComponent implements OnInit, OnDestroy { }).then((send) => { if (send.value) { this.uploadService.upload(blob, this.messageService.conversation.id, - this.messageService.conversation.aesKey, FileType.Image); + this.messageService.conversation.aesKey, FileType.Image)?.then(t => { + this.messageService.insertMessage(t.value); + setTimeout(() => this.uploadService.scrollBottom(true), 0); + }); } URL.revokeObjectURL(urlString); }); @@ -437,7 +436,11 @@ export class TalkingComponent implements OnInit, OnDestroy { fileType = FileType.Video; } - this.uploadService.upload(t, this.messageService.conversation.id, this.messageService.conversation.aesKey, fileType); + this.uploadService.upload(t, this.messageService.conversation.id, this.messageService.conversation.aesKey, fileType) + ?.then(msg => { + this.messageService.insertMessage(msg.value); + setTimeout(() => this.uploadService.scrollBottom(true), 0); + }); }); this.removeDragData(event); } @@ -495,7 +498,7 @@ export class TalkingComponent implements OnInit, OnDestroy { showSearch: false }); this.picker.on('emoji', emoji => { - this.content = this.content ? this.content + emoji : emoji; + this.insertToSelection(emoji); }); } this.picker.togglePicker(chatBox); @@ -555,6 +558,13 @@ export class TalkingComponent implements OnInit, OnDestroy { } } + public insertToSelection(content: string) { + const input = document.getElementById('chatInput'); + this.content = this.content ? `${this.content.slice(0, input.selectionStart) + }${content}${this.content.slice(input.selectionStart)}` : content; + this.updateInputHeight(); + } + public getAudio(target: HTMLElement, filePath: string): void { target.style.display = 'none'; const audioElement = document.createElement('audio'); diff --git a/src/app/Models/Timers.ts b/src/app/Models/Timers.ts index 06157d7f..b37cb214 100644 --- a/src/app/Models/Timers.ts +++ b/src/app/Models/Timers.ts @@ -1,10 +1,11 @@ export enum Timers { '5s' = 5, - '30s' = 30, '1m' = 60, '10m' = 600, '1h' = 3600, '1d' = 3600 * 24, '1w' = 3600 * 24 * 7, + '1mo' = 3600 * 24 * 30, + '1y' = 3600 * 24 * 365, 'off' = Math.pow(2, 31) - 1 } diff --git a/src/app/Services/CacheService.ts b/src/app/Services/CacheService.ts index 01a77ddd..8c6d22a8 100644 --- a/src/app/Services/CacheService.ts +++ b/src/app/Services/CacheService.ts @@ -7,6 +7,7 @@ import { DevicesApiService } from './DevicesApiService'; import { ConversationApiService } from './ConversationApiService'; import { ProbeService } from './ProbeService'; import { PushSubscriptionSetting } from '../Models/PushSubscriptionSetting'; +import { ThemeService } from './ThemeService'; @Injectable() export class CacheService { @@ -19,7 +20,9 @@ export class CacheService { private devicesApiService: DevicesApiService, private conversationApiService: ConversationApiService, private probeService: ProbeService, - ) { } + private themeService: ThemeService, + ) { + } public reset() { this.cachedData = new CacheModel(); @@ -129,6 +132,15 @@ export class CacheService { } }); this.cachedData.devices = response.items; + // should check if current device id has already been invalid + if (localStorage.getItem('setting-pushSubscription')) { + const val = JSON.parse(localStorage.getItem('setting-pushSubscription')) as PushSubscriptionSetting; + if (val.deviceId && !this.cachedData.devices.find(t => t.id === val.deviceId)) { + // invalid id, remove it + val.deviceId = null; + localStorage.setItem('setting-pushSubscription', JSON.stringify(val)); + } + } this.saveCache(); }); } @@ -155,6 +167,7 @@ export class CacheService { public updateTotalUnread(): void { this.totalUnread = this.cachedData.conversations .filter(item => !item.muted).map(item => item.unReadAmount).reduce((a, b) => a + b, 0); + this.themeService.NotifyIcon = this.totalUnread; } public initCache(): void { diff --git a/src/app/Services/InitService.ts b/src/app/Services/InitService.ts index 8639b6cd..f10b53e8 100644 --- a/src/app/Services/InitService.ts +++ b/src/app/Services/InitService.ts @@ -224,11 +224,9 @@ export class InitService { 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)); - } + this.devicesApiService.DropDevice(data.deviceId).subscribe(_t => { + data.deviceId = 0; + localStorage.setItem('setting-pushSubscription', JSON.stringify(data)); }); } if (data.enabled) { diff --git a/src/app/Services/MessageService.ts b/src/app/Services/MessageService.ts index d02580fe..f97706af 100644 --- a/src/app/Services/MessageService.ts +++ b/src/app/Services/MessageService.ts @@ -628,4 +628,15 @@ export class MessageService { this.fileAccessToken = encodeURIComponent(t.value); }); } + + public insertMessage(p: Message) { + if (this.rawMessages.find(t => t.id === p.id)) { + return; + } + this.rawMessages.push(p); + this.localMessages.push(this.modifyMessage(Object.assign({}, p))); + this.reorderLocalMessages(); + this.updateAtLink(); + this.saveMessage(); + } } diff --git a/src/app/Services/ThemeService.ts b/src/app/Services/ThemeService.ts index e48c234a..379d8d61 100644 --- a/src/app/Services/ThemeService.ts +++ b/src/app/Services/ThemeService.ts @@ -14,6 +14,7 @@ export class ThemeService { } public mediaListener: MediaQueryList; + public readonly TITLE = 'Kahla - Aiursoft'; ApplyThemeFromRemote(remoteInfo: KahlaUser) { if (this.LocalThemeSetting !== remoteInfo.themeId) { @@ -132,4 +133,16 @@ export class ThemeService { localStorage.setItem('setting-theme', theme.toString()); } + public set NotifyIcon(value: number) { + if (value !== 0) { + document.title = `(${value}) ${this.TITLE}`; + document.querySelector('link[rel=icon]') + .setAttribute('href', 'favicon_notify.ico'); + } else { + document.title = this.TITLE; + document.querySelector('link[rel=icon]') + .setAttribute('href', 'favicon.ico'); + } + } + } diff --git a/src/app/Services/TimerService.ts b/src/app/Services/TimerService.ts index 67133315..e250824e 100644 --- a/src/app/Services/TimerService.ts +++ b/src/app/Services/TimerService.ts @@ -19,12 +19,13 @@ export class TimerService { input: 'select', inputOptions: { 5: '5 seconds', - 30: '30 seconds', 60: '1 minute', 600: '10 minutes', 3600: '1 hour', [3600 * 24]: '1 day', [3600 * 24 * 7]: '1 week', + [3600 * 24 * 30]: '1 month', + [3600 * 24 * 365]: '1 year', [Math.pow(2, 31) - 1]: 'off' }, inputPlaceholder: 'Select one', diff --git a/src/app/Services/UploadService.ts b/src/app/Services/UploadService.ts index 3017939e..daa55668 100644 --- a/src/app/Services/UploadService.ts +++ b/src/app/Services/UploadService.ts @@ -11,6 +11,8 @@ import { FileType } from '../Models/FileType'; import { ProbeService } from './ProbeService'; import { uuid4 } from '../Helpers/Uuid'; import { MessageFileRef } from '../Models/MessageFileRef'; +import { AiurValue } from '../Models/AiurValue'; +import { Message } from '../Models/Message'; @Injectable({ providedIn: 'root' @@ -25,7 +27,7 @@ export class UploadService { ) { } - public upload(file: File, conversationID: number, aesKey: string, fileType: FileType): void { + public upload(file: File, conversationID: number, aesKey: string, fileType: FileType): Promise> { if (!this.validateFileSize(file)) { Swal.fire('Error', 'File size should larger than or equal to one bit and less then or equal to 2047MB.', 'error'); return; @@ -67,30 +69,36 @@ export class UploadService { }); } else { const alert = this.fireUploadingAlert(`Uploading your ${fileType === FileType.Image ? 'image' : (fileType === FileType.Video ? 'video' : 'file')}...`); - - this.filesApiService.InitFileAccess(conversationID, true).subscribe(response => { - if (response.code === 0) { - const mission = this.filesApiService.UploadFile(formData, response.uploadAddress).subscribe(res => { - if (Number(res)) { - this.updateAlertProgress(Number(res)); - } else if (res) { - Swal.close(); - this.buildFileRef(res, fileType, file)?.then(t => { - this.encryptThenSend(t, conversationID, aesKey).then(() => { - this.scrollBottom(true); + return new Promise>((resolve, reject) => { + this.filesApiService.InitFileAccess(conversationID, true).subscribe(response => { + if (response.code === 0) { + const mission = this.filesApiService.UploadFile(formData, response.uploadAddress).subscribe(res => { + if (Number(res)) { + this.updateAlertProgress(Number(res)); + } else if (res) { + Swal.close(); + this.buildFileRef(res, fileType, file)?.then(t => { + this.encryptThenSend(t, conversationID, aesKey).then((t_) => { + this.scrollBottom(true); + resolve(t_); + }); }); - }); - } - }, () => { - Swal.close(); - Swal.fire('Error', 'Upload failed', 'error'); - }); - alert.then(result => { - if (result.dismiss) { - mission.unsubscribe(); - } - }); - } + } + }, () => { + Swal.close(); + Swal.fire('Error', 'Upload failed', 'error'); + reject(); + }); + alert.then(result => { + if (result.dismiss) { + mission.unsubscribe(); + reject(); + } + }); + } else { + reject(); + } + }); }); } } @@ -113,7 +121,7 @@ export class UploadService { (Swal.getContent().querySelector('#progressText')).innerText = `${progress}%`; } - public encryptThenSend(fileRef: MessageFileRef, conversationID: number, aesKey: string): Promise { + public encryptThenSend(fileRef: MessageFileRef, conversationID: number, aesKey: string): Promise> { if (!fileRef) { return null; } @@ -133,11 +141,11 @@ export class UploadService { } } - private sendMessage(message: string, conversationID: number, aesKey: string): Promise { - return new Promise((resolve, reject) => { + private sendMessage(message: string, conversationID: number, aesKey: string): Promise> { + return new Promise((resolve, reject) => { this.conversationApiService.SendMessage(conversationID, AES.encrypt(message, aesKey).toString(), uuid4(), []) - .subscribe(() => { - resolve(); + .subscribe((t) => { + resolve(t); }, () => { Swal.fire('Send Failed.', 'Please check your network connection.', 'error'); reject(); @@ -155,9 +163,7 @@ export class UploadService { public scrollBottom(smooth: boolean): void { if (!this.talkingDestroyed) { const h = document.documentElement.scrollHeight; - if (document.querySelector('.message-list').scrollHeight < window.innerHeight - 50) { - window.scroll(0, 0); - } else if (smooth) { + if (smooth) { window.scroll({top: h, behavior: 'smooth'}); } else { window.scroll(0, h); diff --git a/src/app/Views/talking.html b/src/app/Views/talking.html index 44d6091c..44aba2bf 100644 --- a/src/app/Views/talking.html +++ b/src/app/Views/talking.html @@ -1,6 +1,6 @@  -