Merge branch 'main' into android

This commit is contained in:
Халимов Рустам
2026-04-19 21:24:40 +03:00
5 changed files with 581 additions and 584 deletions
@@ -32,7 +32,8 @@ public record MessageDetailDto(
bool? PollIsMultipleChoice = null, bool? PollIsMultipleChoice = null,
bool? PollIsAnonymous = null, bool? PollIsAnonymous = null,
bool? PollIsClosed = null, bool? PollIsClosed = null,
List<Guid>? UserVotedOptionIds = null List<Guid>? UserVotedOptionIds = null,
bool IsDeletedForUser = false
); );
public record ReplyToMessageDto( public record ReplyToMessageDto(
@@ -42,10 +42,16 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessage
if (request.DeleteForAll) if (request.DeleteForAll)
{ {
// Only message sender can delete for everyone
if (message.SenderId == request.UserId) if (message.SenderId == request.UserId)
{ {
message.Delete(); message.Delete();
} }
else
{
// If not the sender, just delete for current user
message.DeleteForUser(request.UserId);
}
} }
else else
{ {
@@ -55,24 +61,13 @@ public sealed class DeleteMessagesCommandHandler : ICommandHandler<DeleteMessage
await _messageRepository.UpdateAsync(message, cancellationToken); await _messageRepository.UpdateAsync(message, cancellationToken);
} }
if (request.DeleteForAll) // Notify all clients in the chat about the deletion
{
await _hubContext.Clients.Group(request.ChatId.ToString()).SendAsync("messages_deleted", new await _hubContext.Clients.Group(request.ChatId.ToString()).SendAsync("messages_deleted", new
{ {
chatId = request.ChatId, chatId = request.ChatId,
messageIds = request.MessageIds, messageIds = request.MessageIds,
deleteForAll = true deleteForAll = request.DeleteForAll
}); });
}
else
{
await _hubContext.Clients.User(request.UserId.ToString()).SendAsync("messages_deleted", new
{
chatId = request.ChatId,
messageIds = request.MessageIds,
deleteForAll = false
});
}
return global::Knot.Shared.Kernel.Result.Success(); return global::Knot.Shared.Kernel.Result.Success();
} }
@@ -3,11 +3,11 @@ using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Messaging.Application.Abstractions; using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain; using Knot.Contracts.Messaging.Domain;
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Modules.Conversations.Application.DTOs; using Knot.Modules.Conversations.Application.DTOs;
using Knot.Contracts.Conversations.Domain;
using Knot.Shared.Kernel; using Knot.Shared.Kernel;
using MediatR; using MediatR;
@@ -115,6 +115,7 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
senders.TryGetValue(message.SenderId, out var sender); senders.TryGetValue(message.SenderId, out var sender);
reactionsByMessage.TryGetValue(message.Id, out var reactions); reactionsByMessage.TryGetValue(message.Id, out var reactions);
Message? replyMsg = null; Message? replyMsg = null;
if (message.ReplyToId.HasValue) if (message.ReplyToId.HasValue)
{ {
@@ -141,7 +142,7 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
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<MediaDto>(), 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<MediaDto>(),
replySender != null ? new MessageSenderDto(replySender.Id, replySender.Username, replySender.DisplayName, replySender.Avatar) : null replySender != null ? new MessageSenderDto(replySender.Id, replySender.Username, replySender.DisplayName, replySender.Avatar) : null
) : null, ) : null,
message is TextMessage tm ? tm.Quote : null, (message as TextMessage)?.Quote,
message.IsEdited, message.IsEdited,
message.IsDeleted, message.IsDeleted,
message.CreatedAt, message.CreatedAt,
@@ -154,19 +155,22 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
(message as MediaMessage)?.Media.Select(m => new MediaDto(m.Id, m.Type, m.Url, m.Filename, m.Size, m.Duration)).ToList() ?? new List<MediaDto>(), (message as MediaMessage)?.Media.Select(m => new MediaDto(m.Id, m.Type, m.Url, m.Filename, m.Size, m.Duration)).ToList() ?? new List<MediaDto>(),
sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : new MessageSenderDto(message.SenderId, "unknown", "Unknown", null), sender != null ? new MessageSenderDto(sender.Id, sender.Username, sender.DisplayName, sender.Avatar) : new MessageSenderDto(message.SenderId, "unknown", "Unknown", null),
new List<ReadByDto>(), // ReadBy not implemented in this detailed view yet new List<ReadByDto>(), // ReadBy not implemented in this detailed view yet
reactions?.Select(r => { reactions?.Select(r =>
{
senders.TryGetValue(r.UserId, out var ru); 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); 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<MessageReactionDto>(), }).ToList() ?? new List<MessageReactionDto>(),
(message as CallMessage)?.CallType, (message as CallMessage)?.CallType,
(message as CallMessage)?.CallStatus, (message as CallMessage)?.CallStatus,
(message as CallMessage)?.Duration, (message as CallMessage)?.Duration,
(message as PollMessage)?.Options.Select(o => { (message as PollMessage)?.Options.Select(o =>
{
var pm = (PollMessage)message; var pm = (PollMessage)message;
var voters = pm.IsAnonymous == false var voters = pm.IsAnonymous == false
? pm.Votes ? pm.Votes
.Where(v => v.OptionId == o.Id) .Where(v => v.OptionId == o.Id)
.Select(v => { .Select(v =>
{
senders.TryGetValue(v.UserId, out var vu); senders.TryGetValue(v.UserId, out var vu);
return vu != null return vu != null
? new MessageSenderDto(vu.Id, vu.Username, vu.DisplayName, vu.Avatar) ? new MessageSenderDto(vu.Id, vu.Username, vu.DisplayName, vu.Avatar)
@@ -179,7 +183,8 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
(message as PollMessage)?.IsMultipleChoice, (message as PollMessage)?.IsMultipleChoice,
(message as PollMessage)?.IsAnonymous, (message as PollMessage)?.IsAnonymous,
(message as PollMessage)?.IsClosed, (message as PollMessage)?.IsClosed,
(message as PollMessage)?.Votes.Where(v => v.UserId == request.UserId).Select(v => v.OptionId).ToList() (message as PollMessage)?.Votes.Where(v => v.UserId == request.UserId).Select(v => v.OptionId).ToList(),
message.IsDeletedForUser(request.UserId)
)); ));
} }
+1
View File
@@ -84,6 +84,7 @@ export interface Message {
storyMediaType?: string | null; storyMediaType?: string | null;
isEdited: boolean; isEdited: boolean;
isDeleted: boolean; isDeleted: boolean;
isDeletedForUser?: boolean;
scheduledAt?: string | null; scheduledAt?: string | null;
createdAt: string; createdAt: string;
updatedAt?: string; updatedAt?: string;
@@ -290,7 +290,7 @@ function MessageBubble({
}; };
}, [showContext]); }, [showContext]);
if (message.isDeleted) { if (message.isDeleted || message.isDeletedForUser) {
return null; return null;
} }
@@ -491,8 +491,7 @@ function MessageBubble({
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu}
onDoubleClick={handleReply} onDoubleClick={handleReply}
title={t('reply') ? `${t('reply')} (Double Click)` : 'Double click to reply'} title={t('reply') ? `${t('reply')} (Double Click)` : 'Double click to reply'}
className={`cursor-pointer max-w-full min-w-[60px] transition-all duration-500 overflow-hidden ${ className={`cursor-pointer max-w-full min-w-[60px] transition-all duration-500 overflow-hidden ${!needsFrame
!needsFrame
? 'p-0 shadow-none border-none bg-transparent' ? 'p-0 shadow-none border-none bg-transparent'
: isMine : isMine
? 'bubble-sent px-4 py-3 hover:brightness-110' ? 'bubble-sent px-4 py-3 hover:brightness-110'
@@ -503,8 +502,7 @@ function MessageBubble({
{/* Reply */} {/* Reply */}
{message.replyTo && ( {message.replyTo && (
<div <div
className={`mb-2 pl-3 py-2 cursor-pointer transition-all -mx-1 px-2 rounded-xl ${ className={`mb-2 pl-3 py-2 cursor-pointer transition-all -mx-1 px-2 rounded-xl ${isMine ? 'bg-[#1a1a1a] border-l-[3px] border-l-primary hover:bg-[#202020]' : 'bg-white/5 border-l-[3px] border-l-primary hover:bg-white/10'
isMine ? 'bg-[#1a1a1a] border-l-[3px] border-l-primary hover:bg-[#202020]' : 'bg-white/5 border-l-[3px] border-l-primary hover:bg-white/10'
}`} }`}
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
@@ -565,8 +563,7 @@ function MessageBubble({
{/* Story Reply Quote */} {/* Story Reply Quote */}
{message.storyId && ( {message.storyId && (
<div <div
className={`mb-1.5 pl-2.5 py-0.5 border-l-[3px] transition-colors -mx-1 px-1 rounded-sm ${ className={`mb-1.5 pl-2.5 py-0.5 border-l-[3px] transition-colors -mx-1 px-1 rounded-sm ${isMine ? 'border-l-white/80 hover:bg-white/10' : 'border-l-knot-500 hover:bg-knot-500/10'
isMine ? 'border-l-white/80 hover:bg-white/10' : 'border-l-knot-500 hover:bg-knot-500/10'
}`} }`}
> >
<p className={`text-[11px] font-bold uppercase tracking-wider mb-1 ${isMine ? 'text-white/80' : 'text-knot-500/80'}`}> <p className={`text-[11px] font-bold uppercase tracking-wider mb-1 ${isMine ? 'text-white/80' : 'text-knot-500/80'}`}>
@@ -617,8 +614,7 @@ function MessageBubble({
${isSingleGif ? 'max-w-[260px]' : ''} ${isSingleGif ? 'max-w-[260px]' : ''}
overflow-hidden relative rounded-[1.25rem] overflow-hidden relative rounded-[1.25rem]
`}> `}>
<div className={`grid gap-[2px] ${galleryMedia.length > 1 ? 'w-[80vw] sm:w-[380px] md:w-[450px]' : 'w-full'} ${ <div className={`grid gap-[2px] ${galleryMedia.length > 1 ? 'w-[80vw] sm:w-[380px] md:w-[450px]' : 'w-full'} ${galleryMedia.length === 1 ? 'grid-cols-1' : 'grid-cols-6'
galleryMedia.length === 1 ? 'grid-cols-1' : 'grid-cols-6'
}`}> }`}>
{galleryMedia.map((m, idx) => { {galleryMedia.map((m, idx) => {
const gif = isMediaGif(m); const gif = isMediaGif(m);
@@ -1071,8 +1067,7 @@ function MessageBubble({
<button <button
key={emoji} key={emoji}
onClick={(e) => { e.stopPropagation(); handleReaction(emoji); }} onClick={(e) => { e.stopPropagation(); handleReaction(emoji); }}
className={`flex items-center gap-2 px-2.5 py-1.5 rounded-[10px] transition-all border ${ className={`flex items-center gap-2 px-2.5 py-1.5 rounded-[10px] transition-all border ${data.isMine
data.isMine
? 'bg-primary/20 border-primary text-white shadow-lg' ? 'bg-primary/20 border-primary text-white shadow-lg'
: 'bg-[#201F1F] border-white/5 text-zinc-300 hover:bg-[#2a2a2a]' : 'bg-[#201F1F] border-white/5 text-zinc-300 hover:bg-[#2a2a2a]'
} shadow-md group/react`} } shadow-md group/react`}