From 5efa9d7cfd93d4fff0eef30d9c9ce372d524da3f Mon Sep 17 00:00:00 2001 From: Mal <=> Date: Sat, 27 Mar 2021 16:23:55 +0100 Subject: [PATCH] Version 1.6.0: User online list implemented --- config.xml | 2 +- src/app/app.config.ts | 2 +- src/app/chat/chat.component.html | 11 +- src/app/chat/chat.component.ts | 140 ++++++++++++++++++++ src/app/login/login.component.ts | 7 +- src/app/socket.user.disconnected.message.ts | 4 + src/app/user.ts | 5 + src/app/userlist/userlist.component.html | 3 + src/app/userlist/userlist.component.scss | 0 src/app/userlist/userlist.component.spec.ts | 24 ++++ src/app/userlist/userlist.component.ts | 14 ++ src/app/websocket.listener.ts | 7 + src/app/websocket.service.ts | 24 +++- src/global.scss | 117 +++++++++++++++- 14 files changed, 348 insertions(+), 12 deletions(-) create mode 100644 src/app/socket.user.disconnected.message.ts create mode 100644 src/app/user.ts create mode 100644 src/app/userlist/userlist.component.html create mode 100644 src/app/userlist/userlist.component.scss create mode 100644 src/app/userlist/userlist.component.spec.ts create mode 100644 src/app/userlist/userlist.component.ts diff --git a/config.xml b/config.xml index 7824d23..36663bc 100644 --- a/config.xml +++ b/config.xml @@ -1,5 +1,5 @@ - + METAsocket WowApp's awesome instant messenger for the whole Greifentanzgeschwader sabolli diff --git a/src/app/app.config.ts b/src/app/app.config.ts index d30c8f7..1489efb 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -1,4 +1,4 @@ export default class AppConfig { - public static readonly VERSION: string = '1.5.0'; + public static readonly VERSION: string = '1.6.0'; } diff --git a/src/app/chat/chat.component.html b/src/app/chat/chat.component.html index 33a87bb..4af77df 100644 --- a/src/app/chat/chat.component.html +++ b/src/app/chat/chat.component.html @@ -1,4 +1,4 @@ -
+
Konnte keine Verbindung herstellen!
@@ -19,3 +19,12 @@
+ + diff --git a/src/app/chat/chat.component.ts b/src/app/chat/chat.component.ts index 24a58a2..c5274f0 100644 --- a/src/app/chat/chat.component.ts +++ b/src/app/chat/chat.component.ts @@ -15,6 +15,7 @@ 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; @@ -24,12 +25,17 @@ const {App} = Plugins; 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('chatPostArea') chatPostArea: ElementRef; @ViewChild('errorMessage') errorMessage: ElementRef; @ViewChild('chatTextArea') chatTextArea: ElementRef; @@ -42,6 +48,9 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W private hasBeenReloaded = false; private hasFocus = true; private isReconnection = false; + private lastTouchX = null; + private lastTouchY = null; + private isSwiping = false; public constructor( private apiService: ApiService, @@ -60,6 +69,11 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W 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); } @@ -230,6 +244,60 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W this.isReconnection = true; } + onChatTouch(event: TouchEvent): void + { + if (this.isSwiping) { + return; + } + + if (this.getTouchEvent(event) === -1) { + event.preventDefault(); + + this.chat.nativeElement.id = 'chat-swiped-out'; + this.sidebar.nativeElement.id = 'sidebar-swiped-in'; + + this.isSwiping = true; + + setTimeout( + () => { + this.isSwiping = false; + }, + 500 + ); + } + } + + onSidebarTouch(event: TouchEvent): void + { + if (this.isSwiping) { + return; + } + + if (this.getTouchEvent(event) === 1) { + this.chat.nativeElement.id = 'chat-swiped-in'; + this.sidebar.nativeElement.id = 'sidebar-swiped-out'; + + this.isSwiping = true; + + setTimeout( + () => { + this.isSwiping = false; + }, + 500 + ); + } + } + + onTouchStart(event: TouchEvent): void + { + } + + onTouchEnd(): void + { + this.lastTouchX = null; + this.lastTouchY = null; + } + onError(message: string): void { this.errorMessage.nativeElement.style.display = 'block'; @@ -237,6 +305,38 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W 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); @@ -317,6 +417,26 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W ); } + 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) { @@ -410,4 +530,24 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W this.buttonSend.nativeElement.classList.add('button-send-offline'); this.buttonSend.nativeElement.disabled = true; } + + 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; + } } diff --git a/src/app/login/login.component.ts b/src/app/login/login.component.ts index c64021f..a98e9e7 100644 --- a/src/app/login/login.component.ts +++ b/src/app/login/login.component.ts @@ -40,7 +40,8 @@ export class LoginComponent implements OnInit { login(event): void { event.target.disabled = true; - event.target.style.visibility = 'hidden'; + event.target.classList.add('submit-progress'); + this.error = null; this.apiService.getAuthToken(this.username, this.password).toPromise() .then( @@ -62,7 +63,7 @@ export class LoginComponent implements OnInit { this.error = error.name + ': ' + error.message; event.target.disabled = false; - event.target.style.visibility = 'visible'; + event.target.classList.remove('submit-progress'); } ); } @@ -94,7 +95,7 @@ export class LoginComponent implements OnInit { this.password = ''; event.target.disabled = false; - event.target.style.visibility = 'visible'; + event.target.classList.remove('submit-progress'); } ); } diff --git a/src/app/socket.user.disconnected.message.ts b/src/app/socket.user.disconnected.message.ts new file mode 100644 index 0000000..bd09dc0 --- /dev/null +++ b/src/app/socket.user.disconnected.message.ts @@ -0,0 +1,4 @@ +export interface SocketUserDisconnectedMessage { + type: number; + userId: number; +} diff --git a/src/app/user.ts b/src/app/user.ts new file mode 100644 index 0000000..aef5737 --- /dev/null +++ b/src/app/user.ts @@ -0,0 +1,5 @@ +export interface User +{ + userId: number; + username: string; +} diff --git a/src/app/userlist/userlist.component.html b/src/app/userlist/userlist.component.html new file mode 100644 index 0000000..0d27b40 --- /dev/null +++ b/src/app/userlist/userlist.component.html @@ -0,0 +1,3 @@ +

+ userlist works! +

diff --git a/src/app/userlist/userlist.component.scss b/src/app/userlist/userlist.component.scss new file mode 100644 index 0000000..e69de29 diff --git a/src/app/userlist/userlist.component.spec.ts b/src/app/userlist/userlist.component.spec.ts new file mode 100644 index 0000000..f49af23 --- /dev/null +++ b/src/app/userlist/userlist.component.spec.ts @@ -0,0 +1,24 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { IonicModule } from '@ionic/angular'; + +import { UserlistComponent } from './userlist.component'; + +describe('UserlistComponent', () => { + let component: UserlistComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + declarations: [ UserlistComponent ], + imports: [IonicModule.forRoot()] + }).compileComponents(); + + fixture = TestBed.createComponent(UserlistComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/userlist/userlist.component.ts b/src/app/userlist/userlist.component.ts new file mode 100644 index 0000000..c0d73c2 --- /dev/null +++ b/src/app/userlist/userlist.component.ts @@ -0,0 +1,14 @@ +import { Component, OnInit } from '@angular/core'; + +@Component({ + selector: 'app-userlist', + templateUrl: './userlist.component.html', + styleUrls: ['./userlist.component.scss'], +}) +export class UserlistComponent implements OnInit { + + constructor() { } + + ngOnInit() {} + +} diff --git a/src/app/websocket.listener.ts b/src/app/websocket.listener.ts index 39abb2d..0ca38fc 100644 --- a/src/app/websocket.listener.ts +++ b/src/app/websocket.listener.ts @@ -1,4 +1,5 @@ import {ChatMessage} from './chat.message'; +import {User} from './user'; export interface WebsocketListener { @@ -10,5 +11,11 @@ export interface WebsocketListener onReconnect(): void; + onUserListInit(userList: User[]): void; + + onUserConnected(user: User): void; + + onUserDisconnected(userId: number): void; + onError(message: string): void; } diff --git a/src/app/websocket.service.ts b/src/app/websocket.service.ts index ae88806..95dca34 100644 --- a/src/app/websocket.service.ts +++ b/src/app/websocket.service.ts @@ -6,6 +6,7 @@ import {SocketReceivedChatMessage} from './socketReceivedChatMessage'; import {WebsocketListener} from './websocket.listener'; import {SocketSendMessage} from './socket.send.message'; import {SocketKeepaliveMessage} from './socket.keepalive.message'; +import {User} from './user'; @Injectable({ providedIn: 'root' @@ -30,7 +31,8 @@ export class WebsocketService { destroy(): void { - this.socket = null; + this.isReconnectDesired = false; + this.socket.close(); } setListener(listener: WebsocketListener): void { @@ -84,14 +86,31 @@ export class WebsocketService { case Response.REGISTRATION_MESSAGE: this.userList.set(response.userId, response.username); + this.listener.onUserConnected({userId: response.userId, username: response.username}); break; case Response.USERLIST: + const userList: User[] = []; + response.users.forEach( (user) => { this.userList.set(user.userId, user.username); + + userList.push( + { + userId: user.userId, + username: user.username, + } + ); } ); + + this.listener.onUserListInit(userList); + + break; + + case Response.USER_DISCONNECTED: + this.listener.onUserDisconnected(response.userId); break; default: @@ -157,5 +176,6 @@ enum Response { CHAT_MESSAGE = 1, REGISTRATION_MESSAGE, USERLIST, - KEEP_ALIVE + KEEP_ALIVE, + USER_DISCONNECTED = 5, } diff --git a/src/global.scss b/src/global.scss index c6c65cc..f2ed15f 100644 --- a/src/global.scss +++ b/src/global.scss @@ -8,10 +8,36 @@ to { background-color: rgba(0, 0, 0, 0.5) } } +@keyframes chat-swipe-in { + from {left: -100%} + to {left: 0} +} + +@keyframes chat-swipe-out { + from {left: 0} + to {left: -100%} +} + +@keyframes sidebar-swipe-in { + from {left: 100%} + to {left: 0} +} + +@keyframes sidebar-swipe-out { + from {left: 0} + to {left: 100%} +} + @keyframes fullscreen-notification-window-entrance { from { transform: scale(0)} } +@keyframes progress { + 0% {opacity: 0} + 50% {opacity: 1.0} + 100% {opacity: 0} +} + * { box-sizing: border-box; font-family: sans-serif; @@ -22,7 +48,7 @@ html, body { background-color: #251a25; } -#chat { +#chat, #chat-swiped-in, #chat-swiped-out { background: rgb(27, 47, 114) url("assets/graphics/bg_responsive.jpg") repeat; margin: 0; font-size: 16px; @@ -30,13 +56,13 @@ html, body { position: fixed; top: 0; left: 0; - right: 0; + right: 300px; bottom: 0; color: white; } #chat-post-area { - position: fixed; + position: absolute; top: 0; left: 0; right: 0; @@ -47,7 +73,7 @@ html, body { } #chat-type-area { - position: fixed; + position: absolute; height: 50px; background-color: #cccccc; left: 0; @@ -172,6 +198,12 @@ html, body { font-weight: bold; } +.submit-progress { + animation-name: progress; + animation-duration: 2s; + animation-iteration-count: infinite; +} + .app-version { color: white; margin-top: 50px; @@ -322,9 +354,86 @@ a.notification-button { margin-top: 5px; } +#sidebar, #sidebar-swiped-in, #sidebar-swiped-out { + position: fixed; + width: 300px; + top: 0; + right: 0; + bottom: 0; + background-color: #cccccc; + border: 2px grey solid; + border-top: 2px solid white; + border-left: 2px solid white; + padding: 20px; +} + +#userlist { + width: 100%; + height: 100%; + flex-flow: column wrap; + border: 2px grey solid; + border-bottom: 2px solid white; + border-right: 2px solid white; + background-color: rgb(26, 37, 26); + color: white; + overflow: auto; +} + +.user { + display: block; + margin: 10px; +} + +.userlist-element { + display: table-cell; + vertical-align: middle; +} + +.userlist-avatar { + width: 48px; + height: 48px; + border-radius: 24px; +} + @media only screen and (max-height: 500px) { #login { position: static; display: block; } } + +@media only screen and (max-width: 800px) { + #sidebar, #sidebar-swiped-in, #sidebar-swiped-out { + width: 100%; + height: 100%; + left: 100%; + animation-duration: 0.5s; + } + + #sidebar-swiped-in { + left: 0; + animation-name: sidebar-swipe-in; + } + + #sidebar-swiped-out { + animation-name: sidebar-swipe-out + } + + #chat, #chat-swiped-in, #chat-swiped-out { + width: 100%; + top: 0; + bottom: 0; + right: 0; + animation-duration: 0.5s; + } + + #chat-swiped-in { + animation-name: chat-swipe-in; + } + + #chat-swiped-out { + overflow: hidden; + animation-name: chat-swipe-out; + left: -100%; + } +}