Разделение сообщения

This commit is contained in:
Халимов Рустам
2026-04-02 22:11:03 +03:00
parent 757142eda7
commit 943699139b
22 changed files with 297 additions and 193 deletions
@@ -0,0 +1,17 @@
using System;
namespace Knot.Contracts.Messaging.Domain;
public class Media
{
public Guid Id { get; set; }
public string Type { get; set; } = string.Empty;
public string? Url { get; set; }
public string? ThumbnailUrl { get; set; }
public long? Size { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string? FileId { get; set; }
public string? Filename { get; set; }
public string? Duration { get; set; }
}
@@ -0,0 +1,29 @@
using System;
using System.Collections.Generic;
namespace Knot.Contracts.Messaging.Domain;
public class MediaMessage : Message
{
public override string Type => MediaType.ToString().ToLower();
public override string? Content { get; protected set; }
public string? Caption { get => Content; private set => Content = value; }
public MediaType MediaType { get; private set; }
private List<Media> _media = new();
public IReadOnlyCollection<Media> Media => _media.AsReadOnly();
private MediaMessage() : base() { MediaType = MediaType.File; }
public MediaMessage(Guid id, Guid chatId, Guid senderId, MediaType mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) { MediaType = mediaType; Caption = caption; }
public MediaMessage(Guid id, Guid chatId, Guid senderId, string mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
: this(id, chatId, senderId, Enum.TryParse<MediaType>(mediaType, true, out var mt) ? mt : MediaType.File, caption, replyToId, forwardedFromId, createdAt, isImported) { }
public void AddMedia(string type, string url, string? filename, long? size) => _media.Add(new Media { Type = type, Url = url, FileId = filename, Size = size });
public override void Edit(string newCaption) => base.Edit(newCaption);
public override void Delete() { Caption = null; base.Delete(); }
}
@@ -16,18 +16,17 @@ public abstract class Message : AggregateRoot<Guid>
public MessageState State { get; protected set; } public MessageState State { get; protected set; }
public abstract string Type { get; } public abstract string Type { get; }
public abstract string? Content { get; protected set; } public abstract string? Content { get; protected set; }
public virtual string? Quote { get; protected set; } = null;
public virtual Guid? StoryId => null;
public virtual string? StoryMediaUrl => null;
public virtual string? StoryMediaType => null;
public virtual IReadOnlyCollection<Media> Media => Array.Empty<Media>();
public bool IsEdited => HasState(MessageState.IsEdited); public bool IsEdited => HasState(MessageState.IsEdited);
public bool IsDeleted => HasState(MessageState.IsDeleted); public bool IsDeleted => HasState(MessageState.IsDeleted);
public bool IsDeletedForUser(Guid userId) => _deletedFor.Exists(d => d.UserId == userId);
protected List<DeletedMessage> _deletedFor = new(); protected List<DeletedMessage> _deletedFor = new();
public IReadOnlyCollection<DeletedMessage> DeletedFor => _deletedFor.AsReadOnly(); public IReadOnlyCollection<DeletedMessage> DeletedFor => _deletedFor.AsReadOnly();
protected Message() : base(Guid.Empty) { } protected Message() : base(Guid.Empty) { }
protected Message(Guid id, Guid chatId, Guid senderId, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported) : base(id)
protected Message(Guid id, Guid chatId, Guid senderId, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
: base(id)
{ {
ChatId = chatId; ChatId = chatId;
SenderId = senderId; SenderId = senderId;
@@ -36,91 +35,22 @@ public abstract class Message : AggregateRoot<Guid>
CreatedAt = createdAt; CreatedAt = createdAt;
if (isImported) AddState(MessageState.IsImported); if (isImported) AddState(MessageState.IsImported);
} }
public void AddState(MessageState state) => State |= state; public void AddState(MessageState state) => State |= state;
public void RemoveState(MessageState state) => State &= ~state; public void RemoveState(MessageState state) => State &= ~state;
public bool HasState(MessageState state) => (State & state) == state; public bool HasState(MessageState state) => (State & state) == state;
public bool IsDeletedForUser(Guid userId) => _deletedFor.Exists(d => d.UserId == userId);
public virtual void Delete() => AddState(MessageState.IsDeleted); public virtual void Delete() => AddState(MessageState.IsDeleted);
public virtual void Edit(string newContent) { Content = newContent; AddState(MessageState.IsEdited); } public virtual void Edit(string newContent)
public void DeleteForUser(Guid userId) { if (!_deletedFor.Exists(x => x.UserId == userId)) _deletedFor.Add(new DeletedMessage(Id, userId)); }
}
public class Media
{
public Guid Id { get; set; }
public string Type { get; set; } = string.Empty;
public string? Url { get; set; }
public string? ThumbnailUrl { get; set; }
public long? Size { get; set; }
public int Width { get; set; }
public int Height { get; set; }
public string? FileId { get; set; }
public string? Filename { get; set; }
public string? Duration { get; set; }
}
public class TextMessage : Message
{
public override string Type => "text";
public override string? Content { get; protected set; }
public TextMessage() : base() { }
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) => Content = content;
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, string? quote, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
: this(id, chatId, senderId, content, replyToId, forwardedFromId, createdAt, isImported) => Quote = quote;
}
public class MediaMessage : Message
{
public override string Type => MediaType.ToString().ToLower();
public override string? Content { get; protected set; }
public string? Caption { get => Content; private set => Content = value; }
public MediaType MediaType { get; private set; }
private List<Media> _media = new();
public override IReadOnlyCollection<Media> Media => _media.AsReadOnly();
private MediaMessage() : base() { MediaType = MediaType.File; }
public MediaMessage(Guid id, Guid chatId, Guid senderId, MediaType mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) { MediaType = mediaType; Caption = caption; }
public MediaMessage(Guid id, Guid chatId, Guid senderId, string mediaType, string? caption, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
: this(id, chatId, senderId, Enum.TryParse<MediaType>(mediaType, true, out var mt) ? mt : MediaType.File, caption, replyToId, forwardedFromId, createdAt, isImported) { }
public void AddMedia(string type, string url, string? filename, long? size) => _media.Add(new Media { Type = type, Url = url, FileId = filename, Size = size });
public override void Edit(string newCaption) => base.Edit(newCaption);
public override void Delete() { Caption = null; base.Delete(); }
}
public class StoryMessage : Message
{
public override string Type => "story";
public override string? Content { get; protected set; }
public override Guid? StoryId { get; }
public string? InternalStoryMediaUrl { get; private set; }
public override string? StoryMediaUrl => InternalStoryMediaUrl;
public override string? StoryMediaType { get; }
public StoryMessage() : base() { }
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, DateTime createdAt)
: base(id, chatId, senderId, null, null, createdAt, false) { StoryId = storyId; InternalStoryMediaUrl = storyMediaUrl; StoryMediaType = storyMediaType; }
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, string? content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
: this(id, chatId, senderId, storyId, storyMediaUrl, storyMediaType, createdAt) { Content = content; ReplyToId = replyToId; ForwardedFromId = forwardedFromId; if (isImported) AddState(MessageState.IsImported); }
}
public class PollMessage : Message
{
public override string Type => "poll";
public override string? Content { get; protected set; }
public List<PollOption> Options { get; } = new();
public List<PollVote> Votes { get; } = new();
public bool IsMultipleChoice { get; set; }
public DateTime? ExpiresAt { get; set; }
public bool IsClosed { get; set; }
public PollMessage() : base() { }
public PollMessage(Guid id, Guid chatId, Guid senderId, string? question, List<string>? options, bool isAnonymous, bool isMultiple, DateTime? expiresAt, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported)
{ {
Content = question ?? "Poll"; Content = newContent;
if (options != null) foreach (var opt in options) Options.Add(new PollOption { Text = opt }); AddState(MessageState.IsEdited);
IsMultipleChoice = isMultiple; }
ExpiresAt = expiresAt;
public void DeleteForUser(Guid userId)
{
if (!_deletedFor.Exists(x => x.UserId == userId))
_deletedFor.Add(new DeletedMessage(Id, userId));
} }
} }
public class PollOption { public string Text { get; set; } = string.Empty; public int VoteCount { get; set; } }
public class PollVote { public Guid OptionIndex { get; set; } public Guid UserId { get; set; } public DateTime VotedAt { get; set; } }
@@ -0,0 +1,32 @@
using System;
using System.Collections.Generic;
namespace Knot.Contracts.Messaging.Domain;
public class PollMessage : Message
{
public override string Type => "poll";
public override string? Content { get; protected set; }
public List<PollOption> Options { get; } = new();
public List<PollVote> Votes { get; } = new();
public bool IsMultipleChoice { get; set; }
public DateTime? ExpiresAt { get; set; }
public bool IsClosed { get; set; }
public PollMessage() : base() { }
public PollMessage(Guid id, Guid chatId, Guid senderId, string? question, List<string>? options, bool isAnonymous, bool isMultiple, DateTime? expiresAt, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported)
{
Content = question ?? "Poll";
if (options != null)
{
foreach (var opt in options)
{
Options.Add(new PollOption { Text = opt });
}
}
IsMultipleChoice = isMultiple;
ExpiresAt = expiresAt;
}
}
@@ -0,0 +1,7 @@
namespace Knot.Contracts.Messaging.Domain;
public class PollOption
{
public string Text { get; set; } = string.Empty;
public int VoteCount { get; set; }
}
@@ -0,0 +1,10 @@
using System;
namespace Knot.Contracts.Messaging.Domain;
public class PollVote
{
public Guid OptionIndex { get; set; }
public Guid UserId { get; set; }
public DateTime VotedAt { get; set; }
}
@@ -0,0 +1,31 @@
using System;
namespace Knot.Contracts.Messaging.Domain;
public class StoryMessage : Message
{
public override string Type => "story";
public override string? Content { get; protected set; }
public Guid? StoryId { get; private set; }
public string? StoryMediaUrl { get; private set; }
public string? StoryMediaType { get; private set; }
public StoryMessage() : base() { }
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, DateTime createdAt)
: base(id, chatId, senderId, null, null, createdAt, false)
{
StoryId = storyId;
StoryMediaUrl = storyMediaUrl;
StoryMediaType = storyMediaType;
}
public StoryMessage(Guid id, Guid chatId, Guid senderId, Guid storyId, string? storyMediaUrl, string? storyMediaType, string? content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported)
: this(id, chatId, senderId, storyId, storyMediaUrl, storyMediaType, createdAt)
{
Content = content;
ReplyToId = replyToId;
ForwardedFromId = forwardedFromId;
if (isImported) AddState(MessageState.IsImported);
}
}
@@ -0,0 +1,18 @@
using System;
namespace Knot.Contracts.Messaging.Domain;
public class TextMessage : Message
{
public override string Type => "text";
public override string? Content { get; protected set; }
public string? Quote { get; protected set; }
public TextMessage() : base() { }
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
: base(id, chatId, senderId, replyToId, forwardedFromId, createdAt, isImported) => Content = content;
public TextMessage(Guid id, Guid chatId, Guid senderId, string content, Guid? replyToId, string? quote, Guid? forwardedFromId, DateTime createdAt, bool isImported = false)
: this(id, chatId, senderId, content, replyToId, forwardedFromId, createdAt, isImported) => Quote = quote;
}
@@ -109,6 +109,10 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
.Select(m => new ReadByDto(m.UserId)) .Select(m => new ReadByDto(m.UserId))
.ToList(); .ToList();
var textMessage = latestMessage as TextMessage;
var mediaMessage = latestMessage as MediaMessage;
var storyMessage = latestMessage as StoryMessage;
messagesList.Add(new ChatMessageDto( messagesList.Add(new ChatMessageDto(
latestMessage.Id, latestMessage.Id,
latestMessage.ChatId, latestMessage.ChatId,
@@ -116,15 +120,15 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
latestMessage.Content, latestMessage.Content,
latestMessage.Type, latestMessage.Type,
latestMessage.ReplyToId, latestMessage.ReplyToId,
latestMessage.Quote, textMessage?.Quote,
latestMessage.StoryId, storyMessage?.StoryId,
latestMessage.StoryMediaUrl, storyMessage?.StoryMediaUrl,
latestMessage.StoryMediaType, storyMessage?.StoryMediaType,
latestMessage.IsEdited, latestMessage.IsEdited,
latestMessage.IsDeleted, latestMessage.IsDeleted,
latestMessage.CreatedAt, latestMessage.CreatedAt,
latestMessage.SequenceId, latestMessage.SequenceId,
latestMessage.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), mediaMessage?.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList() ?? new List<MediaDto>(),
senderObj != null ? new MessageSenderDto( senderObj != null ? new MessageSenderDto(
senderObj.Id, senderObj.Id,
senderObj.Username, senderObj.Username,
@@ -101,6 +101,10 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
)); ));
} }
var textMessage = latestMessage as TextMessage;
var mediaMessage = latestMessage as MediaMessage;
var storyMessage = latestMessage as StoryMessage;
messagesList.Add(new ChatMessageDto( messagesList.Add(new ChatMessageDto(
latestMessage.Id, latestMessage.Id,
latestMessage.ChatId, latestMessage.ChatId,
@@ -108,15 +112,15 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
latestMessage.Content, latestMessage.Content,
latestMessage.Type, latestMessage.Type,
latestMessage.ReplyToId, latestMessage.ReplyToId,
latestMessage.Quote, textMessage?.Quote,
latestMessage.StoryId, storyMessage?.StoryId,
latestMessage.StoryMediaUrl, storyMessage?.StoryMediaUrl,
latestMessage.StoryMediaType, storyMessage?.StoryMediaType,
latestMessage.IsEdited, latestMessage.IsEdited,
latestMessage.IsDeleted, latestMessage.IsDeleted,
latestMessage.CreatedAt, latestMessage.CreatedAt,
latestMessage.SequenceId, latestMessage.SequenceId,
latestMessage.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), mediaMessage?.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList() ?? new List<MediaDto>(),
senderObj != null ? new MessageSenderDto( senderObj != null ? new MessageSenderDto(
senderObj.Id, senderObj.Id,
senderObj.Username, senderObj.Username,
@@ -96,7 +96,7 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
replyMsg.Id, replyMsg.Id,
replyMsg.Content, replyMsg.Content,
replyMsg.IsDeleted, replyMsg.IsDeleted,
replyMsg.Media.Select(rm => new MediaDto(rm.Id, rm.Type, rm.Url, rm.Filename, rm.Size)).ToList(), (replyMsg as MediaMessage)?.Media.Select(rm => new MediaDto(rm.Id, rm.Type, rm.Url, rm.Filename, rm.Size)).ToList() ?? new List<MediaDto>(),
senderObj senderObj
); );
} }
@@ -117,6 +117,10 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
)); ));
} }
var textMessage = message as TextMessage;
var mediaMessage = message as MediaMessage;
var storyMessage = message as StoryMessage;
result.Add(new MessageDetailDto( result.Add(new MessageDetailDto(
message.Id, message.Id,
message.ChatId, message.ChatId,
@@ -125,17 +129,17 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
message.Type, message.Type,
message.ReplyToId, message.ReplyToId,
replyToObj, replyToObj,
message.Quote, textMessage?.Quote,
message.IsEdited, message.IsEdited,
message.IsDeleted, message.IsDeleted,
message.CreatedAt, message.CreatedAt,
message.SequenceId, message.SequenceId,
message.ForwardedFromId, message.ForwardedFromId,
message.ForwardedFromId.HasValue && senders.TryGetValue(message.ForwardedFromId.Value, out var fwdUser) ? new MessageSenderDto(fwdUser.Id, fwdUser.Username, fwdUser.DisplayName, fwdUser.Avatar) : null, message.ForwardedFromId.HasValue && senders.TryGetValue(message.ForwardedFromId.Value, out var fwdUser) ? new MessageSenderDto(fwdUser.Id, fwdUser.Username, fwdUser.DisplayName, fwdUser.Avatar) : null,
message.StoryId, storyMessage?.StoryId,
message.StoryMediaUrl, storyMessage?.StoryMediaUrl,
message.StoryMediaType, storyMessage?.StoryMediaType,
message.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), mediaMessage?.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList() ?? new List<MediaDto>(),
senders.TryGetValue(message.SenderId, out var senderUser) ? new MessageSenderDto(senderUser.Id, senderUser.Username, senderUser.DisplayName, senderUser.Avatar) : null, senders.TryGetValue(message.SenderId, out var senderUser) ? new MessageSenderDto(senderUser.Id, senderUser.Username, senderUser.DisplayName, senderUser.Avatar) : null,
chat.Members.Where(m => m.LastReadSequenceId >= message.SequenceId && m.UserId != message.SenderId).Select(m => new ReadByDto(m.UserId)).ToList(), chat.Members.Where(m => m.LastReadSequenceId >= message.SequenceId && m.UserId != message.SenderId).Select(m => new ReadByDto(m.UserId)).ToList(),
reactionsWithUser reactionsWithUser
@@ -48,13 +48,20 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
foreach (var message in messages) foreach (var message in messages)
{ {
var mediaMessage = message as MediaMessage;
var textMessage = message as TextMessage;
var storyMessage = message as StoryMessage;
if (filterType == "links") if (filterType == "links")
{ {
var messageContent = message.Content; var messageContent = message.Content;
var linkRegex = new Regex(@"https?://[^\s]+", RegexOptions.IgnoreCase); var linkRegex = new Regex(@"https?://[^\s]+", RegexOptions.IgnoreCase);
var contentLinks = !string.IsNullOrEmpty(messageContent) ? linkRegex.Matches(messageContent).Select(match => match.Value).ToList() : new List<string>(); var contentLinks = !string.IsNullOrEmpty(messageContent) ? linkRegex.Matches(messageContent).Select(match => match.Value).ToList() : new List<string>();
var messageMediaColl = message.Media;
var mediaLinks = (messageMediaColl ?? Enumerable.Empty<Media>()).Where(media => media.Type?.ToString().ToLower() == "link").Select(media => media.Url).ToList(); var mediaLinks = (mediaMessage?.Media ?? Enumerable.Empty<Media>())
.Where(media => media.Type?.ToString().ToLower() == "link" && !string.IsNullOrEmpty(media.Url))
.Select(media => media.Url!)
.ToList();
var allLinks = contentLinks.Concat(mediaLinks).Distinct().ToList(); var allLinks = contentLinks.Concat(mediaLinks).Distinct().ToList();
if (allLinks.Any()) if (allLinks.Any())
@@ -72,7 +79,7 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
continue; continue;
} }
var messageMedia = message.Media; var messageMedia = mediaMessage?.Media;
if (messageMedia == null || !messageMedia.Any()) if (messageMedia == null || !messageMedia.Any())
{ {
continue; continue;
@@ -80,8 +87,8 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
var filteredMedia = messageMedia.Where(media => var filteredMedia = messageMedia.Where(media =>
{ {
var mediaType = media.Type?.ToLower() ?? "file"; var mType = media.Type?.ToLower() ?? "file";
var isGif = mediaType == "image" && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase)); var isGif = mType == "image" && media.Url != null && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase));
if (filterType == "gifs") if (filterType == "gifs")
{ {
@@ -90,12 +97,12 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
if (filterType == "files") if (filterType == "files")
{ {
return mediaType != "image" && mediaType != "video" && mediaType != "link"; return mType != "image" && mType != "video" && mType != "link";
} }
if (filterType == "media") if (filterType == "media")
{ {
return (mediaType == "image" || mediaType == "video") && !isGif; return (mType == "image" || mType == "video") && !isGif;
} }
return true; return true;
@@ -111,10 +118,10 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
null, null,
sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : null, sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : null,
message.ReplyToId, message.ReplyToId,
message.Quote, textMessage?.Quote,
message.StoryId, storyMessage?.StoryId,
message.StoryMediaUrl, storyMessage?.StoryMediaUrl,
message.StoryMediaType, storyMessage?.StoryMediaType,
message.IsEdited, message.IsEdited,
message.Type, message.Type,
filteredMedia.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList() filteredMedia.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList()
@@ -41,28 +41,34 @@ internal sealed class SearchMessagesQueryHandler : IQueryHandler<SearchMessagesQ
var allReactions = await _reactionRepository.GetReactionsForMessagesAsync(messageIds, cancellationToken); var allReactions = await _reactionRepository.GetReactionsForMessagesAsync(messageIds, cancellationToken);
var reactionsByMessage = allReactions.GroupBy(r => r.MessageId).ToDictionary(g => g.Key, g => g.ToList()); var reactionsByMessage = allReactions.GroupBy(r => r.MessageId).ToDictionary(g => g.Key, g => g.ToList());
var result = messages.Select(message => new SearchMessageDto( var result = messages.Select(message => {
message.Id, var textMessage = message as TextMessage;
message.ChatId, var mediaMessage = message as MediaMessage;
message.SenderId, var storyMessage = message as StoryMessage;
return new SearchMessageDto(
message.Id,
message.ChatId,
message.SenderId,
message.Content, message.Content,
message.Type, message.Type,
message.ReplyToId, message.ReplyToId,
message.Quote, textMessage?.Quote,
message.IsEdited, message.IsEdited,
message.IsDeleted, message.IsDeleted,
message.CreatedAt, message.CreatedAt,
message.SequenceId, message.SequenceId,
message.ForwardedFromId, message.ForwardedFromId,
null, null,
message.StoryId, storyMessage?.StoryId,
message.StoryMediaUrl, storyMessage?.StoryMediaUrl,
message.StoryMediaType, storyMessage?.StoryMediaType,
message.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(), mediaMessage?.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList() ?? new List<MediaDto>(),
senders.TryGetValue(message.SenderId, out var senderUser) ? new MessageSenderDto(senderUser.Id, senderUser.Username, senderUser.DisplayName, senderUser.Avatar) : new MessageSenderDto(message.SenderId, "unknown", "Unknown", null), senders.TryGetValue(message.SenderId, out var senderUser) ? new MessageSenderDto(senderUser.Id, senderUser.Username, senderUser.DisplayName, senderUser.Avatar) : new MessageSenderDto(message.SenderId, "unknown", "Unknown", null),
reactionsByMessage.TryGetValue(message.Id, out var mr) ? mr.Select(reaction => new SimpleReactionDto(reaction.UserId, reaction.Emoji)).ToList() : new List<SimpleReactionDto>(), reactionsByMessage.TryGetValue(message.Id, out var mr) ? mr.Select(reaction => new SimpleReactionDto(reaction.UserId, reaction.Emoji)).ToList() : new List<SimpleReactionDto>(),
new List<ReadByDto>() new List<ReadByDto>()
)).ToList(); );
}).ToList();
return Result.Success(result); return Result.Success(result);
} }
@@ -13,7 +13,7 @@ public class MediaMessage : Message
public MediaType MediaType { get; private set; } // image, video, file, voice public MediaType MediaType { get; private set; } // image, video, file, voice
private List<Media> _media = new(); private List<Media> _media = new();
public override IReadOnlyCollection<Media> Media => _media.AsReadOnly(); public IReadOnlyCollection<Media> Media => _media.AsReadOnly();
private MediaMessage() : base() private MediaMessage() : base()
{ {
@@ -34,12 +34,6 @@ public abstract class Message : AggregateRoot<Guid>
public abstract string Type { get; } public abstract string Type { get; }
public abstract string? Content { get; protected set; } public abstract string? Content { get; protected set; }
public virtual string? Quote { get; protected set; } = null;
public virtual Guid? StoryId => null;
public virtual string? StoryMediaUrl => null;
public virtual string? StoryMediaType => null;
public virtual IReadOnlyCollection<Media> Media => Array.Empty<Media>();
public bool IsEdited => HasState(MessageState.IsEdited); public bool IsEdited => HasState(MessageState.IsEdited);
public bool IsDeleted => HasState(MessageState.IsDeleted); public bool IsDeleted => HasState(MessageState.IsDeleted);
public bool IsDeletedForUser(Guid userId) => _deletedFor.Exists(d => d.UserId == userId); public bool IsDeletedForUser(Guid userId) => _deletedFor.Exists(d => d.UserId == userId);
@@ -112,47 +112,3 @@ public class PollMessage : Message
base.Delete(); base.Delete();
} }
} }
public class PollOption
{
public Guid Id { get; private set; }
public Guid PollId { get; private set; }
public string Text { get; private set; }
// Денормализованные данные для быстрой отдачи
public int VotesCount { get; private set; }
public int Percentage { get; private set; }
private PollOption() { } // EF
public PollOption(Guid id, Guid pollId, string text)
{
Id = id;
PollId = pollId;
Text = text;
}
public void UpdateResults(int count, int percentage)
{
VotesCount = count;
Percentage = percentage;
}
}
public class PollVote
{
public Guid PollId { get; private set; }
public Guid UserId { get; private set; }
public Guid OptionId { get; private set; }
public DateTime VotedAt { get; private set; }
private PollVote() { } // EF
public PollVote(Guid pollId, Guid userId, Guid optionId, DateTime votedAt)
{
PollId = pollId;
UserId = userId;
OptionId = optionId;
VotedAt = votedAt;
}
}
@@ -0,0 +1,29 @@
using System;
namespace Knot.Modules.Messaging.Domain;
public class PollOption
{
public Guid Id { get; private set; }
public Guid PollId { get; private set; }
public string Text { get; private set; } = string.Empty;
// Денормализованные данные для быстрой отдачи
public int VotesCount { get; private set; }
public int Percentage { get; private set; }
private PollOption() { } // EF
public PollOption(Guid id, Guid pollId, string text)
{
Id = id;
PollId = pollId;
Text = text;
}
public void UpdateResults(int count, int percentage)
{
VotesCount = count;
Percentage = percentage;
}
}
@@ -0,0 +1,21 @@
using System;
namespace Knot.Modules.Messaging.Domain;
public class PollVote
{
public Guid PollId { get; private set; }
public Guid UserId { get; private set; }
public Guid OptionId { get; private set; }
public DateTime VotedAt { get; private set; }
private PollVote() { } // EF
public PollVote(Guid pollId, Guid userId, Guid optionId, DateTime votedAt)
{
PollId = pollId;
UserId = userId;
OptionId = optionId;
VotedAt = votedAt;
}
}
@@ -10,13 +10,13 @@ public class StoryMessage : Message
public override string? Content { get; protected set; } public override string? Content { get; protected set; }
public Guid InternalStoryId { get; private set; } public Guid InternalStoryId { get; private set; }
public override Guid? StoryId => InternalStoryId; public Guid? StoryId => InternalStoryId;
public string InternalStoryMediaUrl { get; private set; } public string InternalStoryMediaUrl { get; private set; }
public override string? StoryMediaUrl => InternalStoryMediaUrl; public string? StoryMediaUrl => InternalStoryMediaUrl;
public MediaType InternalStoryMediaType { get; private set; } public MediaType InternalStoryMediaType { get; private set; }
public override string? StoryMediaType => InternalStoryMediaType.ToString().ToLower(); public string? StoryMediaType => InternalStoryMediaType.ToString().ToLower();
private StoryMessage() : base() private StoryMessage() : base()
{ {
@@ -8,7 +8,7 @@ public class TextMessage : Message
{ {
public override string Type => "text"; public override string Type => "text";
public override string? Content { get; protected set; } public override string? Content { get; protected set; }
public override string? Quote { get; protected set; } public string? Quote { get; protected set; }
private TextMessage() : base() private TextMessage() : base()
{ {
@@ -54,8 +54,8 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
{ {
Id = replyMsg.Id, Id = replyMsg.Id,
Content = replyMsg.Content, Content = replyMsg.Content,
Quote = message.Quote, Quote = message is TextMessage tm ? tm.Quote : null,
media = replyMsg.Media.Select(rm => new { rm.Type, rm.Url }).ToList(), media = (replyMsg as MediaMessage)?.Media.Select(rm => new { rm.Type, rm.Url }).ToList() ?? (object)Array.Empty<object>(),
Sender = replySenderInfo != null Sender = replySenderInfo != null
? new { Id = replySenderInfo.Id, Username = replySenderInfo.Username, DisplayName = replySenderInfo.DisplayName } ? new { Id = replySenderInfo.Id, Username = replySenderInfo.Username, DisplayName = replySenderInfo.DisplayName }
: new { Id = replyMsg.SenderId, Username = "unknown", DisplayName = "Unknown" } : new { Id = replyMsg.SenderId, Username = "unknown", DisplayName = "Unknown" }
@@ -75,19 +75,19 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
forwardedFrom = forwardedFromObj, forwardedFrom = forwardedFromObj,
replyToId = message.ReplyToId, replyToId = message.ReplyToId,
replyTo = replyToObj, replyTo = replyToObj,
quote = message.Quote, quote = message is TextMessage tm2 ? tm2.Quote : null,
media = message.Media.Select(m => new media = (message as MediaMessage)?.Media.Select(m => new
{ {
type = m.Type, type = m.Type,
url = m.Url, url = m.Url,
filename = m.FileId, filename = m.FileId,
size = m.Size size = m.Size
}).ToList(), }).ToList() ?? (object)Array.Empty<object>(),
sender = senderObj, sender = senderObj,
readBy = new List<object>(), readBy = new List<object>(),
storyId = message.StoryId, storyId = (message as StoryMessage)?.StoryId,
storyMediaUrl = message.StoryMediaUrl, storyMediaUrl = (message as StoryMessage)?.StoryMediaUrl,
storyMediaType = message.StoryMediaType storyMediaType = (message as StoryMessage)?.StoryMediaType
}, cancellationToken); }, cancellationToken);
} }
} }
@@ -30,13 +30,15 @@ public static class MongoDbMapConfigurator
cm.AutoMap(); cm.AutoMap();
cm.MapField("_deletedFor").SetElementName("DeletedFor"); cm.MapField("_deletedFor").SetElementName("DeletedFor");
cm.MapProperty(c => c.Content).SetSerializer(new EncryptedStringSerializer()); cm.MapProperty(c => c.Content).SetSerializer(new EncryptedStringSerializer());
cm.MapProperty(c => c.Quote).SetSerializer(new EncryptedStringSerializer()); cm.SetIgnoreExtraElements(true);
cm.SetIsRootClass(true); cm.SetIsRootClass(true);
}); });
BsonClassMap.RegisterClassMap<TextMessage>(cm => BsonClassMap.RegisterClassMap<TextMessage>(cm =>
{ {
cm.AutoMap(); cm.AutoMap();
cm.MapProperty(c => c.Quote).SetSerializer(new EncryptedStringSerializer());
cm.SetIgnoreExtraElements(true);
cm.SetDiscriminator("TextMessage"); cm.SetDiscriminator("TextMessage");
}); });
@@ -44,6 +46,7 @@ public static class MongoDbMapConfigurator
{ {
cm.AutoMap(); cm.AutoMap();
cm.MapField("_media").SetElementName("Media"); cm.MapField("_media").SetElementName("Media");
cm.SetIgnoreExtraElements(true);
cm.SetDiscriminator("MediaMessage"); cm.SetDiscriminator("MediaMessage");
cm.UnmapProperty(c => c.Caption); cm.UnmapProperty(c => c.Caption);
}); });
@@ -51,8 +54,9 @@ public static class MongoDbMapConfigurator
BsonClassMap.RegisterClassMap<StoryMessage>(cm => BsonClassMap.RegisterClassMap<StoryMessage>(cm =>
{ {
cm.AutoMap(); cm.AutoMap();
cm.SetIgnoreExtraElements(true);
cm.SetDiscriminator("StoryMessage"); cm.SetDiscriminator("StoryMessage");
cm.MapProperty(c => c.InternalStoryMediaUrl).SetSerializer(new EncryptedStringSerializer()); cm.MapProperty(c => c.StoryMediaUrl).SetSerializer(new EncryptedStringSerializer());
}); });
BsonClassMap.RegisterClassMap<DeletedMessage>(cm => cm.AutoMap()); BsonClassMap.RegisterClassMap<DeletedMessage>(cm => cm.AutoMap());
@@ -63,6 +67,7 @@ public static class MongoDbMapConfigurator
cm.AutoMap(); cm.AutoMap();
cm.MapProperty(c => c.Options).SetElementName("_options"); cm.MapProperty(c => c.Options).SetElementName("_options");
cm.MapProperty(c => c.Votes).SetElementName("_votes"); cm.MapProperty(c => c.Votes).SetElementName("_votes");
cm.SetIgnoreExtraElements(true);
cm.SetDiscriminator("PollMessage"); cm.SetDiscriminator("PollMessage");
}); });