Опросы

This commit is contained in:
Халимов Рустам
2026-04-07 11:11:35 +03:00
parent c45f4db61c
commit 852efa090e
25 changed files with 530 additions and 175 deletions
@@ -3,4 +3,5 @@ namespace Knot.Contracts.Messaging.Application.Abstractions;
public interface IMessageNotifier
{
Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken);
Task NotifyMessageUpdateAsync(Guid chatId, string updateType, object updatePayload, CancellationToken cancellationToken);
}
@@ -12,6 +12,7 @@ public interface IMessageRepository
Task<List<Message>> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken);
Task<List<Message>> GetChatMessagesCursorAsync(Guid chatId, DateTime? cursor, long? sequenceId, int limit, CancellationToken cancellationToken);
Task<List<Message>> GetChatMessagesAroundAsync(Guid chatId, long sequenceId, int limit, CancellationToken cancellationToken);
Task<Message?> GetLastStoryMessageAsync(Guid chatId, Guid storyId, CancellationToken cancellationToken);
Task UpdateAsync(Message message, CancellationToken cancellationToken);
@@ -7,26 +7,32 @@ 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 List<PollOption> Options { get; set; } = new();
public List<PollVote> Votes { get; set; } = new();
public bool IsMultipleChoice { get; set; }
public bool IsAnonymous { 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)
public PollMessage(Guid id, Guid chatId, Guid senderId, string? question, List<PollOption>? 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 });
}
}
Options = options ?? new List<PollOption>();
IsAnonymous = isAnonymous;
IsMultipleChoice = isMultiple;
ExpiresAt = expiresAt;
}
public static PollMessage Create(Guid id, Guid chatId, Guid senderId, string? question, List<string> options, bool isAnonymous, bool isMultiple, DateTime? expiresAt, Guid? replyToId, Guid? forwardedFromId)
{
var poll = new PollMessage(id, chatId, senderId, question, null, isAnonymous, isMultiple, expiresAt, replyToId, forwardedFromId, DateTime.UtcNow, false);
foreach (var opt in options)
{
poll.Options.Add(new PollOption { Text = opt });
}
return poll;
}
}
@@ -2,6 +2,7 @@ namespace Knot.Contracts.Messaging.Domain;
public class PollOption
{
public Guid Id { get; set; } = Guid.NewGuid();
public string Text { get; set; } = string.Empty;
public int VoteCount { get; set; }
}
@@ -4,7 +4,7 @@ namespace Knot.Contracts.Messaging.Domain;
public class PollVote
{
public Guid OptionIndex { get; set; }
public Guid OptionId { get; set; }
public Guid UserId { get; set; }
public DateTime VotedAt { get; set; }
}
@@ -27,9 +27,11 @@ public record ChatMessageDto(
int? Duration = null,
List<PollOptionDto>? PollOptions = null,
bool? PollIsMultipleChoice = null,
bool? PollIsClosed = null
bool? PollIsAnonymous = null,
bool? PollIsClosed = null,
List<Guid>? UserVotedOptionIds = null
);
public record PollOptionDto(string Text, int VoteCount);
public record PollOptionDto(Guid Id, string Text, int VoteCount, List<MessageSenderDto>? Voters = null, List<Guid>? VoterIds = null);
@@ -27,7 +27,12 @@ public record MessageDetailDto(
List<MessageReactionDto> Reactions,
string? CallType = null,
string? CallStatus = null,
int? Duration = null
int? Duration = null,
List<PollOptionDto>? PollOptions = null,
bool? PollIsMultipleChoice = null,
bool? PollIsAnonymous = null,
bool? PollIsClosed = null,
List<Guid>? UserVotedOptionIds = null
);
public record ReplyToMessageDto(
@@ -10,7 +10,8 @@ public static class MessageMapper
Message message,
IReadOnlyDictionary<Guid, UserInfo> usersInfo,
IEnumerable<MessageReaction> reactions,
IEnumerable<Guid> readByUsers)
IEnumerable<Guid> readByUsers,
Guid? currentUserId = null)
{
usersInfo.TryGetValue(message.SenderId, out var senderObj);
@@ -60,9 +61,24 @@ public static class MessageMapper
callMessage?.CallType,
callMessage?.CallStatus,
callMessage?.Duration,
(message as PollMessage)?.Options.Select(o => new PollOptionDto(o.Text, o.VoteCount)).ToList(),
message is PollMessage pm ? pm.Options.Select(o => {
var voters = pm.IsAnonymous == false
? pm.Votes
.Where(v => v.OptionId == o.Id)
.Select(v => {
usersInfo.TryGetValue(v.UserId, out var vu);
return vu != null
? new MessageSenderDto(vu.Id, vu.Username, vu.DisplayName, vu.Avatar)
: new MessageSenderDto(v.UserId, "unknown", "Unknown", null);
})
.ToList()
: null;
return new PollOptionDto(o.Id, o.Text, o.VoteCount, voters, pm.IsAnonymous == false ? pm.Votes.Where(v => v.OptionId == o.Id).Select(v => v.UserId).ToList() : null);
}).ToList() : null,
(message as PollMessage)?.IsMultipleChoice,
(message as PollMessage)?.IsClosed
(message as PollMessage)?.IsAnonymous,
(message as PollMessage)?.IsClosed,
(message is PollMessage poll && currentUserId.HasValue) ? poll.Votes.Where(v => v.UserId == currentUserId.Value).Select(v => v.OptionId).ToList() : null
);
}
}
@@ -13,7 +13,7 @@ using MediatR;
namespace Knot.Modules.Conversations.Application.Messages.GetMessages;
public record GetMessagesQuery(Guid UserId, Guid ChatId, string? Cursor) : IQuery<List<MessageDetailDto>>;
public record GetMessagesQuery(Guid UserId, Guid ChatId, string? Cursor, long? Pivot = null, int? Limit = null) : IQuery<List<MessageDetailDto>>;
internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery, List<MessageDetailDto>>
{
@@ -38,24 +38,34 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
return Result.Failure<List<MessageDetailDto>>(ChatErrors.ChatsForbidden);
}
DateTime? cursorDate = null;
long? cursorSequenceId = null;
List<Message> messages;
int queryLimit = request.Limit ?? ChatConstants.DefaultMessageQueryLimit;
if (!string.IsNullOrEmpty(request.Cursor))
if (request.Pivot.HasValue)
{
if (long.TryParse(request.Cursor, out var seqId))
messages = await _messageRepository.GetChatMessagesAroundAsync(request.ChatId, request.Pivot.Value, queryLimit, cancellationToken);
}
else
{
DateTime? cursorDate = null;
long? cursorSequenceId = null;
if (!string.IsNullOrEmpty(request.Cursor))
{
cursorSequenceId = seqId;
}
else if (DateTime.TryParse(request.Cursor, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed))
{
cursorDate = parsed.ToUniversalTime();
if (long.TryParse(request.Cursor, out var seqId))
{
cursorSequenceId = seqId;
}
else if (DateTime.TryParse(request.Cursor, null, System.Globalization.DateTimeStyles.RoundtripKind, out var parsed))
{
cursorDate = parsed.ToUniversalTime();
}
}
messages = await _messageRepository.GetChatMessagesCursorAsync(request.ChatId, cursorDate, cursorSequenceId, queryLimit, cancellationToken);
}
var messages = await _messageRepository.GetChatMessagesCursorAsync(request.ChatId, cursorDate, cursorSequenceId, ChatConstants.DefaultMessageQueryLimit, cancellationToken);
var result = new List<MessageDetailDto>();
var userIdsToFetch = new HashSet<Guid>();
var replyMessages = new Dictionary<Guid, Message>();
@@ -66,6 +76,14 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
{
userIdsToFetch.Add(m.SenderId);
if (m is PollMessage poll && !poll.IsAnonymous)
{
foreach (var vote in poll.Votes)
{
userIdsToFetch.Add(vote.UserId);
}
}
if (!m.ReplyToId.HasValue)
{
continue;
@@ -94,76 +112,77 @@ internal sealed class GetMessagesQueryHandler : IQueryHandler<GetMessagesQuery,
continue;
}
ReplyToMessageDto? replyToObj = null;
if (message.ReplyToId.HasValue && replyMessages.TryGetValue(message.ReplyToId.Value, out var replyMsg))
senders.TryGetValue(message.SenderId, out var sender);
reactionsByMessage.TryGetValue(message.Id, out var reactions);
Message? replyMsg = null;
if (message.ReplyToId.HasValue)
{
senders.TryGetValue(replyMsg.SenderId, out var rs);
var senderObj = rs != null
? new MessageSenderDto(rs.Id, rs.Username, rs.DisplayName, rs.Avatar)
: null;
replyToObj = new ReplyToMessageDto(
replyMsg.Id,
replyMsg.Content,
replyMsg.IsDeleted,
(replyMsg as MediaMessage)?.Media.Select(rm => new MediaDto(rm.Id, rm.Type, rm.Url, rm.Filename, rm.Size)).ToList() ?? new List<MediaDto>(),
senderObj
);
replyMessages.TryGetValue(message.ReplyToId.Value, out replyMsg);
}
var reactionsWithUser = new List<MessageReactionDto>();
var messageReactions = reactionsByMessage.TryGetValue(message.Id, out var mr) ? mr : new List<MessageReaction>();
foreach (var reaction in messageReactions)
UserInfo? replySender = null;
if (replyMsg != null)
{
var userObj = senders.TryGetValue(reaction.UserId, out var reactionUser)
? new MessageSenderDto(reactionUser.Id, reactionUser.Username, reactionUser.DisplayName, reactionUser.Avatar)
: new MessageSenderDto(reaction.UserId, "unknown", "Unknown", null);
reactionsWithUser.Add(new MessageReactionDto(
reaction.Id,
reaction.Emoji,
reaction.UserId,
userObj
));
senders.TryGetValue(replyMsg.SenderId, out replySender);
}
var textMessage = message as TextMessage;
var mediaMessage = message as MediaMessage;
var storyMessage = message as StoryMessage;
result.Add(new MessageDetailDto(
message.Id,
message.ChatId,
message.SenderId,
message.Content,
message.Type,
message.Type.ToLower(),
message.ReplyToId,
replyToObj,
textMessage?.Quote,
replyMsg != null ? new ReplyToMessageDto(
replyMsg.Id,
replyMsg.Content,
replyMsg.IsDeleted,
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
) : null,
message is TextMessage tm ? tm.Quote : null,
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,
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,
null, // ForwardedFrom details not implemented here yet
(message as StoryMessage)?.StoryId,
(message as StoryMessage)?.StoryMediaUrl,
(message as StoryMessage)?.StoryMediaType,
(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),
new List<ReadByDto>(), // ReadBy not implemented in this detailed view yet
reactions?.Select(r => {
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);
}).ToList() ?? new List<MessageReactionDto>(),
(message as CallMessage)?.CallType,
(message as CallMessage)?.CallStatus,
(message as CallMessage)?.Duration
));
(message as CallMessage)?.Duration,
(message as PollMessage)?.Options.Select(o => {
var pm = (PollMessage)message;
var voters = pm.IsAnonymous == false
? pm.Votes
.Where(v => v.OptionId == o.Id)
.Select(v => {
senders.TryGetValue(v.UserId, out var vu);
return vu != null
? new MessageSenderDto(vu.Id, vu.Username, vu.DisplayName, vu.Avatar)
: new MessageSenderDto(v.UserId, "unknown", "Unknown", null);
})
.ToList()
: null;
return new PollOptionDto(o.Id, o.Text, o.VoteCount, voters, pm.IsAnonymous == false ? pm.Votes.Where(v => v.OptionId == o.Id).Select(v => v.UserId).ToList() : null);
}).ToList(),
(message as PollMessage)?.IsMultipleChoice,
(message as PollMessage)?.IsAnonymous,
(message as PollMessage)?.IsClosed,
(message as PollMessage)?.Votes.Where(v => v.UserId == request.UserId).Select(v => v.OptionId).ToList()
));
}
return Result.Success(result);
}
}
@@ -135,8 +135,9 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
else if (request.Type == "poll")
{
if (!_messagesSettings.Current.AllowPolls) return Result.Failure<Guid>(ChatErrors.PollsDisabled);
if (chat.Type != ChatType.Group) return Result.Failure<Guid>(new Error("Poll.InvalidChat", "Polls are only allowed in groups."));
message = new PollMessage(
message = PollMessage.Create(
Guid.NewGuid(),
request.ChatId,
request.SenderId,
@@ -146,9 +147,7 @@ public sealed class SendMessageCommandHandler : ICommandHandler<SendMessageComma
request.PollAllowMultipleAnswers ?? false,
request.PollExpiresAt,
request.ReplyToId,
request.ForwardedFromId,
DateTime.UtcNow,
false);
request.ForwardedFromId);
}
else if (request.Type == "call")
{
@@ -0,0 +1,110 @@
using Knot.Contracts.Conversations.Application.Abstractions;
using Knot.Contracts.Conversations.Domain;
using Knot.Contracts.Messaging.Domain;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Shared.Kernel;
using MediatR;
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Collections.Generic;
namespace Knot.Modules.Conversations.Application.Messages.Vote;
public sealed record VotePollCommand(
Guid MessageId,
Guid ChatId,
Guid UserId,
Guid OptionId) : ICommand;
public sealed class VotePollCommandHandler : ICommandHandler<VotePollCommand>
{
private readonly IMessageRepository _messageRepository;
private readonly IChatRepository _chatRepository;
private readonly IChatsUnitOfWork _unitOfWork;
private readonly IMessageNotifier _notifier;
private readonly IUserDisplayNameProvider _userProvider;
public VotePollCommandHandler(
IMessageRepository messageRepository,
IChatRepository chatRepository,
IChatsUnitOfWork unitOfWork,
IMessageNotifier notifier,
IUserDisplayNameProvider userProvider)
{
_messageRepository = messageRepository;
_chatRepository = chatRepository;
_unitOfWork = unitOfWork;
_notifier = notifier;
_userProvider = userProvider;
}
public async Task<Result> Handle(VotePollCommand request, CancellationToken cancellationToken)
{
var message = await _messageRepository.GetByIdAsync(request.MessageId, cancellationToken);
if (message is not PollMessage poll) return Result.Failure(new Error("Poll.NotFound", "Poll not found"));
if (poll.IsClosed) return Result.Failure(new Error("Poll.Closed", "This poll is closed."));
var chat = await _chatRepository.GetByIdAsync(request.ChatId, cancellationToken);
if (chat == null || !chat.Members.Any(m => m.UserId == request.UserId)) return Result.Failure(ChatErrors.ChatsForbidden);
var targetOption = poll.Options.FirstOrDefault(o => o.Id == request.OptionId);
if (targetOption == null) return Result.Failure(new Error("Poll.InvalidOption", "Invalid option ID."));
// Prevent duplicate or changed votes
var existingVote = poll.Votes.FirstOrDefault(v => v.UserId == request.UserId && v.OptionId == request.OptionId);
if (existingVote != null) return Result.Failure(new Error("Poll.AlreadyVoted", "You have already voted for this option."));
if (!poll.IsMultipleChoice)
{
var hasVotedInThisPoll = poll.Votes.Any(v => v.UserId == request.UserId);
if (hasVotedInThisPoll) return Result.Failure(new Error("Poll.AlreadyVoted", "You have already voted in this poll."));
}
poll.Votes.Add(new PollVote { UserId = request.UserId, OptionId = request.OptionId, VotedAt = DateTime.UtcNow });
targetOption.VoteCount++;
await _messageRepository.UpdateAsync(poll, cancellationToken);
await _unitOfWork.SaveChangesAsync(cancellationToken);
// Notify updated poll
var voterIds = poll.Votes.Select(v => v.UserId).Distinct().ToList();
var votersInfo = poll.IsAnonymous == false
? await _userProvider.GetUsersInfoAsync(voterIds, cancellationToken)
: new Dictionary<Guid, UserInfo>();
await _notifier.NotifyMessageUpdateAsync(poll.ChatId, "poll_updated", new
{
id = poll.Id,
chatId = poll.ChatId,
senderId = poll.SenderId,
createdAt = poll.CreatedAt,
type = "poll",
content = poll.Content,
pollOptions = poll.Options.Select(o => new {
id = o.Id,
text = o.Text,
voteCount = o.VoteCount,
voters = poll.IsAnonymous == false
? poll.Votes.Where(v => v.OptionId == o.Id)
.Select(v => {
votersInfo.TryGetValue(v.UserId, out var vu);
return vu != null
? new { id = vu.Id, username = vu.Username, displayName = vu.DisplayName, avatar = vu.Avatar }
: new { id = v.UserId, username = "unknown", displayName = "Unknown", avatar = (string?)null };
}).ToList()
: null,
voterIds = poll.IsAnonymous == false
? poll.Votes.Where(v => v.OptionId == o.Id).Select(v => v.UserId).ToList()
: null
}).ToList(),
pollIsMultipleChoice = poll.IsMultipleChoice,
pollIsClosed = poll.IsClosed,
pollIsAnonymous = poll.IsAnonymous
}, cancellationToken);
return Result.Success();
}
}
@@ -15,6 +15,7 @@ using Knot.Contracts.Auth.Domain;
using Knot.Contracts.Auth.Application.Abstractions;
using Knot.Modules.Conversations.Application.Messages.Pin;
using Knot.Modules.Conversations.Application.Messages.Unpin;
using Knot.Modules.Conversations.Application.Messages.Vote;
using Knot.Modules.Conversations.Application.DTOs;
using Knot.Contracts.Messaging.Application.Abstractions;
using Knot.Contracts.Messaging.Domain;
@@ -283,6 +284,17 @@ public sealed class ChatHub : Hub
});
}
[HubMethodName("vote_poll")]
public async Task VotePoll(VotePollRequest request)
{
var command = new VotePollCommand(request.MessageId, request.ChatId, _userContext.UserId, request.OptionId);
var result = await _sender.Send(command);
if (result.IsFailure)
{
throw new HubException(result.Error.Description);
}
}
// ────────────────────────────────────────────────────────────────
// Friend signals (Proxy methods for real-time notification)
// ────────────────────────────────────────────────────────────────
@@ -843,6 +855,7 @@ public sealed class ChatHub : Hub
public record GroupRenegotiateRequest(string ChatId, string TargetUserId, object Offer);
public record GroupRenegotiateAnswerRequest(string ChatId, string TargetUserId, object Answer);
public record FriendSignalRequest(string FriendId);
public record VotePollRequest(Guid MessageId, Guid ChatId, Guid OptionId);
public class CallSession
{
@@ -5,4 +5,23 @@ using System.Threading;
using System.Threading.Tasks;
using Knot.Contracts.Messaging.Application.Abstractions;
using Microsoft.AspNetCore.SignalR;
public class MessageNotifier : IMessageNotifier { private readonly IHubContext<ChatHub> _hubContext; public MessageNotifier(IHubContext<ChatHub> hubContext) { _hubContext = hubContext; } public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken) { return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken); } }
public class MessageNotifier : IMessageNotifier
{
private readonly IHubContext<ChatHub> _hubContext;
public MessageNotifier(IHubContext<ChatHub> hubContext)
{
_hubContext = hubContext;
}
public Task NotifyNewMessageAsync(Guid chatId, object messagePayload, CancellationToken cancellationToken)
{
return _hubContext.Clients.Group(chatId.ToString()).SendAsync("new_message", messagePayload, cancellationToken);
}
public Task NotifyMessageUpdateAsync(Guid chatId, string updateType, object updatePayload, CancellationToken cancellationToken)
{
return _hubContext.Clients.Group(chatId.ToString()).SendAsync(updateType, updatePayload, cancellationToken);
}
}
@@ -90,7 +90,11 @@ public sealed class MessageSentDomainEventHandler : INotificationHandler<Message
storyMediaType = (message as StoryMessage)?.StoryMediaType,
callType = (message as CallMessage)?.CallType,
callStatus = (message as CallMessage)?.CallStatus,
duration = (message as CallMessage)?.Duration
duration = (message as CallMessage)?.Duration,
pollOptions = (message as PollMessage)?.Options.Select(o => new { id = o.Id, text = o.Text, voteCount = o.VoteCount }).ToList(),
pollIsMultipleChoice = (message as PollMessage)?.IsMultipleChoice,
pollIsClosed = (message as PollMessage)?.IsClosed,
pollIsAnonymous = (message as PollMessage)?.IsAnonymous
}, cancellationToken);
}
}
@@ -95,6 +95,36 @@ public sealed class MessageRepository : IMessageRepository
.ToListAsync(cancellationToken);
}
public async Task<List<Message>> GetChatMessagesAroundAsync(Guid chatId, long sequenceId, int limit, CancellationToken cancellationToken)
{
var builder = Builders<Message>.Filter;
// Target message
var targetFilter = builder.And(builder.Eq(m => m.ChatId, chatId), builder.Eq(m => m.SequenceId, sequenceId));
var targetMsg = await _messages.Find(targetFilter).FirstOrDefaultAsync(cancellationToken);
// Older messages
var olderFilter = builder.And(builder.Eq(m => m.ChatId, chatId), builder.Lt(m => m.SequenceId, sequenceId));
var older = await _messages.Find(olderFilter)
.SortByDescending(m => m.SequenceId)
.Limit(limit / 2)
.ToListAsync(cancellationToken);
// Newer messages
var newerFilter = builder.And(builder.Eq(m => m.ChatId, chatId), builder.Gt(m => m.SequenceId, sequenceId));
var newer = await _messages.Find(newerFilter)
.SortBy(m => m.SequenceId)
.Limit(limit / 2)
.ToListAsync(cancellationToken);
var result = new List<Message>();
result.AddRange(older);
if (targetMsg != null) result.Add(targetMsg);
result.AddRange(newer);
return result.OrderBy(m => m.SequenceId).ToList();
}
public async Task<List<Message>> SearchMessagesAsync(string query, Guid? chatId, Guid requestingUserId, CancellationToken cancellationToken)
{
// Not ideal for SQL/Mongo combination but keeping the signature