metasocket-cordova/src/app/chat/chat.component.ts

566 lines
15 KiB
TypeScript

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 += '</div>';
}
parsedText += '<div class="message-cite">';
isInsideCite = true;
break;
case '\n':
parsedText += isInsideCite ? '</div>' : '<br>';
break;
default:
if (char === ' ' && isInsideUsername) {
isInsideUsername = false;
parsedText += '</span>';
}
parsedText += char;
}
}
if (isInsideCite) {
parsedText += '</div>';
}
const matches = parsedText.match(/(?!(\b))@\w+/g);
if (matches === null) {
return parsedText;
}
matches.forEach(
(match) => {
parsedText = parsedText.replace(match, '<span class="message-cite-username">' + match.substr(match.indexOf('@') + 1) + '</span>');
}
);
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;
}
}