♻️ Refactored the chat mention logic
This commit is contained in:
@@ -87,7 +87,8 @@ public partial class ChatController(
|
|||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var member = await db.ChatMembers
|
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();
|
.FirstOrDefaultAsync();
|
||||||
if (member == null || member.Role < ChatMemberRole.Member)
|
if (member == null || member.Role < ChatMemberRole.Member)
|
||||||
return StatusCode(403, "You are not a member of this chat room.");
|
return StatusCode(403, "You are not a member of this chat room.");
|
||||||
@@ -103,10 +104,10 @@ public partial class ChatController(
|
|||||||
.Skip(offset)
|
.Skip(offset)
|
||||||
.Take(take)
|
.Take(take)
|
||||||
.ToListAsync();
|
.ToListAsync();
|
||||||
|
|
||||||
var members = messages.Select(m => m.Sender).DistinctBy(x => x.Id).ToList();
|
var members = messages.Select(m => m.Sender).DistinctBy(x => x.Id).ToList();
|
||||||
members = await crs.LoadMemberAccounts(members);
|
members = await crs.LoadMemberAccounts(members);
|
||||||
|
|
||||||
foreach (var message in messages)
|
foreach (var message in messages)
|
||||||
message.Sender = members.First(x => x.Id == message.SenderId);
|
message.Sender = members.First(x => x.Id == message.SenderId);
|
||||||
|
|
||||||
@@ -129,7 +130,8 @@ public partial class ChatController(
|
|||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var member = await db.ChatMembers
|
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();
|
.FirstOrDefaultAsync();
|
||||||
if (member == null || member.Role < ChatMemberRole.Member)
|
if (member == null || member.Role < ChatMemberRole.Member)
|
||||||
return StatusCode(403, "You are not a member of this chat room.");
|
return StatusCode(403, "You are not a member of this chat room.");
|
||||||
@@ -141,16 +143,81 @@ public partial class ChatController(
|
|||||||
.FirstOrDefaultAsync();
|
.FirstOrDefaultAsync();
|
||||||
|
|
||||||
if (message is null) return NotFound();
|
if (message is null) return NotFound();
|
||||||
|
|
||||||
message.Sender = await crs.LoadMemberAccount(message.Sender);
|
message.Sender = await crs.LoadMemberAccount(message.Sender);
|
||||||
|
|
||||||
return Ok(message);
|
return Ok(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
[GeneratedRegex("@([A-Za-z0-9_-]+)")]
|
[GeneratedRegex(@"@(?:u/)?([A-Za-z0-9_-]+)")]
|
||||||
private static partial Regex MentionRegex();
|
private static partial Regex MentionRegex();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Extracts mentioned users from message content, replies, and forwards
|
||||||
|
/// </summary>
|
||||||
|
private async Task<List<Guid>> ExtractMentionedUsersAsync(string? content, Guid? repliedMessageId,
|
||||||
|
Guid? forwardedMessageId, Guid roomId, Guid? excludeSenderId = null)
|
||||||
|
{
|
||||||
|
var mentionedUsers = new List<Guid>();
|
||||||
|
|
||||||
|
// 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")]
|
[HttpPost("{roomId:guid}/messages")]
|
||||||
[Authorize]
|
[Authorize]
|
||||||
[RequiredPermission("global", "chat.messages.create")]
|
[RequiredPermission("global", "chat.messages.create")]
|
||||||
@@ -188,6 +255,7 @@ public partial class ChatController(
|
|||||||
.ToList();
|
.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Validate reply and forward message IDs exist
|
||||||
if (request.RepliedMessageId.HasValue)
|
if (request.RepliedMessageId.HasValue)
|
||||||
{
|
{
|
||||||
var repliedMessage = await db.ChatMessages
|
var repliedMessage = await db.ChatMessages
|
||||||
@@ -208,28 +276,9 @@ public partial class ChatController(
|
|||||||
message.ForwardedMessageId = forwardedMessage.Id;
|
message.ForwardedMessageId = forwardedMessage.Id;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (request.Content is not null)
|
// Extract mentioned users
|
||||||
{
|
message.MembersMentioned = await ExtractMentionedUsersAsync(request.Content, request.RepliedMessageId,
|
||||||
var mentioned = MentionRegex()
|
request.ForwardedMessageId, roomId);
|
||||||
.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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var result = await cs.SendMessageAsync(message, member, member.ChatRoom);
|
var result = await cs.SendMessageAsync(message, member, member.ChatRoom);
|
||||||
|
|
||||||
@@ -259,6 +308,7 @@ public partial class ChatController(
|
|||||||
(request.AttachmentsId == null || request.AttachmentsId.Count == 0))
|
(request.AttachmentsId == null || request.AttachmentsId.Count == 0))
|
||||||
return BadRequest("You cannot send an empty message.");
|
return BadRequest("You cannot send an empty message.");
|
||||||
|
|
||||||
|
// Validate reply and forward message IDs exist
|
||||||
if (request.RepliedMessageId.HasValue)
|
if (request.RepliedMessageId.HasValue)
|
||||||
{
|
{
|
||||||
var repliedMessage = await db.ChatMessages
|
var repliedMessage = await db.ChatMessages
|
||||||
@@ -275,6 +325,11 @@ public partial class ChatController(
|
|||||||
return BadRequest("The message you're forwarding does not exist.");
|
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
|
// Call service method to update the message
|
||||||
await cs.UpdateMessageAsync(
|
await cs.UpdateMessageAsync(
|
||||||
message,
|
message,
|
||||||
@@ -324,7 +379,8 @@ public partial class ChatController(
|
|||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var isMember = await db.ChatMembers
|
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)
|
if (!isMember)
|
||||||
return StatusCode(403, "You are not a member of this chat room.");
|
return StatusCode(403, "You are not a member of this chat room.");
|
||||||
|
|
||||||
@@ -333,18 +389,20 @@ public partial class ChatController(
|
|||||||
}
|
}
|
||||||
|
|
||||||
[HttpPost("{roomId:guid}/autocomplete")]
|
[HttpPost("{roomId:guid}/autocomplete")]
|
||||||
public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete([FromBody] AutocompletionRequest request, Guid roomId)
|
public async Task<ActionResult<List<DysonNetwork.Shared.Models.Autocompletion>>> ChatAutoComplete(
|
||||||
|
[FromBody] AutocompletionRequest request, Guid roomId)
|
||||||
{
|
{
|
||||||
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
if (HttpContext.Items["CurrentUser"] is not Account currentUser)
|
||||||
return Unauthorized();
|
return Unauthorized();
|
||||||
|
|
||||||
var accountId = Guid.Parse(currentUser.Id);
|
var accountId = Guid.Parse(currentUser.Id);
|
||||||
var isMember = await db.ChatMembers
|
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)
|
if (!isMember)
|
||||||
return StatusCode(403, "You are not a member of this chat room.");
|
return StatusCode(403, "You are not a member of this chat room.");
|
||||||
|
|
||||||
var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10);
|
var result = await aus.GetAutocompletion(request.Content, chatId: roomId, limit: 10);
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
}
|
}
|
||||||
}
|
}
|
@@ -198,8 +198,6 @@ public partial class ChatService(
|
|||||||
public async Task<SnChatMessage> SendMessageAsync(SnChatMessage message, SnChatMember sender, SnChatRoom room)
|
public async Task<SnChatMessage> SendMessageAsync(SnChatMessage message, SnChatMember sender, SnChatRoom room)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
|
if (string.IsNullOrWhiteSpace(message.Nonce)) message.Nonce = Guid.NewGuid().ToString();
|
||||||
message.CreatedAt = SystemClock.Instance.GetCurrentInstant();
|
|
||||||
message.UpdatedAt = message.CreatedAt;
|
|
||||||
|
|
||||||
// First complete the save operation
|
// First complete the save operation
|
||||||
db.ChatMessages.Add(message);
|
db.ChatMessages.Add(message);
|
||||||
@@ -209,20 +207,25 @@ public partial class ChatService(
|
|||||||
await CreateFileReferencesForMessageAsync(message);
|
await CreateFileReferencesForMessageAsync(message);
|
||||||
|
|
||||||
// Then start the delivery process
|
// Then start the delivery process
|
||||||
|
var localMessage = message;
|
||||||
|
var localSender = sender;
|
||||||
|
var localRoom = room;
|
||||||
|
var localLogger = logger;
|
||||||
_ = Task.Run(async () =>
|
_ = Task.Run(async () =>
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await DeliverMessageAsync(message, sender, room);
|
await DeliverMessageAsync(localMessage, localSender, localRoom);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
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
|
// 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.Sender = sender;
|
||||||
message.ChatRoom = room;
|
message.ChatRoom = room;
|
||||||
|
Reference in New Issue
Block a user