import {Plugins, AppState} from '@capacitor/core'; import {AfterViewChecked, AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import {ChatMessage} from '../chat.message'; import {Setting} from '../setting'; import {ApiService} from '../api.service'; import {WebsocketListener} from '../websocket.listener'; import {WebsocketService} from '../websocket.service'; import {LocalNotifications} from '@ionic-native/local-notifications/ngx'; import {BackgroundMode} from '@ionic-native/background-mode/ngx'; import {AppComponent} from '../app.component'; import {Platform} from '@ionic/angular'; import {VersionInfo} from '../version.info'; import FullscreenError from '../fullscreen.error'; import FullscreenNotification from '../fullscreen.notification'; import AppConfig from '../app.config'; import FullscreenDialog from '../fullscreen.dialog'; import AppStorage from '../app.storage'; import {User} from '../user'; const {App} = Plugins; @Component({ selector: 'app-chat', templateUrl: './chat.component.html', styleUrls: ['./chat.component.scss'], }) export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, WebsocketListener { public static readonly SWIPE_SENSITIVITY: number = 30; messages: ChatMessage[] = []; userToken: string; userId: number; url: string; chatText: string; userList: User[] = []; @ViewChild('chat') chat: ElementRef; @ViewChild('sidebar') sidebar: ElementRef; @ViewChild('sidebarBackground') sidebarBackground: ElementRef; @ViewChild('chatPostArea') chatPostArea: ElementRef; @ViewChild('errorMessage') errorMessage: ElementRef; @ViewChild('chatTextArea') chatTextArea: ElementRef; @ViewChild('chatTypeArea') chatTypeArea: ElementRef; @ViewChild('buttonSend') buttonSend: ElementRef; private oldScrollHeight = 0; private messageOffset = 0; private messageLimit = 10; private hasBeenReloaded = false; private hasFocus = true; private isReconnection = false; private lastTouchX = null; private lastTouchY = null; private isSwiping = false; public constructor( private apiService: ApiService, private websocketService: WebsocketService, private localNotifications: LocalNotifications, private backgroundMode: BackgroundMode, private platform: Platform ) { this.userToken = AppStorage.getToken(); this.userId = AppStorage.getUserId(); this.url = Setting.URL; this.websocketService.setListener(this); this.websocketService.initializeSocket(AppStorage.getChatToken()); this.backgroundMode.setDefaults({silent: true}); this.backgroundMode.enable(); this.backgroundMode.disableBatteryOptimizations(); this.backgroundMode.disableWebViewOptimizations(); } private static isHorizontalSwipe(deltaX: number, deltaY: number): boolean { return Math.abs(deltaX) / window.innerHeight > Math.abs(deltaY) / window.innerWidth; } ngAfterViewInit(): void { this.chatPostArea.nativeElement.scroll(0, this.chatPostArea.nativeElement.scrollHeight); } ngAfterViewChecked(): void { if (this.oldScrollHeight !== this.chatPostArea.nativeElement.scrollHeight) { const scrollTop = this.chatPostArea.nativeElement.scrollTop; const clientHeight = this.chatPostArea.nativeElement.clientHeight; if (this.hasBeenReloaded) { this.chatPostArea.nativeElement.scroll(0, this.chatPostArea.nativeElement.scrollHeight - this.oldScrollHeight); this.hasBeenReloaded = false; } else if (scrollTop + clientHeight > this.oldScrollHeight - 10) { this.chatPostArea.nativeElement.scroll(0, this.oldScrollHeight); } this.oldScrollHeight = this.chatPostArea.nativeElement.scrollHeight; } } ngOnInit(): void { if (this.userToken === null) { return; } this.localNotifications.requestPermission(); this.apiService.getChatHistory(this.userToken, this.messageOffset, this.messageLimit).toPromise() .then( (response) => { response.forEach( (message: ChatMessage) => { message.htmlMessage = this.parseHtml(message.message); this.messages.push(message); this.messageOffset++; } ); } ).catch( (error) => { const notification = new FullscreenError('Verbindung zur Web-API gescheitert!\n\nStatus-Code: ' + error.status); } ); App.addListener('appStateChange', (state: AppState) => { this.hasFocus = state.isActive; }); this.platform.backButton.subscribe( () => { const question = new FullscreenDialog('App beenden', 'Möchtest du wirklich ausloggen?'); question.addButton('Ja', () => {this.onLogout(); }); question.addButton('Nein'); } ); setInterval( () => { this.websocketService.sendKeepAliveMessage(); }, 1000 * 60 // every minute ); setTimeout( () => { this.checkVersion(); }, 5000 ); } onScroll(): void { if (this.chatPostArea.nativeElement.scrollTop === 0) { this.apiService.getChatHistory(this.userToken, this.messageOffset, this.messageLimit).subscribe( (response) => { response.reverse(); response.forEach( (message: ChatMessage) => { message.htmlMessage = this.parseHtml(message.message); this.messages = [message].concat(this.messages); this.messageOffset++; } ); this.hasBeenReloaded = true; } ); } } onInput(): void { this.chatTypeArea.nativeElement.style.height = '50px'; this.chatTypeArea.nativeElement.style.height = this.chatTextArea.nativeElement.scrollHeight > 50 ? this.chatTextArea.nativeElement.scrollHeight + 'px' : '50px'; if (this.chatTypeArea.nativeElement.clientHeight > window.innerHeight) { this.chatTypeArea.nativeElement.style.height = window.innerHeight + 'px'; } } postMessage(): void { if (this.chatTextArea.nativeElement.value.trim() === '') { return; } this.websocketService.sendChatMessage(this.chatTextArea.nativeElement.value); this.chatTextArea.nativeElement.value = ''; this.onInput(); } onLogout() { AppStorage.setToken(null); this.websocketService.destroy(); AppComponent.token = null; } onChatMessage(message: ChatMessage): void { message.htmlMessage = this.parseHtml(message.message); this.messages.push(message); this.messageOffset++; if (!this.backgroundMode.isActive() || message.userId === this.userId) { return; } this.triggerNotification(message); } onConnection(): void { this.errorMessage.nativeElement.style.display = 'none'; this.enableSendButton(); if (this.isReconnection) { this.apiService.getChatMessagesMissed(this.userToken, this.messages[this.messages.length - 1].id).toPromise() .then( (messagesMissed) => { if (messagesMissed.length === 0) { return; } for (const message of messagesMissed) { message.htmlMessage = this.parseHtml(message.message); this.messages.push(message); this.messageOffset++; } } ).catch( (error) => { console.log('Failed to load messages missed after reconnect!', error); } ); } this.isReconnection = true; } onReconnect(): void { this.isReconnection = true; } onChatTouch(event: TouchEvent): void { if (this.isSwiping) { return; } if (this.getTouchEvent(event) === -1) { event.preventDefault(); this.sidebar.nativeElement.id = 'sidebar-swiped-in'; this.sidebarBackground.nativeElement.id = 'sidebar-background-visible'; this.isSwiping = true; setTimeout( () => { this.isSwiping = false; }, 500 ); } } onSidebarTouch(event: TouchEvent): void { if (this.isSwiping) { return; } if (this.getTouchEvent(event) === 1) { this.pushAwaySidebar(); } } onTouchEnd(): void { this.lastTouchX = null; this.lastTouchY = null; } onError(message: string): void { this.errorMessage.nativeElement.style.display = 'block'; this.errorMessage.nativeElement.innerText = message; this.disableSendButton(); } onUserListInit(userList: User[]): void { userList.forEach( (user) => { if (user.userId !== this.userId && !this.hasUserListUser(user)) { this.userList.push(user); } } ); this.sortUserList(); } onUserConnected(user: User): void { if (!this.hasUserListUser(user)) { this.userList.push(user); this.sortUserList(); } } onUserDisconnected(userId: number): void { for (let u = 0; u < this.userList.length; u++) { if (this.userList[u].userId === userId) { this.userList = this.userList.slice(0, u).concat(this.userList.slice(u + 1)); return; } } } citeUsername(username: string): void { this.insertIntoTextareaCursorPosition('@' + username, false); } citeMessage(username: string, message: string): void { const originalMessage = []; message.split('\n').forEach( (paragraph: string) => { if (paragraph.substr(0, 1) !== '>') { originalMessage.push('> @' + username + ': ' + paragraph); } } ); this.insertIntoTextareaCursorPosition(originalMessage.join('\n') + '\n'); } triggerNotification(message: ChatMessage): void { this.localNotifications.schedule( { title: message.username, text: message.message, id: 1, priority: 2, lockscreen: true, autoClear: true, icon: Setting.URL + '/user/' + message.userId + '/avatar?token=' + this.userToken, smallIcon: 'ic_stat_notification_icon_enabled', led: '#ff00ff', trigger: { at: new Date(new Date().getTime() + 1000) }, sound: 'file://assets/audio/murloc.wav', vibrate: true } ); } private checkVersion(): void { const lastChecked = AppStorage.getLastVersionCheck(); const today = new Date(); if (lastChecked !== null) { const dateLastChecked = new Date(lastChecked); if (dateLastChecked.toDateString() === today.toDateString()) { return; } } AppStorage.setLastVersionCheck(today.getTime()); this.apiService.getCurrentVersion().toPromise() .then( (versionInfo: VersionInfo) => { const isUpdateNotificationDesired = AppStorage.isUpdateNotificationDesired(); if (isUpdateNotificationDesired && AppConfig.VERSION < versionInfo.currentVersionAndroid) { const notification = new FullscreenNotification('Neue Version', 'Eine neue Version von METAsocket ist verfügbar.'); const label = document.createElement('label'); const checkbox = document.createElement('input'); const text = document.createElement('span'); text.innerText = ' Nerv mich nie wieder damit'; checkbox.type = 'checkbox'; label.appendChild(checkbox); label.appendChild(text); label.onclick = () => { AppStorage.setIsUpdateNotificationDesired(!checkbox.checked); }; notification.addExtendedInputElement(label); notification.addUrlButton('Runterladen', 'https://sabolli.de/wow/metasocket/download/android'); } } ); setTimeout( () => { this.checkVersion(); }, 1000 * 60 * 60 * 24 ); } private getTouchEvent(event: TouchEvent): number { if (this.lastTouchX !== null && this.lastTouchY !== null) { const deltaX = event.changedTouches[0].screenX - this.lastTouchX; const deltaY = event.changedTouches[0].screenY - this.lastTouchY; this.lastTouchX = null; this.lastTouchY = null; if (ChatComponent.isHorizontalSwipe(deltaX, deltaY) && Math.abs(deltaX) >= ChatComponent.SWIPE_SENSITIVITY) { return deltaX >= 0 ? 1 : -1; } return 0; } this.lastTouchX = event.changedTouches[0].screenX; this.lastTouchY = event.changedTouches[0].screenY; } private hasChatMessage(message: ChatMessage): boolean { for (const messageStored of this.messages) { if (messageStored.id === message.id) { return true; } } return false; } private parseHtml(messageText: string): string { let parsedText = ''; let isInsideCite = false; let isInsideUsername = false; for (const char of messageText) { switch (char) { case '>': if (isInsideCite) { parsedText += ''; } parsedText += '
'; isInsideCite = true; break; case '\n': parsedText += isInsideCite ? '
' : '
'; break; default: if (char === ' ' && isInsideUsername) { isInsideUsername = false; parsedText += ''; } parsedText += char; } } if (isInsideCite) { parsedText += ''; } const matches = parsedText.match(/(?!(\b))@\w+/g); if (matches === null) { return parsedText; } matches.forEach( (match) => { parsedText = parsedText.replace(match, '' + match.substr(match.indexOf('@') + 1) + ''); } ); return parsedText; } private insertIntoTextareaCursorPosition(text: string, isCite: boolean = true): void { const textarea = this.chatTextArea.nativeElement; const selectionStartBuffer = textarea.selectionStart; const selectionEndBuffer = textarea.selectionEnd; const textLength = textarea.value.length; const messagePartA = textarea.value.substr(0, textarea.selectionStart) + (isCite && textarea.selectionStart > 0 ? '\n' : ''); const messagePartB = textarea.value.substr(textarea.selectionStart, textLength); textarea.value = isCite ? messagePartA + text + messagePartB : messagePartA + text + messagePartB.trim(); textarea.focus(); textarea.selectionStart = selectionStartBuffer + text.length; textarea.selectionEnd = selectionEndBuffer + text.length; this.onInput(); } private enableSendButton(): void { this.buttonSend.nativeElement.classList.add('button-send-online'); this.buttonSend.nativeElement.classList.remove('button-send-offline'); this.buttonSend.nativeElement.disabled = false; } private disableSendButton(): void { this.buttonSend.nativeElement.classList.remove('button-send-online'); this.buttonSend.nativeElement.classList.add('button-send-offline'); this.buttonSend.nativeElement.disabled = true; } pushAwaySidebar(): void { this.sidebar.nativeElement.id = 'sidebar-swiped-out'; this.sidebarBackground.nativeElement.classList.add('sidebar-fadeout'); this.isSwiping = true; setTimeout( () => { this.isSwiping = false; this.sidebarBackground.nativeElement.classList.remove('sidebar-fadeout'); this.sidebarBackground.nativeElement.id = 'sidebar-background'; }, 500 ); } private sortUserList(): void { this.userList.sort( (a: User, b: User) => { return a.username > b.username ? 1 : -1; } ); } private hasUserListUser(user: User): boolean { for (const u of this.userList) { if (user.userId === u.userId) { return true; } } return false; } }