From 661b612537e2c2ad10067aba24cad041b144d0a7 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 27 Jun 2025 23:24:27 +0800 Subject: [PATCH] :sparkles: Chat and realm members with status --- .../Account/AccountEventService.cs | 66 ++++++++++++++++++- .../Chat/ChatRoomController.cs | 57 ++++++++++++---- DysonNetwork.Sphere/Realm/RealmController.cs | 63 +++++++++++++----- 3 files changed, 157 insertions(+), 29 deletions(-) diff --git a/DysonNetwork.Sphere/Account/AccountEventService.cs b/DysonNetwork.Sphere/Account/AccountEventService.cs index d413251..87e5bb3 100644 --- a/DysonNetwork.Sphere/Account/AccountEventService.cs +++ b/DysonNetwork.Sphere/Account/AccountEventService.cs @@ -65,7 +65,7 @@ public class AccountEventService( }; } - return new Status + return new Status { Attitude = StatusAttitude.Neutral, IsOnline = false, @@ -75,6 +75,70 @@ public class AccountEventService( }; } + public async Task> GetStatuses(List userIds) + { + var results = new Dictionary(); + var cacheMissUserIds = new List(); + + foreach (var userId in userIds) + { + var cacheKey = $"{StatusCacheKey}{userId}"; + var cachedStatus = await cache.GetAsync(cacheKey); + if (cachedStatus != null) + { + cachedStatus.IsOnline = !cachedStatus.IsInvisible && ws.GetAccountIsConnected(userId); + results[userId] = cachedStatus; + } + else + { + cacheMissUserIds.Add(userId); + } + } + + if (cacheMissUserIds.Any()) + { + var now = SystemClock.Instance.GetCurrentInstant(); + var statusesFromDb = await db.AccountStatuses + .Where(e => cacheMissUserIds.Contains(e.AccountId)) + .Where(e => e.ClearedAt == null || e.ClearedAt > now) + .GroupBy(e => e.AccountId) + .Select(g => g.OrderByDescending(e => e.CreatedAt).First()) + .ToListAsync(); + + var foundUserIds = new HashSet(); + + foreach (var status in statusesFromDb) + { + var isOnline = ws.GetAccountIsConnected(status.AccountId); + status.IsOnline = !status.IsInvisible && isOnline; + results[status.AccountId] = status; + var cacheKey = $"{StatusCacheKey}{status.AccountId}"; + await cache.SetAsync(cacheKey, status, TimeSpan.FromMinutes(5)); + foundUserIds.Add(status.AccountId); + } + + var usersWithoutStatus = cacheMissUserIds.Except(foundUserIds).ToList(); + if (usersWithoutStatus.Any()) + { + foreach (var userId in usersWithoutStatus) + { + var isOnline = ws.GetAccountIsConnected(userId); + var defaultStatus = new Status + { + Attitude = StatusAttitude.Neutral, + IsOnline = isOnline, + IsCustomized = false, + Label = isOnline ? "Online" : "Offline", + AccountId = userId, + }; + results[userId] = defaultStatus; + } + } + } + + return results; + } + public async Task CreateStatus(Account user, Status status) { var now = SystemClock.Instance.GetCurrentInstant(); diff --git a/DysonNetwork.Sphere/Chat/ChatRoomController.cs b/DysonNetwork.Sphere/Chat/ChatRoomController.cs index 861659e..1d5be21 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoomController.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoomController.cs @@ -22,7 +22,8 @@ public class ChatRoomController( ActionLogService als, NotificationService nty, RelationshipService rels, - IStringLocalizer localizer + IStringLocalizer localizer, + AccountEventService aes ) : ControllerBase { [HttpGet("{id:guid}")] @@ -149,7 +150,7 @@ public class ChatRoomController( public class ChatRoomRequest { - [Required][MaxLength(1024)] public string? Name { get; set; } + [Required] [MaxLength(1024)] public string? Name { get; set; } [MaxLength(4096)] public string? Description { get; set; } [MaxLength(32)] public string? PictureId { get; set; } [MaxLength(32)] public string? BackgroundId { get; set; } @@ -384,7 +385,7 @@ public class ChatRoomController( [HttpGet("{roomId:guid}/members")] public async Task>> ListMembers(Guid roomId, [FromQuery] int take = 20, - [FromQuery] int skip = 0) + [FromQuery] int skip = 0, [FromQuery] bool withStatus = false, [FromQuery] string? status = null) { var currentUser = HttpContext.Items["CurrentUser"] as Account.Account; @@ -400,24 +401,54 @@ public class ChatRoomController( if (member is null) return StatusCode(403, "You need to be a member to see members of private chat room."); } - var query = db.ChatMembers + IQueryable query = db.ChatMembers .Where(m => m.ChatRoomId == roomId) .Where(m => m.LeaveAt == null) // Add this condition to exclude left members .Include(m => m.Account) .Include(m => m.Account.Profile); - var total = await query.CountAsync(); - Response.Headers.Append("X-Total", total.ToString()); + if (withStatus) + { + var members = await query + .OrderBy(m => m.JoinedAt) + .ToListAsync(); - var members = await query - .OrderBy(m => m.JoinedAt) - .Skip(skip) - .Take(take) - .ToListAsync(); + var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList()); - return Ok(members); + if (!string.IsNullOrEmpty(status)) + { + members = members.Where(m => + memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null && + s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline) + .ToList(); + + var total = members.Count; + Response.Headers.Append("X-Total", total.ToString()); + + var result = members.Skip(skip).Take(take).ToList(); + + return Ok(result); + } + else + { + var total = await query.CountAsync(); + Response.Headers.Append("X-Total", total.ToString()); + + var members = await query + .OrderBy(m => m.JoinedAt) + .Skip(skip) + .Take(take) + .ToListAsync(); + + return Ok(members); + } } + + public class ChatMemberRequest { [Required] public Guid RelatedUserId { get; set; } @@ -799,4 +830,4 @@ public class ChatRoomController( AccountService.SetCultureInfo(member.Account); await nty.SendNotification(member.Account, "invites.chats", title, null, body, actionUri: "/chat"); } -} +} \ No newline at end of file diff --git a/DysonNetwork.Sphere/Realm/RealmController.cs b/DysonNetwork.Sphere/Realm/RealmController.cs index 82997ee..9e81519 100644 --- a/DysonNetwork.Sphere/Realm/RealmController.cs +++ b/DysonNetwork.Sphere/Realm/RealmController.cs @@ -15,7 +15,8 @@ public class RealmController( RealmService rs, FileReferenceService fileRefService, RelationshipService rels, - ActionLogService als + ActionLogService als, + AccountEventService aes ) : Controller { [HttpGet("{slug}")] @@ -179,7 +180,9 @@ public class RealmController( public async Task>> ListMembers( string slug, [FromQuery] int offset = 0, - [FromQuery] int take = 20 + [FromQuery] int take = 20, + [FromQuery] bool withStatus = false, + [FromQuery] string? status = null ) { var realm = await db.Realms @@ -194,24 +197,54 @@ public class RealmController( return StatusCode(403, "You must be a member to view this realm's members."); } - var query = db.RealmMembers + IQueryable query = db.RealmMembers .Where(m => m.RealmId == realm.Id) - .Where(m => m.LeaveAt == null); - - var total = await query.CountAsync(); - Response.Headers["X-Total"] = total.ToString(); - - var members = await query - .OrderBy(m => m.CreatedAt) - .Skip(offset) - .Take(take) + .Where(m => m.LeaveAt == null) .Include(m => m.Account) - .Include(m => m.Account.Profile) - .ToListAsync(); + .Include(m => m.Account.Profile); - return Ok(members); + if (withStatus) + { + var members = await query + .OrderBy(m => m.CreatedAt) + .ToListAsync(); + + var memberStatuses = await aes.GetStatuses(members.Select(m => m.AccountId).ToList()); + + if (!string.IsNullOrEmpty(status)) + { + members = members.Where(m => + memberStatuses.TryGetValue(m.AccountId, out var s) && s.Label != null && + s.Label.Equals(status, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + members = members.OrderByDescending(m => memberStatuses.TryGetValue(m.AccountId, out var s) && s.IsOnline) + .ToList(); + + var total = members.Count; + Response.Headers["X-Total"] = total.ToString(); + + var result = members.Skip(offset).Take(take).ToList(); + + return Ok(result); + } + else + { + var total = await query.CountAsync(); + Response.Headers["X-Total"] = total.ToString(); + + var members = await query + .OrderBy(m => m.CreatedAt) + .Skip(offset) + .Take(take) + .ToListAsync(); + + return Ok(members); + } } + + [HttpGet("{slug}/members/me")] [Authorize] public async Task> GetCurrentIdentity(string slug)