566 lines
15 KiB
TypeScript
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;
|
|
}
|
|
}
|