Compare commits
No commits in common. "90eb40caa7f9a097bbf6d74ee46510e559b73ee3" and "819739ff609b2e60269471d85e059ddb1e8e4628" have entirely different histories.
90eb40caa7
...
819739ff60
|
|
@ -1,11 +1,10 @@
|
||||||
@if (selectedChannel(); as chatChannel) {
|
@if (selectedChannel(); as chatChannel) {
|
||||||
<div class="full-width"
|
<div class="full-width"
|
||||||
[style.background-color]="getServerColor(chatChannel.name)">
|
[style.background-color]="getServerColor(chatChannel.name ?? '')">
|
||||||
<span class="channel" [style.color]="getServerTextColor(chatChannel.name)">
|
<span class="channel" [style.color]="getServerTextColor(chatChannel.name ?? '')">
|
||||||
@for (segment of getDisplayName(chatChannel); track $index) {
|
@for (segment of getDisplayName(chatChannel); track $index) {
|
||||||
<span [style.color]="segment.color">{{ segment.text }}</span>
|
<span [style.color]="segment.color">{{ segment.text }}</span>
|
||||||
}
|
}
|
||||||
<span class="message-count">({{ chatChannel.totalMessages }})</span>
|
|
||||||
</span>
|
</span>
|
||||||
<div class="notification-icon">
|
<div class="notification-icon">
|
||||||
@if (notificationEnabled()) {
|
@if (notificationEnabled()) {
|
||||||
|
|
|
||||||
|
|
@ -13,10 +13,6 @@
|
||||||
letter-spacing: 1px;
|
letter-spacing: 1px;
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
|
|
||||||
.message-count {
|
|
||||||
padding-left: 5px;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.notification-icon {
|
.notification-icon {
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,7 @@
|
||||||
<span class="date-time" [class.blocked]="!message.notBlocked">
|
<span class="date-time" [class.blocked]="!message.notBlocked">
|
||||||
[{{ message.timestamp | date: 'mediumTime' }}]
|
[{{ message.timestamp | date: 'mediumTime' }}]
|
||||||
</span>
|
</span>
|
||||||
@defer (on viewport; prefetch on idle) {
|
<mini-message [node]="message.messageJson"></mini-message>
|
||||||
<mini-message [node]="message.messageJson"></mini-message>
|
|
||||||
} @placeholder {
|
|
||||||
<span>…</span>
|
|
||||||
}
|
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,28 +1,31 @@
|
||||||
@if (node) {
|
@if (node) {
|
||||||
@if (node.isUrlLink) {
|
<ng-container>
|
||||||
<a
|
<a
|
||||||
[style]="node.ngStyle"
|
*ngIf="isUrlLink; else plain"
|
||||||
[mmObfuscated]="node.style.obfuscated"
|
[ngStyle]="ngStyle"
|
||||||
[attr.title]="node.hoverTitle"
|
[mmObfuscated]="style.obfuscated"
|
||||||
|
[attr.title]="hoverTitle"
|
||||||
[attr.href]="node.clickEvent?.value"
|
[attr.href]="node.clickEvent?.value"
|
||||||
[attr.data-insertion]="node.insertion || null"
|
[attr.data-insertion]="node.insertion || null"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="mm-node mm-link"
|
class="mm-node mm-link"
|
||||||
>{{ node.text }}</a>
|
>{{ text }}</a>
|
||||||
} @else {
|
|
||||||
<span
|
|
||||||
[style]="node.ngStyle"
|
|
||||||
[mmObfuscated]="node.style.obfuscated"
|
|
||||||
[attr.title]="node.hoverTitle"
|
|
||||||
[attr.data-insertion]="node.insertion || null"
|
|
||||||
[class.mm-clickable]="node.hasNonUrlClick"
|
|
||||||
class="mm-node"
|
|
||||||
(click)="onClick()"
|
|
||||||
>{{ node.text }}</span>
|
|
||||||
}
|
|
||||||
|
|
||||||
@for (child of node.extra; track child) {
|
<ng-template #plain>
|
||||||
<mini-message [node]="child"></mini-message>
|
<span
|
||||||
}
|
[ngStyle]="ngStyle"
|
||||||
|
[mmObfuscated]="style.obfuscated"
|
||||||
|
[attr.title]="hoverTitle"
|
||||||
|
[attr.data-insertion]="node.insertion || null"
|
||||||
|
[class.mm-clickable]="hasNonUrlClick"
|
||||||
|
class="mm-node"
|
||||||
|
(click)="onClick()"
|
||||||
|
>{{ text }}</span
|
||||||
|
>
|
||||||
|
</ng-template>
|
||||||
|
@for (child of node.extra; track child) {
|
||||||
|
<mini-message [node]="child" [parentStyle]="style"></mini-message>
|
||||||
|
}
|
||||||
|
</ng-container>
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,7 @@
|
||||||
import {CommonModule} from '@angular/common';
|
import {CommonModule} from '@angular/common';
|
||||||
import {Component, Input, Optional} from '@angular/core';
|
import {Component, Input, Optional} from '@angular/core';
|
||||||
import {ProcessedMiniMessageNode} from '../../mini-message/mini-message.types';
|
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 {ObfuscatedDirective} from '../../mini-message/obfuscated.directive';
|
||||||
import {MiniMessageInteractionService} from '../../mini-message/mini-message-interaction.service';
|
import {MiniMessageInteractionService} from '../../mini-message/mini-message-interaction.service';
|
||||||
|
|
||||||
|
|
@ -9,7 +10,7 @@ import {MiniMessageInteractionService} from '../../mini-message/mini-message-int
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* providers: [MiniMessageInteractionService] // once, at the root that hosts <mini-message>
|
* providers: [MiniMessageInteractionService] // once, at the root that hosts <mini-message>
|
||||||
* <mini-message [node]="myProcessedComponentJson"></mini-message>
|
* <mini-message [node]="myComponentJson"></mini-message>
|
||||||
*
|
*
|
||||||
* Then subscribe to click events (run_command / suggest_command / copy_to_clipboard /
|
* Then subscribe to click events (run_command / suggest_command / copy_to_clipboard /
|
||||||
* change_page) via the injected MiniMessageInteractionService.clicks$; open_url is handled
|
* change_page) via the injected MiniMessageInteractionService.clicks$; open_url is handled
|
||||||
|
|
@ -23,17 +24,42 @@ import {MiniMessageInteractionService} from '../../mini-message/mini-message-int
|
||||||
styleUrl: './mini-message.component.scss',
|
styleUrl: './mini-message.component.scss',
|
||||||
})
|
})
|
||||||
export class MiniMessageComponent {
|
export class MiniMessageComponent {
|
||||||
@Input() node!: ProcessedMiniMessageNode;
|
@Input() node!: MiniMessageNode;
|
||||||
|
@Input() parentStyle?: ResolvedStyle;
|
||||||
|
|
||||||
constructor(@Optional() private readonly interaction: MiniMessageInteractionService | null) {
|
constructor(@Optional() private readonly interaction: MiniMessageInteractionService | null) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get style(): ResolvedStyle {
|
||||||
|
return mergeStyle(this.parentStyle, this.node);
|
||||||
|
}
|
||||||
|
|
||||||
|
get ngStyle(): Record<string, string> {
|
||||||
|
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 {
|
onClick(): void {
|
||||||
const click = this.node.clickEvent;
|
const click = this.node.clickEvent;
|
||||||
if (!click) return;
|
if (!click) return;
|
||||||
|
|
||||||
if (click.action === 'copy_to_clipboard') {
|
if (click.action === 'copy_to_clipboard') {
|
||||||
navigator.clipboard?.writeText(click.value).then();
|
navigator.clipboard?.writeText(click.value);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.interaction?.emitClick({action: click.action, value: click.value});
|
this.interaction?.emitClick({action: click.action, value: click.value});
|
||||||
|
|
|
||||||
|
|
@ -73,19 +73,6 @@ export interface MiniMessageComponent {
|
||||||
extra?: MiniMessageComponent[];
|
extra?: MiniMessageComponent[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Fully resolved node, pre-computed for rendering */
|
|
||||||
export interface ProcessedMiniMessageNode {
|
|
||||||
text: string;
|
|
||||||
style: ResolvedStyle;
|
|
||||||
ngStyle: Record<string, string>;
|
|
||||||
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 */
|
/** Fully resolved style at a given node, after inheriting from all ancestors */
|
||||||
export interface ResolvedStyle {
|
export interface ResolvedStyle {
|
||||||
color?: string;
|
color?: string;
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,4 @@
|
||||||
import {
|
import { MiniMessageComponent, MiniMessageHoverEvent, ResolvedStyle } from './mini-message.types';
|
||||||
MiniMessageComponent,
|
|
||||||
MiniMessageHoverEvent,
|
|
||||||
ProcessedMiniMessageNode,
|
|
||||||
ResolvedStyle
|
|
||||||
} from './mini-message.types';
|
|
||||||
|
|
||||||
/** The 16 legacy Minecraft color names, as used by NamedTextColor / MiniMessage */
|
/** The 16 legacy Minecraft color names, as used by NamedTextColor / MiniMessage */
|
||||||
const NAMED_COLORS: Record<string, string> = {
|
const NAMED_COLORS: Record<string, string> = {
|
||||||
|
|
@ -137,25 +132,3 @@ export function hoverEventToTitle(hover: MiniMessageHoverEvent | undefined): str
|
||||||
}
|
}
|
||||||
return null;
|
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)),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -2,5 +2,4 @@ export interface ChatChannel {
|
||||||
name: string;
|
name: string;
|
||||||
type: 'SERVER' | 'DM' | 'PARTY' | 'GAC' | 'SPY';
|
type: 'SERVER' | 'DM' | 'PARTY' | 'GAC' | 'SPY';
|
||||||
unreadMessages: number;
|
unreadMessages: number;
|
||||||
totalMessages: number;
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import {ProcessedMiniMessageNode} from '@pages/altitude/chat/mini-message/mini-message.types';
|
import {MiniMessageComponent} from '@pages/altitude/chat/mini-message/mini-message.types';
|
||||||
|
|
||||||
export interface ChatMessage {
|
export interface ChatMessage {
|
||||||
uuid: string;
|
uuid: string;
|
||||||
|
|
@ -7,7 +7,7 @@ export interface ChatMessage {
|
||||||
type: string;
|
type: string;
|
||||||
channel: string;
|
channel: string;
|
||||||
receiver: string;
|
receiver: string;
|
||||||
messageJson: ProcessedMiniMessageNode;
|
messageJson: MiniMessageComponent;
|
||||||
notBlocked: boolean;
|
notBlocked: boolean;
|
||||||
channelName?: string;
|
channelName?: string;
|
||||||
channelType?: 'SERVER' | 'DM' | 'PARTY' | 'GAC' | 'SPY';
|
channelType?: 'SERVER' | 'DM' | 'PARTY' | 'GAC' | 'SPY';
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import {EventSourcePolyfill} from 'event-source-polyfill';
|
||||||
import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object';
|
import {ChatMessage} from '@pages/altitude/chat/objects/chat-message.object';
|
||||||
import {RawChatMessage} from '@pages/altitude/chat/objects/raw-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 {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 {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
|
||||||
import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service';
|
import {NotificationService} from '@pages/altitude/chat/service/chat-notification.service';
|
||||||
import {ChatInfoService} from '@api';
|
import {ChatInfoService} from '@api';
|
||||||
|
|
@ -88,8 +87,8 @@ export class ChatService implements OnDestroy {
|
||||||
const messages: ChatMessage[] = raw.map((m) => {
|
const messages: ChatMessage[] = raw.map((m) => {
|
||||||
const message: ChatMessage = {
|
const message: ChatMessage = {
|
||||||
...m,
|
...m,
|
||||||
messageJson: precomputeNode(normalizeComponent(JSON.parse(m.messageJson))),
|
messageJson: normalizeComponent(JSON.parse(m.messageJson)),
|
||||||
} as ChatMessage;
|
};
|
||||||
const channelKey = this.getChannelKey(message);
|
const channelKey = this.getChannelKey(message);
|
||||||
if (channelKey === null) {
|
if (channelKey === null) {
|
||||||
return null;
|
return null;
|
||||||
|
|
@ -106,11 +105,8 @@ export class ChatService implements OnDestroy {
|
||||||
this._channels.update((old) => [...old, {
|
this._channels.update((old) => [...old, {
|
||||||
name: message.channelName!,
|
name: message.channelName!,
|
||||||
type: message.channelType!,
|
type: message.channelType!,
|
||||||
unreadMessages: 0,
|
unreadMessages: 0
|
||||||
totalMessages: 0
|
|
||||||
}]);
|
}]);
|
||||||
} else {
|
|
||||||
found.totalMessages++;
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user