Заготовка опросов
This commit is contained in:
@@ -63,6 +63,9 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
||||
}
|
||||
}
|
||||
|
||||
var pinnedMessages = await _messageRepository.GetPinnedMessagesAsync(chat.Id, cancellationToken);
|
||||
foreach (var pm in pinnedMessages) userIdsToFetch.Add(pm.SenderId);
|
||||
|
||||
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
||||
|
||||
var members = new List<ChatMemberDto>();
|
||||
@@ -88,55 +91,19 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
||||
var messagesList = new List<ChatMessageDto>();
|
||||
if (latestMessage != null)
|
||||
{
|
||||
usersInfo.TryGetValue(latestMessage.SenderId, out var senderObj);
|
||||
messagesList.Add(MessageMapper.MapToDto(
|
||||
latestMessage,
|
||||
usersInfo,
|
||||
latestReactions,
|
||||
chat.Members.Where(m => m.LastReadSequenceId >= latestMessage.SequenceId && m.UserId != latestMessage.SenderId).Select(m => m.UserId)));
|
||||
}
|
||||
|
||||
var reactionsWithUser = new List<ReactionDto>();
|
||||
foreach (var reaction in latestReactions)
|
||||
{
|
||||
usersInfo.TryGetValue(reaction.UserId, out var reactionUser);
|
||||
reactionsWithUser.Add(new ReactionDto(
|
||||
reaction.Id,
|
||||
reaction.Emoji,
|
||||
reaction.UserId,
|
||||
reactionUser != null
|
||||
? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar)
|
||||
: new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null)
|
||||
));
|
||||
}
|
||||
|
||||
var readByList = chat.Members
|
||||
.Where(m => m.LastReadSequenceId >= latestMessage.SequenceId && m.UserId != latestMessage.SenderId)
|
||||
.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,
|
||||
latestMessage.SenderId,
|
||||
latestMessage.Content,
|
||||
latestMessage.Type,
|
||||
latestMessage.ReplyToId,
|
||||
textMessage?.Quote,
|
||||
storyMessage?.StoryId,
|
||||
storyMessage?.StoryMediaUrl,
|
||||
storyMessage?.StoryMediaType,
|
||||
latestMessage.IsEdited,
|
||||
latestMessage.IsDeleted,
|
||||
latestMessage.CreatedAt,
|
||||
latestMessage.SequenceId,
|
||||
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,
|
||||
senderObj.DisplayName,
|
||||
senderObj.Avatar
|
||||
) : new MessageSenderDto(latestMessage.SenderId, "unknown", "Unknown", null),
|
||||
reactionsWithUser,
|
||||
readByList
|
||||
var pinnedDtoList = new List<PinnedMessageDto>();
|
||||
foreach (var pm in pinnedMessages)
|
||||
{
|
||||
pinnedDtoList.Add(new PinnedMessageDto(
|
||||
pm.Id,
|
||||
MessageMapper.MapToDto(pm, usersInfo, new List<MessageReaction>(), new List<Guid>())
|
||||
));
|
||||
}
|
||||
|
||||
@@ -152,6 +119,7 @@ internal sealed class GetChatByIdQueryHandler : IQueryHandler<GetChatByIdQuery,
|
||||
chat.CreatedAt,
|
||||
members,
|
||||
messagesList,
|
||||
pinnedDtoList,
|
||||
unreadCount
|
||||
);
|
||||
|
||||
|
||||
@@ -59,6 +59,9 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
|
||||
}
|
||||
}
|
||||
|
||||
var pinnedMessages = await _messageRepository.GetPinnedMessagesAsync(chat.Id, cancellationToken);
|
||||
foreach (var pm in pinnedMessages) userIdsToFetch.Add(pm.SenderId);
|
||||
|
||||
var usersInfo = await _userProvider.GetUsersInfoAsync(userIdsToFetch, cancellationToken);
|
||||
|
||||
var members = new List<ChatMemberDto>();
|
||||
@@ -85,53 +88,19 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
|
||||
|
||||
if (latestMessage != null)
|
||||
{
|
||||
usersInfo.TryGetValue(latestMessage.SenderId, out var senderObj);
|
||||
messagesList.Add(MessageMapper.MapToDto(
|
||||
latestMessage,
|
||||
usersInfo,
|
||||
latestReactions,
|
||||
chat.Members.Where(m => m.LastReadSequenceId >= latestMessage.SequenceId && m.UserId != latestMessage.SenderId).Select(m => m.UserId)));
|
||||
}
|
||||
|
||||
var reactionsWithUser = new List<ReactionDto>();
|
||||
foreach (var reaction in latestReactions)
|
||||
{
|
||||
usersInfo.TryGetValue(reaction.UserId, out var reactionUser);
|
||||
reactionsWithUser.Add(new ReactionDto(
|
||||
reaction.Id,
|
||||
reaction.Emoji,
|
||||
reaction.UserId,
|
||||
reactionUser != null
|
||||
? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar)
|
||||
: new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null)
|
||||
));
|
||||
}
|
||||
|
||||
var textMessage = latestMessage as TextMessage;
|
||||
var mediaMessage = latestMessage as MediaMessage;
|
||||
var storyMessage = latestMessage as StoryMessage;
|
||||
|
||||
messagesList.Add(new ChatMessageDto(
|
||||
latestMessage.Id,
|
||||
latestMessage.ChatId,
|
||||
latestMessage.SenderId,
|
||||
latestMessage.Content,
|
||||
latestMessage.Type,
|
||||
latestMessage.ReplyToId,
|
||||
textMessage?.Quote,
|
||||
storyMessage?.StoryId,
|
||||
storyMessage?.StoryMediaUrl,
|
||||
storyMessage?.StoryMediaType,
|
||||
latestMessage.IsEdited,
|
||||
latestMessage.IsDeleted,
|
||||
latestMessage.CreatedAt,
|
||||
latestMessage.SequenceId,
|
||||
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,
|
||||
senderObj.DisplayName,
|
||||
senderObj.Avatar
|
||||
) : new MessageSenderDto(latestMessage.SenderId, "unknown", "Unknown", null),
|
||||
reactionsWithUser,
|
||||
chat.Members.Where(m => m.LastReadSequenceId >= latestMessage.SequenceId && m.UserId != latestMessage.SenderId).Select(m => new ReadByDto(m.UserId)).ToList(),
|
||||
(latestMessage as CallMessage)?.CallType,
|
||||
(latestMessage as CallMessage)?.CallStatus,
|
||||
(latestMessage as CallMessage)?.Duration
|
||||
var pinnedDtoList = new List<PinnedMessageDto>();
|
||||
foreach (var pm in pinnedMessages)
|
||||
{
|
||||
pinnedDtoList.Add(new PinnedMessageDto(
|
||||
pm.Id,
|
||||
MessageMapper.MapToDto(pm, usersInfo, new List<MessageReaction>(), new List<Guid>())
|
||||
));
|
||||
}
|
||||
|
||||
@@ -147,6 +116,7 @@ internal sealed class GetChatsQueryHandler : IQueryHandler<GetChatsQuery, List<C
|
||||
chat.CreatedAt,
|
||||
members,
|
||||
messagesList,
|
||||
pinnedDtoList,
|
||||
unreadCount,
|
||||
chat.IsImporting,
|
||||
chat.ImportJobId
|
||||
|
||||
@@ -12,6 +12,7 @@ public record ChatDto(
|
||||
DateTime CreatedAt,
|
||||
List<ChatMemberDto> Members,
|
||||
List<ChatMessageDto> Messages,
|
||||
List<PinnedMessageDto> PinnedMessages,
|
||||
int UnreadCount,
|
||||
bool IsImporting = false,
|
||||
Guid? ImportJobId = null
|
||||
|
||||
@@ -24,6 +24,12 @@ public record ChatMessageDto(
|
||||
List<ReadByDto> ReadBy,
|
||||
string? CallType = null,
|
||||
string? CallStatus = null,
|
||||
int? Duration = null
|
||||
int? Duration = null,
|
||||
List<PollOptionDto>? PollOptions = null,
|
||||
bool? PollIsMultipleChoice = null,
|
||||
bool? PollIsClosed = null
|
||||
);
|
||||
|
||||
public record PollOptionDto(string Text, int VoteCount);
|
||||
|
||||
|
||||
|
||||
@@ -7,6 +7,6 @@ public record MediaDto(
|
||||
string Type,
|
||||
string? Url,
|
||||
string? Filename,
|
||||
long? Size
|
||||
long? Size,
|
||||
string? Duration = null
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
using Knot.Shared.Kernel;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public static class MessageMapper
|
||||
{
|
||||
public static ChatMessageDto MapToDto(
|
||||
Message message,
|
||||
IReadOnlyDictionary<Guid, UserInfo> usersInfo,
|
||||
IEnumerable<MessageReaction> reactions,
|
||||
IEnumerable<Guid> readByUsers)
|
||||
{
|
||||
usersInfo.TryGetValue(message.SenderId, out var senderObj);
|
||||
|
||||
var reactionsWithUser = new List<ReactionDto>();
|
||||
foreach (var reaction in reactions)
|
||||
{
|
||||
usersInfo.TryGetValue(reaction.UserId, out var reactionUser);
|
||||
reactionsWithUser.Add(new ReactionDto(
|
||||
reaction.Id,
|
||||
reaction.Emoji,
|
||||
reaction.UserId,
|
||||
reactionUser != null
|
||||
? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar)
|
||||
: new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null)
|
||||
));
|
||||
}
|
||||
|
||||
var textMessage = message as TextMessage;
|
||||
var mediaMessage = message as MediaMessage;
|
||||
var storyMessage = message as StoryMessage;
|
||||
var callMessage = message as CallMessage;
|
||||
|
||||
return new ChatMessageDto(
|
||||
message.Id,
|
||||
message.ChatId,
|
||||
message.SenderId,
|
||||
message.Content,
|
||||
message.Type,
|
||||
message.ReplyToId,
|
||||
textMessage?.Quote,
|
||||
storyMessage?.StoryId,
|
||||
storyMessage?.StoryMediaUrl,
|
||||
storyMessage?.StoryMediaType,
|
||||
message.IsEdited,
|
||||
message.IsDeleted,
|
||||
message.CreatedAt,
|
||||
message.SequenceId,
|
||||
mediaMessage?.Media.Select(media => new MediaDto(media.Id, media.Type, media.Url, media.Filename, media.Size, media.Duration)).ToList() ?? new List<MediaDto>(),
|
||||
senderObj != null ? new MessageSenderDto(
|
||||
senderObj.Id,
|
||||
senderObj.Username,
|
||||
senderObj.DisplayName,
|
||||
senderObj.Avatar
|
||||
) : new MessageSenderDto(message.SenderId, "unknown", "Unknown", null),
|
||||
reactionsWithUser,
|
||||
readByUsers.Select(id => new ReadByDto(id)).ToList(),
|
||||
callMessage?.CallType,
|
||||
callMessage?.CallStatus,
|
||||
callMessage?.Duration,
|
||||
(message as PollMessage)?.Options.Select(o => new PollOptionDto(o.Text, o.VoteCount)).ToList(),
|
||||
(message as PollMessage)?.IsMultipleChoice,
|
||||
(message as PollMessage)?.IsClosed
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace Knot.Modules.Conversations.Application.DTOs;
|
||||
|
||||
public record PinnedMessageDto(
|
||||
Guid Id,
|
||||
ChatMessageDto Message
|
||||
);
|
||||
@@ -97,7 +97,7 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
|
||||
|
||||
if (filterType == "gifs") return isGif;
|
||||
if (filterType == "media") return (mType == "image" || mType == "video") && !isGif;
|
||||
if (filterType == "files") return mType == "file" || (mType != "image" && mType != "video" && mType != "link" && !isGif);
|
||||
if (filterType == "files") return (mType == "file" || mType == "audio") && !isGif && mType != "image" && mType != "video";
|
||||
if (filterType == "links") return mType == "link";
|
||||
|
||||
return true;
|
||||
@@ -119,7 +119,7 @@ internal sealed class GetSharedMediaQueryHandler : IQueryHandler<GetSharedMediaQ
|
||||
storyMessage?.StoryMediaType,
|
||||
message.IsEdited,
|
||||
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, media.Duration)).ToList()
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||
using Knot.Contracts.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.Pin;
|
||||
|
||||
public sealed record PinMessageCommand(Guid MessageId, Guid ChatId, Guid UserId) : ICommand<Guid>;
|
||||
|
||||
public sealed class PinMessageCommandHandler : ICommandHandler<PinMessageCommand, Guid>
|
||||
{
|
||||
private readonly IMessageRepository _messageRepository;
|
||||
private readonly IChatRepository _chatRepository;
|
||||
private readonly IChatsUnitOfWork _unitOfWork;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public PinMessageCommandHandler(
|
||||
IMessageRepository messageRepository,
|
||||
IChatRepository chatRepository,
|
||||
IChatsUnitOfWork unitOfWork,
|
||||
IMediator mediator)
|
||||
{
|
||||
_messageRepository = messageRepository;
|
||||
_chatRepository = chatRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<Result<Guid>> Handle(PinMessageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
|
||||
if (chat is null) return Result.Failure<Guid>(ChatErrors.ChatsNotFound);
|
||||
|
||||
// Security check
|
||||
if (!chat.Members.Any(m => m.UserId == request.UserId))
|
||||
return Result.Failure<Guid>(ChatErrors.ChatsForbidden);
|
||||
|
||||
var message = await _messageRepository.GetByIdAsync(request.MessageId, cancellationToken);
|
||||
if (message is null) return Result.Failure<Guid>(ChatErrors.NotFound);
|
||||
|
||||
if (message.ChatId != request.ChatId)
|
||||
return Result.Failure<Guid>(ChatErrors.NotFound);
|
||||
|
||||
message.AddState(MessageState.IsPinned);
|
||||
|
||||
await _messageRepository.UpdateAsync(message, cancellationToken);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Notify chat about pinned message change
|
||||
await _mediator.Publish(new MessagePinnedDomainEvent(message.Id, message.ChatId, message.SenderId, message.Content), cancellationToken);
|
||||
|
||||
return Result.Success(message.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public record MessagePinnedDomainEvent(Guid MessageId, Guid ChatId, Guid SenderId, string? Content) : INotification;
|
||||
@@ -0,0 +1,58 @@
|
||||
using Knot.Contracts.Messaging.Application.Abstractions;
|
||||
using Knot.Contracts.Messaging.Domain;
|
||||
using Knot.Contracts.Conversations.Application.Abstractions;
|
||||
using Knot.Contracts.Conversations.Domain;
|
||||
using Knot.Shared.Kernel;
|
||||
using MediatR;
|
||||
|
||||
namespace Knot.Modules.Conversations.Application.Messages.Unpin;
|
||||
|
||||
public sealed record UnpinMessageCommand(Guid MessageId, Guid ChatId, Guid UserId) : ICommand<Guid>;
|
||||
|
||||
public sealed class UnpinMessageCommandHandler : ICommandHandler<UnpinMessageCommand, Guid>
|
||||
{
|
||||
private readonly IMessageRepository _messageRepository;
|
||||
private readonly IChatRepository _chatRepository;
|
||||
private readonly IChatsUnitOfWork _unitOfWork;
|
||||
private readonly IMediator _mediator;
|
||||
|
||||
public UnpinMessageCommandHandler(
|
||||
IMessageRepository messageRepository,
|
||||
IChatRepository chatRepository,
|
||||
IChatsUnitOfWork unitOfWork,
|
||||
IMediator mediator)
|
||||
{
|
||||
_messageRepository = messageRepository;
|
||||
_chatRepository = chatRepository;
|
||||
_unitOfWork = unitOfWork;
|
||||
_mediator = mediator;
|
||||
}
|
||||
|
||||
public async Task<Result<Guid>> Handle(UnpinMessageCommand request, CancellationToken cancellationToken)
|
||||
{
|
||||
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
|
||||
if (chat is null) return Result.Failure<Guid>(ChatErrors.ChatsNotFound);
|
||||
|
||||
// Security check
|
||||
if (!chat.Members.Any(m => m.UserId == request.UserId))
|
||||
return Result.Failure<Guid>(ChatErrors.ChatsForbidden);
|
||||
|
||||
var message = await _messageRepository.GetByIdAsync(request.MessageId, cancellationToken);
|
||||
if (message is null) return Result.Failure<Guid>(ChatErrors.NotFound);
|
||||
|
||||
if (message.ChatId != request.ChatId)
|
||||
return Result.Failure<Guid>(ChatErrors.NotFound);
|
||||
|
||||
message.RemoveState(MessageState.IsPinned);
|
||||
|
||||
await _messageRepository.UpdateAsync(message, cancellationToken);
|
||||
await _unitOfWork.SaveChangesAsync(cancellationToken);
|
||||
|
||||
// Notify chat about unpinned message change
|
||||
await _mediator.Publish(new MessageUnpinnedDomainEvent(message.Id, message.ChatId), cancellationToken);
|
||||
|
||||
return Result.Success(message.Id);
|
||||
}
|
||||
}
|
||||
|
||||
public record MessageUnpinnedDomainEvent(Guid MessageId, Guid ChatId) : INotification;
|
||||
Reference in New Issue
Block a user