Extendable type area added and missed messages fixed

This commit is contained in:
Mal 2021-03-10 22:54:27 +01:00
parent bed3c705cb
commit 1e333b7412
6 changed files with 214 additions and 43 deletions

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?>
<widget id="io.ionic.starter" version="1.3.0" xmlns="http://www.w3.org/ns/widgets" xmlns:cdv="http://cordova.apache.org/ns/1.0">
<widget id="io.ionic.starter" version="1.4.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</description>
<description>WowApp's awesome instant messenger for the whole Greifentanzgeschwader</description>
<author email="webmaster@sabolli.de" href="https://sabolli.de/metasocket/">sabolli</author>
<content src="index.html" />
<access origin="*" />

View File

@ -5,4 +5,5 @@ export interface ChatMessage
username: string;
message: string;
datetime: string;
htmlMessage: string;
}

View File

@ -3,16 +3,19 @@
<div #chatPostArea id="chat-post-area" (scroll)="onScroll()">
<div *ngFor="let message of messages" class="chat-post" [class.chat-own-post]="userId === message.userId">
<img class="chat-avatar" src="{{url}}/user/{{message.userId}}/avatar?token={{userToken}}">
<div class="chat-message-area">
<img (click)="citeUsername(message.username)" class="chat-avatar" src="{{url}}/user/{{message.userId}}/avatar?token={{userToken}}">
<div class="chat-message-area" (click)="citeMessage(message.username, message.message)">
<div class="chat-username">{{message.username}}</div>
<div class="chat-post-message">{{message.message}}</div>
<div [innerHTML]="message.htmlMessage" class="chat-post-message"></div>
<div class="chat-datetime">{{message.datetime}}</div>
</div>
</div>
</div>
<div id="chat-type-area">
<textarea [(ngModel)]="chatText" id="chat-textarea" (keydown)="onTextInput($event)" autofocus maxlength="2000"></textarea>
<div #chatTypeArea id="chat-type-area">
<div id="textarea-container">
<textarea #chatTextArea [(ngModel)]="chatText" (input)="onInput($event)" id="chat-textarea" autofocus maxlength="2000"></textarea>
</div>
<button #buttonSend class="button-send-offline" (click)="postMessage()" id="button-send"></button>
</div>
</div>

View File

@ -22,10 +22,13 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
userToken: string;
userId: number;
url: string;
chatText: string;
@ViewChild('chatPostArea') chatPostArea: ElementRef;
@ViewChild('errorMessage') errorMessage: ElementRef;
chatText: string;
@ViewChild('chatTextArea') chatTextArea: ElementRef;
@ViewChild('chatTypeArea') chatTypeArea: ElementRef;
@ViewChild('buttonSend') buttonSend: ElementRef;
private oldScrollHeight = 0;
private messageOffset = 0;
@ -82,6 +85,8 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
(response) => {
response.forEach(
(message: ChatMessage) => {
message.htmlMessage = this.parseHtml(message.message);
this.messages.push(message);
this.messageOffset++;
}
@ -116,35 +121,39 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
if (this.chatPostArea.nativeElement.scrollTop === 0) {
this.apiService.getChatHistory(this.userToken, this.messageOffset, this.messageLimit).subscribe(
(response) => {
this.messages = response.concat(this.messages);
this.messageOffset += this.messageLimit;
response.reverse();
response.forEach(
(message: ChatMessage) => {
message.htmlMessage = this.parseHtml(message.message);
this.messages = [message].concat(this.messages);
this.messageOffset++;
}
);
this.hasBeenReloaded = true;
}
);
}
}
onTextInput(event: Event): void {
if (!(event instanceof KeyboardEvent)) {
return;
onInput(event): void {
this.chatTypeArea.nativeElement.style.height = '50px';
this.chatTypeArea.nativeElement.style.height = event.target.scrollHeight > 50 ? event.target.scrollHeight + 'px' : '50px';
if (this.chatTypeArea.nativeElement.clientHeight > window.innerHeight) {
this.chatTypeArea.nativeElement.style.height = window.innerHeight + 'px';
}
}
switch (event.key) {
case 'Enter':
event.preventDefault();
if (this.chatText.trim() === '') {
return;
}
this.websocketService.sendChatMessage(this.chatText);
this.chatText = '';
postMessage(): void {
if (this.chatTextArea.nativeElement.value.trim() === '') {
return;
}
default:
return;
}
this.websocketService.sendChatMessage(this.chatTextArea.nativeElement.value);
this.chatTextArea.nativeElement.value = '';
}
onLogout() {
@ -154,10 +163,12 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
}
onChatMessage(message: ChatMessage): void {
message.htmlMessage = this.parseHtml(message.message);
this.messages.push(message);
this.messageOffset++;
if (message.userId === this.userId) {
if (this.hasFocus || message.userId === this.userId) {
return;
}
@ -168,21 +179,29 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
{
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.enableSendButton();
this.messages.concat(messagesMissed);
this.messageOffset += messagesMissed.length;
}
).catch(
(error) => {
console.log('Failed to load messages missed after reconnect!', error);
}
);
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;
}
@ -196,6 +215,27 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
{
this.errorMessage.nativeElement.style.display = 'block';
this.errorMessage.nativeElement.innerText = message;
this.disableSendButton();
}
citeUsername(username: string): void
{
this.insertIntoTextareaCursorPosition('@' + username);
}
citeMessage(username: string, message: string): void
{
const originalMessage = [];
message.split('\n').forEach(
(paragraph: string) => {
if (paragraph.substr(0, 1) !== '>') {
originalMessage.push(paragraph);
}
}
);
this.insertIntoTextareaCursorPosition('> @' + username + ': ' + originalMessage.join('\n') + '\n');
}
triggerNotification(message: ChatMessage): void
@ -228,4 +268,80 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
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(/(\s|^)@\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): void
{
const textarea = this.chatTextArea.nativeElement;
const textLength = textarea.value.length;
const messagePartA = textarea.value.substr(0, textarea.selectionStart) + (textarea.selectionStart > 0 ? '\n' : '');
const messagePartB = textarea.value.substr(textarea.selectionStart, textLength);
textarea.value = messagePartA.trim() + text + messagePartB.trim();
textarea.focus();
}
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;
}
}

View File

@ -69,7 +69,8 @@ export class WebsocketService {
userId: messageReceived.userId,
username: this.userList.get(messageReceived.userId),
datetime: messageReceived.datetime,
message: messageReceived.message
message: messageReceived.message,
htmlMessage: '',
};
this.listener.onChatMessage(message);

View File

@ -40,12 +40,22 @@ html, body {
#chat-type-area {
position: fixed;
height: 50px;
background-color: #cccccc;
left: 0;
right: 0;
bottom: 0;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.9);
}
#textarea-container {
position: absolute;
left: 0;
top: 0;
right: 50px;
bottom: 0;
height: 100%;
}
#chat-textarea {
width: 100%;
height: 100%;
@ -54,10 +64,13 @@ html, body {
font-size: 16px;
color: white;
border: 2px grey solid;
border-bottom: 2px solid white;
border-right: 2px solid white;
box-sizing: border-box;
padding: 5px;
resize: none;
outline: none;
overflow: hidden;
}
.chat-post {
@ -196,3 +209,40 @@ html, body {
display: none;
z-index: 10;
}
.message-cite {
padding: 5px;
background-color: green;
border-left: 8px solid #005500;
font-style: italic;
margin: 5px 0;
}
.message-cite-username {
font-style: italic;
font-weight: bold;
}
#button-send {
position: absolute;
right: 0;
width: 50px;
height: 50px;
top: 0;
background-image: url("assets/graphics/button_send.svg");
background-size: contain;
background-position: center center;
background-repeat: no-repeat;
border-radius: 0;
border: 2px solid white;
border-bottom: 2px solid grey;
border-right: 2px solid grey;
}
.button-send-online {
background-color: green;
}
.button-send-offline {
background-color: #550000;
}