From 8c02543bca817a2a6e64447d09509536ed4fc672 Mon Sep 17 00:00:00 2001 From: akastijn Date: Sun, 9 Aug 2026 00:59:44 +0200 Subject: [PATCH] Add user list toggle with responsive visibility in `ChatService` and `ChatInfoComponent`, implement `UserListComponent`, and update styles for user list and toggles. --- .../pages/altitude/chat/chat.component.html | 21 ++++++--- .../pages/altitude/chat/chat.component.scss | 16 +++++++ .../app/pages/altitude/chat/chat.component.ts | 19 ++++++-- .../chat-info/chat-info.component.html | 12 ++++- .../chat-info/chat-info.component.scss | 34 +++++++++++++- .../chat-info/chat-info.component.ts | 12 ++++- .../user-list/user-list.component.html | 16 +++++++ .../user-list/user-list.component.scss | 47 +++++++++++++++++++ .../user-list/user-list.component.ts | 35 ++++++++++++++ .../altitude/chat/service/chat.service.ts | 5 ++ .../chat/service/name-format.service.ts | 2 +- .../chat/svg/notification-disabled.svg.ts | 2 +- .../chat/svg/notification-enabled.svg.ts | 2 +- .../app/pages/altitude/chat/svg/users.svg.ts | 17 +++++++ 14 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.html create mode 100644 frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.scss create mode 100644 frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.ts create mode 100644 frontend/src/app/pages/altitude/chat/svg/users.svg.ts diff --git a/frontend/src/app/pages/altitude/chat/chat.component.html b/frontend/src/app/pages/altitude/chat/chat.component.html index d69253a..56cc064 100644 --- a/frontend/src/app/pages/altitude/chat/chat.component.html +++ b/frontend/src/app/pages/altitude/chat/chat.component.html @@ -24,20 +24,29 @@
-
- + @if (!isMobile() || !userListVisible()) { +
+ - -
+
+
+ }
- +
+ +
+ @if (userListVisible()) { +
+ +
+ }
diff --git a/frontend/src/app/pages/altitude/chat/chat.component.scss b/frontend/src/app/pages/altitude/chat/chat.component.scss index db43c08..a76bf7a 100644 --- a/frontend/src/app/pages/altitude/chat/chat.component.scss +++ b/frontend/src/app/pages/altitude/chat/chat.component.scss @@ -60,6 +60,12 @@ min-width: 100px; } + .user-list { + flex: 0 0 auto; + width: max-content; + min-width: 100px; + } + .chat { display: flex; flex-direction: column; @@ -70,6 +76,16 @@ .chat-info { flex: 0 0 auto; padding: 5px 5px 2px 5px; + + .chat-info-container { + display: flex; + align-items: center; + gap: 10px; + + app-chat-info { + flex: 1 1 auto; + } + } } .chat-box { diff --git a/frontend/src/app/pages/altitude/chat/chat.component.ts b/frontend/src/app/pages/altitude/chat/chat.component.ts index 76a00df..16277ce 100644 --- a/frontend/src/app/pages/altitude/chat/chat.component.ts +++ b/frontend/src/app/pages/altitude/chat/chat.component.ts @@ -8,6 +8,7 @@ import {FullSizeComponent} from '@shared-components/full-size/full-size.componen import {ChatBoxComponent} from '@pages/altitude/chat/components/chat/chat-box.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 {UserListComponent} from '@pages/altitude/chat/components/user-list/user-list.component'; import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout'; @Component({ @@ -17,7 +18,8 @@ import {BreakpointObserver, Breakpoints} from '@angular/cdk/layout'; FullSizeComponent, ChatBoxComponent, ChannelListComponent, - ChatInfoComponent + ChatInfoComponent, + UserListComponent ], templateUrl: './chat.component.html', styleUrl: './chat.component.scss', @@ -31,6 +33,7 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { protected readonly selectedChannel = this.chatService.selectedChannel; protected readonly showFullscreenPrompt = signal(false); protected readonly isMobile = signal(false); + protected readonly userListVisible = this.chatService.userListVisible; constructor(private interaction: MiniMessageInteractionService, private breakpointObserver: BreakpointObserver) { interaction.clicks$.subscribe(({action, value}) => { @@ -41,8 +44,14 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { this.breakpointObserver .observe([Breakpoints.Handset]) .subscribe(result => { - this.isMobile.set(result.matches); - this.showFullscreenPrompt.set(this.isMobile()); + const isMobile = result.matches; + this.isMobile.set(isMobile); + this.showFullscreenPrompt.set(isMobile); + if (isMobile) { + this.userListVisible.set(false); + } else { + this.userListVisible.set(true); + } }); } @@ -50,6 +59,10 @@ export class ChatComponent implements OnInit, AfterViewInit, OnDestroy { this.chatService.selectChannel(channel); } + protected toggleUserList(): void { + this.chatService.toggleUserList(); + } + protected enterFullscreen(): void { this.showFullscreenPrompt.set(false); 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 752e87f..53fdb71 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 @@ -7,11 +7,19 @@ } ({{ chatChannel.totalMessages }}) +
+ + @if (isMobile()) { + + {{ userListVisible() ? 'Channels' : 'Users' }} + + } +
@if (notificationEnabled()) { - + } @else { - + }
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 fbcf524..95a63ef 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 @@ -20,10 +20,40 @@ } } +.user-list-toggle { + cursor: pointer; + display: flex; + align-items: center; + gap: 8px; + margin-left: auto; + margin-right: 15px; + opacity: 0.8; + transition: opacity 0.2s; + + &:hover { + opacity: 1; + } + + &.active { + opacity: 1; + } + + span { + font-weight: bold; + font-size: 0.9rem; + text-transform: uppercase; + } +} + .notification-icon { cursor: pointer; width: 20px; height: 20px; - margin-left: auto; - margin-right: 10px; + margin-right: 15px; + opacity: 0.8; + transition: opacity 0.2s; + + &:hover { + opacity: 1; + } } 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 8a69c21..ec74d85 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,15 +3,18 @@ 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 {UsersSvgComponent} from '@pages/altitude/chat/svg/users.svg'; import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object'; import {ColoredNameSegment} from '@pages/altitude/chat/objects/colored-name-segment.object'; import {NameFormatService} from '@pages/altitude/chat/service/name-format.service'; +import {ChatService} from '@pages/altitude/chat/service/chat.service'; @Component({ selector: 'app-chat-info', imports: [ NotificationEnabledSvgComponent, - NotificationDisabledSvgComponent + NotificationDisabledSvgComponent, + UsersSvgComponent ], templateUrl: './chat-info.component.html', styleUrl: './chat-info.component.scss' @@ -19,8 +22,11 @@ import {NameFormatService} from '@pages/altitude/chat/service/name-format.servic export class ChatInfoComponent { private readonly notificationService: NotificationService = inject(NotificationService); private readonly nameFormatService: NameFormatService = inject(NameFormatService); + private readonly chatService: ChatService = inject(ChatService); public readonly selectedChannel = input.required(); + public readonly isMobile = input(false); + protected readonly userListVisible = this.chatService.userListVisible; protected readonly channelKey = computed(() => { const channel = this.selectedChannel(); return channel ? `${channel.type}:${channel.name}` : ''; @@ -37,6 +43,10 @@ export class ChatInfoComponent { this.notificationService.disableNotifications(this.channelKey()); } + public toggleUserList() { + this.chatService.toggleUserList(); + } + protected getDisplayName(chatChannel: ChatChannel): ColoredNameSegment[] { return this.nameFormatService.getDisplayName(chatChannel); } diff --git a/frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.html b/frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.html new file mode 100644 index 0000000..03a0e6e --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.html @@ -0,0 +1,16 @@ +
+
+ @for (server of serverState()?.servers; track server.name) { +

{{ server.name }} ({{ server.players.length }})

+ @for (player of server.players; track player.uuid) { +
+ + @for (segment of getDisplayName(player); track $index) { + {{ segment.text }} + } + +
+ } + } +
+
diff --git a/frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.scss b/frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.scss new file mode 100644 index 0000000..8806867 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.scss @@ -0,0 +1,47 @@ +.user-list { + height: 100%; + background-color: #172133; + display: flex; + flex-direction: column; + min-height: 0; + + .user { + position: relative; + display: flex; + align-items: center; + padding: 8px 20px; + border-bottom: 1px solid #222; + cursor: default; + + .user-name { + color: white; + font-weight: bold; + } + } + + .user.indented { + padding-left: 40px; + } + + .server-title { + padding: 10px 20px; + color: #888; + font-size: 0.8rem; + font-weight: bold; + text-transform: uppercase; + user-select: none; + } + + p { + margin: 0; + } + + .scroll { + flex: 1 1 auto; + min-height: 0; + overflow-y: auto; + box-sizing: border-box; + scrollbar-width: thin; + scrollbar-color: rgba(255, 255, 255, 0.35) transparent; + } +} diff --git a/frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.ts b/frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.ts new file mode 100644 index 0000000..688df81 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/components/user-list/user-list.component.ts @@ -0,0 +1,35 @@ +import {Component, computed, inject} from '@angular/core'; +import {ChatService} from '@pages/altitude/chat/service/chat.service'; +import {NameFormatService} from '@pages/altitude/chat/service/name-format.service'; +import {ColoredNameSegment} from '@pages/altitude/chat/objects/colored-name-segment.object'; +import {User} from '@pages/altitude/chat/objects/server-state.object'; + +@Component({ + selector: 'app-user-list', + standalone: true, + imports: [], + templateUrl: './user-list.component.html', + styleUrl: './user-list.component.scss' +}) +export class UserListComponent { + private readonly chatService = inject(ChatService); + private readonly nameFormatService = inject(NameFormatService); + + protected readonly serverState = computed(() => { + const state = this.chatService.serverState(); + if (!state) return null; + + return { + servers: [...state.servers] + .sort((a, b) => a.name.localeCompare(b.name)) + .map(server => ({ + ...server, + players: [...server.players].sort((a, b) => a.name.localeCompare(b.name)) + })) + }; + }); + + protected getDisplayName(user: User): ColoredNameSegment[] { + return this.nameFormatService.parseColoredName(user.styledName || user.name); + } +} 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 2f0ad47..666dc63 100644 --- a/frontend/src/app/pages/altitude/chat/service/chat.service.ts +++ b/frontend/src/app/pages/altitude/chat/service/chat.service.ts @@ -29,9 +29,14 @@ export class ChatService implements OnDestroy { public readonly serverState = this._serverState.asReadonly(); public readonly partieMap = signal>(new Map()); public readonly userNameMap = signal>(new Map()); + public readonly userListVisible = signal(true); private readonly pendingPartyNameRequests = new Set(); private readonly pendingUserNameRequests = new Set(); + public toggleUserList(): void { + this.userListVisible.update(v => !v); + } + public readonly messages = computed(() => { const selected = this._selectedChannel(); if (!selected) return []; diff --git a/frontend/src/app/pages/altitude/chat/service/name-format.service.ts b/frontend/src/app/pages/altitude/chat/service/name-format.service.ts index ca27be4..ab368b1 100644 --- a/frontend/src/app/pages/altitude/chat/service/name-format.service.ts +++ b/frontend/src/app/pages/altitude/chat/service/name-format.service.ts @@ -44,7 +44,7 @@ export class NameFormatService { return channel.name; } - private parseColoredName(name: string): ColoredNameSegment[] { + public parseColoredName(name: string): ColoredNameSegment[] { const markerRegex = /\{#([0-9a-fA-F]{6})(<>|[<>]?)\}|&([0-9a-fA-F])/g; const segments: ColoredNameSegment[] = []; let currentColor: string | undefined; diff --git a/frontend/src/app/pages/altitude/chat/svg/notification-disabled.svg.ts b/frontend/src/app/pages/altitude/chat/svg/notification-disabled.svg.ts index 984aaa0..7bab2f4 100644 --- a/frontend/src/app/pages/altitude/chat/svg/notification-disabled.svg.ts +++ b/frontend/src/app/pages/altitude/chat/svg/notification-disabled.svg.ts @@ -7,7 +7,7 @@ import {Component} from '@angular/core'; + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> ` }) diff --git a/frontend/src/app/pages/altitude/chat/svg/notification-enabled.svg.ts b/frontend/src/app/pages/altitude/chat/svg/notification-enabled.svg.ts index e6a45f0..321172e 100644 --- a/frontend/src/app/pages/altitude/chat/svg/notification-enabled.svg.ts +++ b/frontend/src/app/pages/altitude/chat/svg/notification-enabled.svg.ts @@ -7,7 +7,7 @@ import {Component} from '@angular/core'; + stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> ` }) diff --git a/frontend/src/app/pages/altitude/chat/svg/users.svg.ts b/frontend/src/app/pages/altitude/chat/svg/users.svg.ts new file mode 100644 index 0000000..78f0853 --- /dev/null +++ b/frontend/src/app/pages/altitude/chat/svg/users.svg.ts @@ -0,0 +1,17 @@ +import {Component} from '@angular/core'; + +@Component({ + selector: 'svg-users', + standalone: true, + imports: [], + template: ` + + + + + + + ` +}) +export class UsersSvgComponent { +}