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;