Version 1.6.0: User online list implemented

This commit is contained in:
Mal 2021-03-27 16:23:55 +01:00
parent 00a9a8c67f
commit 5efa9d7cfd
14 changed files with 348 additions and 12 deletions

View File

@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="io.ionic.starter" version="1.5.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<widget id="io.ionic.starter" version="1.6.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<name>METAsocket</name>
<description>WowApp's awesome instant messenger for the whole Greifentanzgeschwader</description>
<author email="webmaster@sabolli.de" href="https://sabolli.de/metasocket/">sabolli</author>

View File

@ -1,4 +1,4 @@
export default class AppConfig
{
public static readonly VERSION: string = '1.5.0';
public static readonly VERSION: string = '1.6.0';
}

View File

@ -1,4 +1,4 @@
<div id="chat">
<div #chat id="chat" (touchmove)="onChatTouch($event)" (touchend)="onTouchEnd()">
<div #errorMessage id="error-message">Konnte keine Verbindung herstellen!</div>
<div #chatPostArea id="chat-post-area" (scroll)="onScroll()">
@ -19,3 +19,12 @@
<button #buttonSend class="button-send-offline" (click)="postMessage()" id="button-send"></button>
</div>
</div>
<div #sidebar id="sidebar" (touchmove)="onSidebarTouch($event)" (touchend)="onTouchEnd()">
<div id="userlist">
<div *ngFor="let user of userList" class="user">
<div class="userlist-element"><img src="{{url}}/user/{{user.userId}}/avatar?token={{userToken}}" class="chat-avatar userlist-avatar"></div>
<div class="userlist-element"><div class="">{{user.username}}</div></div>
</div>
</div>
</div>

View File

@ -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;
}
}

View File

@ -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');
}
);
}

View File

@ -0,0 +1,4 @@
export interface SocketUserDisconnectedMessage {
type: number;
userId: number;
}

5
src/app/user.ts Normal file
View File

@ -0,0 +1,5 @@
export interface User
{
userId: number;
username: string;
}

View File

@ -0,0 +1,3 @@
<p>
userlist works!
</p>

View File

View File

@ -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<UserlistComponent>;
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();
});
});

View File

@ -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() {}
}

View File

@ -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;
}

View File

@ -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,
}

View File

@ -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%;
}
}