From bed3c705cbaf5b29e1c5a5604cd7de0d6ee82703 Mon Sep 17 00:00:00 2001 From: Mal <=> Date: Sun, 7 Mar 2021 13:57:41 +0100 Subject: [PATCH] Approvement of reconnection. --- config.xml | 4 +- package-lock.json | 20 ++++ package.json | 7 +- src/app/api.service.ts | 17 +++- src/app/app.component.html | 2 +- src/app/app.module.ts | 4 +- src/app/chat.message.ts | 1 + src/app/chat/chat.component.html | 5 +- src/app/chat/chat.component.ts | 97 ++++++++++++++++--- src/app/host.ts | 5 - src/app/login/login.component.html | 4 +- src/app/login/login.component.ts | 12 ++- src/app/setting.ts | 6 ++ ...essage.ts => socketReceivedChatMessage.ts} | 3 +- src/app/websocket.listener.ts | 8 ++ src/app/websocket.service.ts | 89 +++++++++++++---- src/global.scss | 25 ++++- 17 files changed, 252 insertions(+), 57 deletions(-) delete mode 100644 src/app/host.ts create mode 100644 src/app/setting.ts rename src/app/{socket.received.message.ts => socketReceivedChatMessage.ts} (55%) diff --git a/config.xml b/config.xml index 3e6546d..56e1864 100644 --- a/config.xml +++ b/config.xml @@ -1,8 +1,8 @@ - + METAsocket WowApp's awesome instant messenger - Ionic Framework Team + sabolli diff --git a/package-lock.json b/package-lock.json index 4fcbe94..9198ab3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1581,6 +1581,14 @@ } } }, + "@ionic-native/app-minimize": { + "version": "5.31.1", + "resolved": "https://registry.npmjs.org/@ionic-native/app-minimize/-/app-minimize-5.31.1.tgz", + "integrity": "sha512-tDr8iLCPr+MK/MJVTgyqolt5iSSpHpFHm/VPfhi0lIBAvbIbF/pRmNiQFpNUdOVKcqDxGda8zVeKaDSbnTbzQg==", + "requires": { + "@types/cordova": "^0.0.34" + } + }, "@ionic-native/background-mode": { "version": "5.31.1", "resolved": "https://registry.npmjs.org/@ionic-native/background-mode/-/background-mode-5.31.1.tgz", @@ -4181,6 +4189,18 @@ } } }, + "cordova-plugin-app-exit": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/cordova-plugin-app-exit/-/cordova-plugin-app-exit-0.0.2.tgz", + "integrity": "sha512-2gnV5Y97JBrU2f5Y/n7RDIpsUP6O+dyn6duG7JcqYcarK9QYnKUsD2ETZ9EQ6oreM8/LH6ymyP06jJTjmnWQYQ==", + "dev": true + }, + "cordova-plugin-appminimize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cordova-plugin-appminimize/-/cordova-plugin-appminimize-1.0.1.tgz", + "integrity": "sha512-UJZ5g8iFBP42EplS0gKwAQhwr9cUfur95o6w+2NW21pjbgioj1RVZddngy7dO++ABDpkd4HMPYnJw7DqMp5rww==", + "dev": true + }, "cordova-plugin-background-mode": { "version": "0.7.3", "resolved": "https://registry.npmjs.org/cordova-plugin-background-mode/-/cordova-plugin-background-mode-0.7.3.tgz", diff --git a/package.json b/package.json index 22176bd..0811f9f 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "@angular/platform-browser-dynamic": "~11.2.0", "@angular/router": "~11.2.0", "@capacitor/core": "^2.4.6", + "@ionic-native/app-minimize": "^5.31.1", "@ionic-native/background-mode": "^5.31.1", "@ionic-native/core": "^5.31.1", "@ionic-native/foreground-service": "^5.31.1", @@ -41,6 +42,8 @@ "@types/node": "^12.11.1", "codelyzer": "^6.0.0", "cordova-android": "^9.0.0", + "cordova-plugin-app-exit": "0.0.2", + "cordova-plugin-appminimize": "^1.0.1", "cordova-plugin-background-mode": "^0.7.3", "cordova-plugin-badge": "^0.8.8", "cordova-plugin-device": "^2.0.3", @@ -77,7 +80,9 @@ }, "cordova-plugin-ionic-keyboard": {}, "cordova-plugin-background-mode": {}, - "cordova-plugin-foreground-service": {} + "cordova-plugin-foreground-service": {}, + "cordova-plugin-app-exit": {}, + "cordova-plugin-appminimize": {} }, "platforms": [ "android" diff --git a/src/app/api.service.ts b/src/app/api.service.ts index 7e6cee8..4dd5562 100644 --- a/src/app/api.service.ts +++ b/src/app/api.service.ts @@ -2,7 +2,7 @@ import {Injectable} from '@angular/core'; import {Observable} from 'rxjs'; import {HttpClient} from '@angular/common/http'; import {Token} from './token'; -import {Host} from './host'; +import {Setting} from './setting'; import {ChatMessage} from './chat.message'; import {ChatTokenResponse} from './chat.token'; @@ -25,19 +25,26 @@ export class ApiService { } getAuthToken(username: string, password: string): Observable { - return this.client.post(Host.URL + '/token', {username, password}); + return this.client.post(Setting.URL + '/token', {username, password}); } getChatToken(authToken: string): Observable { return this.client.get( - Host.URL + '/session/chat', + Setting.URL + '/session/chat', {headers: {Authorization: 'Bearer ' + authToken}} ); } getChatHistory(token: string, offset: number, limit: number): Observable { return this.client.get( - Host.URL + '/session/chat/history?limit=' + limit + '&offset=' + offset, + Setting.URL + '/session/chat/history?limit=' + limit + '&offset=' + offset + (Setting.DEBUG ? '&debug=true' : ''), + {headers: {Authorization: 'Bearer ' + token}} + ); + } + + getChatMessagesMissed(token: string, lastMessageId: number): Observable { + return this.client.get( + Setting.URL + '/session/chat/history/missed?lastMessageId=' + lastMessageId, {headers: {Authorization: 'Bearer ' + token}} ); } @@ -45,7 +52,7 @@ export class ApiService { deleteAuthToken(token: string): Observable { return this.client.delete( - Host.URL + '/token/' + token, + Setting.URL + '/token/' + token, {headers: {Authorization: 'Bearer ' + token}} ); } diff --git a/src/app/app.component.html b/src/app/app.component.html index 0b117df..38457b4 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -1,4 +1,4 @@ - + diff --git a/src/app/app.module.ts b/src/app/app.module.ts index be80f6c..be80cab 100644 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -14,14 +14,14 @@ import {LoginComponent} from './login/login.component'; import {TopbarComponent} from './topbar/topbar.component'; import {LocalNotifications} from '@ionic-native/local-notifications/ngx'; import {BackgroundMode} from '@ionic-native/background-mode/ngx'; -import {ForegroundService} from '@ionic-native/foreground-service/ngx'; +import {AppMinimize} from '@ionic-native/app-minimize/ngx'; @NgModule({ declarations: [AppComponent, ChatComponent, LoginComponent, TopbarComponent], entryComponents: [], imports: [BrowserModule, IonicModule.forRoot(), AppRoutingModule, FormsModule, HttpClientModule], providers: [ - { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, LocalNotifications, BackgroundMode, ForegroundService + { provide: RouteReuseStrategy, useClass: IonicRouteStrategy }, LocalNotifications, BackgroundMode, AppMinimize ], bootstrap: [AppComponent], }) diff --git a/src/app/chat.message.ts b/src/app/chat.message.ts index 69b362c..68433c0 100644 --- a/src/app/chat.message.ts +++ b/src/app/chat.message.ts @@ -1,5 +1,6 @@ export interface ChatMessage { + id: number; userId: number; username: string; message: string; diff --git a/src/app/chat/chat.component.html b/src/app/chat/chat.component.html index 6ea002a..54f1aba 100644 --- a/src/app/chat/chat.component.html +++ b/src/app/chat/chat.component.html @@ -1,5 +1,6 @@
- +
Konnte keine Verbindung herstellen!
+
@@ -12,6 +13,6 @@
- +
diff --git a/src/app/chat/chat.component.ts b/src/app/chat/chat.component.ts index f3ee026..fa12e7b 100644 --- a/src/app/chat/chat.component.ts +++ b/src/app/chat/chat.component.ts @@ -1,13 +1,14 @@ import {Plugins, AppState} from '@capacitor/core'; import {AfterViewChecked, AfterViewInit, Component, ElementRef, OnInit, ViewChild} from '@angular/core'; import {ChatMessage} from '../chat.message'; -import {Host} from '../host'; +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 {ForegroundService} from '@ionic-native/foreground-service/ngx'; +import {AppComponent} from '../app.component'; +import {Platform} from '@ionic/angular'; const {App} = Plugins; @@ -23,6 +24,7 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W url: string; @ViewChild('chatPostArea') chatPostArea: ElementRef; + @ViewChild('errorMessage') errorMessage: ElementRef; chatText: string; private oldScrollHeight = 0; @@ -30,17 +32,18 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W private messageLimit = 10; private hasBeenReloaded = false; private hasFocus = true; + private isReconnection = false; public constructor( private apiService: ApiService, private websocketService: WebsocketService, private localNotifications: LocalNotifications, private backgroundMode: BackgroundMode, - private foregroundService: ForegroundService + private platform: Platform ) { this.userToken = this.apiService.getFromStorage('token'); this.userId = Number(this.apiService.getFromStorage('userId')); - this.url = Host.URL; + this.url = Setting.URL; this.websocketService.setListener(this); this.websocketService.initializeSocket(this.apiService.getFromStorage('chatToken')); this.backgroundMode.disableBatteryOptimizations(); @@ -74,19 +77,34 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W this.localNotifications.requestPermission(); - this.foregroundService.start('METAsocket', 'The chat for WowApp', 'ic_stat_notification_icon_enabled'); - - this.apiService.getChatHistory(this.userToken, this.messageOffset, this.messageLimit).subscribe( - (response) => { - this.messages = response; - this.messageOffset += this.messageLimit; - } - ); + this.apiService.getChatHistory(this.userToken, this.messageOffset, this.messageLimit).toPromise() + .then( + (response) => { + response.forEach( + (message: ChatMessage) => { + this.messages.push(message); + this.messageOffset++; + } + ); + } + ).catch( + (error) => { + window.alert('Fehler ' + error.status + ': Verbindung zur Web-API gescheitert!'); + } + ); App.addListener('appStateChange', (state: AppState) => { this.hasFocus = state.isActive; }); + this.platform.backButton.subscribe( + () => { + if (window.confirm('Möchtest du wirklich ausloggen?')) { + this.onLogout(); + } + } + ); + setInterval( () => { this.websocketService.sendKeepAliveMessage(); @@ -129,6 +147,12 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W } } + onLogout() { + this.apiService.storeData('token', null); + + AppComponent.token = null; + } + onChatMessage(message: ChatMessage): void { this.messages.push(message); this.messageOffset++; @@ -140,6 +164,40 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W this.triggerNotification(message); } + onConnection(): void + { + this.errorMessage.nativeElement.style.display = 'none'; + + this.apiService.getChatMessagesMissed(this.userToken, this.messages[this.messages.length - 1].id).toPromise() + .then( + (messagesMissed) => { + if (messagesMissed.length === 0) { + return; + } + + this.messages.concat(messagesMissed); + this.messageOffset += messagesMissed.length; + } + ).catch( + (error) => { + console.log('Failed to load messages missed after reconnect!', error); + } + ); + + this.isReconnection = true; + } + + onReconnect(): void + { + this.isReconnection = true; + } + + onError(message: string): void + { + this.errorMessage.nativeElement.style.display = 'block'; + this.errorMessage.nativeElement.innerText = message; + } + triggerNotification(message: ChatMessage): void { this.localNotifications.schedule( @@ -150,13 +208,24 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W priority: 2, lockscreen: true, autoClear: true, - icon: Host.URL + '/user/' + message.userId + '/avatar?token=' + this.userToken, + icon: Setting.URL + '/user/' + message.userId + '/avatar?token=' + this.userToken, smallIcon: 'ic_stat_notification_icon_enabled', - led: {color: '#ff00ff', on: 500, off: 500}, + led: '#ff00ff', trigger: { at: new Date(new Date().getTime() + 1000) }, sound: 'file://assets/audio/murloc.wav', vibrate: true } ); } + + private hasChatMessage(message: ChatMessage): boolean + { + for (const messageStored of this.messages) { + if (messageStored.id === message.id) { + return true; + } + } + + return false; + } } diff --git a/src/app/host.ts b/src/app/host.ts deleted file mode 100644 index e0bb978..0000000 --- a/src/app/host.ts +++ /dev/null @@ -1,5 +0,0 @@ -export class Host -{ - public static readonly URL: string = 'https://sabolli.de/wow/api/v1'; - public static readonly WEBSOCKET: string = 'wss://sabolli.de/metasocket'; -} diff --git a/src/app/login/login.component.html b/src/app/login/login.component.html index 9b2927b..d5bd6aa 100644 --- a/src/app/login/login.component.html +++ b/src/app/login/login.component.html @@ -6,12 +6,12 @@
diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index c4e05ff..1957a47 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -1,5 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { ApiService } from '../api.service'; +import {Platform} from '@ionic/angular'; +import {AppMinimize} from '@ionic-native/app-minimize/ngx'; @Component({ selector: 'app-login', @@ -11,9 +13,15 @@ export class LoginComponent implements OnInit { password = ''; error: string = null; - constructor(private apiService: ApiService) { } + constructor(private apiService: ApiService, private platform: Platform, private appMinimize: AppMinimize) { } - ngOnInit(): void {} + ngOnInit(): void { + this.platform.backButton.subscribe( + (t) => { + this.appMinimize.minimize(); + } + ); + } login(event): void { diff --git a/src/app/setting.ts b/src/app/setting.ts new file mode 100644 index 0000000..d9e400a --- /dev/null +++ b/src/app/setting.ts @@ -0,0 +1,6 @@ +export class Setting +{ + public static readonly DEBUG: boolean = false; + public static readonly URL: string = 'https://sabolli.de/wow/api/v1'; + public static readonly WEBSOCKET: string = Setting.DEBUG ? 'wss://sabolli.de/metasocket-debug' : 'wss://sabolli.de/metasocket'; +} diff --git a/src/app/socket.received.message.ts b/src/app/socketReceivedChatMessage.ts similarity index 55% rename from src/app/socket.received.message.ts rename to src/app/socketReceivedChatMessage.ts index a33bc81..70799e8 100644 --- a/src/app/socket.received.message.ts +++ b/src/app/socketReceivedChatMessage.ts @@ -1,5 +1,6 @@ -export interface SocketReceivedMessage { +export interface SocketReceivedChatMessage { type: number; + id: number; userId: number; message: string; datetime: string; diff --git a/src/app/websocket.listener.ts b/src/app/websocket.listener.ts index f318a2b..39abb2d 100644 --- a/src/app/websocket.listener.ts +++ b/src/app/websocket.listener.ts @@ -3,4 +3,12 @@ import {ChatMessage} from './chat.message'; export interface WebsocketListener { onChatMessage(message: ChatMessage): void; + + onLogout(): void; + + onConnection(): void; + + onReconnect(): void; + + onError(message: string): void; } diff --git a/src/app/websocket.service.ts b/src/app/websocket.service.ts index 927ea46..514935c 100644 --- a/src/app/websocket.service.ts +++ b/src/app/websocket.service.ts @@ -1,8 +1,8 @@ import {Injectable} from '@angular/core'; -import {Host} from './host'; +import {Setting} from './setting'; import {ChatMessage} from './chat.message'; import {SocketRegistrationMessage} from './socket.registration.message'; -import {SocketReceivedMessage} from './socket.received.message'; +import {SocketReceivedChatMessage} from './socketReceivedChatMessage'; import {WebsocketListener} from './websocket.listener'; import {SocketSendMessage} from './socket.send.message'; import {SocketKeepaliveMessage} from './socket.keepalive.message'; @@ -11,28 +11,21 @@ import {SocketKeepaliveMessage} from './socket.keepalive.message'; providedIn: 'root' }) export class WebsocketService { - private socket: WebSocket = new WebSocket(Host.WEBSOCKET); + private socket: WebSocket = new WebSocket(Setting.WEBSOCKET); private userList: Map = new Map(); private listener: WebsocketListener; private chatToken: string; + private isReconnectDesired = true; + private lastReconnectAttempts: Date[] = []; initializeSocket(chatToken: string): void { this.chatToken = chatToken; - this.socket = new WebSocket(Host.WEBSOCKET); - this.socket.addEventListener('open', () => { - this.authorize(); - }); - this.socket.addEventListener('message', (transmission: MessageEvent) => { - this.handleIncomingTransmission(transmission); - }); - this.socket.addEventListener( - 'close', - () => { - this.initializeSocket(this.chatToken); - this.authorize(); - } - ); + this.socket = new WebSocket(Setting.WEBSOCKET); + this.socket.addEventListener('open', () => {this.authorize(); this.listener.onConnection(); }); + this.socket.addEventListener('message', (transmission: MessageEvent) => {this.onMessage(transmission); }); + this.socket.addEventListener('close', () => {this.onClose(); }); + this.socket.addEventListener('error', () => {this.onError(); }); } setListener(listener: WebsocketListener): void { @@ -48,6 +41,10 @@ export class WebsocketService { } sendKeepAliveMessage(): void { + if (this.socket === null) { + return; + } + const socketMessage: SocketKeepaliveMessage = { type: Response.KEEP_ALIVE }; @@ -60,14 +57,15 @@ export class WebsocketService { this.socket.send(JSON.stringify(message)); } - private handleIncomingTransmission(transmission: MessageEvent): void { + private onMessage(transmission: MessageEvent): void { const response = JSON.parse(transmission.data); switch (response.type) { case Response.CHAT_MESSAGE: - const messageReceived: SocketReceivedMessage = response; + const messageReceived: SocketReceivedChatMessage = response; const message: ChatMessage = { + id: messageReceived.id, userId: messageReceived.userId, username: this.userList.get(messageReceived.userId), datetime: messageReceived.datetime, @@ -94,6 +92,59 @@ export class WebsocketService { throw new Error('Unknown message type: ' + response.type); } } + + private reconnect(): void + { + this.initializeSocket(this.chatToken); + this.authorize(); + this.listener.onReconnect(); + } + + private needsAuthorizationForFurtherReconnectAttempts(): boolean { + if (this.lastReconnectAttempts.length < 3) { + return false; + } + + const now = new Date().getTime(); + let recentAttempts = 0; + + for (const attempt of this.lastReconnectAttempts) { + if (now - attempt.getTime() < 20000) { + recentAttempts++; + } + } + + return recentAttempts >= 3; + } + + private onError(): void { + if (this.needsAuthorizationForFurtherReconnectAttempts()) { + this.lastReconnectAttempts = []; + this.isReconnectDesired = false; + + setTimeout( + () => { + this.isReconnectDesired = true; + this.onClose(); + }, 10000 + ); + } + + if (this.isReconnectDesired) { + this.lastReconnectAttempts.push(new Date()); + } + + this.listener.onError('Die Verbindung konnte nicht hergestellt werden!'); + } + + private onClose(): void + { + this.listener.onError('Verbindung unterbrochen!'); + + if (this.isReconnectDesired) { + this.reconnect(); + } + } } enum Response { diff --git a/src/global.scss b/src/global.scss index 3116d92..fdcdc21 100644 --- a/src/global.scss +++ b/src/global.scss @@ -1,9 +1,18 @@ +@keyframes error-message-entrance { + from { top: -1000px} + to { top: 0} +} + * { box-sizing: border-box; font-family: sans-serif; outline: none; } +html, body { + background-color: #251a25; +} + #chat { background: rgb(27, 47, 114) url("assets/graphics/bg_responsive.jpg") repeat; margin: 0; @@ -19,7 +28,7 @@ #chat-post-area { position: fixed; - top: 40px; + top: 0; left: 0; right: 0; bottom: 50px; @@ -173,3 +182,17 @@ text-align: center; } +#error-message { + position: absolute; + top: 0; + background-color: #550000; + color: white; + left: 0; + right: 0; + padding: 10px; + box-shadow: 0 0 20px rgba(0, 0, 0, 0.9); + animation-name: error-message-entrance; + animation-duration: 1s; + display: none; + z-index: 10; +}