From f9f05dcc88590f73bfccb78941b6b5fd6c52818f Mon Sep 17 00:00:00 2001 From: akastijn Date: Sat, 1 Aug 2026 19:10:33 +0200 Subject: [PATCH] Replace servers with channels in chat functionality: refactored UI components, service logic, and message model to support channels (SERVER, DM, PARTY, GAC). --- .../pages/altitude/chat/chat.component.html | 10 +-- .../pages/altitude/chat/chat.component.scss | 2 +- .../app/pages/altitude/chat/chat.component.ts | 13 +-- .../channel-list/channel-list.component.html | 14 +++ .../channel-list.component.scss} | 10 ++- .../channel-list/channel-list.component.ts | 27 ++++++ .../chat-info/chat-info.component.html | 6 +- .../chat-info/chat-info.component.scss | 2 +- .../chat-info/chat-info.component.ts | 13 ++- .../server-list/server-list.component.html | 13 --- .../server-list/server-list.component.ts | 27 ------ .../chat/objects/chat-channel.object.ts | 5 ++ .../chat/objects/chat-message.object.ts | 2 + .../chat/objects/chat-server.object.ts | 4 - .../chat/service/chat-notification.service.ts | 20 ++--- .../altitude/chat/service/chat.service.ts | 86 +++++++++++++------ 16 files changed, 152 insertions(+), 102 deletions(-) create mode 100644 frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.html rename frontend/src/app/pages/altitude/chat/components/{server-list/server-list.component.scss => channel-list/channel-list.component.scss} (80%) create mode 100644 frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.ts delete mode 100644 frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.html delete 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-channel.object.ts delete mode 100644 frontend/src/app/pages/altitude/chat/objects/chat-server.object.ts diff --git a/frontend/src/app/pages/altitude/chat/chat.component.html b/frontend/src/app/pages/altitude/chat/chat.component.html index 0e9e0f1..d69253a 100644 --- a/frontend/src/app/pages/altitude/chat/chat.component.html +++ b/frontend/src/app/pages/altitude/chat/chat.component.html @@ -24,15 +24,15 @@
-
- +
+ - +
- +
diff --git a/frontend/src/app/pages/altitude/chat/chat.component.scss b/frontend/src/app/pages/altitude/chat/chat.component.scss index 6d18af0..db43c08 100644 --- a/frontend/src/app/pages/altitude/chat/chat.component.scss +++ b/frontend/src/app/pages/altitude/chat/chat.component.scss @@ -54,7 +54,7 @@ width: 100%; height: 100%; - .server-list { + .channel-list { flex: 0 0 auto; width: max-content; min-width: 100px; diff --git a/frontend/src/app/pages/altitude/chat/chat.component.ts b/frontend/src/app/pages/altitude/chat/chat.component.ts index 5609440..76a00df 100644 --- a/frontend/src/app/pages/altitude/chat/chat.component.ts +++ b/frontend/src/app/pages/altitude/chat/chat.component.ts @@ -1,11 +1,12 @@ import {AfterViewInit, Component, inject, OnDestroy, OnInit, signal} from '@angular/core'; import {Subscription} from 'rxjs'; import {ChatService} from '@pages/altitude/chat/service/chat.service'; +import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object'; import {HeaderComponent} from '@header/header.component'; import {MiniMessageInteractionService} from '@pages/altitude/chat/mini-message/mini-message-interaction.service'; 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 {ChannelListComponent} from '@pages/altitude/chat/components/channel-list/channel-list.component'; import {ChatInfoComponent} from '@pages/altitude/chat/components/chat-info/chat-info.component'; import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout'; @@ -15,7 +16,7 @@ import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout'; HeaderComponent, FullSizeComponent, ChatBoxComponent, - ServerListComponent, + ChannelListComponent, ChatInfoComponent ], templateUrl: './chat.component.html', @@ -26,8 +27,8 @@ export class ChatComponent implements OnInit, AfterViewInit, 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; + protected readonly channels = this.chatService.channels; + protected readonly selectedChannel = this.chatService.selectedChannel; protected readonly showFullscreenPrompt = signal(false); protected readonly isMobile = signal(false); @@ -45,8 +46,8 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { }); } - public onSelectedServerChange(server: string) { - this.chatService.selectServer(server); + public onSelectedChannelChange(channel: ChatChannel) { + this.chatService.selectChannel(channel); } protected enterFullscreen(): void { diff --git a/frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.html b/frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.html new file mode 100644 index 0000000..4ac82d3 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.html @@ -0,0 +1,14 @@ +
+

Channels

+ @for (channel of channels(); track channel.type + channel.name) { +
+ {{ channel.name }} + @if (channel.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/channel-list/channel-list.component.scss similarity index 80% rename from frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.scss rename to frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.scss index 3adb54e..20094a1 100644 --- a/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.scss +++ b/frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.scss @@ -1,8 +1,8 @@ -.server-list { +.channel-list { height: 100%; background-color: #172133; - .server { + .channel { position: relative; display: flex; align-items: center; @@ -15,7 +15,11 @@ background-color: #202c42; } - .server-name { + &.selected { + background-color: #202c42; + } + + .channel-name { color: white; font-weight: bold; } diff --git a/frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.ts b/frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.ts new file mode 100644 index 0000000..6fd291a --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/channel-list/channel-list.component.ts @@ -0,0 +1,27 @@ +import {Component, effect, input, output} from '@angular/core'; +import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object'; +import {CircleSvgComponent} from '@pages/altitude/chat/svg/circle.svg'; + +@Component({ + selector: 'app-channel-list', + imports: [ + CircleSvgComponent + ], + templateUrl: './channel-list.component.html', + styleUrl: './channel-list.component.scss' +}) +export class ChannelListComponent { + public readonly channels = input.required(); + public readonly selectedChannel = input.required(); + public readonly selectedChannelChange = output(); + + constructor() { + effect(() => { + const chatChannels = this.channels(); + if (this.selectedChannel() === null && chatChannels.length > 0) { + this.selectedChannelChange.emit(chatChannels[0]); + } + }); + } + +} 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 index 060c7ec..f2fce40 100644 --- 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 @@ -1,7 +1,7 @@
- - {{ selectedServer() }} + [style.background-color]="getServerColor(selectedChannel()?.name ?? '')"> + + {{ selectedChannel()?.name }}
@if (notificationEnabled()) { 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 index ecaf1f3..fd81703 100644 --- 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 @@ -7,7 +7,7 @@ margin-bottom: 10px; } -.server { +.channel { font-size: 20px; font-family: minecraft-text, 'opensans-bold', sans-serif; letter-spacing: 1px; 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 index 8eced1e..58fe6d8 100644 --- 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 @@ -3,6 +3,7 @@ import {getServerColor, getServerTextColor} from '@pages/altitude/chat/util/serv import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service'; import {NotificationEnabledSvgComponent} from '@pages/altitude/chat/svg/notification-enabled.svg'; import {NotificationDisabledSvgComponent} from '@pages/altitude/chat/svg/notification-disabled.svg'; +import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object'; @Component({ selector: 'app-chat-info', @@ -16,17 +17,21 @@ import {NotificationDisabledSvgComponent} from '@pages/altitude/chat/svg/notific export class ChatInfoComponent { private readonly notificationService: NotificationService = inject(NotificationService); - public readonly selectedServer = input.required(); - protected readonly notificationEnabled = computed(() => this.notificationService.isNotificationEnabled(this.selectedServer())); + public readonly selectedChannel = input.required(); + protected readonly channelKey = computed(() => { + const channel = this.selectedChannel(); + return channel ? `${channel.type}:${channel.name}` : ''; + }); + protected readonly notificationEnabled = computed(() => this.notificationService.isNotificationEnabled(this.channelKey())); protected readonly getServerColor = getServerColor; protected readonly getServerTextColor = getServerTextColor; public enableNotifications() { - this.notificationService.enableNotifications(this.selectedServer()); + this.notificationService.enableNotifications(this.channelKey()); } public disableNotifications() { - this.notificationService.disableNotifications(this.selectedServer()); + this.notificationService.disableNotifications(this.channelKey()); } } 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 deleted file mode 100644 index a5dde72..0000000 --- a/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.html +++ /dev/null @@ -1,13 +0,0 @@ -
-

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.ts b/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.ts deleted file mode 100644 index 0c9fa28..0000000 --- a/frontend/src/app/pages/altitude/chat/components/server-list/server-list.component.ts +++ /dev/null @@ -1,27 +0,0 @@ -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-channel.object.ts b/frontend/src/app/pages/altitude/chat/objects/chat-channel.object.ts new file mode 100644 index 0000000..8670caf --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/objects/chat-channel.object.ts @@ -0,0 +1,5 @@ +export interface ChatChannel { + name: string; + type: 'SERVER' | 'DM' | 'PARTY' | 'GAC'; + unreadMessages: number; +} diff --git a/frontend/src/app/pages/altitude/chat/objects/chat-message.object.ts b/frontend/src/app/pages/altitude/chat/objects/chat-message.object.ts index 5c2d8d5..b661785 100644 --- a/frontend/src/app/pages/altitude/chat/objects/chat-message.object.ts +++ b/frontend/src/app/pages/altitude/chat/objects/chat-message.object.ts @@ -9,4 +9,6 @@ export interface ChatMessage { receiver: string; messageJson: MiniMessageComponent; notBlocked: boolean; + channelName?: string; + channelType?: 'SERVER' | 'DM' | 'PARTY' | 'GAC'; } 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 deleted file mode 100644 index 208c129..0000000 --- a/frontend/src/app/pages/altitude/chat/objects/chat-server.object.ts +++ /dev/null @@ -1,4 +0,0 @@ -export interface ChatServer { - server: string; - unreadMessages: number; -} diff --git a/frontend/src/app/pages/altitude/chat/service/chat-notification.service.ts b/frontend/src/app/pages/altitude/chat/service/chat-notification.service.ts index 0589180..57dfc27 100644 --- a/frontend/src/app/pages/altitude/chat/service/chat-notification.service.ts +++ b/frontend/src/app/pages/altitude/chat/service/chat-notification.service.ts @@ -9,8 +9,8 @@ export class NotificationService { this.getStoredNotificationSettings() ); - public notify(server: string) { - if (!this.isNotificationEnabled(server)) { + public notify(channel: string) { + if (!this.isNotificationEnabled(channel)) { return; } this.playSound(); @@ -38,23 +38,23 @@ export class NotificationService { }); } - public isNotificationEnabled(server: string): boolean { - return this._notificationEnabled()[server]; + public isNotificationEnabled(channel: string): boolean { + return this._notificationEnabled()[channel]; } - public enableNotifications(server: string) { - this.setNotificationEnabled(server, true); + public enableNotifications(channel: string) { + this.setNotificationEnabled(channel, true); this.enableSound() } - public disableNotifications(server: string) { - this.setNotificationEnabled(server, false); + public disableNotifications(channel: string) { + this.setNotificationEnabled(channel, false); } - private setNotificationEnabled(server: string, enabled: boolean) { + private setNotificationEnabled(channel: string, enabled: boolean) { const notificationSettings = { ...this._notificationEnabled(), - [server]: enabled + [channel]: enabled }; this._notificationEnabled.set(notificationSettings); 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 14c459d..b7955b5 100644 --- a/frontend/src/app/pages/altitude/chat/service/chat.service.ts +++ b/frontend/src/app/pages/altitude/chat/service/chat.service.ts @@ -4,7 +4,7 @@ import {EventSourcePolyfill} from 'event-source-polyfill'; import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object'; 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'; +import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object'; import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service'; interface SsePayloadEvent { @@ -17,11 +17,18 @@ export class ChatService implements OnDestroy { private readonly notificationService: NotificationService = inject(NotificationService); private readonly authService: AuthService = inject(AuthService) private readonly _messages = signal([]) - public readonly messages = computed(() => this._messages().filter((message) => message.server === this._selectedServer()).sort((a, b) => b.timestamp - a.timestamp)); - 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() + private readonly _channels = signal([]) + public readonly channels = computed(() => this._channels().sort((a, b) => a.name.localeCompare(b.name))); + private readonly _selectedChannel = signal(null); + public readonly selectedChannel = this._selectedChannel.asReadonly() + + public readonly messages = computed(() => { + const selected = this._selectedChannel(); + if (!selected) return []; + return this._messages() + .filter((message) => message.channelName === selected.name && message.channelType === selected.type) + .sort((a, b) => b.timestamp - a.timestamp); + }); connect() { if (this.eventSource) { @@ -50,14 +57,17 @@ export class ChatService implements OnDestroy { this.on(source, 'chat', (event) => { const chatMessages = this.processChatMessages(event); chatMessages.forEach((message) => { - const find = this.servers().find(server => message.server === server.server && message.server !== this._selectedServer()); + const selected = this._selectedChannel(); + const isSelected = selected && selected.name === message.channelName && selected.type === message.channelType; + + const find = this._channels().find(channel => message.channelName === channel.name && message.channelType === channel.type && !isSelected); if (find) { find.unreadMessages++; } }) - new Set(chatMessages.map(message => message.server)) - .forEach((server) => { - this.notificationService.notify(server); + new Set(chatMessages.map(message => `${message.channelType}:${message.channelName}`)) + .forEach((channelKeyStr) => { + this.notificationService.notify(channelKeyStr); }); }); @@ -68,30 +78,56 @@ export class ChatService implements OnDestroy { 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)), - })); + const messages: ChatMessage[] = raw.map((m) => { + const message: ChatMessage = { + ...m, + messageJson: normalizeComponent(JSON.parse(m.messageJson)), + }; + const channelKey = this.getChannelKey(message); + message.channelName = channelKey.name; + message.channelType = channelKey.type; + return message; + }); 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); + messages.forEach((message) => { + const found = this._channels().find(existing => existing.name === message.channelName && existing.type === message.channelType); if (!found) { - this._servers.update((old) => [...old, {server: server, unreadMessages: 0}]); + this._channels.update((old) => [...old, { + name: message.channelName!, + type: message.channelType!, + unreadMessages: 0 + }]); } }) return messages; } - public selectServer(server: string) { - this._selectedServer.set(server); - this._servers.update((old) => - old.map((chatServer) => - chatServer.server === server - ? {...chatServer, unreadMessages: 0} - : chatServer + private getChannelKey(message: ChatMessage): { name: string, type: ChatChannel['type'] } { + if (message.type === 'GAC') { + return {name: 'Global Admin Chat', type: 'GAC'}; + } + if (message.type === 'PARTY') { + return {name: message.channel, type: 'PARTY'}; + } + if (message.type === 'MSG') { + const uuid = this.authService.getUuid(); + if (message.uuid === uuid) { + return {name: message.receiver, type: 'DM'}; + } + return {name: message.uuid, type: 'DM'}; + } + return {name: message.server, type: 'SERVER'}; + } + + public selectChannel(channel: ChatChannel) { + this._selectedChannel.set(channel); + this._channels.update((old) => + old.map((c) => + c.name === channel.name && c.type === channel.type + ? {...c, unreadMessages: 0} + : c ) ); }