using AutoMapper; using LANCommander.SDK.Hubs; using LANCommander.SDK.Models; using LANCommander.Server.Services; using Microsoft.AspNetCore.SignalR; using ZiggyCreatures.Caching.Fusion; namespace LANCommander.Server.Hubs; public class ChatHub( IFusionCache cache, IMapper mapper, ILogger logger, ChatService chatService) : Hub, IChatHub { private string GetThreadParticipantCacheKey(Guid threadId) => $"Chat/Thread/{threadId}/Participants"; private async Task> GetThreadParticipants(Guid threadId) { logger.LogDebug("Getting thread participants for {ThreadId}", threadId); var cacheKey = GetThreadParticipantCacheKey(threadId); var participants = await cache.TryGetAsync>(cacheKey); return participants.GetValueOrDefault([]); } public async Task StartThreadAsync(string[] userIdentifiers) { logger.LogDebug("Starting new thread with {ParticipantCount} participants", userIdentifiers.Length); var thread = await chatService.StartThreadAsync(); foreach (var userIdentifier in userIdentifiers) await AddParticipantAsync(thread.Id, userIdentifier); logger.LogDebug("Created new thread with ID {ThreadId}", thread.Id); return thread.Id; } public async Task AddParticipantAsync(Guid threadId, string participantId) { var cacheKey = GetThreadParticipantCacheKey(threadId); var participants = await cache.TryGetAsync>(cacheKey); if (!participants.HasValue || !participants.Value.Contains(participantId)) { if (Guid.TryParse(participantId, out var userId)) { await chatService.AddParticipantAsync(threadId, userId); // Get the updated thread and notify the added participant var thread = await chatService.GetThreadAsync(threadId); if (thread != null) { await Clients.User(participantId).AddedToThreadAsync(mapper.Map(thread)); } } if (!participants.HasValue) participants = new List(); participants.Value.Add(participantId); await cache.SetAsync(cacheKey, participants.Value); } } public async Task GetThreadAsync(Guid threadId) { var thread = await chatService.GetThreadAsync(threadId); return mapper.Map(thread); } public async Task> GetThreadsAsync() { if (Guid.TryParse(Context.UserIdentifier, out var userId)) { var threads = await chatService.GetThreadsAsync(userId); // Ensure participants are loaded for all threads var threadsWithParticipants = new List(); foreach (var thread in threads) { Data.Models.ChatThread threadToMap; // If participants are missing, reload the thread with participants included if (thread.Participants == null || thread.Participants.Count == 0) { var threadWithParticipants = await chatService.GetThreadAsync(thread.Id); threadToMap = threadWithParticipants ?? thread; } else { threadToMap = thread; } threadsWithParticipants.Add(threadToMap); // Populate participant cache var cacheKey = GetThreadParticipantCacheKey(thread.Id); var participants = await cache.TryGetAsync>(cacheKey); if (!participants.HasValue && threadToMap.Participants != null) await cache.SetAsync(cacheKey, threadToMap.Participants.Select(p => p.Id.ToString()).ToList()); } return mapper.Map>(threadsWithParticipants); } return []; } public async Task SendMessageAsync(Guid threadId, string content) { var message = await chatService.SendMessageAsync(threadId, content); var participants = await GetThreadParticipants(threadId); // Update read status for the sender (they've read their own message) if (Guid.TryParse(Context.UserIdentifier, out var userId)) { await chatService.UpdateReadStatus(threadId, userId); } await Clients.Users(participants).ReceiveMessageAsync(threadId, mapper.Map(message)); } public async Task> GetMessagesAsync(Guid threadId, Guid? cursor, int? count) => await chatService.GetMessagesAsync(threadId, count, cursor); public async Task StartTypingAsync(Guid threadId) { var participants = await GetThreadParticipants(threadId); await Clients.Users(participants).StartTypingAsync(threadId, Context.UserIdentifier); } public async Task StopTypingAsync(Guid threadId) { var participants = await GetThreadParticipants(threadId); await Clients.Users(participants).StopTypingAsync(threadId, Context.UserIdentifier); } public async Task UpdateReadStatusAsync(Guid threadId) { if (Guid.TryParse(Context.UserIdentifier, out var userId)) await chatService.UpdateReadStatus(threadId, userId); } public async Task GetUnreadMessageCountAsync(Guid threadId) { if (Guid.TryParse(Context.UserIdentifier, out var userId)) return await chatService.GetUnreadMessageCountAsync(threadId, userId); return 0; } public async Task> GetUsersAsync() { var users = await chatService.GetUsersAsync(); return mapper.Map>(users); } }