From d61f2ed9388a69aaf89a406b1c74f67c457e0979 Mon Sep 17 00:00:00 2001 From: akastijn Date: Sun, 19 Jul 2026 17:12:03 +0200 Subject: [PATCH] Modularize chat page with new components (chat-box, server-list, chat-info). Introduce server filtering, real-time updates, and redesigned layout for enhanced usability. --- .../pages/altitude/chat/chat.component.html | 35 +++++++----- .../pages/altitude/chat/chat.component.scss | 35 ++++++++++-- .../app/pages/altitude/chat/chat.component.ts | 29 +++++----- .../chat-info/chat-info.component.html | 6 ++ .../chat-info/chat-info.component.scss | 6 ++ .../chat-info/chat-info.component.ts | 14 +++++ .../components/chat/chat-box.component.html | 8 +++ .../components/chat/chat-box.component.scss | 7 +++ .../components/chat/chat-box.component.ts | 17 ++++++ .../mini-message/mini-message.component.html | 31 +++++++++++ .../mini-message/mini-message.component.scss | 12 ++++ .../mini-message/mini-message.component.ts | 55 ++----------------- .../server-list/server-list.component.html | 13 +++++ .../server-list/server-list.component.scss | 23 ++++++++ .../server-list/server-list.component.ts | 27 +++++++++ .../chat/objects/chat-server.object.ts | 4 ++ .../altitude/chat/service/chat.service.ts | 55 +++++++++++++------ .../app/pages/altitude/chat/svg/circle.svg.ts | 19 +++++++ .../pages/altitude/chat/util/server-color.ts | 21 +++++++ 19 files changed, 318 insertions(+), 99 deletions(-) create mode 100644 frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.html create mode 100644 frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.scss create mode 100644 frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.ts create mode 100644 frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.html create mode 100644 frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.scss create mode 100644 frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.ts create mode 100644 frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.html create mode 100644 frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.scss rename frontend/src/app/pages/altitude/chat/{ => components}/mini-message/mini-message.component.ts (58%) create mode 100644 frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.html create mode 100644 frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.scss create mode 100644 frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.ts create mode 100644 frontend/src/app/pages/altitude/chat/objects/chat-server.object.ts create mode 100644 frontend/src/app/pages/altitude/chat/svg/circle.svg.ts create mode 100644 frontend/src/app/pages/altitude/chat/util/server-color.ts diff --git a/frontend/src/app/pages/altitude/chat/chat.component.html b/frontend/src/app/pages/altitude/chat/chat.component.html index efeadcc..8170bb8 100644 --- a/frontend/src/app/pages/altitude/chat/chat.component.html +++ b/frontend/src/app/pages/altitude/chat/chat.component.html @@ -1,5 +1,5 @@ -

Chat

@@ -7,18 +7,25 @@
-
- @for (message of messages(); track message.timestamp) { -

- [{{ message.timestamp | date: 'mediumTime' }}] - @if (message.notBlocked) { - [{{ message.server }}] - } @else { - [{{ message.server }}] - } - -

- } -
+ +
+
+
+ + + +
+
+
+ +
+
+ +
+
+
+
+
diff --git a/frontend/src/app/pages/altitude/chat/chat.component.scss b/frontend/src/app/pages/altitude/chat/chat.component.scss index 9219bed..40df6d3 100644 --- a/frontend/src/app/pages/altitude/chat/chat.component.scss +++ b/frontend/src/app/pages/altitude/chat/chat.component.scss @@ -1,7 +1,32 @@ -.blocked { - color: red; -} +.page-layout { + display: flex; + flex-direction: row; + width: 100%; + height: 100%; -.date-time { - color: gray; + .server-list { + flex: 0 0 auto; + width: max-content; + min-width: 100px; + } + + .chat { + display: flex; + flex-direction: column; + flex: 1 1 auto; + min-width: 0; + min-height: 0; + + .chat-info { + flex: 0 0 auto; + padding: 5px 5px 2px 5px; + } + + .chat-box { + flex: 1 1 auto; + min-width: 0; + padding: 0 5px 0 5px; + min-height: 0; + } + } } diff --git a/frontend/src/app/pages/altitude/chat/chat.component.ts b/frontend/src/app/pages/altitude/chat/chat.component.ts index 6dc18f1..5171aa9 100644 --- a/frontend/src/app/pages/altitude/chat/chat.component.ts +++ b/frontend/src/app/pages/altitude/chat/chat.component.ts @@ -2,16 +2,20 @@ import {Component, inject, OnDestroy, OnInit} from '@angular/core'; import {Subscription} from 'rxjs'; import {ChatService} from '@pages/altitude/chat/service/chat.service'; import {HeaderComponent} from '@header/header.component'; -import {MiniMessageComponent} from '@pages/altitude/chat/mini-message/mini-message.component'; import {MiniMessageInteractionService} from '@pages/altitude/chat/mini-message/mini-message-interaction.service'; -import {DatePipe} from '@angular/common'; +import {FullSizeComponent} from '@shared-components/full-size/full-size.component'; +import {ChatBoxComponent} from '@pages/altitude/chat/components/chat/chat-box.component'; +import {ServerListComponent} from '@pages/altitude/chat/components/server-list/server-list.component'; +import {ChatInfoComponent} from '@pages/altitude/chat/components/chat-info/chat-info.component'; @Component({ selector: 'app-chat', imports: [ HeaderComponent, - MiniMessageComponent, - DatePipe + FullSizeComponent, + ChatBoxComponent, + ServerListComponent, + ChatInfoComponent ], templateUrl: './chat.component.html', styleUrl: './chat.component.scss', @@ -21,6 +25,8 @@ export class ChatComponent implements OnInit, OnDestroy { private sub?: Subscription; private readonly chatService: ChatService = inject(ChatService) protected readonly messages = this.chatService.messages; + protected readonly servers = this.chatService.servers; + protected readonly selectedServer = this.chatService.selectedServer; constructor(private interaction: MiniMessageInteractionService) { interaction.clicks$.subscribe(({action, value}) => { @@ -30,6 +36,10 @@ export class ChatComponent implements OnInit, OnDestroy { }); } + public onSelectedServerChange(server: string) { + this.chatService.selectServer(server); + } + ngOnInit(): void { this.chatService.connect(); @@ -39,15 +49,4 @@ export class ChatComponent implements OnInit, OnDestroy { this.sub?.unsubscribe(); this.chatService.disconnect(); // triggers onCompletion server-side, cleans up the emitter } - - protected getServerColor(server: string) { - if (server === 'bayou') { - return "#007FFF"; - } else if (server === 'lobby') { - return "white"; - } else if (server === 'creative') { - return "#FFA500"; - } - return "white"; - } } diff --git a/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.html b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.html new file mode 100644 index 0000000..03ec28a --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.html @@ -0,0 +1,6 @@ +
+ + {{ selectedServer() }} + +
diff --git a/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.scss b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.scss new file mode 100644 index 0000000..d73a494 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.scss @@ -0,0 +1,6 @@ +.server { + padding-left: 10px; + font-size: 25px; + font-family: minecraft-text, 'opensans-bold', sans-serif; + letter-spacing: 2px; +} diff --git a/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.ts b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.ts new file mode 100644 index 0000000..c1f08db --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/chat-info/chat-info.component.ts @@ -0,0 +1,14 @@ +import {Component, input} from '@angular/core'; +import {getServerColor, getServerTextColor} from '@pages/altitude/chat/util/server-color'; + +@Component({ + selector: 'app-chat-info', + imports: [], + templateUrl: './chat-info.component.html', + styleUrl: './chat-info.component.scss' +}) +export class ChatInfoComponent { + public readonly selectedServer = input.required(); + protected readonly getServerColor = getServerColor; + protected readonly getServerTextColor = getServerTextColor; +} diff --git a/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.html b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.html new file mode 100644 index 0000000..b4e7eec --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.html @@ -0,0 +1,8 @@ +@for (message of messages(); track message.timestamp) { +

+ + [{{ message.timestamp * 1000 | date: 'mediumTime' }}] + + +

+} diff --git a/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.scss b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.scss new file mode 100644 index 0000000..9219bed --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.scss @@ -0,0 +1,7 @@ +.blocked { + color: red; +} + +.date-time { + color: gray; +} diff --git a/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.ts b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.ts new file mode 100644 index 0000000..24a7c6a --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/chat/chat-box.component.ts @@ -0,0 +1,17 @@ +import {Component, input} from '@angular/core'; +import {DatePipe} from '@angular/common'; +import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object'; +import {MiniMessageComponent} from '@pages/altitude/chat/components/mini-message/mini-message.component'; + +@Component({ + selector: 'app-chat-box', + imports: [ + MiniMessageComponent, + DatePipe + ], + templateUrl: './chat-box.component.html', + styleUrl: './chat-box.component.scss' +}) +export class ChatBoxComponent { + readonly messages = input.required(); +} diff --git a/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.html b/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.html new file mode 100644 index 0000000..b933e9b --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.html @@ -0,0 +1,31 @@ +@if (node) { + + {{ text }} + + + {{ text }} + + @for (child of node.extra; track child) { + + } + +} diff --git a/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.scss b/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.scss new file mode 100644 index 0000000..a04178f --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.scss @@ -0,0 +1,12 @@ +.mm-node { + white-space: pre-wrap; +} + +.mm-clickable { + cursor: pointer; +} + +.mm-link { + text-decoration: inherit; + cursor: pointer; +} diff --git a/frontend/src/app/pages/altitude/chat/mini-message/mini-message.component.ts b/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.ts similarity index 58% rename from frontend/src/app/pages/altitude/chat/mini-message/mini-message.component.ts rename to frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.ts index 54af95c..510367f 100644 --- a/frontend/src/app/pages/altitude/chat/mini-message/mini-message.component.ts +++ b/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.ts @@ -1,9 +1,9 @@ import {CommonModule} from '@angular/common'; import {Component, Input, Optional} from '@angular/core'; -import {MiniMessageComponent as MiniMessageNode, ResolvedStyle} from './mini-message.types'; -import {hoverEventToTitle, mergeStyle, ownText, styleToNgStyle} from './mini-message.util'; -import {ObfuscatedDirective} from './obfuscated.directive'; -import {MiniMessageInteractionService} from './mini-message-interaction.service'; +import {MiniMessageComponent as MiniMessageNode, ResolvedStyle} from '../../mini-message/mini-message.types'; +import {hoverEventToTitle, mergeStyle, ownText, styleToNgStyle} from '../../mini-message/mini-message.util'; +import {ObfuscatedDirective} from '../../mini-message/obfuscated.directive'; +import {MiniMessageInteractionService} from '../../mini-message/mini-message-interaction.service'; /** * Renders a Paper/Adventure component-JSON tree as styled HTML. @@ -20,51 +20,8 @@ import {MiniMessageInteractionService} from './mini-message-interaction.service' selector: 'mini-message', standalone: true, imports: [CommonModule, ObfuscatedDirective], - template: ` - - {{ text }} - - {{ text }} - - - - `, - styles: [ - ` - .mm-node { - white-space: pre-wrap; - } - - .mm-clickable { - cursor: pointer; - } - - .mm-link { - text-decoration: inherit; - cursor: pointer; - } - `, - ], + templateUrl: './mini-message.component.html', + styleUrl: './mini-message.component.scss', }) export class MiniMessageComponent { @Input() node!: MiniMessageNode; diff --git a/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.html b/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.html new file mode 100644 index 0000000..a5dde72 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.html @@ -0,0 +1,13 @@ +
+

Servers

+ @for (server of servers(); track server.server) { +
+ {{ server.server }} + @if (server.unreadMessages > 0) { +
+ +
+ } +
+ } +
diff --git a/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.scss b/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.scss new file mode 100644 index 0000000..be1af43 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.scss @@ -0,0 +1,23 @@ +.server-list { + height: 100%; + background-color: #172133; + + .server { + position: relative; + display: flex; + padding: 2px 2px 2px 10px; + border-bottom: 1px solid #222; + + .server-name { + color: white + } + + .notification { + position: absolute; + right: 10px; + top: 50%; + transform: translateY(-50%); + color: blue; + } + } +} diff --git a/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.ts b/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.ts new file mode 100644 index 0000000..0c9fa28 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.ts @@ -0,0 +1,27 @@ +import {Component, effect, input, output} from '@angular/core'; +import {ChatServer} from '@pages/altitude/chat/objects/chat-server.object'; +import {CircleSvgComponent} from '@pages/altitude/chat/svg/circle.svg'; + +@Component({ + selector: 'app-server-list', + imports: [ + CircleSvgComponent + ], + templateUrl: './server-list.component.html', + styleUrl: './server-list.component.scss' +}) +export class ServerListComponent { + public readonly servers = input.required(); + public readonly selectedServer = input.required(); + public readonly selectedServerChange = output(); + + constructor() { + effect(() => { + const chatServers = this.servers(); + if (this.selectedServer() === '' && chatServers.length > 0) { + this.selectedServerChange.emit(chatServers[0].server); + } + }); + } + +} diff --git a/frontend/src/app/pages/altitude/chat/objects/chat-server.object.ts b/frontend/src/app/pages/altitude/chat/objects/chat-server.object.ts new file mode 100644 index 0000000..208c129 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/objects/chat-server.object.ts @@ -0,0 +1,4 @@ +export interface ChatServer { + server: string; + unreadMessages: number; +} diff --git a/frontend/src/app/pages/altitude/chat/service/chat.service.ts b/frontend/src/app/pages/altitude/chat/service/chat.service.ts index 31d0020..f5b78f6 100644 --- a/frontend/src/app/pages/altitude/chat/service/chat.service.ts +++ b/frontend/src/app/pages/altitude/chat/service/chat.service.ts @@ -1,10 +1,10 @@ -import {inject, Injectable, OnDestroy, signal} from '@angular/core'; +import {computed, inject, Injectable, OnDestroy, signal} from '@angular/core'; import {AuthService} from '@services/auth.service'; import {EventSourcePolyfill} from 'event-source-polyfill'; import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object'; -import {MiniMessageComponent} from '@pages/altitude/chat/mini-message/mini-message.types'; import {RawChatMessage} from '@pages/altitude/chat/objects/raw-chat-message.object'; import {normalizeComponent} from '@pages/altitude/chat/mini-message/normalize.util'; +import {ChatServer} from '@pages/altitude/chat/objects/chat-server.object'; export interface ChatEvent { type: string; @@ -20,7 +20,11 @@ export class ChatService implements OnDestroy { private eventSource?: EventSourcePolyfill; private readonly authService: AuthService = inject(AuthService) private readonly _messages = signal([]) - public readonly messages = this._messages.asReadonly() + public readonly messages = computed(() => this._messages().filter((message) => message.server === this._selectedServer())); + private readonly _servers = signal([]) + public readonly servers = computed(() => this._servers().sort((a, b) => a.server.localeCompare(b.server))); + private readonly _selectedServer = signal(''); + public readonly selectedServer = this._selectedServer.asReadonly() connect() { if (this.eventSource) { @@ -43,21 +47,17 @@ export class ChatService implements OnDestroy { this.eventSource = source; this.on(source, 'connected', (event) => { - const raw = JSON.parse(event.data) as RawChatMessage[]; - const messages: ChatMessage[] = raw.map((m) => ({ - ...m, - messageJson: normalizeComponent(JSON.parse(m.messageJson)), - })); - this._messages.update((old) => [...old, ...messages]); + this.processChatMessages(event); }); this.on(source, 'chat', (event) => { - const raw = JSON.parse(event.data) as RawChatMessage[]; - const messages: ChatMessage[] = raw.map((m) => ({ - ...m, - messageJson: JSON.parse(m.messageJson) as MiniMessageComponent, - })); - this._messages.update((old) => [...old, ...messages]); + const chatMessages = this.processChatMessages(event); + chatMessages.forEach((message) => { + const find = this.servers().find(server => message.server === server.server); + if (find) { + find.unreadMessages++; + } + }) }); source.onerror = (err) => { @@ -65,7 +65,30 @@ export class ChatService implements OnDestroy { }; } - // single, deliberate escape hatch from the broken DOM/polyfill type overlap — + private processChatMessages(event: SsePayloadEvent): ChatMessage[] { + const raw = JSON.parse(event.data) as RawChatMessage[]; + const messages: ChatMessage[] = raw.map((m) => ({ + ...m, + messageJson: normalizeComponent(JSON.parse(m.messageJson)), + })); + this._messages.update((old) => [...old, ...messages]); + + //TODO [Stijn] [2026-07-19]: Handle servers by getting them from backend + messages.map((message) => message.server).forEach((server) => { + const found = this._servers().find(existing => existing.server === server); + if (!found) { + this._servers.update((old) => [...old, {server: server, unreadMessages: 0}]); + } + }) + + return messages; + } + + public selectServer(server: string) { + this._selectedServer.set(server); + } + +// single, deliberate escape hatch from the broken DOM/polyfill type overlap — // everything else in this file stays fully typed private on(source: EventSourcePolyfill, eventName: string, handler: (event: SsePayloadEvent) => void): void { (source as unknown as { addEventListener: (type: string, listener: (event: SsePayloadEvent) => void) => void }) diff --git a/frontend/src/app/pages/altitude/chat/svg/circle.svg.ts b/frontend/src/app/pages/altitude/chat/svg/circle.svg.ts new file mode 100644 index 0000000..6b8da57 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/svg/circle.svg.ts @@ -0,0 +1,19 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'svg-circle', + imports: [], + template: ` + + + + ` +}) +export class CircleSvgComponent { + +} diff --git a/frontend/src/app/pages/altitude/chat/util/server-color.ts b/frontend/src/app/pages/altitude/chat/util/server-color.ts new file mode 100644 index 0000000..1a36376 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/util/server-color.ts @@ -0,0 +1,21 @@ +export function getServerColor(server: string) { + if (server === 'bayou') { + return "#0290e8"; + } else if (server === 'lobby') { + return "white"; + } else if (server === 'creative') { + return "#FFA500"; + } + return "white"; +} + +export function getServerTextColor(server: string) { + if (server === 'bayou') { + return "white"; + } else if (server === 'lobby') { + return "black"; + } else if (server === 'creative') { + return "black"; + } + return "white"; +}