diff --git a/DysonNetwork.Sphere/Account/NotificationService.cs b/DysonNetwork.Sphere/Account/NotificationService.cs index a88439c..e21bd07 100644 --- a/DysonNetwork.Sphere/Account/NotificationService.cs +++ b/DysonNetwork.Sphere/Account/NotificationService.cs @@ -114,7 +114,7 @@ public class NotificationService _db.Add(notification); await _db.SaveChangesAsync(); - if (!isSilent) _ = DeliveryNotification(notification).ConfigureAwait(false); + if (!isSilent) _ = DeliveryNotification(notification); return notification; } diff --git a/DysonNetwork.Sphere/Chat/ChatRoomController.cs b/DysonNetwork.Sphere/Chat/ChatRoomController.cs index 802dd07..7547003 100644 --- a/DysonNetwork.Sphere/Chat/ChatRoomController.cs +++ b/DysonNetwork.Sphere/Chat/ChatRoomController.cs @@ -2,10 +2,12 @@ using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.ComponentModel.DataAnnotations; using DysonNetwork.Sphere.Account; +using DysonNetwork.Sphere.Localization; using DysonNetwork.Sphere.Permission; using DysonNetwork.Sphere.Realm; using DysonNetwork.Sphere.Storage; using Microsoft.AspNetCore.Authorization; +using Microsoft.Extensions.Localization; using NodaTime; namespace DysonNetwork.Sphere.Chat; @@ -19,7 +21,8 @@ public class ChatRoomController( RealmService rs, ActionLogService als, NotificationService nty, - RelationshipService rels + RelationshipService rels, + IStringLocalizer localizer ) : ControllerBase { [HttpGet("{id:guid}")] @@ -74,7 +77,7 @@ public class ChatRoomController( var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); if (relatedUser is null) return BadRequest("Related user was not found"); - + if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked)) return StatusCode(403, "You cannot create direct message with a user that blocked you."); @@ -121,7 +124,8 @@ public class ChatRoomController( ); var invitedMember = dmRoom.Members.First(m => m.AccountId == request.RelatedUserId); - await _SendInviteNotify(invitedMember); + invitedMember.ChatRoom = dmRoom; + await _SendInviteNotify(invitedMember, currentUser); return Ok(dmRoom); } @@ -377,7 +381,7 @@ public class ChatRoomController( var relatedUser = await db.Accounts.FindAsync(request.RelatedUserId); if (relatedUser is null) return BadRequest("Related user was not found"); - + if (await rels.HasRelationshipWithStatus(currentUser.Id, relatedUser.Id, RelationshipStatus.Blocked)) return StatusCode(403, "You cannot invite a user that blocked you."); @@ -428,7 +432,8 @@ public class ChatRoomController( db.ChatMembers.Add(newMember); await db.SaveChangesAsync(); - await _SendInviteNotify(newMember); + newMember.ChatRoom = chatRoom; + await _SendInviteNotify(newMember, currentUser); als.CreateActionLogFromRequest( ActionLogType.ChatroomInvite, @@ -680,9 +685,9 @@ public class ChatRoomController( return BadRequest("The last owner cannot leave the chat. Transfer ownership first or delete the chat."); } - member.LeaveAt = NodaTime.Instant.FromDateTimeUtc(DateTime.UtcNow); + member.LeaveAt = Instant.FromDateTimeUtc(DateTime.UtcNow); await db.SaveChangesAsync(); - crs.PurgeRoomMembersCache(roomId); + await crs.PurgeRoomMembersCache(roomId); als.CreateActionLogFromRequest( ActionLogType.ChatroomLeave, @@ -692,9 +697,14 @@ public class ChatRoomController( return NoContent(); } - private async Task _SendInviteNotify(ChatMember member) + private async Task _SendInviteNotify(ChatMember member, Account.Account sender) { - await nty.SendNotification(member.Account, "invites.chats", "New Chat Invitation", null, - $"You just got invited to join {member.ChatRoom.Name}"); + string title = localizer["ChatInviteTitle"]; + + string body = member.ChatRoom.Type == ChatRoomType.DirectMessage + ? localizer["ChatInviteDirectBody", sender.Nick] + : localizer["ChatInviteBody", member.ChatRoom.Name ?? "Unnamed"]; + + await nty.SendNotification(member.Account, "invites.chats", title, null, body); } } \ No newline at end of file diff --git a/DysonNetwork.Sphere/Post/PostController.cs b/DysonNetwork.Sphere/Post/PostController.cs index 38a5bcf..3629463 100644 --- a/DysonNetwork.Sphere/Post/PostController.cs +++ b/DysonNetwork.Sphere/Post/PostController.cs @@ -257,6 +257,7 @@ public class PostController( var post = await db.Posts .Where(e => e.Id == id) .Include(e => e.Publisher) + .ThenInclude(e => e.Account) .FilterWithVisibility(currentUser, userFriends) .FirstOrDefaultAsync(); if (post is null) return NotFound(); @@ -274,7 +275,14 @@ public class PostController( PostId = post.Id, AccountId = currentUser.Id }; - var isRemoving = await ps.ModifyPostVotes(post, reaction, isExistingReaction, isSelfReact); + var isRemoving = await ps.ModifyPostVotes( + post, + reaction, + currentUser, + post.Publisher.Account, + isExistingReaction, + isSelfReact + ); if (isRemoving) return NoContent(); diff --git a/DysonNetwork.Sphere/Post/PostService.cs b/DysonNetwork.Sphere/Post/PostService.cs index a35198d..66be55c 100644 --- a/DysonNetwork.Sphere/Post/PostService.cs +++ b/DysonNetwork.Sphere/Post/PostService.cs @@ -1,12 +1,21 @@ using System.Text.Json; +using DysonNetwork.Sphere.Account; using DysonNetwork.Sphere.Activity; +using DysonNetwork.Sphere.Localization; using DysonNetwork.Sphere.Storage; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Localization; using NodaTime; namespace DysonNetwork.Sphere.Post; -public class PostService(AppDatabase db, FileService fs, ActivityService act) +public class PostService( + AppDatabase db, + FileService fs, + ActivityService act, + IStringLocalizer localizer, + NotificationService nty +) { public static List TruncatePostContent(List input) { @@ -158,8 +167,18 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act) /// /// Post that modifying /// The new / target reaction adding / removing + /// The original poster account of this post /// Indicate this operation is adding / removing - public async Task ModifyPostVotes(Post post, PostReaction reaction, bool isRemoving, bool isSelfReact) + /// Indicate this reaction is by the original post himself + /// The account that creates this reaction + public async Task ModifyPostVotes( + Post post, + PostReaction reaction, + Account.Account sender, + Account.Account? op, + bool isRemoving, + bool isSelfReact + ) { var isExistingReaction = await db.Set() .AnyAsync(r => r.PostId == post.Id && r.AccountId == reaction.AccountId); @@ -197,6 +216,21 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act) } await db.SaveChangesAsync(); + + if (!isSelfReact && op is not null) + { + await nty.SendNotification( + op, + "posts.reactions.new", + localizer["PostReactTitle", sender.Nick], + null, + string.IsNullOrWhiteSpace(post.Title) + ? localizer["PostReactBody", sender.Nick, reaction.Symbol] + : localizer["PostReactContentBody", sender.Nick, reaction.Symbol, + post.Title] + ); + } + return isRemoving; } diff --git a/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs b/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs index 3bc4a79..9148cad 100644 --- a/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs +++ b/DysonNetwork.Sphere/Publisher/PublisherSubscriptionService.cs @@ -1,9 +1,13 @@ using DysonNetwork.Sphere.Account; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Localization; namespace DysonNetwork.Sphere.Publisher; -public class PublisherSubscriptionService(AppDatabase db, NotificationService nty) +public class PublisherSubscriptionService( + AppDatabase db, + NotificationService nty, + IStringLocalizer localizer) { /// /// Checks if a subscription exists between the account and publisher @@ -48,12 +52,11 @@ public class PublisherSubscriptionService(AppDatabase db, NotificationService nt return 0; // Create notification data - var title = $"@{post.Publisher.Name} Posted"; - var message = !string.IsNullOrEmpty(post.Title) - ? post.Title - : (post.Content?.Length > 100 + var message = !string.IsNullOrEmpty(post.Description) + ? post.Description?.Length > 40 ? post.Description[..37] + "..." : post.Description + : post.Content?.Length > 100 ? string.Concat(post.Content.AsSpan(0, 97), "...") - : post.Content); + : post.Content; // Data to include with the notification var data = new Dictionary @@ -71,8 +74,8 @@ public class PublisherSubscriptionService(AppDatabase db, NotificationService nt await nty.SendNotification( subscription.Account, "posts.new", - title, - post.Description?.Length > 40 ? post.Description[..37] + "..." : post.Description, + localizer["New post from {0}", post.Publisher.Name], + string.IsNullOrWhiteSpace(post.Title) ? null : post.Title, message, data ); @@ -169,9 +172,7 @@ public class PublisherSubscriptionService(AppDatabase db, NotificationService nt { var subscription = await GetSubscriptionAsync(accountId, publisherId); if (subscription is not { Status: SubscriptionStatus.Active }) - { return false; - } subscription.Status = SubscriptionStatus.Cancelled; await db.SaveChangesAsync(); diff --git a/DysonNetwork.Sphere/Realm/RealmController.cs b/DysonNetwork.Sphere/Realm/RealmController.cs index 97a35ce..d0762c2 100644 --- a/DysonNetwork.Sphere/Realm/RealmController.cs +++ b/DysonNetwork.Sphere/Realm/RealmController.cs @@ -111,6 +111,7 @@ public class RealmController(AppDatabase db, RealmService rs, FileService fs, Re ); member.Account = relatedUser; + member.Realm = realm; await rs.SendInviteNotify(member); return Ok(member); diff --git a/DysonNetwork.Sphere/Realm/RealmService.cs b/DysonNetwork.Sphere/Realm/RealmService.cs index 927e258..6423441 100644 --- a/DysonNetwork.Sphere/Realm/RealmService.cs +++ b/DysonNetwork.Sphere/Realm/RealmService.cs @@ -1,21 +1,28 @@ using DysonNetwork.Sphere.Account; +using DysonNetwork.Sphere.Localization; using Microsoft.EntityFrameworkCore; +using Microsoft.Extensions.Localization; namespace DysonNetwork.Sphere.Realm; -public class RealmService(AppDatabase db, NotificationService nty) +public class RealmService(AppDatabase db, NotificationService nty, IStringLocalizer localizer) { public async Task SendInviteNotify(RealmMember member) { - await nty.SendNotification(member.Account, "invites.realms", "New Realm Invitation", null, - $"You just got invited to join {member.Realm.Name}"); + await nty.SendNotification( + member.Account, + "invites.realms", + localizer["RealmInviteTitle"], + null, + localizer["RealmInviteBody", member.Realm.Name] + ); } - + public async Task IsMemberWithRole(Guid realmId, Guid accountId, params RealmMemberRole[] requiredRoles) { if (requiredRoles.Length == 0) return false; - + var maxRequiredRole = requiredRoles.Max(); var member = await db.RealmMembers .FirstOrDefaultAsync(m => m.RealmId == realmId && m.AccountId == accountId); diff --git a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs index 8bbd20e..fc2a062 100644 --- a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs +++ b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.Designer.cs @@ -44,5 +44,59 @@ namespace DysonNetwork.Sphere.Resources.Localization { resourceCulture = value; } } + + internal static string ChatInviteTitle { + get { + return ResourceManager.GetString("ChatInviteTitle", resourceCulture); + } + } + + internal static string ChatInviteBody { + get { + return ResourceManager.GetString("ChatInviteBody", resourceCulture); + } + } + + internal static string ChatInviteDirectBody { + get { + return ResourceManager.GetString("ChatInviteDirectBody", resourceCulture); + } + } + + internal static string RealmInviteTitle { + get { + return ResourceManager.GetString("RealmInviteTitle", resourceCulture); + } + } + + internal static string RealmInviteBody { + get { + return ResourceManager.GetString("RealmInviteBody", resourceCulture); + } + } + + internal static string PostSubscriptionTitle { + get { + return ResourceManager.GetString("PostSubscriptionTitle", resourceCulture); + } + } + + internal static string PostReactTitle { + get { + return ResourceManager.GetString("PostReactTitle", resourceCulture); + } + } + + internal static string PostReactBody { + get { + return ResourceManager.GetString("PostReactBody", resourceCulture); + } + } + + internal static string PostReactContentBody { + get { + return ResourceManager.GetString("PostReactContentBody", resourceCulture); + } + } } } diff --git a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx index a4c5284..a6ab5db 100644 --- a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx +++ b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.resx @@ -18,4 +18,31 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + New Chat Invitation + + + You just got invited to join {0} + + + {0} sent an direct message invitation to you + + + New Realm Invitation + + + You just got invited to join {0} + + + {0} just posted + + + {0} reacted your post + + + {0} added a reaction {1} to your post + + + {0} added a reaction {1} to your post {2} + \ No newline at end of file diff --git a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx index 0db1973..a078557 100644 --- a/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx +++ b/DysonNetwork.Sphere/Resources/Localization/NotificationResource.zh-hans.resx @@ -11,4 +11,31 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + 新聊天邀请 + + + 你刚被邀请加入聊天 {} + + + {0} 向你发送了一个私聊邀请 + + + 新加入领域邀请 + + + 你刚被邀请加入领域 {0} + + + {0} 有新帖子 + + + {0} 反应了你的帖子 + + + {0} 给你的帖子添加了一个 {1} 的反应 + + + {0} 给你的帖子添加了一个 {1} 的反应 {2} + \ No newline at end of file diff --git a/DysonNetwork.sln.DotSettings.user b/DysonNetwork.sln.DotSettings.user index 8cc86d5..e07bd88 100644 --- a/DysonNetwork.sln.DotSettings.user +++ b/DysonNetwork.sln.DotSettings.user @@ -101,7 +101,7 @@ False True True - True + False True