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

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
@@ -109,6 +109,10 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
.Select(m => new ReadByDto(m.UserId))
.ToList();
var textMessage = latestMessage as TextMessage;
var mediaMessage = latestMessage as MediaMessage;
var storyMessage = latestMessage as StoryMessage;
messagesList.Add(new ChatMessageDto(
latestMessage.Id,
latestMessage.ChatId,
@@ -116,15 +120,15 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
latestMessage.Content,
latestMessage.Type,
latestMessage.ReplyToId,
latestMessage.Quote,
latestMessage.StoryId,
latestMessage.StoryMediaUrl,
latestMessage.StoryMediaType,
textMessage?.Quote,
storyMessage?.StoryId,
storyMessage?.StoryMediaUrl,
storyMessage?.StoryMediaType,
latestMessage.IsEdited,
latestMessage.IsDeleted,
latestMessage.CreatedAt,
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.Id,
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(
latestMessage.Id,
latestMessage.ChatId,
@@ -108,15 +112,15 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
latestMessage.Content,
latestMessage.Type,
latestMessage.ReplyToId,
latestMessage.Quote,
latestMessage.StoryId,
latestMessage.StoryMediaUrl,
latestMessage.StoryMediaType,
textMessage?.Quote,
storyMessage?.StoryId,
storyMessage?.StoryMediaUrl,
storyMessage?.StoryMediaType,
latestMessage.IsEdited,
latestMessage.IsDeleted,
latestMessage.CreatedAt,
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.Id,
senderObj.Username,
@@ -96,7 +96,7 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
replyMsg.Id,
replyMsg.Content,
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
);
}
@@ -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(
message.Id,
message.ChatId,
@@ -125,17 +129,17 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
message.Type,
message.ReplyToId,
replyToObj,
message.Quote,
textMessage?.Quote,
message.IsEdited,
message.IsDeleted,
message.CreatedAt,
message.SequenceId,
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.StoryId,
message.StoryMediaUrl,
message.StoryMediaType,
message.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(),
storyMessage?.StoryId,
storyMessage?.StoryMediaUrl,
storyMessage?.StoryMediaType,
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,
chat.Members.Where(m => m.LastReadSequenceId >= message.SequenceId && m.UserId != message.SenderId).Select(m => new ReadByDto(m.UserId)).ToList(),
reactionsWithUser
@@ -48,13 +48,20 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
foreach (var message in messages)
{
var mediaMessage = message as MediaMessage;
var textMessage = message as TextMessage;
var storyMessage = message as StoryMessage;
if (filterType == "links")
{
var messageContent = message.Content;
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 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();
if (allLinks.Any())
@@ -72,7 +79,7 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
continue;
}
var messageMedia = message.Media;
var messageMedia = mediaMessage?.Media;
if (messageMedia == null || !messageMedia.Any())
{
continue;
@@ -80,8 +87,8 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
var filteredMedia = messageMedia.Where(media =>
{
var mediaType = media.Type?.ToLower() ?? "file";
var isGif = mediaType == "image" && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase));
var mType = media.Type?.ToLower() ?? "file";
var isGif = mType == "image" && media.Url != null && (media.Url.EndsWith(".mp4", StringComparison.OrdinalIgnoreCase) || media.Url.EndsWith(".gif", StringComparison.OrdinalIgnoreCase));
if (filterType == "gifs")
{
@@ -90,12 +97,12 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
if (filterType == "files")
{
return mediaType != "image" && mediaType != "video" && mediaType != "link";
return mType != "image" && mType != "video" && mType != "link";
}
if (filterType == "media")
{
return (mediaType == "image" || mediaType == "video") && !isGif;
return (mType == "image" || mType == "video") && !isGif;
}
return true;
@@ -111,10 +118,10 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
null,
sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : null,
message.ReplyToId,
message.Quote,
message.StoryId,
message.StoryMediaUrl,
message.StoryMediaType,
textMessage?.Quote,
storyMessage?.StoryId,
storyMessage?.StoryMediaUrl,
storyMessage?.StoryMediaType,
message.IsEdited,
message.Type,
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 reactionsByMessage = allReactions.GroupBy(r => r.MessageId).ToDictionary(g => g.Key, g => g.ToList());
var result = messages.Select(message => new SearchMessageDto(
message.Id,
message.ChatId,
message.SenderId,
var result = messages.Select(message => {
var textMessage = message as TextMessage;
var mediaMessage = message as MediaMessage;
var storyMessage = message as StoryMessage;
return new SearchMessageDto(
message.Id,
message.ChatId,
message.SenderId,
message.Content,
message.Type,
message.ReplyToId,
message.Quote,
textMessage?.Quote,
message.IsEdited,
message.IsDeleted,
message.CreatedAt,
message.SequenceId,
message.ForwardedFromId,
null,
message.StoryId,
message.StoryMediaUrl,
message.StoryMediaType,
message.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size)).ToList(),
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>(),
new List<ReadByDto>()
)).ToList();
storyMessage?.StoryId,
storyMessage?.StoryMediaUrl,
storyMessage?.StoryMediaType,
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),
reactionsByMessage.TryGetValue(message.Id, out var mr) ? mr.Select(reaction => new SimpleReactionDto(reaction.UserId, reaction.Emoji)).ToList() : new List<SimpleReactionDto>(),
new List<ReadByDto>()
);
}).ToList();
return Result.Success(result);
}
@@ -13,7 +13,7 @@ public class MediaMessage : Message
public MediaType MediaType { get; private set; } // image, video, file, voice
private List<Media> _media = new();
public override IReadOnlyCollection<Media> Media => _media.AsReadOnly();
public IReadOnlyCollection<Media> Media => _media.AsReadOnly();
private MediaMessage() : base()
{
@@ -34,12 +34,6 @@ public abstract class Message : AggregateRoot<Guid>
public abstract string Type { get; }
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 IsDeleted => HasState(MessageState.IsDeleted);
public bool IsDeletedForUser(Guid userId) => _deletedFor.Exists(d => d.UserId == userId);
@@ -112,47 +112,3 @@ public class PollMessage : Message
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 Guid InternalStoryId { get; private set; }
public override Guid? StoryId => InternalStoryId;
public Guid? StoryId => InternalStoryId;
public string InternalStoryMediaUrl { get; private set; }
public override string? StoryMediaUrl => InternalStoryMediaUrl;
public string? StoryMediaUrl => InternalStoryMediaUrl;
public MediaType InternalStoryMediaType { get; private set; }
public override string? StoryMediaType => InternalStoryMediaType.ToString().ToLower();
public string? StoryMediaType => InternalStoryMediaType.ToString().ToLower();
private StoryMessage() : base()
{
@@ -8,7 +8,7 @@ public class TextMessage : Message
{
public override string Type => "text";
public override string? Content { get; protected set; }
public override string? Quote { get; protected set; }
public string? Quote { get; protected set; }
private TextMessage() : base()
{
@@ -54,8 +54,8 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
{
Id = replyMsg.Id,
Content = replyMsg.Content,
Quote = message.Quote,
media = replyMsg.Media.Select(rm => new { rm.Type, rm.Url }).ToList(),
Quote = message is TextMessage tm ? tm.Quote : null,
media = (replyMsg as MediaMessage)?.Media.Select(rm => new { rm.Type, rm.Url }).ToList() ?? (object)Array.Empty<object>(),
Sender = replySenderInfo != null
? new { Id = replySenderInfo.Id, Username = replySenderInfo.Username, DisplayName = replySenderInfo.DisplayName }
: new { Id = replyMsg.SenderId, Username = "unknown", DisplayName = "Unknown" }
@@ -75,19 +75,19 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
forwardedFrom = forwardedFromObj,
replyToId = message.ReplyToId,
replyTo = replyToObj,
quote = message.Quote,
media = message.Media.Select(m => new
quote = message is TextMessage tm2 ? tm2.Quote : null,
media = (message as MediaMessage)?.Media.Select(m => new
{
type = m.Type,
url = m.Url,
filename = m.FileId,
size = m.Size
}).ToList(),
}).ToList() ?? (object)Array.Empty<object>(),
sender = senderObj,
readBy = new List<object>(),
storyId = message.StoryId,
storyMediaUrl = message.StoryMediaUrl,
storyMediaType = message.StoryMediaType
storyId = (message as StoryMessage)?.StoryId,
storyMediaUrl = (message as StoryMessage)?.StoryMediaUrl,
storyMediaType = (message as StoryMessage)?.StoryMediaType
}, cancellationToken);
}
}
@@ -30,13 +30,15 @@ public static class MongoDbMapConfigurator
cm.AutoMap();
cm.MapField("_deletedFor").SetElementName("DeletedFor");
cm.MapProperty(c => c.Content).SetSerializer(new EncryptedStringSerializer());
cm.MapProperty(c => c.Quote).SetSerializer(new EncryptedStringSerializer());
cm.SetIgnoreExtraElements(true);
cm.SetIsRootClass(true);
});
BsonClassMap.RegisterClassMap<TextMessage>(cm =>
{
cm.AutoMap();
cm.MapProperty(c => c.Quote).SetSerializer(new EncryptedStringSerializer());
cm.SetIgnoreExtraElements(true);
cm.SetDiscriminator("TextMessage");
});
@@ -44,6 +46,7 @@ public static class MongoDbMapConfigurator
{
cm.AutoMap();
cm.MapField("_media").SetElementName("Media");
cm.SetIgnoreExtraElements(true);
cm.SetDiscriminator("MediaMessage");
cm.UnmapProperty(c => c.Caption);
});
@@ -51,8 +54,9 @@ public static class MongoDbMapConfigurator
BsonClassMap.RegisterClassMap<StoryMessage>(cm =>
{
cm.AutoMap();
cm.SetIgnoreExtraElements(true);
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());
@@ -63,6 +67,7 @@ public static class MongoDbMapConfigurator
cm.AutoMap();
cm.MapProperty(c => c.Options).SetElementName("_options");
cm.MapProperty(c => c.Votes).SetElementName("_votes");
cm.SetIgnoreExtraElements(true);
cm.SetDiscriminator("PollMessage");
});