Extendable type area added and missed messages fixed
This commit is contained in:
parent
bed3c705cb
commit
1e333b7412
@ -1,7 +1,7 @@
|
|||||||
<?xml version='1.0' encoding='utf-8'?>
|
<?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>
|
<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>
|
<author email="webmaster@sabolli.de" href="https://sabolli.de/metasocket/">sabolli</author>
|
||||||
<content src="index.html" />
|
<content src="index.html" />
|
||||||
<access origin="*" />
|
<access origin="*" />
|
||||||
|
@ -5,4 +5,5 @@ export interface ChatMessage
|
|||||||
username: string;
|
username: string;
|
||||||
message: string;
|
message: string;
|
||||||
datetime: string;
|
datetime: string;
|
||||||
|
htmlMessage: string;
|
||||||
}
|
}
|
||||||
|
@ -3,16 +3,19 @@
|
|||||||
|
|
||||||
<div #chatPostArea id="chat-post-area" (scroll)="onScroll()">
|
<div #chatPostArea id="chat-post-area" (scroll)="onScroll()">
|
||||||
<div *ngFor="let message of messages" class="chat-post" [class.chat-own-post]="userId === message.userId">
|
<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}}">
|
<img (click)="citeUsername(message.username)" class="chat-avatar" src="{{url}}/user/{{message.userId}}/avatar?token={{userToken}}">
|
||||||
<div class="chat-message-area">
|
<div class="chat-message-area" (click)="citeMessage(message.username, message.message)">
|
||||||
<div class="chat-username">{{message.username}}</div>
|
<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 class="chat-datetime">{{message.datetime}}</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="chat-type-area">
|
<div #chatTypeArea id="chat-type-area">
|
||||||
<textarea [(ngModel)]="chatText" id="chat-textarea" (keydown)="onTextInput($event)" autofocus maxlength="2000"></textarea>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -22,10 +22,13 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
|
|||||||
userToken: string;
|
userToken: string;
|
||||||
userId: number;
|
userId: number;
|
||||||
url: string;
|
url: string;
|
||||||
|
chatText: string;
|
||||||
|
|
||||||
@ViewChild('chatPostArea') chatPostArea: ElementRef;
|
@ViewChild('chatPostArea') chatPostArea: ElementRef;
|
||||||
@ViewChild('errorMessage') errorMessage: ElementRef;
|
@ViewChild('errorMessage') errorMessage: ElementRef;
|
||||||
chatText: string;
|
@ViewChild('chatTextArea') chatTextArea: ElementRef;
|
||||||
|
@ViewChild('chatTypeArea') chatTypeArea: ElementRef;
|
||||||
|
@ViewChild('buttonSend') buttonSend: ElementRef;
|
||||||
|
|
||||||
private oldScrollHeight = 0;
|
private oldScrollHeight = 0;
|
||||||
private messageOffset = 0;
|
private messageOffset = 0;
|
||||||
@ -82,6 +85,8 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
|
|||||||
(response) => {
|
(response) => {
|
||||||
response.forEach(
|
response.forEach(
|
||||||
(message: ChatMessage) => {
|
(message: ChatMessage) => {
|
||||||
|
message.htmlMessage = this.parseHtml(message.message);
|
||||||
|
|
||||||
this.messages.push(message);
|
this.messages.push(message);
|
||||||
this.messageOffset++;
|
this.messageOffset++;
|
||||||
}
|
}
|
||||||
@ -116,35 +121,39 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
|
|||||||
if (this.chatPostArea.nativeElement.scrollTop === 0) {
|
if (this.chatPostArea.nativeElement.scrollTop === 0) {
|
||||||
this.apiService.getChatHistory(this.userToken, this.messageOffset, this.messageLimit).subscribe(
|
this.apiService.getChatHistory(this.userToken, this.messageOffset, this.messageLimit).subscribe(
|
||||||
(response) => {
|
(response) => {
|
||||||
this.messages = response.concat(this.messages);
|
response.reverse();
|
||||||
this.messageOffset += this.messageLimit;
|
|
||||||
|
response.forEach(
|
||||||
|
(message: ChatMessage) => {
|
||||||
|
message.htmlMessage = this.parseHtml(message.message);
|
||||||
|
|
||||||
|
this.messages = [message].concat(this.messages);
|
||||||
|
this.messageOffset++;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
this.hasBeenReloaded = true;
|
this.hasBeenReloaded = true;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
onTextInput(event: Event): void {
|
onInput(event): void {
|
||||||
if (!(event instanceof KeyboardEvent)) {
|
this.chatTypeArea.nativeElement.style.height = '50px';
|
||||||
return;
|
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) {
|
postMessage(): void {
|
||||||
case 'Enter':
|
if (this.chatTextArea.nativeElement.value.trim() === '') {
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
if (this.chatText.trim() === '') {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.websocketService.sendChatMessage(this.chatText);
|
|
||||||
this.chatText = '';
|
|
||||||
|
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
default:
|
this.websocketService.sendChatMessage(this.chatTextArea.nativeElement.value);
|
||||||
return;
|
this.chatTextArea.nativeElement.value = '';
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
onLogout() {
|
onLogout() {
|
||||||
@ -154,10 +163,12 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
|
|||||||
}
|
}
|
||||||
|
|
||||||
onChatMessage(message: ChatMessage): void {
|
onChatMessage(message: ChatMessage): void {
|
||||||
|
message.htmlMessage = this.parseHtml(message.message);
|
||||||
|
|
||||||
this.messages.push(message);
|
this.messages.push(message);
|
||||||
this.messageOffset++;
|
this.messageOffset++;
|
||||||
|
|
||||||
if (message.userId === this.userId) {
|
if (this.hasFocus || message.userId === this.userId) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,21 +179,29 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
|
|||||||
{
|
{
|
||||||
this.errorMessage.nativeElement.style.display = 'none';
|
this.errorMessage.nativeElement.style.display = 'none';
|
||||||
|
|
||||||
this.apiService.getChatMessagesMissed(this.userToken, this.messages[this.messages.length - 1].id).toPromise()
|
this.enableSendButton();
|
||||||
.then(
|
|
||||||
(messagesMissed) => {
|
|
||||||
if (messagesMissed.length === 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.messages.concat(messagesMissed);
|
if (this.isReconnection) {
|
||||||
this.messageOffset += messagesMissed.length;
|
this.apiService.getChatMessagesMissed(this.userToken, this.messages[this.messages.length - 1].id).toPromise()
|
||||||
}
|
.then(
|
||||||
).catch(
|
(messagesMissed) => {
|
||||||
(error) => {
|
if (messagesMissed.length === 0) {
|
||||||
console.log('Failed to load messages missed after reconnect!', error);
|
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;
|
this.isReconnection = true;
|
||||||
}
|
}
|
||||||
@ -196,6 +215,27 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
|
|||||||
{
|
{
|
||||||
this.errorMessage.nativeElement.style.display = 'block';
|
this.errorMessage.nativeElement.style.display = 'block';
|
||||||
this.errorMessage.nativeElement.innerText = message;
|
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
|
triggerNotification(message: ChatMessage): void
|
||||||
@ -228,4 +268,80 @@ export class ChatComponent implements OnInit, AfterViewInit, AfterViewChecked, W
|
|||||||
|
|
||||||
return false;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -69,7 +69,8 @@ export class WebsocketService {
|
|||||||
userId: messageReceived.userId,
|
userId: messageReceived.userId,
|
||||||
username: this.userList.get(messageReceived.userId),
|
username: this.userList.get(messageReceived.userId),
|
||||||
datetime: messageReceived.datetime,
|
datetime: messageReceived.datetime,
|
||||||
message: messageReceived.message
|
message: messageReceived.message,
|
||||||
|
htmlMessage: '',
|
||||||
};
|
};
|
||||||
|
|
||||||
this.listener.onChatMessage(message);
|
this.listener.onChatMessage(message);
|
||||||
|
@ -40,12 +40,22 @@ html, body {
|
|||||||
#chat-type-area {
|
#chat-type-area {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
|
background-color: #cccccc;
|
||||||
left: 0;
|
left: 0;
|
||||||
right: 0;
|
right: 0;
|
||||||
bottom: 0;
|
bottom: 0;
|
||||||
box-shadow: 0 0 20px rgba(0, 0, 0, 0.9);
|
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 {
|
#chat-textarea {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@ -54,10 +64,13 @@ html, body {
|
|||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: white;
|
color: white;
|
||||||
border: 2px grey solid;
|
border: 2px grey solid;
|
||||||
|
border-bottom: 2px solid white;
|
||||||
|
border-right: 2px solid white;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
padding: 5px;
|
padding: 5px;
|
||||||
resize: none;
|
resize: none;
|
||||||
outline: none;
|
outline: none;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-post {
|
.chat-post {
|
||||||
@ -196,3 +209,40 @@ html, body {
|
|||||||
display: none;
|
display: none;
|
||||||
z-index: 10;
|
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;
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user