-
- {{ selectedChannel()?.name }}
-
-
- @if (notificationEnabled()) {
-
- } @else {
-
+@if (selectedChannel(); as chatChannel) {
+
+
+ @for (segment of getDisplayName(chatChannel); track $index) {
+ {{ segment.text }}
}
+
+
+ @if (notificationEnabled()) {
+
+ } @else {
+
+ }
+
-
+}
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 58fe6d8..8a69c21 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
@@ -4,6 +4,8 @@ import {NotificationService} from '@pages/altitude/chat/service/chat-notificatio
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';
+import {ColoredNameSegment} from '@pages/altitude/chat/objects/colored-name-segment.object';
+import {NameFormatService} from '@pages/altitude/chat/service/name-format.service';
@Component({
selector: 'app-chat-info',
@@ -16,6 +18,7 @@ import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
})
export class ChatInfoComponent {
private readonly notificationService: NotificationService = inject(NotificationService);
+ private readonly nameFormatService: NameFormatService = inject(NameFormatService);
public readonly selectedChannel = input.required
();
protected readonly channelKey = computed(() => {
@@ -34,4 +37,8 @@ export class ChatInfoComponent {
this.notificationService.disableNotifications(this.channelKey());
}
+ protected getDisplayName(chatChannel: ChatChannel): ColoredNameSegment[] {
+ return this.nameFormatService.getDisplayName(chatChannel);
+ }
+
}
diff --git a/frontend/src/app/pages/altitude/chat/objects/colored-name-segment.object.ts b/frontend/src/app/pages/altitude/chat/objects/colored-name-segment.object.ts
new file mode 100644
index 0000000..9313980
--- /dev/null
+++ b/frontend/src/app/pages/altitude/chat/objects/colored-name-segment.object.ts
@@ -0,0 +1,4 @@
+export interface ColoredNameSegment {
+ text: string;
+ color?: string;
+}
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
new file mode 100644
index 0000000..cd6d966
--- /dev/null
+++ b/frontend/src/app/pages/altitude/chat/service/name-format.service.ts
@@ -0,0 +1,131 @@
+import {inject, Injectable} from '@angular/core';
+import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
+import {ColoredNameSegment} from '@pages/altitude/chat/objects/colored-name-segment.object';
+import {ChatService} from "./chat.service";
+
+@Injectable({providedIn: 'root'})
+export class NameFormatService {
+
+ private readonly chatService: ChatService = inject(ChatService);
+
+ public getDisplayName(channel: ChatChannel): ColoredNameSegment[] {
+ return this.parseColoredName(this.getNameFromType(channel));
+ }
+
+ private getNameFromType(channel: ChatChannel): string {
+ if (channel.type === 'PARTY') {
+ return this.chatService.partieMap().get(channel.name) ?? channel.name;
+ }
+ if (channel.type === 'DM' || channel.type === 'SPY') {
+ return this.chatService.userNameMap().get(channel.name) ?? channel.name;
+ }
+ return channel.name;
+ }
+
+ private parseColoredName(name: string): ColoredNameSegment[] {
+ const markerRegex = /\{#([0-9a-fA-F]{6})(<>|[<>]?)}/g;
+ const segments: ColoredNameSegment[] = [];
+ let currentColor: string | undefined;
+ let gradientStartColor: string | undefined;
+ let gradientStartSegmentIndex: number | undefined;
+ let lastIndex = 0;
+ let match: RegExpExecArray | null;
+
+ while ((match = markerRegex.exec(name)) !== null) {
+ this.addColoredTextSegment(segments, name.slice(lastIndex, match.index), currentColor);
+
+ const markerColor = `#${match[1]}`;
+ const markerType = match[2];
+
+ if (markerType === '>') {
+ currentColor = markerColor;
+ gradientStartColor = markerColor;
+ gradientStartSegmentIndex = segments.length;
+ } else if (markerType === '<') {
+ if (gradientStartColor !== undefined && gradientStartSegmentIndex !== undefined) {
+ this.applyGradient(segments, gradientStartSegmentIndex, gradientStartColor, markerColor);
+ }
+ currentColor = markerColor;
+ gradientStartColor = undefined;
+ gradientStartSegmentIndex = undefined;
+ } else if (markerType === '<>') {
+ if (gradientStartColor !== undefined && gradientStartSegmentIndex !== undefined) {
+ this.applyGradient(segments, gradientStartSegmentIndex, gradientStartColor, markerColor);
+ }
+ currentColor = markerColor;
+ gradientStartColor = markerColor;
+ gradientStartSegmentIndex = segments.length;
+ } else {
+ currentColor = markerColor;
+ gradientStartColor = undefined;
+ gradientStartSegmentIndex = undefined;
+ }
+
+ lastIndex = markerRegex.lastIndex;
+ }
+
+ this.addColoredTextSegment(segments, name.slice(lastIndex), currentColor);
+
+ return segments.length > 0 ? segments : [{text: name}];
+ }
+
+ private addColoredTextSegment(segments: ColoredNameSegment[], text: string, color?: string): void {
+ if (text.length === 0) {
+ return;
+ }
+
+ segments.push({text, color});
+ }
+
+ private applyGradient(
+ segments: ColoredNameSegment[],
+ startSegmentIndex: number,
+ startColor: string,
+ endColor: string
+ ): void {
+ const text = segments.slice(startSegmentIndex).map((segment) => segment.text).join('');
+ const length = text.length;
+ const start = this.hexToRgb(startColor);
+ const end = this.hexToRgb(endColor);
+
+ if (length === 0 || start === null || end === null) {
+ return;
+ }
+
+ const gradientSegments: ColoredNameSegment[] = [];
+
+ for (let i = 0; i < length; i++) {
+ const ratio = length === 1 ? 0 : i / (length - 1);
+ gradientSegments.push({
+ text: text[i],
+ color: this.rgbToHex(
+ start.r + (end.r - start.r) * ratio,
+ start.g + (end.g - start.g) * ratio,
+ start.b + (end.b - start.b) * ratio
+ )
+ });
+ }
+
+ segments.splice(startSegmentIndex, segments.length - startSegmentIndex, ...gradientSegments);
+ }
+
+ private hexToRgb(hex: string): { r: number; g: number; b: number } | null {
+ const match = /^#?([0-9a-fA-F]{6})$/.exec(hex);
+ if (!match) {
+ return null;
+ }
+
+ const value = Number.parseInt(match[1], 16);
+ return {
+ r: (value >> 16) & 255,
+ g: (value >> 8) & 255,
+ b: value & 255
+ };
+ }
+
+ private rgbToHex(r: number, g: number, b: number): string {
+ return `#${[r, g, b]
+ .map((value) => Math.round(value).toString(16).padStart(2, '0'))
+ .join('')}`;
+ }
+}
diff --git a/frontend/src/app/pages/altitude/chat/util/colored-name-segment.ts b/frontend/src/app/pages/altitude/chat/util/colored-name-segment.ts
new file mode 100644
index 0000000..e69de29
diff --git a/frontend/src/app/pages/altitude/chat/util/server-color.ts b/frontend/src/app/pages/altitude/chat/util/server-color.ts
index 1a36376..035e9c3 100644
--- a/frontend/src/app/pages/altitude/chat/util/server-color.ts
+++ b/frontend/src/app/pages/altitude/chat/util/server-color.ts
@@ -6,7 +6,7 @@ export function getServerColor(server: string) {
} else if (server === 'creative') {
return "#FFA500";
}
- return "white";
+ return "#0290e8";
}
export function getServerTextColor(server: string) {