diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ChatController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ChatController.java index 709bed2..f69dab7 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ChatController.java +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ChatController.java @@ -2,7 +2,6 @@ package com.alttd.altitudeweb.controllers.chat; import com.alttd.altitudeweb.api.ChatApi; import com.alttd.altitudeweb.model.ChatMessageDto; -import com.alttd.altitudeweb.model.PlayerNameDto; import com.alttd.altitudeweb.model.ServerStateDto; import com.alttd.altitudeweb.services.chat.ChatService; import lombok.RequiredArgsConstructor; @@ -12,7 +11,6 @@ import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.RestController; import java.util.List; -import java.util.UUID; @Slf4j @RestController @@ -33,24 +31,4 @@ public class ChatController implements ChatApi { //TODO [Stijn] [2026-07-18]: Implement handling server state updates (push to listeners) throw new UnsupportedOperationException("Not implemented"); } - - @Override - public ResponseEntity getName(UUID uuid) { - return chatService.getPlayerName(uuid) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } - - @Override - public ResponseEntity getPartyName(String id) { - int partyId; - try { - partyId = Integer.parseInt(id); - } catch (NumberFormatException e) { - return ResponseEntity.badRequest().build(); - } - return chatService.getPartyName(partyId) - .map(ResponseEntity::ok) - .orElse(ResponseEntity.notFound().build()); - } } diff --git a/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ChatInfoController.java b/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ChatInfoController.java new file mode 100644 index 0000000..4a8a359 --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/controllers/chat/ChatInfoController.java @@ -0,0 +1,40 @@ +package com.alttd.altitudeweb.controllers.chat; + +import com.alttd.altitudeweb.api.ChatInfoApi; +import com.alttd.altitudeweb.model.PlayerNameDto; +import com.alttd.altitudeweb.services.chat.ChatInfoService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@Slf4j +@RestController +@RequiredArgsConstructor +public class ChatInfoController implements ChatInfoApi { + + private final ChatInfoService chatInfoService; + + @Override + public ResponseEntity getName(UUID uuid) { + return chatInfoService.getPlayerName(uuid) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + + @Override + public ResponseEntity getPartyName(String id) { + int partyId; + try { + partyId = Integer.parseInt(id); + } catch (NumberFormatException e) { + return ResponseEntity.badRequest().build(); + } + return chatInfoService.getPartyName(partyId) + .map(ResponseEntity::ok) + .orElse(ResponseEntity.notFound().build()); + } + +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/chat/ChatInfoService.java b/backend/src/main/java/com/alttd/altitudeweb/services/chat/ChatInfoService.java new file mode 100644 index 0000000..14675aa --- /dev/null +++ b/backend/src/main/java/com/alttd/altitudeweb/services/chat/ChatInfoService.java @@ -0,0 +1,44 @@ +package com.alttd.altitudeweb.services.chat; + +import com.alttd.altitudeweb.database.Databases; +import com.alttd.altitudeweb.database.chat.NicknameMapper; +import com.alttd.altitudeweb.database.chat.PartyMapper; +import com.alttd.altitudeweb.database.luckperms.UUIDUsernameMapper; +import com.alttd.altitudeweb.model.PlayerNameDto; +import com.alttd.altitudeweb.setup.Connection; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; + +import java.util.Optional; +import java.util.UUID; + +@Slf4j +@Service +@RequiredArgsConstructor +public class ChatInfoService { + + public Optional getPlayerName(UUID uuid) { + String username = Connection.getConnection(Databases.LUCK_PERMS) + .runQueryWithResult(sqlSession -> sqlSession.getMapper(UUIDUsernameMapper.class).getUsernameFromUUID(uuid.toString())); + + if (username == null) { + return Optional.empty(); + } + + Optional nickname = Connection.getConnection(Databases.CHAT) + .runQueryWithResult(sqlSession -> sqlSession.getMapper(NicknameMapper.class).getNicknameFromUUID(uuid.toString())); + + PlayerNameDto playerNameDto = new PlayerNameDto(); + playerNameDto.setName(username); + playerNameDto.setNickname(nickname.orElse(null)); + + return Optional.of(playerNameDto); + } + + public Optional getPartyName(int id) { + return Connection.getConnection(Databases.CHAT) + .runQueryWithResult(sqlSession -> sqlSession.getMapper(PartyMapper.class).getPartyName(id)); + } + +} diff --git a/backend/src/main/java/com/alttd/altitudeweb/services/chat/ChatService.java b/backend/src/main/java/com/alttd/altitudeweb/services/chat/ChatService.java index c177231..23707f6 100644 --- a/backend/src/main/java/com/alttd/altitudeweb/services/chat/ChatService.java +++ b/backend/src/main/java/com/alttd/altitudeweb/services/chat/ChatService.java @@ -4,11 +4,7 @@ import com.alttd.altitudeweb.controllers.chat.ChatMessage; import com.alttd.altitudeweb.controllers.chat.ChatMessageMapper; import com.alttd.altitudeweb.database.Databases; import com.alttd.altitudeweb.database.chat.ChatLogMapper; -import com.alttd.altitudeweb.database.chat.NicknameMapper; -import com.alttd.altitudeweb.database.chat.PartyMapper; -import com.alttd.altitudeweb.database.luckperms.UUIDUsernameMapper; import com.alttd.altitudeweb.model.PermissionClaimDto; -import com.alttd.altitudeweb.model.PlayerNameDto; import com.alttd.altitudeweb.services.chat.event_publisher.EventPublisher; import com.alttd.altitudeweb.services.chat.event_publisher.EventUser; import com.alttd.altitudeweb.setup.Connection; @@ -145,27 +141,4 @@ public class ChatService { return chatMessages.tailMap(instant, false).values().stream().toList(); } - public Optional getPlayerName(UUID uuid) { - String username = Connection.getConnection(Databases.LUCK_PERMS) - .runQueryWithResult(sqlSession -> sqlSession.getMapper(UUIDUsernameMapper.class).getUsernameFromUUID(uuid.toString())); - - if (username == null) { - return Optional.empty(); - } - - Optional nickname = Connection.getConnection(Databases.CHAT) - .runQueryWithResult(sqlSession -> sqlSession.getMapper(NicknameMapper.class).getNicknameFromUUID(uuid.toString())); - - PlayerNameDto playerNameDto = new PlayerNameDto(); - playerNameDto.setName(username); - playerNameDto.setNickname(nickname.orElse(null)); - - return Optional.of(playerNameDto); - } - - public Optional getPartyName(int id) { - return Connection.getConnection(Databases.CHAT) - .runQueryWithResult(sqlSession -> sqlSession.getMapper(PartyMapper.class).getPartyName(id)); - } - } 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 b7955b5..a216a5c 100644 --- a/frontend/src/app/pages/altitude/chat/service/chat.service.ts +++ b/frontend/src/app/pages/altitude/chat/service/chat.service.ts @@ -6,6 +6,7 @@ import {RawChatMessage} from '@pages/altitude/chat/objects/raw-chat-message.obje import {normalizeComponent} from '@pages/altitude/chat/mini-message/normalize.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'; interface SsePayloadEvent { data: string; @@ -16,11 +17,16 @@ export class ChatService implements OnDestroy { private eventSource?: EventSourcePolyfill; private readonly notificationService: NotificationService = inject(NotificationService); private readonly authService: AuthService = inject(AuthService) + private readonly chatInfoService: ChatInfoService = inject(ChatInfoService); private readonly _messages = signal([]) private readonly _channels = signal([]) public readonly channels = computed(() => this._channels().sort((a, b) => a.name.localeCompare(b.name))); private readonly _selectedChannel = signal(null); public readonly selectedChannel = this._selectedChannel.asReadonly() + public readonly partieMap = signal>(new Map()); + public readonly userNameMap = signal>(new Map()); + private readonly pendingPartyNameRequests = new Set(); + private readonly pendingUserNameRequests = new Set(); public readonly messages = computed(() => { const selected = this._selectedChannel(); @@ -109,18 +115,72 @@ export class ChatService implements OnDestroy { return {name: 'Global Admin Chat', type: 'GAC'}; } if (message.type === 'PARTY') { + if (!message.channelName) { + throw new Error('Party name is missing'); + } + this.updatePartyMap(message.channelName) return {name: message.channel, type: 'PARTY'}; } if (message.type === 'MSG') { const uuid = this.authService.getUuid(); if (message.uuid === uuid) { + this.updateUserNameMap(message.receiver) return {name: message.receiver, type: 'DM'}; } + this.updateUserNameMap(message.uuid) return {name: message.uuid, type: 'DM'}; } return {name: message.server, type: 'SERVER'}; } + public updatePartyMap(partieId: string): void { + if (this.partieMap().has(partieId) || this.pendingPartyNameRequests.has(partieId)) { + return; + } + + this.pendingPartyNameRequests.add(partieId); + + this.chatInfoService.getPartyName(partieId).subscribe({ + next: (partyName) => { + this.partieMap.update((old) => { + const newMap = new Map(old); + newMap.set(partieId, partyName); + return newMap; + }); + }, + error: (error) => { + console.error(`Failed to load party name for ${partieId}:`, error); + }, + complete: () => { + this.pendingPartyNameRequests.delete(partieId); + } + }); + } + + public updateUserNameMap(uuid: string): void { + if (this.userNameMap().has(uuid) || this.pendingUserNameRequests.has(uuid)) { + return; + } + + this.pendingUserNameRequests.add(uuid); + + this.chatInfoService.getName(uuid).subscribe({ + next: (userName) => { + this.userNameMap.update((old) => { + const newMap = new Map(old); + newMap.set(uuid, userName.nickname ?? userName.name); + return newMap; + }); + }, + error: (error) => { + console.error(`Failed to load username for ${uuid}:`, error); + }, + complete: () => { + this.pendingUserNameRequests.delete(uuid); + } + }); + } + public selectChannel(channel: ChatChannel) { this._selectedChannel.set(channel); this._channels.update((old) => diff --git a/open_api/src/main/resources/api.yml b/open_api/src/main/resources/api.yml index 34a50ff..14c9b80 100644 --- a/open_api/src/main/resources/api.yml +++ b/open_api/src/main/resources/api.yml @@ -30,6 +30,8 @@ tags: description: All actions related to user email verification - name: site description: Actions related to small features on the site such as displaying vote stats or pt/rank stats + - name: chatInfo + description: All actions related to chat information paths: /api/team/{team}: $ref: './schemas/team/team.yml#/getTeam' @@ -97,3 +99,7 @@ paths: $ref: './schemas/site/vote.yml#/VoteStats' /api/site/get-staff-playtime/{from}/{to}: $ref: './schemas/site/staff_pt.yml#/GetStaffPlaytime' + /api/site/chat/party/{id}/name: + $ref: './schemas/chat_info/chat_info.yml#/PartyName' + /api/site/chat/player/{uuid}/name: + $ref: './schemas/chat_info/chat_info.yml#/UserName' diff --git a/open_api/src/main/resources/chat-api.yml b/open_api/src/main/resources/chat-api.yml index 3fc474b..8f77bc0 100644 --- a/open_api/src/main/resources/chat-api.yml +++ b/open_api/src/main/resources/chat-api.yml @@ -45,64 +45,8 @@ paths: responses: "202": description: Accepted - - /chat/player/{uuid}/name: - get: - tags: - - chat - summary: Gets the name, and nickname of the player associated with that uuid - operationId: getName - parameters: - - name: uuid - in: path - required: true - schema: - type: string - format: uuid - responses: - "200": - description: The player's name and nickname - content: - application/json: - schema: - $ref: "#/components/schemas/PlayerName" - "404": - description: Player not found - - /chat/party/{id}/name: - get: - tags: - - chat - summary: Gets the name, of the party based on the id - operationId: getPartyName - parameters: - - name: id - in: path - required: true - schema: - type: string - responses: - "200": - description: The party name - content: - application/json: - schema: - type: string - "400": - description: Invalid ID - "404": - description: Party not found components: schemas: - PlayerName: - type: object - required: - - name - properties: - name: - type: string - nickname: - type: string User: type: object required: diff --git a/open_api/src/main/resources/schemas/chat_info/chat_info.yml b/open_api/src/main/resources/schemas/chat_info/chat_info.yml new file mode 100644 index 0000000..f272fe0 --- /dev/null +++ b/open_api/src/main/resources/schemas/chat_info/chat_info.yml @@ -0,0 +1,57 @@ +UserName: + get: + tags: + - chatInfo + summary: Gets the name, and nickname of the player associated with that uuid + operationId: getName + parameters: + - name: uuid + in: path + required: true + schema: + type: string + format: uuid + responses: + "200": + description: The player's name and nickname + content: + application/json: + schema: + $ref: "#/components/schemas/PlayerName" + "404": + description: Player not found + +PartyName: + get: + tags: + - chatInfo + summary: Gets the name, of the party based on the id + operationId: getPartyName + parameters: + - name: id + in: path + required: true + schema: + type: string + responses: + "200": + description: The party name + content: + application/json: + schema: + type: string + "400": + description: Invalid ID + "404": + description: Party not found +components: + schemas: + PlayerName: + type: object + required: + - name + properties: + name: + type: string + nickname: + type: string