✨ More & localized notifications
This commit is contained in:
parent
39d9d8a839
commit
bb739c1d90
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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>
|
@ -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>
|
@ -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>
|
||||
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user