using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Domain; using Knot.Contracts.Conversations.Application.Abstractions; using Knot.Modules.Conversations.Application.DTOs; using Knot.Contracts.Conversations.Domain; using Knot.Shared.Kernel; using MediatR; namespace Knot.Modules.Conversations.Application.Messages.GetMessages; public record GetMessagesQuery(Guid UserId, Guid ChatId, string? Cursor, long? Pivot = null, int? Limit = null) : IQuery>; internal sealed class GetMessagesQueryHandler : IQueryHandler> { private readonly IMessageRepository _messageRepository; private readonly IUserDisplayNameProvider _userProvider; private readonly IChatRepository _chatRepository; private readonly IMessageReactionRepository _reactionRepository; public GetMessagesQueryHandler(IMessageRepository messageRepository, IUserDisplayNameProvider userProvider, IChatRepository chatRepository, IMessageReactionRepository reactionRepository) { _messageRepository = messageRepository; _userProvider = userProvider; _chatRepository = chatRepository; _reactionRepository = reactionRepository; } public async Task>> Handle(GetMessagesQuery request, CancellationToken cancellationToken) { var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken); if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) { return Result.Failure>(ChatErrors.ChatsForbidden); } List messages; int queryLimit = request.Limit ?? ChatConstants.DefaultMessageQueryLimit; if (request.Pivot.HasValue) { messages = await _messageRepository.GetChatMessagesAroundAsync(request.ChatId, request.Pivot.Value, queryLimit, cancellationToken); } else { DateTime? cursorDate = null; long? cursorSequenceId = null; if (!string.IsNullOrEmpty(request.Cursor)) { if (long.TryParse(request.Cursor, out var seqId)) { cursorSequenceId = seqId; } else if (DateTime.TryParse(request.Cursor, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed)) { cursorDate = parsed.ToUniversalTime(); } } messages = await _messageRepository.GetChatMessagesCursorAsync(request.ChatId, cursorDate, cursorSequenceId, queryLimit, cancellationToken); } var result = new List(); var userIdsToFetch = new HashSet(); var replyMessages = new Dictionary(); // Filter messages that are deleted for the current user before processing var filteredMessages = messages.Where(m => !m.IsDeletedForUser(request.UserId)).ToList(); foreach (var m in filteredMessages) { userIdsToFetch.Add(m.SenderId); if (m is PollMessage poll && !poll.IsAnonymous) { foreach (var vote in poll.Votes) { userIdsToFetch.Add(vote.UserId); } } if (!m.ReplyToId.HasValue) { continue; } var replyMsg = await _messageRepository.GetByIdAsync(m.ReplyToId.Value, cancellationToken); if (replyMsg == null) { continue; } replyMessages[replyMsg.Id] = replyMsg; userIdsToFetch.Add(replyMsg.SenderId); } var senders = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken); var messageIds = filteredMessages.Select(m => m.Id).ToList(); var allReactions = await _reactionRepository.GetReactionsForMessagesAsync(messageIds, cancellationToken); var reactionsByMessage = allReactions.GroupBy(r => r.MessageId).ToDictionary(g => g.Key, g => g.ToList()); foreach (var message in messages) { if (message.IsDeletedForUser(request.UserId)) { continue; } senders.TryGetValue(message.SenderId, out var sender); reactionsByMessage.TryGetValue(message.Id, out var reactions); Message? replyMsg = null; if (message.ReplyToId.HasValue) { replyMessages.TryGetValue(message.ReplyToId.Value, out replyMsg); } UserInfo? replySender = null; if (replyMsg != null) { senders.TryGetValue(replyMsg.SenderId, out replySender); } result.Add(new MessageDetailDto( message.Id, message.ChatId, message.SenderId, message.Content, message.Type.ToLower(), message.ReplyToId, replyMsg != null ? new ReplyToMessageDto( replyMsg.Id, replyMsg.Content, replyMsg.IsDeleted, replyMsg is MediaMessage mm ? mm.Media.Select(m => new MediaDto(m.Id, m.Type, m.Url, m.Filename, m.Size, m.Duration)).ToList() : new List(), replySender != null ? new MessageSenderDto(replySender.Id, replySender.Username, replySender.DisplayName, replySender.Avatar) : null ) : null, message is TextMessage tm ? tm.Quote : null, message.IsEdited, message.IsDeleted, message.CreatedAt, message.SequenceId, message.ForwardedFromId, null, // ForwardedFrom details not implemented here yet (message as StoryMessage)?.StoryId, (message as StoryMessage)?.StoryMediaUrl, (message as StoryMessage)?.StoryMediaType, (message as MediaMessage)?.Media.Select(m => new MediaDto(m.Id, m.Type, m.Url, m.Filename, m.Size, m.Duration)).ToList() ?? new List(), sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : new MessageSenderDto(message.SenderId, "unknown", "Unknown", null), new List(), // ReadBy not implemented in this detailed view yet reactions?.Select(r => { senders.TryGetValue(r.UserId, out var ru); return new MessageReactionDto(r.Id, r.Emoji, r.UserId, ru != null ? new MessageSenderDto(ru.Id, ru.Username, ru.DisplayName, ru.Avatar) : null); }).ToList() ?? new List(), (message as CallMessage)?.CallType, (message as CallMessage)?.CallStatus, (message as CallMessage)?.Duration, (message as PollMessage)?.Options.Select(o => { var pm = (PollMessage)message; var voters = pm.IsAnonymous == false ? pm.Votes .Where(v => v.OptionId == o.Id) .Select(v => { senders.TryGetValue(v.UserId, out var vu); return vu != null ? new MessageSenderDto(vu.Id, vu.Username, vu.DisplayName, vu.Avatar) : new MessageSenderDto(v.UserId, "unknown", "Unknown", null); }) .ToList() : null; return new PollOptionDto(o.Id, o.Text, o.VoteCount, voters, pm.IsAnonymous == false ? pm.Votes.Where(v => v.OptionId == o.Id).Select(v => v.UserId).ToList() : null); }).ToList(), (message as PollMessage)?.IsMultipleChoice, (message as PollMessage)?.IsAnonymous, (message as PollMessage)?.IsClosed, (message as PollMessage)?.Votes.Where(v => v.UserId == request.UserId).Select(v => v.OptionId).ToList() )); } return Result.Success(result); } }