Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion architecture.html

Large diffs are not rendered by default.

9 changes: 7 additions & 2 deletions architecture.json
Original file line number Diff line number Diff line change
Expand Up @@ -2695,7 +2695,7 @@
"Goals": 33,
"Habits": 74,
"Marketing": 4,
"Notifications": 12,
"Notifications": 13,
"Profile": 37,
"Referrals": 8,
"Social": 30,
Expand Down Expand Up @@ -2900,7 +2900,7 @@
"Goals": 38,
"Habits": 83,
"Marketing": 6,
"Notifications": 16,
"Notifications": 19,
"Profile": 38,
"Referrals": 12,
"Social": 32,
Expand Down Expand Up @@ -4882,6 +4882,11 @@
"Habit"
]
},
{
"testClass": "NotificationUrlsTests",
"file": "tests/Orbit.Application.Tests/Notifications/NotificationUrlsTests.cs",
"references": []
},
{
"testClass": "GetApiKeysQueryHandlerTests",
"file": "tests/Orbit.Application.Tests/Queries/ApiKeys/GetApiKeysQueryHandlerTests.cs",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,6 @@ public class AcceptAccountabilityPairCommandHandler(
IUnitOfWork unitOfWork) : IRequestHandler<AcceptAccountabilityPairCommand, Result>
{
private const string BattleBuddyAchievementId = "battle_buddy";
private const string BuddyNotificationUrl = "/social?tab=buddies";

public async Task<Result> Handle(AcceptAccountabilityPairCommand request, CancellationToken cancellationToken)
{
var access = await socialAccessGuard.EnsureEnabledAsync(request.UserId, cancellationToken);
Expand Down Expand Up @@ -105,6 +103,6 @@ await xpAwarder.AwardAsync(
? $"{accepter.Name} aceitou seu convite de parceria."
: $"{accepter.Name} accepted your accountability invite.";

return Notification.Create(requester.Id, title, body, BuddyNotificationUrl);
return Notification.Create(requester.Id, title, body, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,6 @@ public partial class CheckInAccountabilityCommandHandler(
IUnitOfWork unitOfWork,
ILogger<CheckInAccountabilityCommandHandler> logger) : IRequestHandler<CheckInAccountabilityCommand, Result<Guid>>
{
private const string BuddyNotificationUrl = "/social?tab=buddies";

public async Task<Result<Guid>> Handle(CheckInAccountabilityCommand request, CancellationToken cancellationToken)
{
var access = await social.AccessGuard.EnsureEnabledAsync(request.UserId, cancellationToken);
Expand Down Expand Up @@ -104,7 +102,7 @@ private async Task<Result> ModerateNoteAsync(string? note, Guid checkerId, Cance
? $"{checker.Name} fez check-in com você."
: $"{checker.Name} checked in with you.";

return Notification.Create(buddy.Id, title, body, BuddyNotificationUrl);
return Notification.Create(buddy.Id, title, body, null);
}

[LoggerMessage(EventId = 1, Level = LogLevel.Warning, Message = "Check-in note moderation unavailable for user {UserId}; allowing note (fail open)")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,6 @@ public class InviteAccountabilityBuddyCommandHandler(
SocialNotificationDispatcher notificationDispatcher,
IUnitOfWork unitOfWork) : IRequestHandler<InviteAccountabilityBuddyCommand, Result<Guid>>
{
private const string BuddyNotificationUrl = "/social?tab=buddies";

public async Task<Result<Guid>> Handle(InviteAccountabilityBuddyCommand request, CancellationToken cancellationToken)
{
var access = await socialAccessGuard.EnsureEnabledAsync(request.UserId, cancellationToken);
Expand Down Expand Up @@ -82,6 +80,6 @@ private static Notification BuildBuddyNotification(User buddy, User requester)
? $"{requester.Name} quer ser seu parceiro de responsabilidade."
: $"{requester.Name} wants to be your accountability buddy.";

return Notification.Create(buddy.Id, title, body, BuddyNotificationUrl);
return Notification.Create(buddy.Id, title, body, null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Orbit.Application.Calendar.Queries;
using Orbit.Application.Calendar.Services;
using Orbit.Application.Common;
using Orbit.Application.Notifications;
using Orbit.Domain.Common;
using Orbit.Domain.Entities;
using Orbit.Domain.Enums;
Expand Down Expand Up @@ -265,7 +266,7 @@ private async Task HandleReconnectRequired(User user, string errorCode, Cancella
isPortuguese
? "A sincronização automática está pausada. Reconecte para retomar."
: "Auto-sync paused. Reconnect to resume.",
url: "/calendar-sync");
url: NotificationUrls.CalendarSync);
await deps.NotificationRepository.AddAsync(notification, ct);

await deps.UnitOfWork.SaveChangesAsync(ct);
Expand All @@ -276,7 +277,7 @@ private async Task<bool> HasRecentSuggestionNotification(Guid userId, DateTime u
var cutoff = utcNow - NotificationRateLimitWindow;
return await deps.NotificationRepository.AnyAsync(
n => n.UserId == userId
&& n.Url == "/calendar-sync?mode=review"
&& n.Url == NotificationUrls.CalendarSyncReview
&& n.CreatedAtUtc > cutoff,
ct);
}
Expand All @@ -296,7 +297,7 @@ private async Task CreateSuggestionNotification(User user, int count, Cancellati
user.Id,
title,
body,
url: "/calendar-sync?mode=review");
url: NotificationUrls.CalendarSyncReview);
await deps.NotificationRepository.AddAsync(notification, ct);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,8 @@ private async Task NotifyOneAsync(User requester, User user, SemaphoreSlim throt

using var scope = scopeFactory.CreateScope();
var pushNotificationService = scope.ServiceProvider.GetRequiredService<IPushNotificationService>();
await pushNotificationService.SendToUserAsync(user.Id, title, body, cancellationToken: cancellationToken);
await pushNotificationService.SendToUserAsync(
user.Id, title, body, url: null, cancellationToken: cancellationToken);
}
catch (Exception ex)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -654,7 +654,7 @@ private async Task QueueAchievementNotification(
: $"Achievement Unlocked: {name}";
var body = $"{description} (+{achievement.XpReward} XP)";

var notification = Notification.Create(userId, title, body);
var notification = Notification.Create(userId, title, body, null);
await repos.NotificationRepository.AddAsync(notification, ct);

pushes.Add(new PendingPush(userId, title, body));
Expand All @@ -673,7 +673,7 @@ private async Task QueueLevelUpNotification(
? $"Você alcançou {levelTitle}! Continue assim!"
: $"You've reached {newLevel.Title}! Keep going!";

var notification = Notification.Create(userId, title, body);
var notification = Notification.Create(userId, title, body, null);
await repos.NotificationRepository.AddAsync(notification, ct);

pushes.Add(new PendingPush(userId, title, body));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,12 @@ public async Task<Result<TestPushNotificationResponse>> Handle(TestPushNotificat

try
{
await pushService.SendToUserAsync(request.UserId, "Orbit Test", "Push notifications are working!", "/", cancellationToken);
await pushService.SendToUserAsync(
request.UserId,
"Orbit Test",
"Push notifications are working!",
NotificationUrls.Home,
cancellationToken);
return Result.Success(new TestPushNotificationResponse(subscriptionCount, "sent"));
}
catch (Exception exception) when (exception is not OperationCanceledException)
Expand Down
11 changes: 11 additions & 0 deletions src/Orbit.Application/Notifications/NotificationUrls.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace Orbit.Application.Notifications;

public static class NotificationUrls
{
public const string Home = "/";
public const string Progress = "/progress";
Comment thread
thomasluizon marked this conversation as resolved.
public const string Chat = "/chat";
public const string Profile = "/profile";
public const string CalendarSync = "/calendar-sync";
public const string CalendarSyncReview = "/calendar-sync?mode=review";
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Logging;
using Orbit.Application.Common;
using Orbit.Application.Notifications;
using Orbit.Domain.Common;
using Orbit.Domain.Entities;
using Orbit.Domain.Enums;
Expand Down Expand Up @@ -171,11 +172,11 @@ private async Task SendNotification(User user, bool isReferrer, CancellationToke
var (title, body) = GetNotificationContent(isReferrer, isPt, user.IsPro);

await repos.NotificationRepository.AddAsync(
Notification.Create(user.Id, title, body, "/profile"), cancellationToken);
Notification.Create(user.Id, title, body, NotificationUrls.Profile), cancellationToken);

_ = Task.Run(async () =>
{
try { await pushNotificationService.SendToUserAsync(user.Id, title, body, "/profile", CancellationToken.None); }
try { await pushNotificationService.SendToUserAsync(user.Id, title, body, NotificationUrls.Profile, CancellationToken.None); }
catch (Exception ex) { LogReferralPushNotificationFailed(logger, ex, user.Id); }
}, CancellationToken.None);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,6 @@ public class AcceptFriendRequestCommandHandler(
private const string FirstFriendAchievementId = "first_friend";
private const string SquadGoalsAchievementId = "squad_goals";
private const int SquadGoalsThreshold = 5;
private const string FriendNotificationUrl = "/social?tab=friends";

public async Task<Result> Handle(AcceptFriendRequestCommand request, CancellationToken cancellationToken)
{
var access = await socialAccessGuard.EnsureEnabledAsync(request.UserId, cancellationToken);
Expand Down Expand Up @@ -119,6 +117,6 @@ await xpAwarder.AwardAsync(
? $"{accepter.Name} aceitou seu pedido de amizade."
: $"{accepter.Name} accepted your friend request.";

return Notification.Create(requester.Id, title, body, FriendNotificationUrl);
return Notification.Create(requester.Id, title, body, null);
}
}
4 changes: 1 addition & 3 deletions src/Orbit.Application/Social/Commands/SendCheerCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,6 @@ public partial class SendCheerCommandHandler(
ILogger<SendCheerCommandHandler> logger) : IRequestHandler<SendCheerCommand, Result<Guid>>
{
private const string CheerleaderAchievementId = "cheerleader";
private const string RecipientNotificationUrl = "/social?tab=feed";

public async Task<Result<Guid>> Handle(SendCheerCommand request, CancellationToken cancellationToken)
{
var access = await social.AccessGuard.EnsureEnabledAsync(request.UserId, cancellationToken);
Expand Down Expand Up @@ -153,7 +151,7 @@ private static Notification BuildRecipientNotification(User recipient, User send
? $"{sender.Name} torceu por você!"
: $"{sender.Name} cheered you on!";

return Notification.Create(recipient.Id, title, body, RecipientNotificationUrl);
return Notification.Create(recipient.Id, title, body, null);
}

[LoggerMessage(EventId = 1, Level = LogLevel.Warning, Message = "Cheer note moderation unavailable for sender {UserId}; allowing note (fail open)")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ public class SendFriendRequestCommandHandler(
SocialNotificationDispatcher notificationDispatcher,
IUnitOfWork unitOfWork) : IRequestHandler<SendFriendRequestCommand, Result<Guid>>
{
private const string RequestNotificationUrl = "/social?tab=friends";

public async Task<Result<Guid>> Handle(SendFriendRequestCommand request, CancellationToken cancellationToken)
{
var access = await socialAccessGuard.EnsureEnabledAsync(request.UserId, cancellationToken);
Expand Down Expand Up @@ -63,6 +61,6 @@ private static Notification BuildRequestNotification(User target, User requester
? $"{requester.Name} quer ser seu amigo."
: $"{requester.Name} wants to be your friend.";

return Notification.Create(target.Id, title, body, RequestNotificationUrl);
return Notification.Create(target.Id, title, body, null);
Comment thread
pullfrog[bot] marked this conversation as resolved.
}
}
13 changes: 12 additions & 1 deletion src/Orbit.Domain/Entities/Notification.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class Notification : Entity, ITimestamped, ISoftDeletable
public string Title { get; private set; } = null!;
public string Body { get; private set; } = null!;
public string? Url { get; private set; }
public string? DedupeKey { get; private set; }
public Guid? HabitId { get; private set; }
public bool IsRead { get; private set; }
public DateTime CreatedAtUtc { get; private set; }
Expand All @@ -17,14 +18,24 @@ public class Notification : Entity, ITimestamped, ISoftDeletable

private Notification() { }

public static Notification Create(Guid userId, string title, string body, string? url = null, Guid? habitId = null)
public static Notification Create(
Guid userId,
string title,
string body,
string? url = null,
Guid? habitId = null,
string? dedupeKey = null)
{
if (url is not null && (!url.StartsWith('/') || url.StartsWith("//", StringComparison.Ordinal)))
throw new ArgumentException("Notification URL must be an app-relative path.", nameof(url));

return new Notification
{
UserId = userId,
Title = title,
Body = body,
Url = url,
DedupeKey = dedupeKey,
HabitId = habitId,
IsRead = false,
CreatedAtUtc = DateTime.UtcNow,
Expand Down
Loading
Loading