From 213d81a5ca74e7c0c910084366834392117bc7bc Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 24 May 2025 01:29:17 +0800 Subject: [PATCH] :recycle: Refactor the last read at system of chat --- DysonNetwork.Sphere/AppDatabase.cs | 3 - DysonNetwork.Sphere/Chat/ChatController.cs | 1 - DysonNetwork.Sphere/Chat/ChatRoom.cs | 1 + DysonNetwork.Sphere/Chat/ChatService.cs | 75 +++++++------------ DysonNetwork.Sphere/Chat/Message.cs | 24 ++---- .../Connection/Handlers/MessageReadHandler.cs | 1 - DysonNetwork.Sphere/Program.cs | 8 -- .../MessageReadReceiptFlushHandler.cs | 30 ++------ 8 files changed, 44 insertions(+), 99 deletions(-) diff --git a/DysonNetwork.Sphere/AppDatabase.cs b/DysonNetwork.Sphere/AppDatabase.cs index 9807449..d5bdaa0 100644 --- a/DysonNetwork.Sphere/AppDatabase.cs +++ b/DysonNetwork.Sphere/AppDatabase.cs @@ -63,7 +63,6 @@ public class AppDatabase( public DbSet ChatMembers { get; set; } public DbSet ChatMessages { get; set; } public DbSet ChatRealtimeCall { get; set; } - public DbSet ChatReadReceipts { get; set; } public DbSet ChatReactions { get; set; } public DbSet Stickers { get; set; } @@ -232,8 +231,6 @@ public class AppDatabase( .WithMany() .HasForeignKey(pm => pm.AccountId) .OnDelete(DeleteBehavior.Cascade); - modelBuilder.Entity() - .HasKey(e => new { e.MessageId, e.SenderId }); modelBuilder.Entity() .HasOne(m => m.ForwardedMessage) .WithMany() diff --git a/DysonNetwork.Sphere/Chat/ChatController.cs b/DysonNetwork.Sphere/Chat/ChatController.cs index ba654f6..79da9ed 100644 --- a/DysonNetwork.Sphere/Chat/ChatController.cs +++ b/DysonNetwork.Sphere/Chat/ChatController.cs @@ -15,7 +15,6 @@ public partial class ChatController(AppDatabase db, ChatService cs) : Controller { public class MarkMessageReadRequest { - public Guid MessageId { get; set; } public Guid ChatRoomId { get; set; } } diff --git a/DysonNetwork.Sphere/Chat/ChatRoom.cs b/DysonNetwork.Sphere/Chat/ChatRoom.cs index 6a2ea3e..5a73fd9 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoom.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoom.cs @@ -63,6 +63,7 @@ public class ChatMember : ModelBase public ChatMemberRole Role { get; set; } = ChatMemberRole.Member; public ChatMemberNotify Notify { get; set; } = ChatMemberNotify.All; + public Instant? LastReadAt { get; set; } public Instant? JoinedAt { get; set; } public Instant? LeaveAt { get; set; } public bool IsBot { get; set; } = false; diff --git a/DysonNetwork.Sphere/Chat/ChatService.cs b/DysonNetwork.Sphere/Chat/ChatService.cs index 9108446..ccf64fa 100644 --- a/DysonNetwork.Sphere/Chat/ChatService.cs +++ b/DysonNetwork.Sphere/Chat/ChatService.cs @@ -63,73 +63,54 @@ public class ChatService(AppDatabase db, FileService fs, IServiceScopeFactory sc await Task.WhenAll(tasks); } - public async Task MarkMessageAsReadAsync(Guid messageId, Guid roomId, Guid userId) + /// + /// This method will instant update the LastReadAt field for chat member, + /// for better performance, using the flush buffer one instead + /// + /// The user chat room + /// The user id + /// + public async Task ReadChatRoomAsync(Guid roomId, Guid userId) { - var existingStatus = await db.ChatReadReceipts - .FirstOrDefaultAsync(x => x.MessageId == messageId && x.Sender.AccountId == userId); var sender = await db.ChatMembers .Where(m => m.AccountId == userId && m.ChatRoomId == roomId) .FirstOrDefaultAsync(); if (sender is null) throw new ArgumentException("User is not a member of the chat room."); - if (existingStatus == null) - { - existingStatus = new MessageReadReceipt - { - MessageId = messageId, - SenderId = sender.Id, - }; - db.ChatReadReceipts.Add(existingStatus); - } - + sender.LastReadAt = SystemClock.Instance.GetCurrentInstant(); await db.SaveChangesAsync(); } - public async Task GetMessageReadStatus(Guid messageId, Guid userId) - { - return await db.ChatReadReceipts - .AnyAsync(x => x.MessageId == messageId && x.Sender.AccountId == userId); - } - public async Task CountUnreadMessage(Guid userId, Guid chatRoomId) { - var cutoff = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(30)); - var messages = await db.ChatMessages - .Where(m => m.ChatRoomId == chatRoomId) - .Where(m => m.CreatedAt < cutoff) - .Select(m => new MessageStatusResponse - { - MessageId = m.Id, - IsRead = m.Statuses.Any(rs => rs.Sender.AccountId == userId) - }) - .ToListAsync(); + var sender = await db.ChatMembers + .Where(m => m.AccountId == userId && m.ChatRoomId == chatRoomId) + .Select(m => new { m.LastReadAt }) + .FirstOrDefaultAsync(); + if (sender?.LastReadAt is null) return 0; - return messages.Count(m => !m.IsRead); + return await db.ChatMessages + .Where(m => m.ChatRoomId == chatRoomId) + .Where(m => m.CreatedAt > sender.LastReadAt) + .CountAsync(); } public async Task> CountUnreadMessageForUser(Guid userId) { - var cutoff = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(30)); - var userRooms = await db.ChatMembers + var members = await db.ChatMembers .Where(m => m.AccountId == userId) - .Select(m => m.ChatRoomId) + .Select(m => new { m.ChatRoomId, m.LastReadAt }) .ToListAsync(); - - var messages = await db.ChatMessages - .Where(m => m.CreatedAt < cutoff) - .Where(m => userRooms.Contains(m.ChatRoomId)) - .Select(m => new - { - m.ChatRoomId, - IsRead = m.Statuses.Any(rs => rs.Sender.AccountId == userId) - }) - .ToListAsync(); - - return messages + + var lastReadAt = members.ToDictionary(m => m.ChatRoomId, m => m.LastReadAt); + var roomsId = lastReadAt.Keys.ToList(); + + return await db.ChatMessages + .Where(m => roomsId.Contains(m.ChatRoomId)) .GroupBy(m => m.ChatRoomId) - .ToDictionary( + .ToDictionaryAsync( g => g.Key, - g => g.Count(m => !m.IsRead) + g => g.Count(m => lastReadAt[g.Key] == null || m.CreatedAt > lastReadAt[g.Key]) ); } diff --git a/DysonNetwork.Sphere/Chat/Message.cs b/DysonNetwork.Sphere/Chat/Message.cs index 2ed95fb..d050018 100644 --- a/DysonNetwork.Sphere/Chat/Message.cs +++ b/DysonNetwork.Sphere/Chat/Message.cs @@ -10,7 +10,7 @@ namespace DysonNetwork.Sphere.Chat; public class Message : ModelBase { public Guid Id { get; set; } = Guid.NewGuid(); - public string Type { get; set; } = null!; + [MaxLength(1024)] public string Type { get; set; } = null!; [MaxLength(4096)] public string? Content { get; set; } [Column(TypeName = "jsonb")] public Dictionary? Meta { get; set; } [Column(TypeName = "jsonb")] public List? MembersMentioned { get; set; } @@ -19,7 +19,6 @@ public class Message : ModelBase public ICollection Attachments { get; set; } = new List(); public ICollection Reactions { get; set; } = new List(); - public ICollection Statuses { get; set; } = new List(); public Guid? RepliedMessageId { get; set; } public Message? RepliedMessage { get; set; } @@ -43,7 +42,6 @@ public class Message : ModelBase EditedAt = EditedAt, Attachments = new List(Attachments), Reactions = new List(Reactions), - Statuses = new List(Statuses), RepliedMessageId = RepliedMessageId, RepliedMessage = RepliedMessage?.Clone() as Message, ForwardedMessageId = ForwardedMessageId, @@ -78,19 +76,13 @@ public class MessageReaction : ModelBase public MessageReactionAttitude Attitude { get; set; } } -/// If the status exists, means the user has read the message. -[Index(nameof(MessageId), nameof(SenderId), IsUnique = true)] -public class MessageReadReceipt : ModelBase -{ - public Guid MessageId { get; set; } - public Message Message { get; set; } = null!; - public Guid SenderId { get; set; } - public ChatMember Sender { get; set; } = null!; -} - +/// +/// The data model for updating the last read at field for chat member, +/// after the refactor of the unread system, this no longer stored in the database. +/// Not only used for the data transmission object +/// [NotMapped] -public class MessageStatusResponse +public class MessageReadReceipt { - public Guid MessageId { get; set; } - public bool IsRead { get; set; } + public Guid SenderId { get; set; } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Connection/Handlers/MessageReadHandler.cs b/DysonNetwork.Sphere/Connection/Handlers/MessageReadHandler.cs index 97a5f5a..f157f1a 100644 --- a/DysonNetwork.Sphere/Connection/Handlers/MessageReadHandler.cs +++ b/DysonNetwork.Sphere/Connection/Handlers/MessageReadHandler.cs @@ -79,7 +79,6 @@ public class MessageReadHandler( var readReceipt = new MessageReadReceipt { - MessageId = request.MessageId, SenderId = sender.Id, }; diff --git a/DysonNetwork.Sphere/Program.cs b/DysonNetwork.Sphere/Program.cs index 6614bec..b2355f8 100644 --- a/DysonNetwork.Sphere/Program.cs +++ b/DysonNetwork.Sphere/Program.cs @@ -210,14 +210,6 @@ builder.Services.AddQuartz(q => .WithIntervalInSeconds(60) .RepeatForever()) ); - - var readReceiptRecyclingJob = new JobKey("ReadReceiptRecycling"); - q.AddJob(opts => opts.WithIdentity(readReceiptRecyclingJob)); - q.AddTrigger(opts => opts - .ForJob(readReceiptRecyclingJob) - .WithIdentity("ReadReceiptRecyclingTrigger") - .WithCronSchedule("0 0 0 * * ?") - ); }); builder.Services.AddQuartzHostedService(q => q.WaitForJobsToComplete = true); diff --git a/DysonNetwork.Sphere/Storage/Handlers/MessageReadReceiptFlushHandler.cs b/DysonNetwork.Sphere/Storage/Handlers/MessageReadReceiptFlushHandler.cs index d9af8e5..fb07245 100644 --- a/DysonNetwork.Sphere/Storage/Handlers/MessageReadReceiptFlushHandler.cs +++ b/DysonNetwork.Sphere/Storage/Handlers/MessageReadReceiptFlushHandler.cs @@ -10,22 +10,17 @@ public class MessageReadReceiptFlushHandler(IServiceProvider serviceProvider) : { public async Task FlushAsync(IReadOnlyList items) { - var distinctItems = items - .DistinctBy(x => new { x.MessageId, x.SenderId }) - .Select(x => - { - x.CreatedAt = SystemClock.Instance.GetCurrentInstant(); - x.UpdatedAt = x.CreatedAt; - return x; - }) + var now = SystemClock.Instance.GetCurrentInstant(); + var distinctId = items + .DistinctBy(x => x.SenderId) + .Select(x => x.SenderId) .ToList(); using var scope = serviceProvider.CreateScope(); var db = scope.ServiceProvider.GetRequiredService(); - await db.BulkInsertAsync(distinctItems, config => { - config.ConflictOption = ConflictOption.Ignore; - config.UpdateByProperties = [nameof(MessageReadReceipt.MessageId), nameof(MessageReadReceipt.SenderId)]; - }); + await db.ChatMembers.Where(r => distinctId.Contains(r.Id)) + .ExecuteUpdateAsync(s => s.SetProperty(m => m.LastReadAt, now) + ); } } @@ -36,14 +31,3 @@ public class ReadReceiptFlushJob(FlushBufferService fbs, MessageReadReceiptFlush await fbs.FlushAsync(hdl); } } - -public class ReadReceiptRecyclingJob(AppDatabase db) : IJob -{ - public async Task Execute(IJobExecutionContext context) - { - var cutoff = SystemClock.Instance.GetCurrentInstant().Minus(Duration.FromDays(30)); - await db.ChatReadReceipts - .Where(r => r.CreatedAt < cutoff) - .ExecuteDeleteAsync(); - } -} \ No newline at end of file