More & localized notifications

This commit is contained in:
LittleSheep 2025-05-28 01:50:14 +08:00
parent 39d9d8a839
commit bb739c1d90
11 changed files with 199 additions and 30 deletions

View File

@ -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;
}

View File

@ -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<NotificationResource> 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);
}
}

View File

@ -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();

View File

@ -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<NotificationResource> localizer,
NotificationService nty
)
{
public static List<Post> TruncatePostContent(List<Post> input)
{
@ -158,8 +167,18 @@ public class PostService(AppDatabase db, FileService fs, ActivityService act)
/// </summary>
/// <param name="post">Post that modifying</param>
/// <param name="reaction">The new / target reaction adding / removing</param>
/// <param name="op">The original poster account of this post</param>
/// <param name="isRemoving">Indicate this operation is adding / removing</param>
public async Task<bool> ModifyPostVotes(Post post, PostReaction reaction, bool isRemoving, bool isSelfReact)
/// <param name="isSelfReact">Indicate this reaction is by the original post himself</param>
/// <param name="sender">The account that creates this reaction</param>
public async Task<bool> ModifyPostVotes(
Post post,
PostReaction reaction,
Account.Account sender,
Account.Account? op,
bool isRemoving,
bool isSelfReact
)
{
var isExistingReaction = await db.Set<PostReaction>()
.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;
}

View File

@ -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<Notification> localizer)
{
/// <summary>
/// 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<string, object>
@ -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();

View File

@ -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);

View File

@ -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<NotificationResource> 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<bool> 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);

View File

@ -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);
}
}
}
}

View File

@ -18,4 +18,31 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ChatInviteTitle" xml:space="preserve">
<value>New Chat Invitation</value>
</data>
<data name="ChatInviteBody" xml:space="preserve">
<value>You just got invited to join {0}</value>
</data>
<data name="ChatInviteDirectBody" xml:space="preserve">
<value>{0} sent an direct message invitation to you</value>
</data>
<data name="RealmInviteTitle" xml:space="preserve">
<value>New Realm Invitation</value>
</data>
<data name="RealmInviteBody" xml:space="preserve">
<value>You just got invited to join {0}</value>
</data>
<data name="PostSubscriptionTitle" xml:space="preserve">
<value>{0} just posted</value>
</data>
<data name="PostReactTitle" xml:space="preserve">
<value>{0} reacted your post</value>
</data>
<data name="PostReactBody" xml:space="preserve">
<value>{0} added a reaction {1} to your post</value>
</data>
<data name="PostReactContentBody" xml:space="preserve">
<value>{0} added a reaction {1} to your post {2}</value>
</data>
</root>

View File

@ -11,4 +11,31 @@
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ChatInviteTitle" xml:space="preserve">
<value>新聊天邀请</value>
</data>
<data name="ChatInviteBody" xml:space="preserve">
<value>你刚被邀请加入聊天 {}</value>
</data>
<data name="ChatInviteDirectBody" xml:space="preserve">
<value>{0} 向你发送了一个私聊邀请</value>
</data>
<data name="RealmInviteTitle" xml:space="preserve">
<value>新加入领域邀请</value>
</data>
<data name="RealmInviteBody" xml:space="preserve">
<value>你刚被邀请加入领域 {0}</value>
</data>
<data name="PostSubscriptionTitle" xml:space="preserve">
<value>{0} 有新帖子</value>
</data>
<data name="PostReactTitle" xml:space="preserve">
<value>{0} 反应了你的帖子</value>
</data>
<data name="PostReactBody" xml:space="preserve">
<value>{0} 给你的帖子添加了一个 {1} 的反应</value>
</data>
<data name="PostReactContentBody" xml:space="preserve">
<value>{0} 给你的帖子添加了一个 {1} 的反应 {2}</value>
</data>
</root>

View File

@ -101,7 +101,7 @@
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmails_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmail_002ELandingResource/@EntryIndexRemoved">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmailResource/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FEmailResource/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/ResxEditorPersonal/CheckedGroups/=DysonNetwork_002ESphere_002FResources_002FLocalization_002FNotificationResource/@EntryIndexedValue">True</s:Boolean>