Refactor user name display logic: integrate MiniMessageComponent in UserListComponent and transition to ProcessedMiniMessageNode. Add server state caching in ChatService with eager state dispatch on subscription.

This commit is contained in:
akastijn 2026-08-09 02:04:11 +02:00
parent 8c02543bca
commit 1a0bc902ac
4 changed files with 45 additions and 19 deletions

View File

@ -37,6 +37,7 @@ public class ChatService {
private final EventPublisher eventPublisher; private final EventPublisher eventPublisher;
private final NavigableMap<Instant, ChatMessage> chatMessages = new TreeMap<>(); private final NavigableMap<Instant, ChatMessage> chatMessages = new TreeMap<>();
private final Map<String, EventUser> eventUserMap = new HashMap<>(); private final Map<String, EventUser> eventUserMap = new HashMap<>();
private final Map<String, ServerDto> serverStateCache = new HashMap<>();
@Value("${chat.allowed-servers}") @Value("${chat.allowed-servers}")
private String[] allowedServers; private String[] allowedServers;
@ -73,7 +74,9 @@ public class ChatService {
public SseEmitter subscribe(EventUser eventUser, String json) { public SseEmitter subscribe(EventUser eventUser, String json) {
String key = eventUser.uuid().toString(); String key = eventUser.uuid().toString();
eventUserMap.put(key, eventUser); eventUserMap.put(key, eventUser);
return eventPublisher.subscribe(key, json, this::handleSessionEnd); SseEmitter emitter = eventPublisher.subscribe(key, json, this::handleSessionEnd);
sendServerStateToUser(key, eventUser);
return emitter;
} }
private void handleSessionEnd(String key, Instant sessionStart, Instant sessionEnd) { private void handleSessionEnd(String key, Instant sessionStart, Instant sessionEnd) {
@ -124,18 +127,26 @@ public class ChatService {
sendMessagesToListeners(chatMessageList); sendMessagesToListeners(chatMessageList);
} }
public void updateServerState(ServerStateDto serverStateDto) { public synchronized void updateServerState(ServerStateDto serverStateDto) {
sendToUsers("server-state", (eventUser) -> { serverStateDto.getServers().forEach(serverDto -> serverStateCache.put(serverDto.getName(), serverDto));
if (eventUser.hasPermission(PermissionClaimDto.HEAD_MOD)) { sendToUsers("server-state", this::getServerStateJson);
return ServerStateMapper.toJson(serverStateDto); }
}
List<ServerDto> filteredServers = serverStateDto.getServers().stream() private String getServerStateJson(EventUser eventUser) {
.filter(serverDto -> Arrays.asList(allowedServers).contains(serverDto.getName())) if (eventUser.hasPermission(PermissionClaimDto.HEAD_MOD)) {
.toList(); return ServerStateMapper.toJson(new ServerStateDto().servers(serverStateCache.values().stream().toList()));
}
return ServerStateMapper.toJson(new ServerStateDto().servers(filteredServers)); List<ServerDto> filteredServers = serverStateCache.values().stream()
}); .filter(serverDto -> Arrays.asList(allowedServers).contains(serverDto.getName()))
.toList();
return ServerStateMapper.toJson(new ServerStateDto().servers(filteredServers));
}
private void sendServerStateToUser(String key, EventUser eventUser) {
String json = getServerStateJson(eventUser);
eventPublisher.sendToUser(key, "server-state", json);
} }
private void sendMessagesToListeners(List<ChatMessage> chatMessageList) { private void sendMessagesToListeners(List<ChatMessage> chatMessageList) {

View File

@ -5,9 +5,7 @@
@for (player of server.players; track player.uuid) { @for (player of server.players; track player.uuid) {
<div class="user indented"> <div class="user indented">
<span class="user-name"> <span class="user-name">
@for (segment of getDisplayName(player); track $index) { <mini-message [node]="getDisplayName(player)"></mini-message>
<span [style.color]="segment.color">{{ segment.text }}</span>
}
</span> </span>
</div> </div>
} }

View File

@ -1,13 +1,14 @@
import {Component, computed, inject} from '@angular/core'; import {Component, computed, inject} from '@angular/core';
import {ChatService} from '@pages/altitude/chat/service/chat.service'; import {ChatService} from '@pages/altitude/chat/service/chat.service';
import {NameFormatService} from '@pages/altitude/chat/service/name-format.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'; import {User} from '@pages/altitude/chat/objects/server-state.object';
import {ProcessedMiniMessageNode} from '@pages/altitude/chat/mini-message/mini-message.types';
import {MiniMessageComponent} from '@pages/altitude/chat/components/mini-message/mini-message.component';
@Component({ @Component({
selector: 'app-user-list', selector: 'app-user-list',
standalone: true, standalone: true,
imports: [], imports: [MiniMessageComponent],
templateUrl: './user-list.component.html', templateUrl: './user-list.component.html',
styleUrl: './user-list.component.scss' styleUrl: './user-list.component.scss'
}) })
@ -29,7 +30,7 @@ export class UserListComponent {
}; };
}); });
protected getDisplayName(user: User): ColoredNameSegment[] { protected getDisplayName(user: User): ProcessedMiniMessageNode {
return this.nameFormatService.parseColoredName(user.styledName || user.name); return this.nameFormatService.getUserDisplayName(user);
} }
} }

View File

@ -2,6 +2,10 @@ import {inject, Injectable} from '@angular/core';
import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object'; import {ChatChannel} from '@pages/altitude/chat/objects/chat-channel.object';
import {ColoredNameSegment} from '@pages/altitude/chat/objects/colored-name-segment.object'; import {ColoredNameSegment} from '@pages/altitude/chat/objects/colored-name-segment.object';
import {ChatService} from "./chat.service"; import {ChatService} from "./chat.service";
import {User} from '@pages/altitude/chat/objects/server-state.object';
import {normalizeComponent} from '@pages/altitude/chat/mini-message/normalize.util';
import {precomputeNode} from '@pages/altitude/chat/mini-message/mini-message.util';
import {ProcessedMiniMessageNode} from '@pages/altitude/chat/mini-message/mini-message.types';
@Injectable({providedIn: 'root'}) @Injectable({providedIn: 'root'})
export class NameFormatService { export class NameFormatService {
@ -12,6 +16,18 @@ export class NameFormatService {
return this.parseColoredName(this.getNameFromType(channel)); return this.parseColoredName(this.getNameFromType(channel));
} }
public getUserDisplayName(user: User): ProcessedMiniMessageNode {
if (user.styledName) {
try {
return precomputeNode(normalizeComponent(JSON.parse(user.styledName)));
} catch {
return precomputeNode(normalizeComponent({text: user.name}));
}
}
return precomputeNode(normalizeComponent({text: user.name}));
}
private readonly LEGACY_COLOR_MAP: Record<string, string> = { private readonly LEGACY_COLOR_MAP: Record<string, string> = {
'4': '#AA0000', '4': '#AA0000',
'c': '#FF5555', 'c': '#FF5555',
@ -44,7 +60,7 @@ export class NameFormatService {
return channel.name; return channel.name;
} }
public parseColoredName(name: string): ColoredNameSegment[] { private parseColoredName(name: string): ColoredNameSegment[] {
const markerRegex = /\{#([0-9a-fA-F]{6})(<>|[<>]?)\}|&([0-9a-fA-F])/g; const markerRegex = /\{#([0-9a-fA-F]{6})(<>|[<>]?)\}|&([0-9a-fA-F])/g;
const segments: ColoredNameSegment[] = []; const segments: ColoredNameSegment[] = [];
let currentColor: string | undefined; let currentColor: string | undefined;