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 2636837..752e87f 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,10 +1,11 @@ @if (selectedChannel(); as chatChannel) {
- + [style.background-color]="getServerColor(chatChannel.name)"> + @for (segment of getDisplayName(chatChannel); track $index) { {{ segment.text }} } + ({{ chatChannel.totalMessages }})
@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 fd81703..c9b5a18 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 @@ -13,6 +13,10 @@ letter-spacing: 1px; font-weight: bold; padding-left: 10px; + + .message-count { + padding-left: 5px; + } } .notification-icon { 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 index b933e9b..008b27b 100644 --- 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 @@ -1,31 +1,28 @@ @if (node) { - + @if (node.isUrlLink) { {{ text }} + >{{ node.text }} + } @else { + {{ node.text }} + } - - {{ text }} - - @for (child of node.extra; track child) { - - } - + @for (child of node.extra; track child) { + + } } diff --git a/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.ts b/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.ts index 510367f..6efc3bb 100644 --- a/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.ts +++ b/frontend/src/app/pages/altitude/chat/components/mini-message/mini-message.component.ts @@ -1,7 +1,6 @@ import {CommonModule} from '@angular/common'; import {Component, Input, Optional} from '@angular/core'; -import {MiniMessageComponent as MiniMessageNode, ResolvedStyle} from '../../mini-message/mini-message.types'; -import {hoverEventToTitle, mergeStyle, ownText, styleToNgStyle} from '../../mini-message/mini-message.util'; +import {ProcessedMiniMessageNode} from '../../mini-message/mini-message.types'; import {ObfuscatedDirective} from '../../mini-message/obfuscated.directive'; import {MiniMessageInteractionService} from '../../mini-message/mini-message-interaction.service'; @@ -10,7 +9,7 @@ import {MiniMessageInteractionService} from '../../mini-message/mini-message-int * * Usage: * providers: [MiniMessageInteractionService] // once, at the root that hosts - * + * * * Then subscribe to click events (run_command / suggest_command / copy_to_clipboard / * change_page) via the injected MiniMessageInteractionService.clicks$; open_url is handled @@ -24,42 +23,17 @@ import {MiniMessageInteractionService} from '../../mini-message/mini-message-int styleUrl: './mini-message.component.scss', }) export class MiniMessageComponent { - @Input() node!: MiniMessageNode; - @Input() parentStyle?: ResolvedStyle; + @Input() node!: ProcessedMiniMessageNode; constructor(@Optional() private readonly interaction: MiniMessageInteractionService | null) { } - get style(): ResolvedStyle { - return mergeStyle(this.parentStyle, this.node); - } - - get ngStyle(): Record { - return styleToNgStyle(this.style); - } - - get text(): string { - return ownText(this.node); - } - - get hoverTitle(): string | null { - return hoverEventToTitle(this.node.hoverEvent); - } - - get isUrlLink(): boolean { - return this.node.clickEvent?.action === 'open_url'; - } - - get hasNonUrlClick(): boolean { - return !!this.node.clickEvent && this.node.clickEvent.action !== 'open_url'; - } - onClick(): void { const click = this.node.clickEvent; if (!click) return; if (click.action === 'copy_to_clipboard') { - navigator.clipboard?.writeText(click.value); + navigator.clipboard?.writeText(click.value).then(); } this.interaction?.emitClick({action: click.action, value: click.value}); diff --git a/frontend/src/app/pages/altitude/chat/mini-message/mini-message.types.ts b/frontend/src/app/pages/altitude/chat/mini-message/mini-message.types.ts index 10f7b87..09c7457 100644 --- a/frontend/src/app/pages/altitude/chat/mini-message/mini-message.types.ts +++ b/frontend/src/app/pages/altitude/chat/mini-message/mini-message.types.ts @@ -73,6 +73,19 @@ export interface MiniMessageComponent { extra?: MiniMessageComponent[]; } +/** Fully resolved node, pre-computed for rendering */ +export interface ProcessedMiniMessageNode { + text: string; + style: ResolvedStyle; + ngStyle: Record; + hoverTitle: string | null; + isUrlLink: boolean; + hasNonUrlClick: boolean; + clickEvent?: MiniMessageClickEvent; + insertion?: string; + extra: ProcessedMiniMessageNode[]; +} + /** Fully resolved style at a given node, after inheriting from all ancestors */ export interface ResolvedStyle { color?: string; diff --git a/frontend/src/app/pages/altitude/chat/mini-message/mini-message.util.ts b/frontend/src/app/pages/altitude/chat/mini-message/mini-message.util.ts index 6fd929f..d3ab3d1 100644 --- a/frontend/src/app/pages/altitude/chat/mini-message/mini-message.util.ts +++ b/frontend/src/app/pages/altitude/chat/mini-message/mini-message.util.ts @@ -1,4 +1,9 @@ -import { MiniMessageComponent, MiniMessageHoverEvent, ResolvedStyle } from './mini-message.types'; +import { + MiniMessageComponent, + MiniMessageHoverEvent, + ProcessedMiniMessageNode, + ResolvedStyle +} from './mini-message.types'; /** The 16 legacy Minecraft color names, as used by NamedTextColor / MiniMessage */ const NAMED_COLORS: Record = { @@ -132,3 +137,25 @@ export function hoverEventToTitle(hover: MiniMessageHoverEvent | undefined): str } return null; } + +/** + * Pre-computes a component tree into a ProcessedMiniMessageNode tree. + * This resolves all styles, texts, and attributes once so they don't have to be + * re-computed on every render. + */ +export function precomputeNode(node: MiniMessageComponent, parentStyle?: ResolvedStyle): ProcessedMiniMessageNode { + const style = mergeStyle(parentStyle, node); + const isUrlLink = node.clickEvent?.action === 'open_url'; + + return { + text: ownText(node), + style, + ngStyle: styleToNgStyle(style), + hoverTitle: hoverEventToTitle(node.hoverEvent), + isUrlLink, + hasNonUrlClick: !!node.clickEvent && !isUrlLink, + clickEvent: node.clickEvent, + insertion: node.insertion, + extra: (node.extra ?? []).map((child) => precomputeNode(child, style)), + }; +} 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 index aabfc32..d890398 100644 --- a/frontend/src/app/pages/altitude/chat/objects/chat-channel.object.ts +++ b/frontend/src/app/pages/altitude/chat/objects/chat-channel.object.ts @@ -2,4 +2,5 @@ export interface ChatChannel { name: string; type: 'SERVER' | 'DM' | 'PARTY' | 'GAC' | 'SPY'; unreadMessages: number; + totalMessages: 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 adeaa46..7d27131 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 @@ -1,4 +1,4 @@ -import {MiniMessageComponent} from '@pages/altitude/chat/mini-message/mini-message.types'; +import {ProcessedMiniMessageNode} from '@pages/altitude/chat/mini-message/mini-message.types'; export interface ChatMessage { uuid: string; @@ -7,7 +7,7 @@ export interface ChatMessage { type: string; channel: string; receiver: string; - messageJson: MiniMessageComponent; + messageJson: ProcessedMiniMessageNode; notBlocked: boolean; channelName?: string; channelType?: 'SERVER' | 'DM' | 'PARTY' | 'GAC' | 'SPY'; 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 0508940..840731c 100644 --- a/frontend/src/app/pages/altitude/chat/service/chat.service.ts +++ b/frontend/src/app/pages/altitude/chat/service/chat.service.ts @@ -4,6 +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 {precomputeNode} from '@pages/altitude/chat/mini-message/mini-message.util'; import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object'; import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service'; import {ChatInfoService} from '@api'; @@ -87,8 +88,8 @@ export class ChatService implements OnDestroy { const messages: ChatMessage[] = raw.map((m) => { const message: ChatMessage = { ...m, - messageJson: normalizeComponent(JSON.parse(m.messageJson)), - }; + messageJson: precomputeNode(normalizeComponent(JSON.parse(m.messageJson))), + } as ChatMessage; const channelKey = this.getChannelKey(message); if (channelKey === null) { return null; @@ -105,8 +106,11 @@ export class ChatService implements OnDestroy { this._channels.update((old) => [...old, { name: message.channelName!, type: message.channelType!, - unreadMessages: 0 + unreadMessages: 0, + totalMessages: 0 }]); + } else { + found.totalMessages++; } })