@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++;
}
})