diff --git a/DysonNetwork.Sphere/Chat/ChatController.cs b/DysonNetwork.Sphere/Chat/ChatController.cs index 22159fc..6d49de3 100644 --- a/DysonNetwork.Sphere/Chat/ChatController.cs +++ b/DysonNetwork.Sphere/Chat/ChatController.cs @@ -87,7 +87,8 @@ public partial class ChatController( var accountId = Guid.Parse(currentUser.Id); var member = await db.ChatMembers - .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null) + .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && + m.LeaveAt == null) .FirstOrDefaultAsync(); if (member == null || member.Role < ChatMemberRole.Member) return StatusCode(403, "You are not a member of this chat room."); @@ -103,10 +104,10 @@ public partial class ChatController( .Skip(offset) .Take(take) .ToListAsync(); - + var members = messages.Select(m => m.Sender).DistinctBy(x => x.Id).ToList(); members = await crs.LoadMemberAccounts(members); - + foreach (var message in messages) message.Sender = members.First(x => x.Id == message.SenderId); @@ -129,7 +130,8 @@ public partial class ChatController( var accountId = Guid.Parse(currentUser.Id); var member = await db.ChatMembers - .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null) + .Where(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && + m.LeaveAt == null) .FirstOrDefaultAsync(); if (member == null || member.Role < ChatMemberRole.Member) return StatusCode(403, "You are not a member of this chat room."); @@ -141,16 +143,81 @@ public partial class ChatController( .FirstOrDefaultAsync(); if (message is null) return NotFound(); - + message.Sender = await crs.LoadMemberAccount(message.Sender); return Ok(message); } - [GeneratedRegex("@([A-Za-z0-9_-]+)")] + [GeneratedRegex(@"@(?:u/)?([A-Za-z0-9_-]+)")] private static partial Regex MentionRegex(); + /// + /// Extracts mentioned users from message content, replies, and forwards + /// + private async Task> ExtractMentionedUsersAsync(string? content, Guid? repliedMessageId, + Guid? forwardedMessageId, Guid roomId, Guid? excludeSenderId = null) + { + var mentionedUsers = new List(); + + // Add sender of a replied message + if (repliedMessageId.HasValue) + { + var replyingTo = await db.ChatMessages + .Where(m => m.Id == repliedMessageId.Value && m.ChatRoomId == roomId) + .Include(m => m.Sender) + .Select(m => m.Sender) + .FirstOrDefaultAsync(); + if (replyingTo != null) + mentionedUsers.Add(replyingTo.AccountId); + } + + // Add sender of a forwarded message + if (forwardedMessageId.HasValue) + { + var forwardedMessage = await db.ChatMessages + .Where(m => m.Id == forwardedMessageId.Value) + .Select(m => new { m.SenderId }) + .FirstOrDefaultAsync(); + if (forwardedMessage != null) + { + mentionedUsers.Add(forwardedMessage.SenderId); + } + } + + // Extract mentions from content using regex + if (!string.IsNullOrWhiteSpace(content)) + { + var mentionedNames = MentionRegex() + .Matches(content) + .Select(m => m.Groups[1].Value) + .Distinct() + .ToList(); + + if (mentionedNames.Count > 0) + { + var queryRequest = new LookupAccountBatchRequest(); + queryRequest.Names.AddRange(mentionedNames); + var queryResponse = (await accounts.LookupAccountBatchAsync(queryRequest)).Accounts; + var mentionedIds = queryResponse.Select(a => Guid.Parse(a.Id)).ToList(); + + if (mentionedIds.Count > 0) + { + var mentionedMembers = await db.ChatMembers + .Where(m => m.ChatRoomId == roomId && mentionedIds.Contains(m.AccountId)) + .Where(m => m.JoinedAt != null && m.LeaveAt == null) + .Where(m => excludeSenderId == null || m.AccountId != excludeSenderId.Value) + .Select(m => m.AccountId) + .ToListAsync(); + mentionedUsers.AddRange(mentionedMembers); + } + } + } + + return mentionedUsers.Distinct().ToList(); + } + [HttpPost("{roomId:guid}/messages")] [Authorize] [RequiredPermission("global", "chat.messages.create")] @@ -188,6 +255,7 @@ public partial class ChatController( .ToList(); } + // Validate reply and forward message IDs exist if (request.RepliedMessageId.HasValue) { var repliedMessage = await db.ChatMessages @@ -208,28 +276,9 @@ public partial class ChatController( message.ForwardedMessageId = forwardedMessage.Id; } - if (request.Content is not null) - { - var mentioned = MentionRegex() - .Matches(request.Content) - .Select(m => m.Groups[1].Value) - .ToList(); - if (mentioned.Count > 0) - { - var queryRequest = new LookupAccountBatchRequest(); - queryRequest.Names.AddRange(mentioned); - var queryResponse = (await accounts.LookupAccountBatchAsync(queryRequest)).Accounts; - var mentionedId = queryResponse - .Select(a => Guid.Parse(a.Id)) - .ToList(); - var mentionedMembers = await db.ChatMembers - .Where(m => m.ChatRoomId == roomId && mentionedId.Contains(m.AccountId)) - .Where(m => m.JoinedAt != null && m.LeaveAt == null) - .Select(m => m.Id) - .ToListAsync(); - message.MembersMentioned = mentionedMembers; - } - } + // Extract mentioned users + message.MembersMentioned = await ExtractMentionedUsersAsync(request.Content, request.RepliedMessageId, + request.ForwardedMessageId, roomId); var result = await cs.SendMessageAsync(message, member, member.ChatRoom); @@ -259,6 +308,7 @@ public partial class ChatController( (request.AttachmentsId == null || request.AttachmentsId.Count == 0)) return BadRequest("You cannot send an empty message."); + // Validate reply and forward message IDs exist if (request.RepliedMessageId.HasValue) { var repliedMessage = await db.ChatMessages @@ -275,6 +325,11 @@ public partial class ChatController( return BadRequest("The message you're forwarding does not exist."); } + // Update mentions based on new content and references + var updatedMentions = await ExtractMentionedUsersAsync(request.Content, request.RepliedMessageId, + request.ForwardedMessageId, roomId, accountId); + message.MembersMentioned = updatedMentions; + // Call service method to update the message await cs.UpdateMessageAsync( message, @@ -324,7 +379,8 @@ public partial class ChatController( var accountId = Guid.Parse(currentUser.Id); var isMember = await db.ChatMembers - .AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null); + .AnyAsync(m => + m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null); if (!isMember) return StatusCode(403, "You are not a member of this chat room."); @@ -333,18 +389,20 @@ public partial class ChatController( } [HttpPost("{roomId:guid}/autocomplete")] - public async Task>> ChatAutoComplete([FromBody] AutocompletionRequest request, Guid roomId) + public async Task>> ChatAutoComplete( + [FromBody] AutocompletionRequest request, Guid roomId) { if (HttpContext.Items["CurrentUser"] is not Account currentUser) return Unauthorized(); var accountId = Guid.Parse(currentUser.Id); var isMember = await db.ChatMembers - .AnyAsync(m => m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null); + .AnyAsync(m => + m.AccountId == accountId && m.ChatRoomId == roomId && m.JoinedAt != null && m.LeaveAt == null); if (!isMember) return StatusCode(403, "You are not a member of this chat room."); var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10); return Ok(result); } -} +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Chat/ChatService.cs b/DysonNetwork.Sphere/Chat/ChatService.cs index 7ea9571..dfd0df2 100644 --- a/DysonNetwork.Sphere/Chat/ChatService.cs +++ b/DysonNetwork.Sphere/Chat/ChatService.cs @@ -198,8 +198,6 @@ public partial class ChatService( public async Task SendMessageAsync(SnChatMessage message, SnChatMember sender, SnChatRoom room) { if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString(); - message.CreatedAt = SystemClock.Instance.GetCurrentInstant(); - message.UpdatedAt = message.CreatedAt; // First complete the save operation db.ChatMessages.Add(message); @@ -209,20 +207,25 @@ public partial class ChatService( await CreateFileReferencesForMessageAsync(message); // Then start the delivery process + var localMessage = message; + var localSender = sender; + var localRoom = room; + var localLogger = logger; _ = Task.Run(async () => { try { - await DeliverMessageAsync(message, sender, room); + await DeliverMessageAsync(localMessage, localSender, localRoom); } catch (Exception ex) { - logger.LogError($"Error when delivering message: {ex.Message} {ex.StackTrace}"); + localLogger.LogError($"Error when delivering message: {ex.Message} {ex.StackTrace}"); } }); // Process link preview in the background to avoid delaying message sending - _ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(message)); + var localMessageForPreview = message; + _ = Task.Run(async () => await ProcessMessageLinkPreviewAsync(localMessageForPreview)); message.Sender = sender; message.ChatRoom = room;