Skip to content

Feature/notification settings#486

Merged
NielsPilgaard merged 24 commits into
mainfrom
feature/notification-settings
Jan 2, 2026
Merged

Feature/notification settings#486
NielsPilgaard merged 24 commits into
mainfrom
feature/notification-settings

Conversation

@NielsPilgaard

Copy link
Copy Markdown
Owner

No description provided.

@coderabbitai

coderabbitai Bot commented Jan 2, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@NielsPilgaard has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 16 minutes and 37 seconds before requesting another review.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

📥 Commits

Reviewing files that changed from the base of the PR and between 21b834e and d1604db.

📒 Files selected for processing (4)
  • src/shared/Jordnaer.Shared/Database/Enums/ChatNotificationPreference.cs
  • src/web/Jordnaer/Features/Chat/ChatNotificationService.cs
  • src/web/Jordnaer/Pages/Settings/Notifications.razor
  • tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs

Walkthrough

Adds per-user notification preferences (chat and group-post), DB migration columns, services and DI for managing preferences, consumer and ChatNotificationService changes to send preference-aware emails, a settings UI and menu entry, and updated tests.

Changes

Cohort / File(s) Summary
Core models & enum
src/shared/Jordnaer.Shared/Database/Enums/ChatNotificationPreference.cs, src/shared/Jordnaer.Shared/Database/UserProfile.cs, src/shared/Jordnaer.Shared/Database/GroupMembership.cs
Adds ChatNotificationPreference enum and properties: UserProfile.ChatNotificationPreference (default FirstMessageOnly) and GroupMembership.EmailOnNewPost (default true).
Notification settings service & DI
src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs, src/web/Jordnaer/Features/Notifications/WebApplicationBuilderExtensions.cs, src/web/Jordnaer/Program.cs
New INotificationSettingsService + NotificationSettingsService with get/set APIs for chat and group preferences; DI extension AddNotificationServices() registered in Program.cs.
Chat notification flow
src/web/Jordnaer/Features/Chat/ChatNotificationService.cs, src/web/Jordnaer/Consumers/SendMessageConsumer.cs, tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs
ChatNotificationService now uses IOptions<AppOptions> for BaseUrl, adds NotifyRecipientsOfNewMessage(...) to send emails honoring user preferences. SendMessageConsumer now injects and calls the service after message persistence. Tests converted to integration-style using SqlServerContainer and IOptions.
Group post notifications
src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
Consumer replaced NavigationManager with IOptions<AppOptions> for base URL and filters recipients by EmailOnNewPost.
UI: settings page & navigation
src/web/Jordnaer/Pages/Settings/Notifications.razor, src/web/Jordnaer/Pages/Shared/TopBar.razor
Adds notification settings page (/settings/notifications) with chat radio and per-group toggles; adds profile menu link to the page.
UI tweaks
src/web/Jordnaer/Pages/Groups/MyGroups.razor, src/web/Jordnaer/Pages/Groups/Members.razor
Minor UI changes: tab padding CSS and BackButton Href removal.
Database migration & snapshots
src/web/Jordnaer/Migrations/20260102194640_Add_NotificationSettings.cs, ...Designer.cs, JordnaerDbContextModelSnapshot.cs, migration_script.sql
EF Core migration adding ChatNotificationPreference (int) to UserProfiles and EmailOnNewPost (bit) to GroupMemberships; includes snapshot and SQL script.
Docs/tasks
tasks/02-group-notifications.md
Removes task documentation that described planned group notification work (implementation added in code).

Sequence Diagram(s)

sequenceDiagram
    autonumber
    participant Consumer as SendMessageConsumer
    participant DB as JordnaerDbContext
    participant NotifSvc as ChatNotificationService
    participant Pub as IPublishEndpoint

    Consumer->>DB: Persist chat message
    DB-->>Consumer: Save success

    Consumer->>NotifSvc: NotifyRecipientsOfNewMessage(message, ct)
    NotifSvc->>DB: Query chat participants + UserProfiles (preferences)
    DB-->>NotifSvc: Users + preferences

    Note over NotifSvc: Filter recipients by preference\n(FirstMessageOnly / AllMessages)
    alt recipients found
        NotifSvc->>Pub: Publish email messages
        Pub-->>NotifSvc: Publish ack
    else no recipients
        NotifSvc-->>NotifSvc: Log no recipients
    end

    NotifSvc-->>Consumer: Completed
    Consumer->>Consumer: Increment metrics
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author, making it impossible to assess whether any description relates to the changeset. Add a description explaining the notification settings feature, including what was added (chat and group notification preferences) and why these changes were made.
Docstring Coverage ⚠️ Warning Docstring coverage is 10.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Feature/notification settings' is directly related to the main changes which add notification preference management across chat and group notifications.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs (1)

33-51: Missing EmailOnNewPost preference filter.

The consumer fetches all active members but does not respect the new EmailOnNewPost preference on GroupMembership. Users who have disabled email notifications for a group will still receive emails.

🔎 Proposed fix to filter by EmailOnNewPost preference
 // Get all active members excluding the post author
-var activeMembers = context.GroupMemberships
+var activeMembers = await context.GroupMemberships
     .AsNoTracking()
     .Where(x => x.GroupId == message.GroupId &&
                x.MembershipStatus == MembershipStatus.Active &&
-               x.UserProfileId != message.AuthorId)
-    .Select(x => x.UserProfileId);
+               x.UserProfileId != message.AuthorId &&
+               x.EmailOnNewPost)
+    .Select(x => x.UserProfileId)
+    .ToListAsync(consumeContext.CancellationToken);

 // Get their email addresses
 var emails = await context.Users
     .AsNoTracking()
-    .Where(user => activeMembers.Any(userId => userId == user.Id) &&
+    .Where(user => activeMembers.Contains(user.Id) &&
                   !string.IsNullOrEmpty(user.Email))
🧹 Nitpick comments (3)
src/web/Jordnaer/Pages/Groups/MyGroups.razor (1)

113-117: Consider avoiding !important for maintainability.

The !important flag can make future styling adjustments more difficult. If the padding override is necessary due to MudBlazor's default styles, consider whether a more specific selector or a different approach could achieve the same result without !important.

tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs (1)

38-47: Mixed mocking frameworks in the same test class.

The test uses both Moq (for JordnaerDbContext, IPublishEndpoint) and NSubstitute (for IOptions<AppOptions>). While functional, mixing frameworks reduces consistency.

Consider standardizing on one mocking framework for consistency.

src/web/Jordnaer/Pages/Settings/Notifications.razor (1)

207-242: Sequential API calls in bulk operations may cause poor UX.

EnableAllGroups and DisableAllGroups make individual API calls in a loop. For users with many group memberships, this could be slow and result in many snackbar messages if errors occur.

Consider adding a bulk update endpoint in INotificationSettingsService to handle all changes in a single database transaction.

🔎 Example bulk update approach
// In INotificationSettingsService
Task<OneOf<Success, NotFound>> SetAllGroupPostPreferencesAsync(
    string userId, bool enabled, CancellationToken cancellationToken = default);

// In the page
private async Task EnableAllGroups()
{
    if (CurrentUser.Id is null) return;
    
    try
    {
        var result = await NotificationSettingsService
            .SetAllGroupPostPreferencesAsync(CurrentUser.Id, true);
        result.Switch(
            success => {
                foreach (var s in _groupSettings) s.EmailOnNewPost = true;
                Snackbar.Add("Alle gruppe notifikationer aktiveret", Severity.Success);
            },
            notFound => Snackbar.Add("Kunne ikke opdatere indstillinger.", Severity.Error)
        );
    }
    catch (Exception)
    {
        Snackbar.Add("Der opstod en fejl. Prøv igen senere.", Severity.Error);
    }
}
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 16e3aa1 and 8c747f5.

📒 Files selected for processing (19)
  • src/shared/Jordnaer.Shared/Database/Enums/ChatNotificationPreference.cs
  • src/shared/Jordnaer.Shared/Database/GroupMembership.cs
  • src/shared/Jordnaer.Shared/Database/UserProfile.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • src/web/Jordnaer/Consumers/SendMessageConsumer.cs
  • src/web/Jordnaer/Features/Chat/ChatNotificationService.cs
  • src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs
  • src/web/Jordnaer/Features/Notifications/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Migrations/20260102194640_Add_NotificationSettings.Designer.cs
  • src/web/Jordnaer/Migrations/20260102194640_Add_NotificationSettings.cs
  • src/web/Jordnaer/Migrations/JordnaerDbContextModelSnapshot.cs
  • src/web/Jordnaer/Migrations/migration_script.sql
  • src/web/Jordnaer/Pages/Groups/Members.razor
  • src/web/Jordnaer/Pages/Groups/MyGroups.razor
  • src/web/Jordnaer/Pages/Settings/Notifications.razor
  • src/web/Jordnaer/Pages/Shared/TopBar.razor
  • src/web/Jordnaer/Program.cs
  • tasks/02-group-notifications.md
  • tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs
💤 Files with no reviewable changes (1)
  • tasks/02-group-notifications.md
🧰 Additional context used
📓 Path-based instructions (4)
**/WebApplicationBuilderExtensions.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

DI setup for features must be in WebApplicationBuilderExtensions.cs extension methods called from Program.cs

Files:

  • src/web/Jordnaer/Features/Notifications/WebApplicationBuilderExtensions.cs
**/{Features,Consumers}/**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/{Features,Consumers}/**/*.cs: Always use await using var context = await contextFactory.CreateDbContextAsync() for scoped DbContext instances
Use AsNoTracking() for read-only database queries in EF Core
Use IHubContext for broadcasting messages to SignalR clients from outside the hub

Files:

  • src/web/Jordnaer/Features/Notifications/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Consumers/SendMessageConsumer.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • src/web/Jordnaer/Features/Chat/ChatNotificationService.cs
  • src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs
**/Features/**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use OneOf return types for explicit error handling instead of exceptions

Files:

  • src/web/Jordnaer/Features/Notifications/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Chat/ChatNotificationService.cs
  • src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs
**/Consumers/**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/Consumers/**/*.cs: MassTransit consumers must be in the Jordnaer.Consumers namespace for auto-discovery
Track metrics in MassTransit consumers using JordnaerMetrics.Counter.Add()

Files:

  • src/web/Jordnaer/Consumers/SendMessageConsumer.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
🧠 Learnings (8)
📚 Learning: 2025-12-30T22:07:15.171Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-30T22:07:15.171Z
Learning: Applies to **/WebApplicationBuilderExtensions.cs : DI setup for features must be in `WebApplicationBuilderExtensions.cs` extension methods called from `Program.cs`

Applied to files:

  • src/web/Jordnaer/Features/Notifications/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • src/web/Jordnaer/Program.cs
  • tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs
📚 Learning: 2025-12-29T18:32:51.546Z
Learnt from: NielsPilgaard
Repo: NielsPilgaard/Jordnaer PR: 481
File: src/web/Jordnaer/Pages/Groups/EditGroup.razor:137-139
Timestamp: 2025-12-29T18:32:51.546Z
Learning: In the Jordnaer codebase, Group.Categories is never null and is always initialized. When reviewing Razor components under src/web/Jordnaer/Pages, avoid adding null checks for Group.Categories and rely on its non-null guarantee. If a path could yield a null value due to data from an external source, add a guard only there, but otherwise remove redundant null checks for this property in these files.

Applied to files:

  • src/web/Jordnaer/Pages/Groups/Members.razor
  • src/web/Jordnaer/Pages/Shared/TopBar.razor
  • src/web/Jordnaer/Pages/Groups/MyGroups.razor
  • src/web/Jordnaer/Pages/Settings/Notifications.razor
📚 Learning: 2025-12-30T22:07:15.171Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-30T22:07:15.171Z
Learning: Applies to **/{Features,Consumers}/**/*.cs : Use `IHubContext` for broadcasting messages to SignalR clients from outside the hub

Applied to files:

  • src/web/Jordnaer/Consumers/SendMessageConsumer.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • src/web/Jordnaer/Features/Chat/ChatNotificationService.cs
  • tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs
📚 Learning: 2025-12-30T22:07:15.171Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-30T22:07:15.171Z
Learning: Applies to **/SignalR/**/*.cs : SignalR hubs must define an interface (e.g., `IChatHub`) for type-safe client method invocations

Applied to files:

  • src/web/Jordnaer/Consumers/SendMessageConsumer.cs
📚 Learning: 2025-12-30T22:07:15.171Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-30T22:07:15.171Z
Learning: Applies to **/Consumers/**/*.cs : MassTransit consumers must be in the Jordnaer.Consumers namespace for auto-discovery

Applied to files:

  • src/web/Jordnaer/Consumers/SendMessageConsumer.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs
📚 Learning: 2025-12-30T22:07:15.171Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-30T22:07:15.171Z
Learning: Applies to **/{Features,Consumers}/**/*.cs : Use `AsNoTracking()` for read-only database queries in EF Core

Applied to files:

  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs
📚 Learning: 2025-12-30T22:07:15.171Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-30T22:07:15.171Z
Learning: Applies to **/{Features,Consumers}/**/*.cs : Always use `await using var context = await contextFactory.CreateDbContextAsync()` for scoped DbContext instances

Applied to files:

  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs
📚 Learning: 2025-12-30T22:07:15.171Z
Learnt from: CR
Repo: NielsPilgaard/Jordnaer PR: 0
File: .github/copilot-instructions.md:0-0
Timestamp: 2025-12-30T22:07:15.171Z
Learning: Applies to **/Consumers/**/*.cs : Track metrics in MassTransit consumers using `JordnaerMetrics.Counter.Add()`

Applied to files:

  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs
🧬 Code graph analysis (8)
src/web/Jordnaer/Features/Notifications/WebApplicationBuilderExtensions.cs (1)
src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs (1)
  • NotificationSettingsService (18-89)
src/web/Jordnaer/Consumers/SendMessageConsumer.cs (1)
src/web/Jordnaer/Features/Chat/ChatNotificationService.cs (1)
  • ChatNotificationService (12-158)
src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs (2)
src/web/Jordnaer/Extensions/AppOptions.cs (1)
  • AppOptions (5-11)
src/shared/Jordnaer.Shared/Groups/Events/GroupPostCreated.cs (1)
  • GroupPostCreated (3-12)
src/web/Jordnaer/Migrations/20260102194640_Add_NotificationSettings.Designer.cs (2)
src/web/Jordnaer/Migrations/JordnaerDbContextModelSnapshot.cs (1)
  • DbContext (14-902)
src/web/Jordnaer/Migrations/20260102194640_Add_NotificationSettings.cs (1)
  • Add_NotificationSettings (8-39)
src/web/Jordnaer/Features/Chat/ChatNotificationService.cs (4)
src/web/Jordnaer/Extensions/AppOptions.cs (1)
  • AppOptions (5-11)
src/web/Jordnaer/Features/Email/SendEmail.cs (1)
  • SendEmail (5-33)
src/web/Jordnaer/Features/Email/EmailRecipient.cs (1)
  • EmailRecipient (6-78)
src/web/Jordnaer/Features/Email/EmailConstants.cs (2)
  • EmailConstants (3-15)
  • Greeting (12-14)
tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs (2)
src/web/Jordnaer/Extensions/AppOptions.cs (1)
  • AppOptions (5-11)
src/web/Jordnaer/Features/Chat/ChatNotificationService.cs (1)
  • ChatNotificationService (12-158)
src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs (1)
src/shared/Jordnaer.Shared/Database/GroupMembership.cs (1)
  • GroupMembership (3-29)
src/shared/Jordnaer.Shared/Database/Enums/ChatNotificationPreference.cs (1)
src/shared/Jordnaer.Shared/Extensions/EnumExtensions.cs (1)
  • EnumExtensions (5-22)
🔇 Additional comments (17)
src/web/Jordnaer/Consumers/SendMessageConsumer.cs (1)

69-70: Notification errors are uncaught and won't affect message delivery.

The call to NotifyRecipientsOfNewMessage is placed outside the try-catch block, which means:

  • If notification sending fails, the exception will propagate and potentially cause the consumer to retry the entire message
  • The message has already been saved and sent via SignalR at this point

Verify this is the intended behavior. If notifications should not affect message delivery reliability, consider wrapping this call in its own try-catch block.

src/web/Jordnaer/Pages/Shared/TopBar.razor (1)

135-139: LGTM! Clean UI integration for notification settings.

The new notification settings menu items are consistently implemented for both desktop and mobile views, using appropriate icons and Danish labels.

Also applies to: 167-171

src/web/Jordnaer/Program.cs (2)

17-17: LGTM!

The import is correctly added for the new Notifications feature namespace.


88-88: LGTM!

The notification services registration follows the established DI pattern correctly. Based on learnings, extension methods are properly called from Program.cs for feature setup.

src/web/Jordnaer/Migrations/JordnaerDbContextModelSnapshot.cs (2)

304-306: LGTM - Auto-generated migration snapshot.

The EmailOnNewPost property is correctly reflected in the model snapshot. Note: This file is auto-generated by EF Core.


475-477: LGTM - Auto-generated migration snapshot.

The ChatNotificationPreference property is correctly reflected in the model snapshot. Note: This file is auto-generated by EF Core.

src/web/Jordnaer/Features/Notifications/WebApplicationBuilderExtensions.cs (1)

1-10: LGTM!

The extension method correctly registers the notification settings service as scoped, following the established DI pattern in WebApplicationBuilderExtensions.cs as per the coding guidelines. The fluent return pattern is consistent with other builders.

src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs (1)

63-64: URL construction change looks correct.

The switch from NavigationManager.BaseUri to IOptions<AppOptions>.Value.BaseUrl is appropriate. Using configuration-based URLs is more reliable in background consumers where NavigationManager may not have access to the request context.

tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs (1)

224-237: Test correctly validates fallback behavior.

The test verifies that when BaseUrl is null, the service falls back to the default domain. The NSubstitute setup on line 229 correctly overrides the value for this specific test case since IOptions<T>.Value is evaluated at call time.

src/web/Jordnaer/Pages/Settings/Notifications.razor (1)

133-149: LGTM on initialization logic.

The OnInitializedAsync correctly handles null user ID, loads chat preference from the cached CurrentUser.UserProfile, and fetches group preferences asynchronously. The finally block ensures loading state is always cleared.

src/web/Jordnaer/Features/Chat/ChatNotificationService.cs (2)

23-47: Notification preferences are correctly integrated.

The updated NotifyRecipients method properly joins with UserProfiles to filter recipients based on their ChatNotificationPreference. Users with None preference are correctly excluded. The early return for empty recipients prevents unnecessary processing.


83-143: New NotifyRecipientsOfNewMessage method correctly handles "AllMessages" preference.

The method properly:

  • Excludes the sender from recipients
  • Filters to only users with AllMessages preference
  • Handles missing sender gracefully with logging
  • Uses PublishBatch for efficiency
src/web/Jordnaer/Migrations/20260102194640_Add_NotificationSettings.Designer.cs (1)

1-906: Auto-generated migration designer file.

This is an EF Core auto-generated file that captures the model snapshot at migration time. The new properties EmailOnNewPost (line 307-308) and ChatNotificationPreference (line 478-479) are correctly included in the model.

src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs (4)

20-29: LGTM!

The method correctly follows the coding guidelines by using await using var context and AsNoTracking() for the read-only query. Returning the default preference when the user profile is not found is a reasonable fallback behavior.


31-46: LGTM!

The method correctly implements the update operation with proper error handling using OneOf return type. Correctly omits AsNoTracking() since this is a write operation.


61-76: LGTM!

The method correctly implements the update operation with proper error handling using OneOf return type and correctly omits AsNoTracking() for the write operation.


78-88: LGTM!

The method correctly follows the coding guidelines by using await using var context and AsNoTracking(). Including the Group navigation property is appropriate for returning complete preference information to callers, and filtering by Active membership status is the correct approach.

Comment thread src/shared/Jordnaer.Shared/Database/Enums/ChatNotificationPreference.cs Outdated
Comment thread src/shared/Jordnaer.Shared/Database/GroupMembership.cs
Comment thread src/shared/Jordnaer.Shared/Database/UserProfile.cs
Comment thread src/web/Jordnaer/Consumers/SendMessageConsumer.cs
Comment thread src/web/Jordnaer/Features/Chat/ChatNotificationService.cs
Comment thread src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs Outdated
Comment thread src/web/Jordnaer/Migrations/migration_script.sql
Comment thread src/web/Jordnaer/Migrations/migration_script.sql
@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Jordnaer Community Website Jan 2, 2026

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs (1)

259-272: Test mutates shared _appOptions which may affect other tests.

The test changes _appOptions.Value.Returns(...) on a shared instance. If tests run in parallel or non-deterministic order, this could cause flaky behavior. Consider creating a separate service instance with null BaseUrl for this test.

🔎 Proposed fix
 [Fact]
 public void GetChatLink_ShouldReturnDefault_WhenServerAddressNotAvailable()
 {
     // Arrange
     var chatId = Guid.NewGuid();
-    _appOptions.Value.Returns(new AppOptions { BaseUrl = null });
+    var nullAppOptions = Substitute.For<IOptions<AppOptions>>();
+    nullAppOptions.Value.Returns(new AppOptions { BaseUrl = null });
+    var serviceWithNullBaseUrl = new ChatNotificationService(
+        Mock.Of<IDbContextFactory<JordnaerDbContext>>(),
+        new NullLogger<StartChatConsumer>(),
+        Mock.Of<IPublishEndpoint>(),
+        nullAppOptions
+    );
     var defaultLink = $"https://mini-moeder.dk/chat/{chatId}";

     // Act
-    var link = _service.GetChatLink(chatId);
+    var link = serviceWithNullBaseUrl.GetChatLink(chatId);

     // Assert
     link.Should().Be(defaultLink);
 }
♻️ Duplicate comments (1)
src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs (1)

49-60: Default return value for non-existent memberships should be false.

This issue was already flagged in a previous review. Returning true when membership doesn't exist means emails would be sent to non-members.

🧹 Nitpick comments (4)
src/web/Jordnaer/Pages/Settings/Notifications.razor (2)

106-110: Consider adding save state tracking for individual group toggles.

Currently, rapid toggling could trigger multiple concurrent saves and snackbar messages. For a smoother UX, consider tracking per-group save state or debouncing the save operation.


133-149: Exception in GetGroupPreferencesAsync will propagate unhandled.

Unlike save methods, OnInitializedAsync doesn't catch exceptions from the service call. If the database is unavailable, users will see an error page instead of a graceful message.

🔎 Proposed fix
     protected override async Task OnInitializedAsync()
     {
         try
         {
             if (CurrentUser.Id is null)
             {
                 return;
             }

             _chatPreference = CurrentUser.UserProfile?.ChatNotificationPreference ?? ChatNotificationPreference.FirstMessageOnly;
             _groupSettings = await NotificationSettingsService.GetGroupPreferencesAsync(CurrentUser.Id);
         }
+        catch (Exception)
+        {
+            Snackbar.Add("Kunne ikke indlæse notifikationsindstillinger.", Severity.Error);
+            _groupSettings = new();
+        }
         finally
         {
             _isLoading = false;
         }
     }
tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs (2)

39-42: Mixing Moq and NSubstitute mocking frameworks.

The test class uses Mock<IPublishEndpoint> (Moq) alongside Substitute.For<IOptions<AppOptions>> (NSubstitute). Consider using one framework consistently for better maintainability.


144-147: Test assertion could be stronger.

Only asserting NotThrowAsync doesn't verify the notification logic works correctly. Consider verifying that _publishEndpointMock.Publish was called with expected email content for recipients with AllMessages preference.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c747f5 and 5fc39eb.

📒 Files selected for processing (5)
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs
  • src/web/Jordnaer/Pages/Groups/MyGroups.razor
  • src/web/Jordnaer/Pages/Settings/Notifications.razor
  • tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
🧰 Additional context used
📓 Path-based instructions (2)
**/{Features,Consumers}/**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

**/{Features,Consumers}/**/*.cs: Always use await using var context = await contextFactory.CreateDbContextAsync() for scoped DbContext instances
Use AsNoTracking() for read-only database queries in EF Core
Use IHubContext for broadcasting messages to SignalR clients from outside the hub

Files:

  • src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs
**/Features/**/*.cs

📄 CodeRabbit inference engine (.github/copilot-instructions.md)

Use OneOf return types for explicit error handling instead of exceptions

Files:

  • src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs
🧠 Learnings (1)
📚 Learning: 2025-12-29T18:32:51.546Z
Learnt from: NielsPilgaard
Repo: NielsPilgaard/Jordnaer PR: 481
File: src/web/Jordnaer/Pages/Groups/EditGroup.razor:137-139
Timestamp: 2025-12-29T18:32:51.546Z
Learning: In the Jordnaer codebase, Group.Categories is never null and is always initialized. When reviewing Razor components under src/web/Jordnaer/Pages, avoid adding null checks for Group.Categories and rely on its non-null guarantee. If a path could yield a null value due to data from an external source, add a guard only there, but otherwise remove redundant null checks for this property in these files.

Applied to files:

  • src/web/Jordnaer/Pages/Groups/MyGroups.razor
  • src/web/Jordnaer/Pages/Settings/Notifications.razor
🧬 Code graph analysis (1)
src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs (2)
src/shared/Jordnaer.Shared/Database/GroupMembership.cs (1)
  • GroupMembership (3-29)
src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs (1)
  • Task (20-88)
🔇 Additional comments (7)
src/web/Jordnaer/Pages/Groups/MyGroups.razor (1)

113-117: Inline styles are acceptable for component-specific styling.

The CSS rule targets the component-specific class correctly. Consider moving to a shared stylesheet if this padding pattern is reused across multiple tab components.

src/web/Jordnaer/Pages/Settings/Notifications.razor (1)

151-175: Error handling pattern is well implemented.

Good use of OneOf.Switch for explicit result handling with user-friendly Danish messages.

src/web/Jordnaer/Features/Notifications/NotificationSettingsService.cs (4)

9-17: Well-designed interface with OneOf return types for explicit error handling.

The interface follows the coding guidelines by using OneOf<Success, NotFound> for operations that can fail, making error handling explicit at compile time.


21-30: Correct use of AsNoTracking() for read-only query.

As per coding guidelines, the read-only query uses AsNoTracking() for better performance.


79-88: Efficient bulk update using ExecuteUpdateAsync.

Good use of EF Core's bulk update capability to update all memberships in a single database operation rather than fetching and updating each entity individually.


90-100: Include(x => x.Group) may return detached entities with navigation property issues.

Since AsNoTracking() is used, the returned GroupMembership entities are detached from the context. The included Group navigation property is populated but the context is disposed after the method returns. This is fine for read-only display but ensure the UI doesn't expect to track these entities.

tests/web/Jordnaer.Tests/Chat/ChatNotificationServiceTests.cs (1)

109-111: No concern here—data persists correctly across context instances.

The test uses Xunit's [Collection] fixture pattern with SqlServerContainer, which manages a single persistent SQL Server database instance shared across all tests. Both _context (created at line 32) and contexts created by the factory (line 37) connect to the same database, so data seeded via _context.SaveChangesAsync() is immediately visible to the service when it calls the factory.

Likely an incorrect or invalid review comment.

Comment thread src/web/Jordnaer/Pages/Settings/Notifications.razor Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/web/Jordnaer/Pages/Settings/Notifications.razor (1)

89-96: Consider disabling bulk action buttons during save operations.

The "Aktivér alle" and "Deaktivér alle" buttons are not disabled during the save operation, which could lead to multiple simultaneous requests if the user clicks repeatedly.

🔎 Proposed fix

Add a saving state for bulk operations:

 private bool _isLoading = true;
 private bool _isSaving = false;
+private bool _isBulkSaving = false;
 private ChatNotificationPreference _chatPreference = ChatNotificationPreference.FirstMessageOnly;

Then disable the buttons:

 <div class="mb-4 d-flex gap-2">
-    <MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary" OnClick="EnableAllGroups">
+    <MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Primary" OnClick="EnableAllGroups" Disabled="_isBulkSaving">
         Aktivér alle
     </MudButton>
-    <MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" OnClick="DisableAllGroups">
+    <MudButton Size="Size.Small" Variant="Variant.Outlined" Color="Color.Secondary" OnClick="DisableAllGroups" Disabled="_isBulkSaving">
         Deaktivér alle
     </MudButton>
 </div>

And wrap the bulk operations:

 private async Task EnableAllGroups()
 {
     if (CurrentUser.Id is null)
     {
         return;
     }

+    _isBulkSaving = true;
     try
     {
         // ... existing code
     }
     catch (Exception)
     {
         Snackbar.Add("Der opstod en fejl. Prøv igen senere.", Severity.Error);
     }
+    finally
+    {
+        _isBulkSaving = false;
+    }
 }

Apply the same pattern to DisableAllGroups.

Also applies to: 207-261

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 487fee9 and 21b834e.

📒 Files selected for processing (1)
  • src/web/Jordnaer/Pages/Settings/Notifications.razor
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-12-29T18:32:51.546Z
Learnt from: NielsPilgaard
Repo: NielsPilgaard/Jordnaer PR: 481
File: src/web/Jordnaer/Pages/Groups/EditGroup.razor:137-139
Timestamp: 2025-12-29T18:32:51.546Z
Learning: In the Jordnaer codebase, Group.Categories is never null and is always initialized. When reviewing Razor components under src/web/Jordnaer/Pages, avoid adding null checks for Group.Categories and rely on its non-null guarantee. If a path could yield a null value due to data from an external source, add a guard only there, but otherwise remove redundant null checks for this property in these files.

Applied to files:

  • src/web/Jordnaer/Pages/Settings/Notifications.razor
🔇 Additional comments (7)
src/web/Jordnaer/Pages/Settings/Notifications.razor (7)

1-11: LGTM!

The page setup is well-structured with appropriate authorization, service injections, and namespace imports.


12-19: LGTM!

Good use of metadata for SEO and consistent styling with the application's design system.


21-74: LGTM!

Well-designed chat notification section with clear option descriptions and proper loading state handling on the save button.


98-115: LGTM!

The null guard on line 99 properly addresses the potential null reference issue. The group list rendering with individual toggles is well-implemented.

Minor note: Line 104 could use @setting.Group!.Name since the Where clause already filters out null groups, but the current null-conditional approach is safe and doesn't cause issues.


127-149: LGTM!

State initialization and OnInitializedAsync are well-implemented with proper null checks and try/finally to ensure the loading state is always reset.


151-175: LGTM!

Clean implementation of the save method with proper loading state management, result handling via Switch, and error handling.


177-205: LGTM!

The method properly updates local state on success to keep the UI in sync with the server. Error handling is appropriate.

@NielsPilgaard NielsPilgaard merged commit ea7af09 into main Jan 2, 2026
5 checks passed
@NielsPilgaard NielsPilgaard deleted the feature/notification-settings branch January 2, 2026 22:47
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Jordnaer Community Website Jan 2, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jan 3, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Feb 14, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

1 participant