using QMax.Api.Contracts; using QMax.Api.Data.Entities; namespace QMax.Api.Services; public sealed class ChatProjectionService { public ChatDto ToDto(Chat chat) { return new ChatDto( chat.Id, chat.ExternalId, chat.Kind, chat.Title, AvatarPath(chat), CleanPreview(chat.LastMessagePreview), chat.LastMessageAt, chat.UnreadCount, chat.IsPinned, chat.IsMuted, chat.IsArchived); } public MessageDto ToDto(Message message) { return new MessageDto( message.Id, message.ChatId, message.ExternalId, message.SenderName, message.Direction, MessageTextSanitizer.CleanMessageText(message.Text, message.Attachments.Count != 0), message.SentAt, message.EditedAt, message.DeletedAt, message.ReplyToMessageId, message.ForwardedFrom, message.DeliveryState, message.Error, message.Reactions .GroupBy(x => x.Emoji) .OrderByDescending(x => x.Count()) .ThenBy(x => x.Key, StringComparer.Ordinal) .Select(x => new ReactionDto(x.Key, x.Count(), x.Any(r => r.ActorKey == "self"))) .ToArray(), message.Attachments .OrderBy(x => x.SortOrder) .Select(x => ToDto(message.ChatId, x)) .ToArray()); } private static AttachmentDto ToDto(Guid chatId, MessageAttachment attachment) { return new AttachmentDto( attachment.Id, attachment.OriginalFileName, attachment.ContentType, attachment.FileSizeBytes, $"/api/chats/{chatId}/attachments/{attachment.Id}/content", attachment.Kind, attachment.SortOrder); } private static string? CleanPreview(string? value) { return MessageTextSanitizer.CleanChatPreview(value); } private static string? AvatarPath(Chat chat) { return string.IsNullOrWhiteSpace(chat.AvatarUrl) ? null : $"/api/chats/{chat.Id}/avatar"; } }