Skip to content

Feature/group invite display#485

Merged
NielsPilgaard merged 13 commits into
mainfrom
feature/group-invite-display
Jan 2, 2026
Merged

Feature/group invite display#485
NielsPilgaard merged 13 commits into
mainfrom
feature/group-invite-display

Conversation

@NielsPilgaard

Copy link
Copy Markdown
Owner

No description provided.

@coderabbitai

coderabbitai Bot commented Dec 30, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds group-post publish events and consumer-driven email notifications, real-time pending-membership deltas via SignalR with admin notifications, pending-count APIs and UI badges, DTOs/models, DI/metrics wiring, and membership leave/re-application handling.

Changes

Cohort / File(s) Summary
Event DTOs
src/shared/Jordnaer.Shared/Groups/Events/GroupPostCreated.cs, src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs
New DTOs: GroupPostCreated (post metadata + CreatedUtc) and GroupMembershipStatusChanged (GroupId + PendingCountChange).
Post publish & consumer
src/web/Jordnaer/Features/GroupPosts/GroupPostService.cs, src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs, src/web/Jordnaer/Features/Metrics/JordnaerMetrics.cs
GroupPostService publishes GroupPostCreated after saving posts. New GroupPostCreatedConsumer resolves recipients (excludes author), builds an HTML preview and publishes a SendEmail payload. New consumer metrics counters added.
SignalR hub & client
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs, src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs, src/web/Jordnaer/Program.cs
Added IGroupMembershipHub and [Authorize] GroupMembershipHub (mapped at /hubs/group-membership). New client GroupMembershipSignalRClient subscribes to MembershipStatusChanged and is registered in DI.
Notification service & integration
src/web/Jordnaer/Features/Groups/GroupMembershipNotificationService.cs, src/web/Jordnaer/Features/Groups/GroupService.cs, src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs
New IGroupMembershipNotificationService + GroupMembershipNotificationService uses IHubContext to notify admins. GroupService gets notification dependency, pending-count APIs, and emits deltas when pending-status toggles. DI registers the SignalR client and notification service.
UI: pending counts & badges
src/web/Jordnaer/Features/Dashboard/GroupsHub.razor, src/web/Jordnaer/Pages/Shared/TopBar.razor, src/web/Jordnaer/Pages/Groups/MyGroups.razor, src/web/Jordnaer/Pages/Groups/GroupDetails.razor, src/web/Jordnaer/Features/Groups/GroupSummaryCard.razor
Components load pending counts via GroupService, subscribe to GroupMembershipSignalRClient, and render badges/alerts for pending membership requests; GroupsHub implements IAsyncDisposable. MyGroups passes per-group PendingRequestCount to cards; TopBar shows summed badge and listens for real-time updates.
Membership flows
src/web/Jordnaer/Features/Membership/MembershipService.cs, src/web/Jordnaer/Features/Groups/GroupService.cs
Added LeaveMembership API and leave handling. MembershipService integrates IGroupMembershipNotificationService. Membership request flow updated for re-application and non-fatal notification handling; pending-status delta computed and notified.
Group member model & UI lists
src/shared/Jordnaer.Shared/Groups/GroupMemberSlim.cs, src/web/Jordnaer/Features/Groups/GroupMemberListComponent.razor
New GroupMemberSlim DTO; member list component switched to GroupMemberSlim and shows Owner/Admin chips next to names.
Email content tweak
src/web/Jordnaer/Features/Email/EmailService.cs
Membership-request email subject and header now include the group name; signature appended to body.
Small UI/style changes
src/web/Jordnaer/Features/Posts/PostCardComponent.razor, src/web/Jordnaer/Features/Sharing/SocialShareButtons.razor, src/web/Jordnaer/Components/CookieBanner.razor
Icon color adjustments and render-fragment builder change; minor styling updates.
Enums & tests
src/shared/Jordnaer.Shared/Database/Enums/MembershipStatus.cs, tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs
Added Left = 4 enum member. Tests updated to construct GroupService with the new IGroupMembershipNotificationService dependency.

Sequence Diagram(s)

sequenceDiagram
    %% Group post publish & email consumer flow
    autonumber
    participant Client as Web Client
    participant Server as GroupPostService
    participant DB as Database
    participant MT as MassTransit
    participant Consumer as GroupPostCreatedConsumer
    participant Email as Email pipeline

    Client->>Server: CreatePostAsync()
    Server->>DB: Save GroupPost
    DB-->>Server: Persisted
    Server->>Server: Query Group & Author profile
    alt Group & Author found
        Server->>MT: Publish GroupPostCreated
        MT->>Consumer: Deliver GroupPostCreated
        Consumer->>DB: Query active group members (exclude author)
        DB-->>Consumer: Member emails
        Consumer->>MT: Publish SendEmail (Bcc, HTML)
        MT->>Email: Deliver SendEmail
    else Missing data
        Server-->>Server: Log warning (no publish)
    end
Loading
sequenceDiagram
    %% Pending-membership delta notification via SignalR
    autonumber
    participant AdminUI as Admin client
    participant Server as GroupService
    participant DB as Database
    participant Hub as GroupMembershipHub
    participant AdminClients as Admin clients (group-admins-{id})

    AdminUI->>Server: UpdateMembership(...)
    Server->>DB: Update membership status
    DB-->>Server: Saved
    alt Pending status toggled
        Server->>Hub: Notify MembershipStatusChanged(delta)
        Hub->>AdminClients: MembershipStatusChanged(notification)
        AdminClients->>AdminUI: Update badge/count UI
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Description check ⚠️ Warning No pull request description was provided by the author, making it impossible to assess relevance to the changeset. Add a detailed pull request description explaining the changes, the rationale behind them, and any relevant implementation details or testing performed.
Docstring Coverage ⚠️ Warning Docstring coverage is 12.90% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Title check ❓ Inconclusive The title 'Feature/group invite display' is vague and generic, using non-descriptive formatting that doesn't clearly communicate the main changes in the extensive changeset. Consider a more descriptive title that specifically captures the primary change, such as 'Add group membership notification system with SignalR integration' or 'Implement pending group requests display and real-time updates'.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/group-invite-display

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: 6

🧹 Nitpick comments (4)
src/web/Jordnaer/Pages/Groups/GroupDetails.razor (1)

280-286: Consider adding real-time updates for pending count.

The pending count is fetched once during initialization. Unlike TopBar.razor which subscribes to GroupMembershipSignalRClient.OnMembershipStatusChanged, this component won't reflect real-time changes if an admin approves/denies requests in another tab or if new requests arrive.

This is acceptable for initial implementation, but you may want to consider subscribing to the SignalR updates for a consistent real-time experience across all admin-facing components.

src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs (1)

1-7: LGTM!

The DTO is appropriately designed for SignalR notifications. Using PendingCountChange as a delta allows efficient updates without requiring clients to re-fetch counts.

Consider using a record for immutability and concise syntax:

🔎 Optional refactor to record
 namespace Jordnaer.Shared;

-public class GroupMembershipStatusChanged
-{
-	public required Guid GroupId { get; init; }
-	public int PendingCountChange { get; init; }
-}
+public record GroupMembershipStatusChanged(Guid GroupId, int PendingCountChange = 0);
src/web/Jordnaer/Features/GroupPosts/GroupPostService.cs (1)

64-89: Consider logging when event publishing is skipped.

If group or author is null, the event is silently skipped. This could mask data integrity issues where a post references a non-existent group or user profile.

🔎 Proposed improvement
 		if (group is not null && author is not null)
 		{
 			await publishEndpoint.Publish(new GroupPostCreated
 			{
 				PostId = post.Id,
 				GroupId = group.Id,
 				GroupName = group.Name,
 				AuthorId = author.Id,
 				AuthorDisplayName = author.DisplayName,
 				PostText = post.Text,
 				CreatedUtc = post.CreatedUtc
 			}, cancellationToken);
 		}
+		else
+		{
+			// Log warning - this indicates a data integrity issue
+			// Consider injecting ILogger if not already available
+		}
src/web/Jordnaer/Features/Dashboard/GroupsHub.razor (1)

165-199: LGTM with consideration for code duplication.

The implementation correctly handles the SignalR lifecycle. However, this initialization pattern is duplicated in TopBar.razor. Consider extracting a shared service or base component if this pattern spreads further.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ef5e5ff and 4e20b42.

📒 Files selected for processing (17)
  • src/shared/Jordnaer.Shared/Groups/Events/GroupPostCreated.cs
  • src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • src/web/Jordnaer/Features/Dashboard/GroupsHub.razor
  • src/web/Jordnaer/Features/Email/EmailService.cs
  • src/web/Jordnaer/Features/GroupPosts/GroupPostService.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Features/Groups/GroupSummaryCard.razor
  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Posts/PostCardComponent.razor
  • src/web/Jordnaer/Features/Sharing/SocialShareButtons.razor
  • src/web/Jordnaer/Pages/Groups/GroupDetails.razor
  • src/web/Jordnaer/Pages/Groups/MyGroups.razor
  • src/web/Jordnaer/Pages/Shared/TopBar.razor
  • src/web/Jordnaer/Program.cs
🧰 Additional context used
📓 Path-based instructions (4)
**/{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/GroupPosts/GroupPostService.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Email/EmailService.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.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/GroupPosts/GroupPostService.cs
  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Email/EmailService.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.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/GroupPostCreatedConsumer.cs
**/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/Groups/WebApplicationBuilderExtensions.cs
🧠 Learnings (7)
📚 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/GroupDetails.razor
  • src/web/Jordnaer/Pages/Shared/TopBar.razor
  • src/web/Jordnaer/Pages/Groups/MyGroups.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 **/SignalR/**/*.cs : SignalR hubs must define an interface (e.g., `IChatHub`) for type-safe client method invocations

Applied to files:

  • src/web/Jordnaer/Program.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Pages/Shared/TopBar.razor
  • src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Dashboard/GroupsHub.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/Program.cs
  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Pages/Shared/TopBar.razor
  • src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Dashboard/GroupsHub.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 **/Consumers/**/*.cs : MassTransit consumers must be in the Jordnaer.Consumers namespace for auto-discovery

Applied to files:

  • src/web/Jordnaer/Features/GroupPosts/GroupPostService.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.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/Features/GroupPosts/GroupPostService.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.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 **/WebApplicationBuilderExtensions.cs : DI setup for features must be in `WebApplicationBuilderExtensions.cs` extension methods called from `Program.cs`

Applied to files:

  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.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/Authentication/**/*.cs : Use `UserCircuitHandler` for managing SignalR circuit state in authentication flows

Applied to files:

  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Dashboard/GroupsHub.razor
🧬 Code graph analysis (4)
src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs (1)
src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs (1)
  • GroupMembershipSignalRClient (9-35)
src/web/Jordnaer/Features/Groups/GroupService.cs (3)
src/web/Jordnaer/Features/Authentication/CurrentUser.cs (1)
  • CurrentUser (7-25)
src/web/Jordnaer/Extensions/LoggerExtensions.cs (1)
  • LogFunctionBegan (8-8)
src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs (1)
  • GroupMembershipStatusChanged (3-7)
src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs (3)
src/web/Jordnaer/Features/Authentication/CurrentUser.cs (1)
  • CurrentUser (7-25)
src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs (1)
  • GroupMembershipStatusChanged (3-7)
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs (4)
  • Task (9-9)
  • Task (15-20)
  • Task (22-36)
  • Task (38-46)
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs (3)
src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs (1)
  • GroupMembershipStatusChanged (3-7)
src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs (1)
  • Task (26-34)
src/shared/Jordnaer.Shared/Extensions/ClaimsPrincipalExtensions.cs (1)
  • GetId (37-37)
🔇 Additional comments (14)
src/web/Jordnaer/Features/Posts/PostCardComponent.razor (1)

46-51: Icon colors added for social share menu items.

The addition of explicit icon colors improves visual consistency. Note that the Bluesky color (Color.Tertiary) differs from the Bluesky button in SocialShareButtons.razor (which uses Color.Info). Please verify this is intentional.

src/web/Jordnaer/Features/Email/EmailService.cs (1)

78-85: LGTM!

The email template changes correctly incorporate the group name in both the subject and body. The implementation follows the coding guidelines with proper use of await using for the DbContext and AsNoTracking() for read-only queries.

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

8-8: LGTM!

The GroupMembershipSignalRClient is correctly registered as a scoped service in the extension method, following the established DI pattern. Based on learnings, this aligns with the guideline that DI setup for features must be in WebApplicationBuilderExtensions.cs.

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

159-160: LGTM!

The GroupMembershipHub endpoint registration follows the established pattern used for ChatHub. The path /hubs/group-membership aligns with the client-side GroupMembershipSignalRClient configuration.

src/web/Jordnaer/Pages/Groups/GroupDetails.razor (1)

66-83: LGTM!

The pending requests alert for admins is well-implemented with proper conditional rendering and correct pluralization logic. The UI provides a clear call-to-action to manage pending membership requests.

src/web/Jordnaer/Pages/Groups/MyGroups.razor (2)

45-46: LGTM!

The PendingRequestCount is correctly passed to each GroupSummaryCard with safe dictionary access using GetValueOrDefault.


152-154: LGTM!

The pending counts are fetched after loading groups, which is an efficient approach. The data flow is clear: groups are loaded first, then pending counts are retrieved for all groups the user can manage in a single call.

src/web/Jordnaer/Features/Groups/GroupSummaryCard.razor (1)

59-64: LGTM!

The new PendingRequestCount parameter with a sensible default of 0 and the MembersPageUrl computed property are well-implemented.

src/shared/Jordnaer.Shared/Groups/Events/GroupPostCreated.cs (1)

1-12: No action needed. The CreatedUtc field is explicitly set from the persisted post entity (post.CreatedUtc) when the event is published on line 87 of GroupPostService.cs, so the default value is not relied upon and there are no timing inconsistencies.

src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs (1)

9-34: LGTM!

The client implementation correctly handles null HubConnection and connection state checks. The Remove/On pattern for event subscription prevents duplicate handlers.

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

198-259: LGTM!

The initialization flow correctly:

  1. Fetches initial pending counts
  2. Subscribes to real-time updates before starting the client (preventing race conditions)
  3. Starts the SignalR client
  4. Joins admin groups only when there are groups to join

The pending count clamping to 0 is a good defensive measure.

src/web/Jordnaer/Features/Groups/GroupService.cs (1)

183-236: LGTM - Well-structured pending count methods.

The implementation correctly:

  • Uses AsNoTracking() for read-only queries
  • Combines admin group discovery and pending counts efficiently
  • Returns empty dictionary early when no admin groups exist
src/web/Jordnaer/Features/Dashboard/GroupsHub.razor (1)

213-216: Approve disposal implementation.

The DisposeAsync correctly stops the SignalR client to prevent resource leaks.

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

56-56: Use IOptions<AppOptions> instead of NavigationManager for the base URL.

NavigationManager is a Blazor component service that requires HTTP context initialization. MassTransit consumers run in background context outside the HTTP request pipeline, making NavigationManager.BaseUri unreliable or null. The project already provides AppOptions with a configured BaseUrl property—inject IOptions<AppOptions> and use its Value.BaseUrl instead.

Similar code in DeleteUserService logs an error when navigationManager.BaseUri is empty, indicating this pattern has caused issues before.

Comment thread src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs Outdated
Comment thread src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
Comment thread src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs Outdated
Comment thread src/web/Jordnaer/Features/Groups/GroupService.cs Outdated
Comment thread src/web/Jordnaer/Features/Groups/GroupSummaryCard.razor
Comment thread src/web/Jordnaer/Features/Sharing/SocialShareButtons.razor Outdated
@github-project-automation github-project-automation Bot moved this from Todo to In Progress in Jordnaer Community Website Dec 30, 2025

@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

♻️ Duplicate comments (2)
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs (1)

42-78: Authorization validation correctly implemented.

The JoinAdminGroups method now properly validates that the user is an admin or owner before allowing them to join admin notification groups. The implementation correctly:

  • Validates user authentication (lines 44-49)
  • Queries the database to verify admin/owner status (lines 56-63)
  • Logs warnings for unauthorized attempts (lines 65-70)
  • Only joins validated groups (lines 72-75)

The code follows coding guidelines by using await using var context = await contextFactory.CreateDbContextAsync() and AsNoTracking() for read-only queries.

src/web/Jordnaer/Features/Groups/GroupService.cs (1)

275-320: SignalR notification correctly separated from DB transaction.

The implementation properly addresses the concern about notification failures affecting the response:

  • The database transaction completes first (lines 288-296)
  • SignalR notification occurs after SaveChangesAsync succeeds (lines 298-320)
  • Notification failures are caught separately and logged without changing the success response (lines 313-319)
  • Line 299's comment clearly documents this design decision

The code correctly follows coding guidelines by using IHubContext to broadcast to the group-admins-{groupId} SignalR group.

🧹 Nitpick comments (3)
tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs (1)

63-64: Consider adding test coverage for new pending membership methods.

The mock setup for IHubContext<GroupMembershipHub, IGroupMembershipHub> is correct. However, the new public methods GetPendingMembershipCountAsync and GetPendingMembershipCountsForUserAsync introduced in GroupService lack test coverage.

src/web/Jordnaer/Features/Groups/GroupService.cs (1)

204-236: Consider replacing Debug.Assert with a guard clause.

The method uses Debug.Assert (line 206) to verify currentUser.Id is not null. Since this is a public method, a guard clause with an early return would be more appropriate than a debug-only assertion, which is stripped in release builds.

🔎 Proposed guard clause
 public async Task<Dictionary<Guid, int>> GetPendingMembershipCountsForUserAsync(CancellationToken cancellationToken = default)
 {
-	Debug.Assert(currentUser.Id is not null, "Current user must be set when fetching pending counts.");
-
 	logger.LogFunctionBegan();

+	if (currentUser.Id is null)
+	{
+		return new Dictionary<Guid, int>();
+	}
+
 	await using var context = await contextFactory.CreateDbContextAsync(cancellationToken);

The database queries correctly follow coding guidelines by using await using var context and AsNoTracking() for read-only operations.

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

33-50: Consider optimizing the query pattern for better performance.

The current pattern uses activeMembers.Any(userId => userId == user.Id), which creates a correlated subquery. For large groups, this might not be optimal.

🔎 Proposed optimization using a join
-// Get all active members excluding the post author
-var activeMembers = context.GroupMemberships
-	.AsNoTracking()
-	.Where(x => x.GroupId == message.GroupId &&
-			   x.MembershipStatus == MembershipStatus.Active &&
-			   x.UserProfileId != message.AuthorId)
-	.Select(x => x.UserProfileId);
-
-// Get their email addresses
-var emails = await context.Users
-	.AsNoTracking()
-	.Where(user => activeMembers.Any(userId => userId == user.Id) &&
-				  !string.IsNullOrEmpty(user.Email))
-	.Select(user => new EmailRecipient
-	{
-		Email = user.Email!,
-		DisplayName = user.UserName
-	})
-	.ToListAsync(consumeContext.CancellationToken);
+// Get email addresses for all active members excluding the post author
+var emails = await (from membership in context.GroupMemberships
+				   join user in context.Users on membership.UserProfileId equals user.Id
+				   where membership.GroupId == message.GroupId &&
+						 membership.MembershipStatus == MembershipStatus.Active &&
+						 membership.UserProfileId != message.AuthorId &&
+						 !string.IsNullOrEmpty(user.Email)
+				   select new EmailRecipient
+				   {
+					   Email = user.Email!,
+					   DisplayName = user.UserName
+				   })
+	.AsNoTracking()
+	.ToListAsync(consumeContext.CancellationToken);
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e20b42 and 3df3f51.

📒 Files selected for processing (6)
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs
  • src/web/Jordnaer/Features/GroupPosts/GroupPostService.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Features/Metrics/JordnaerMetrics.cs
  • tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs
🧰 Additional context used
📓 Path-based instructions (3)
**/{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/GroupPosts/GroupPostService.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Metrics/JordnaerMetrics.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.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/GroupPosts/GroupPostService.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Metrics/JordnaerMetrics.cs
  • src/web/Jordnaer/Features/Groups/GroupService.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/GroupPostCreatedConsumer.cs
🧠 Learnings (5)
📚 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/Features/GroupPosts/GroupPostService.cs
  • tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.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/Features/GroupPosts/GroupPostService.cs
  • tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs
  • src/web/Jordnaer/Features/Metrics/JordnaerMetrics.cs
  • src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.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 `IHubContext` for broadcasting messages to SignalR clients from outside the hub

Applied to files:

  • tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Groups/GroupService.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:

  • tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Groups/GroupService.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/Authentication/**/*.cs : Use `UserCircuitHandler` for managing SignalR circuit state in authentication flows

Applied to files:

  • tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
🧬 Code graph analysis (4)
src/web/Jordnaer/Features/GroupPosts/GroupPostService.cs (1)
src/shared/Jordnaer.Shared/Groups/Events/GroupPostCreated.cs (1)
  • GroupPostCreated (3-12)
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs (2)
src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs (1)
  • GroupMembershipStatusChanged (3-7)
src/shared/Jordnaer.Shared/Extensions/ClaimsPrincipalExtensions.cs (1)
  • GetId (37-37)
src/web/Jordnaer/Features/Groups/GroupService.cs (2)
src/web/Jordnaer/Extensions/LoggerExtensions.cs (1)
  • LogFunctionBegan (8-8)
src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs (1)
  • GroupMembershipStatusChanged (3-7)
src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs (5)
src/shared/Jordnaer.Shared/Groups/Events/GroupPostCreated.cs (1)
  • GroupPostCreated (3-12)
src/web/Jordnaer/Features/Metrics/JordnaerMetrics.cs (1)
  • JordnaerMetrics (6-57)
src/web/Jordnaer/Features/Email/EmailRecipient.cs (1)
  • EmailRecipient (6-78)
src/web/Jordnaer/Features/Email/SendEmail.cs (1)
  • SendEmail (5-33)
src/web/Jordnaer/Features/Email/EmailConstants.cs (1)
  • EmailConstants (3-15)
🔇 Additional comments (6)
src/web/Jordnaer/Features/Groups/GroupService.cs (2)

21-22: IHubContext dependency correctly added.

The constructor now includes IHubContext<GroupMembershipHub, IGroupMembershipHub> for broadcasting SignalR messages from the service, following the coding guidelines for using IHubContext outside hub classes.


183-198: LGTM!

The method correctly uses await using var context = await contextFactory.CreateDbContextAsync() and AsNoTracking() for the read-only query, following the coding guidelines.

src/web/Jordnaer/Features/Metrics/JordnaerMetrics.cs (1)

51-56: LGTM!

The new consumer metrics follow the established naming convention and lifecycle pattern (received/succeeded/failed), which aligns well with the GroupPostCreatedConsumer implementation.

src/web/Jordnaer/Features/GroupPosts/GroupPostService.cs (1)

65-99: LGTM!

The event publishing logic is well-implemented:

  • Correctly queries for group and author data after persisting the post
  • Uses AsNoTracking() for read-only queries per guidelines
  • Includes defensive logging when data is unexpectedly missing
  • Properly publishes the GroupPostCreated event for downstream consumers
src/web/Jordnaer/Consumers/GroupPostCreatedConsumer.cs (2)

19-84: Well-implemented consumer with proper metrics and error handling.

The consumer correctly:

  • Tracks metrics at all lifecycle stages (received/succeeded/failed) per guidelines
  • Uses AsNoTracking() for read-only queries
  • Handles exceptions without rethrowing, appropriate for async notifications
  • HTML-encodes all user inputs (lines 97-104) to prevent XSS

13-17: NavigationManager should not be used in MassTransit consumers.

NavigationManager is Blazor-scoped and depends on an active HTTP request context. MassTransit consumers run as background services without this context, making NavigationManager.BaseUri at line 62 unreliable or unavailable at runtime.

Instead, inject IOptions<ApplicationSettings> (or similar configuration) with a BaseUrl property to construct URLs in background services.

@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/Features/Sharing/SocialShareButtons.razor (1)

7-7: Brand colors applied, consistency achieved.

The switch from Color constants to inline hex values ensures brand-accurate colors for both social buttons, which addresses the previous consistency concern. Using exact brand colors (#1877F2 for Facebook, #1185FE for Bluesky) is appropriate for external platform integrations.

If future theming flexibility is desired, consider defining these brand colors as CSS custom properties or theme palette extensions rather than inline styles, though the current approach is perfectly valid for fixed brand requirements.

Also applies to: 12-12

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3df3f51 and 399f502.

📒 Files selected for processing (2)
  • src/web/Jordnaer/Features/Posts/PostCardComponent.razor
  • src/web/Jordnaer/Features/Sharing/SocialShareButtons.razor
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/web/Jordnaer/Features/Posts/PostCardComponent.razor

@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: 3

🧹 Nitpick comments (3)
src/web/Jordnaer/Pages/Groups/GroupDetails.razor (2)

317-328: Consider updating the member list on successful leave.

After leaving the group, the _groupMembers list still contains the current user until page refresh. Consider removing the user from _groupMembers for immediate visual feedback.

🔎 Proposed enhancement
 leaveResponse.Switch(
 _ =>
 {
     Snackbar.Add("Du har forladt gruppen", Severity.Success);
     _isMember = false;
+    _groupMembers = _groupMembers?.Where(m => m.Id != _currentUser?.Id).ToList();
     StateHasChanged();
 },
 error => Snackbar.Add(error.Value, Severity.Warning));

269-276: Duplicate query can be consolidated.

Both authenticated and unauthenticated paths call GetGroupMembersWithRolesByPredicateAsync with identical predicates. Consider moving the call after the auth check to avoid repetition.

🔎 Proposed refactor
-        if (_currentUser is null)
-        {
-            _groupMembers = await GroupService.GetGroupMembersWithRolesByPredicateAsync(x => x.GroupId == _group.Id &&
-                                                                                                    x.MembershipStatus == MembershipStatus.Active);
-            _isLoading = false;
-            return;
-        }
-
-        _groupMembers = await GroupService.GetGroupMembersWithRolesByPredicateAsync(x => x.GroupId == _group.Id &&
-                                                                                                x.MembershipStatus == MembershipStatus.Active);
+        _groupMembers = await GroupService.GetGroupMembersWithRolesByPredicateAsync(x => x.GroupId == _group.Id &&
+                                                                                                x.MembershipStatus == MembershipStatus.Active);
+
+        if (_currentUser is null)
+        {
+            _isLoading = false;
+            return;
+        }
src/web/Jordnaer/Features/Dashboard/GroupsHub.razor (1)

173-191: Unused variable adminGroupIds.

The adminGroupIds variable is extracted from pendingCounts.Keys but never used. This appears to be leftover from development.

🔎 Proposed fix
         // Get pending counts and admin group IDs
         var pendingCounts = await GroupService.GetPendingMembershipCountsForUserAsync();
         _totalPendingRequests = pendingCounts.Values.Sum();
-        var adminGroupIds = pendingCounts.Keys.ToList();
 
         // Subscribe to group membership status changes
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 399f502 and ace35f0.

📒 Files selected for processing (13)
  • src/shared/Jordnaer.Shared/Database/Enums/MembershipStatus.cs
  • src/shared/Jordnaer.Shared/Groups/GroupMemberSlim.cs
  • src/web/Jordnaer/Components/CookieBanner.razor
  • src/web/Jordnaer/Features/Dashboard/GroupsHub.razor
  • src/web/Jordnaer/Features/Groups/GroupMemberListComponent.razor
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipNotificationService.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Membership/MembershipService.cs
  • src/web/Jordnaer/Pages/Groups/GroupDetails.razor
  • src/web/Jordnaer/Pages/Shared/TopBar.razor
🧰 Additional context used
📓 Path-based instructions (3)
**/{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/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipNotificationService.cs
  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Features/Membership/MembershipService.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/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipNotificationService.cs
  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Features/Membership/MembershipService.cs
**/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/Groups/WebApplicationBuilderExtensions.cs
🧠 Learnings (6)
📚 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/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Dashboard/GroupsHub.razor
  • src/web/Jordnaer/Features/Groups/GroupMembershipNotificationService.cs
  • src/web/Jordnaer/Pages/Shared/TopBar.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/Authentication/**/*.cs : Use `UserCircuitHandler` for managing SignalR circuit state in authentication flows

Applied to files:

  • src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Groups/GroupService.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 `IHubContext` for broadcasting messages to SignalR clients from outside the hub

Applied to files:

  • src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs
  • src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs
  • src/web/Jordnaer/Features/Dashboard/GroupsHub.razor
  • src/web/Jordnaer/Features/Groups/GroupMembershipNotificationService.cs
  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs
  • src/web/Jordnaer/Pages/Shared/TopBar.razor
📚 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/GroupDetails.razor
  • src/web/Jordnaer/Pages/Shared/TopBar.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 **/WebApplicationBuilderExtensions.cs : DI setup for features must be in `WebApplicationBuilderExtensions.cs` extension methods called from `Program.cs`

Applied to files:

  • src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.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/Features/Membership/MembershipService.cs
🧬 Code graph analysis (6)
src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs (3)
src/web/Jordnaer/Features/Authentication/CurrentUser.cs (1)
  • CurrentUser (7-25)
src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs (1)
  • GroupMembershipStatusChanged (3-7)
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs (3)
  • Task (9-9)
  • Task (15-20)
  • Task (22-36)
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs (2)
src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs (1)
  • GroupMembershipStatusChanged (3-7)
src/shared/Jordnaer.Shared/Extensions/ClaimsPrincipalExtensions.cs (1)
  • GetId (37-37)
src/web/Jordnaer/Features/Groups/GroupMembershipNotificationService.cs (2)
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs (3)
  • Task (9-9)
  • Task (15-20)
  • Task (22-36)
src/shared/Jordnaer.Shared/Groups/GroupMembershipStatusChanged.cs (1)
  • GroupMembershipStatusChanged (3-7)
src/web/Jordnaer/Features/Groups/WebApplicationBuilderExtensions.cs (2)
src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs (1)
  • GroupMembershipSignalRClient (9-25)
src/web/Jordnaer/Features/Groups/GroupMembershipNotificationService.cs (1)
  • GroupMembershipNotificationService (22-74)
src/web/Jordnaer/Features/Groups/GroupService.cs (5)
src/web/Jordnaer/Features/Authentication/CurrentUser.cs (1)
  • CurrentUser (7-25)
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs (3)
  • Task (9-9)
  • Task (15-20)
  • Task (22-36)
src/shared/Jordnaer.Shared/Groups/GroupMemberSlim.cs (1)
  • GroupMemberSlim (3-18)
src/shared/Jordnaer.Shared/Database/GroupMembership.cs (1)
  • GroupMembership (3-23)
src/web/Jordnaer/Extensions/LoggerExtensions.cs (1)
  • LogFunctionBegan (8-8)
src/web/Jordnaer/Features/Membership/MembershipService.cs (2)
src/web/Jordnaer/Extensions/LoggerExtensions.cs (3)
  • Error (12-17)
  • Error (19-24)
  • LogFunctionBegan (8-8)
src/web/Jordnaer/Features/Authentication/CurrentUser.cs (1)
  • CurrentUser (7-25)
🔇 Additional comments (19)
src/web/Jordnaer/Features/Groups/GroupMembershipHub.cs (1)

7-37: LGTM! Clean SignalR hub implementation.

The hub correctly defines a type-safe interface per best practices, applies authorization, and includes appropriate connection lifecycle logging. The design as a server-to-client notification hub (without client-invoked methods) is appropriate for this use case.

Based on learnings, SignalR hubs should define interfaces for type-safe client invocations, which is properly implemented here.

src/web/Jordnaer/Features/Groups/GroupMembershipNotificationService.cs (1)

8-74: LGTM! Well-structured notification service.

The implementation correctly follows all coding guidelines:

  • Uses await using var context for DbContext lifecycle
  • Applies AsNoTracking() for the read-only admin query
  • Broadcasts via IHubContext as specified in the guidelines
  • Appropriately swallows notification exceptions to prevent impacting core operations

The query logic correctly targets active admins and owners for notifications.

As per coding guidelines, this correctly uses IHubContext for broadcasting from outside the hub.

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

8-9: LGTM! Proper DI registration.

The service registrations are correctly placed in WebApplicationBuilderExtensions per coding guidelines, and scoped lifetime is appropriate for these services.

As per coding guidelines, DI setup is properly located in WebApplicationBuilderExtensions.cs.

src/web/Jordnaer/Features/Groups/GroupMemberListComponent.razor (1)

30-40: LGTM! Clear role visualization.

The updated parameter type aligns with the new GroupMemberSlim DTO, and the badge rendering logic appropriately prioritizes Owner designation over Admin permission. The visual distinction (filled primary chip for Owner, outlined info chip for Admin) provides good UX clarity.

src/shared/Jordnaer.Shared/Groups/GroupMemberSlim.cs (1)

1-18: LGTM! Well-designed immutable DTO.

The DTO follows best practices with required init-only properties for immutability. The nullable UserName property accommodates users who may not have usernames set, while the ToString() override provides a sensible default representation.

src/web/Jordnaer/Pages/Groups/GroupDetails.razor (2)

54-60: LGTM! Leave group button correctly conditioned.

The button visibility logic properly excludes owners and only shows for active members. The _currentUserIsOwner check aligns with the backend validation in LeaveMembership that prevents owners from leaving.


74-91: LGTM! Pending requests alert for admins.

Clean implementation with proper pluralization for Danish ("er" suffix for plural). The conditional rendering based on _currentUserIsAdmin and count > 0 is appropriate.

src/web/Jordnaer/Features/Groups/GroupMembershipSignalRClient.cs (1)

9-24: LGTM! SignalR client follows established patterns.

The implementation correctly:

  • Inherits from AuthenticatedSignalRClientBase
  • Uses Remove before On to prevent duplicate handler subscriptions
  • Handles null HubConnection defensively
  • References IGroupMembershipHub.MembershipStatusChanged for type-safe method naming

Based on learnings, this aligns with the guideline to use IHubContext for broadcasting from outside the hub, and the client properly connects to the /hubs/group-membership endpoint.

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

24-41: LGTM! Groups button with pending count badge.

Good use of AuthorizeView to differentiate between authenticated and non-authenticated states. The badge displays only when count > 0 with appropriate warning color.


239-251: LGTM! Real-time pending count updates with proper bounds checking.

The subscription follows the same pattern as the chat message handling. The bounds check preventing negative counts is a good defensive measure.


301-306: LGTM! Proper cleanup in DisposeAsync.

Both SignalR clients are stopped during disposal, preventing resource leaks.

src/web/Jordnaer/Features/Dashboard/GroupsHub.razor (2)

19-36: LGTM! Pending requests alert consistent with GroupDetails.

The alert styling and pluralization logic matches the pattern used in GroupDetails.razor, providing a consistent UX across pages.


207-210: LGTM! Proper SignalR client disposal.

The DisposeAsync correctly stops the SignalR client to prevent resource leaks.

src/web/Jordnaer/Features/Membership/MembershipService.cs (2)

52-65: Good enhancement: Re-application support for Left/Rejected users.

This allows users who previously left or were rejected to request membership again, with proper status reset and admin notification.


98-139: LGTM! LeaveMembership implementation is well-structured.

The method correctly:

  • Uses await using var context per coding guidelines
  • Validates membership existence and active status
  • Prevents owners from leaving (must transfer ownership first)
  • Uses soft delete pattern with MembershipStatus.Left
  • Returns appropriate error messages in Danish
src/web/Jordnaer/Features/Groups/GroupService.cs (4)

72-75: LGTM! Added null guard for unauthenticated users.

Early return with empty list for null currentUser.Id prevents potential NullReferenceException and avoids unnecessary database queries.


138-160: LGTM! New method follows established patterns.

GetGroupMembersWithRolesByPredicateAsync correctly:

  • Uses AsNoTracking() for read-only queries per coding guidelines
  • Uses await using var context per coding guidelines
  • Follows the same structure as GetGroupMembersByPredicateAsync
  • Projects to the new GroupMemberSlim type including role information

211-267: LGTM! Pending count methods are well-implemented.

Both methods:

  • Use AsNoTracking() for read-only queries
  • Handle null currentUser.Id appropriately
  • GetPendingMembershipCountsForUserAsync efficiently queries only groups where user is admin/owner before fetching pending counts

330-336: Good: Notification decoupled from DB transaction.

The SignalR notification is correctly placed outside the try-catch block that handles the database operation, addressing the previous review feedback. This ensures notification failures don't affect the success response when the DB update succeeds.

Comment thread src/shared/Jordnaer.Shared/Database/Enums/MembershipStatus.cs
Comment thread src/web/Jordnaer/Components/CookieBanner.razor Outdated
Comment thread src/web/Jordnaer/Features/Membership/MembershipService.cs 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/Features/Groups/GroupService.cs (1)

233-268: Consider consolidating into a single query for better performance.

The current implementation uses two separate queries: one to fetch admin groups (lines 245-251) and another to count pending memberships (lines 259-265). This could potentially be optimized into a single query, especially if a user is an admin of many groups.

💡 Potential optimization with a single query
-	// Get all groups where current user is admin or owner
-	var adminGroupIds = await context.GroupMemberships
-		.AsNoTracking()
-		.Where(x => x.UserProfileId == currentUser.Id &&
-				   (x.PermissionLevel == PermissionLevel.Admin ||
-					x.OwnershipLevel == OwnershipLevel.Owner))
-		.Select(x => x.GroupId)
-		.ToListAsync(cancellationToken);
-
-	if (adminGroupIds.Count == 0)
-	{
-		return new Dictionary<Guid, int>();
-	}
-
-	// Get pending counts for those groups in a single query
-	var pendingCounts = await context.GroupMemberships
+	// Get pending counts for groups where current user is admin or owner in a single query
+	var adminGroupIds = await context.GroupMemberships
 		.AsNoTracking()
-		.Where(x => adminGroupIds.Contains(x.GroupId) &&
-				   x.MembershipStatus == MembershipStatus.PendingApprovalFromGroup)
+		.Where(x => x.UserProfileId == currentUser.Id &&
+				   (x.PermissionLevel == PermissionLevel.Admin ||
+					x.OwnershipLevel == OwnershipLevel.Owner))
+		.Select(x => x.GroupId)
+		.ToListAsync(cancellationToken);
+
+	if (adminGroupIds.Count == 0)
+	{
+		return new Dictionary<Guid, int>();
+	}
+
+	var pendingCounts = await context.GroupMemberships
+		.AsNoTracking()
+		.Where(x => adminGroupIds.Contains(x.GroupId) &&
+				   x.MembershipStatus == MembershipStatus.PendingApprovalFromGroup)
 		.GroupBy(x => x.GroupId)
 		.Select(g => new { GroupId = g.Key, Count = g.Count() })
 		.ToDictionaryAsync(x => x.GroupId, x => x.Count, cancellationToken);
+
+	return pendingCounts;

Note: The optimization benefit would be marginal for most cases. Consider this only if performance profiling indicates this is a bottleneck.

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 50f27c1 and 3924be2.

📒 Files selected for processing (3)
  • src/web/Jordnaer/Components/CookieBanner.razor
  • src/web/Jordnaer/Features/Groups/GroupService.cs
  • src/web/Jordnaer/Features/Membership/MembershipService.cs
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/web/Jordnaer/Components/CookieBanner.razor
🧰 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/Membership/MembershipService.cs
  • src/web/Jordnaer/Features/Groups/GroupService.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/Membership/MembershipService.cs
  • src/web/Jordnaer/Features/Groups/GroupService.cs
🧠 Learnings (3)
📓 Common learnings
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
📚 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/Features/Membership/MembershipService.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/Authentication/**/*.cs : Use `UserCircuitHandler` for managing SignalR circuit state in authentication flows

Applied to files:

  • src/web/Jordnaer/Features/Groups/GroupService.cs
🧬 Code graph analysis (1)
src/web/Jordnaer/Features/Membership/MembershipService.cs (2)
src/web/Jordnaer/Extensions/LoggerExtensions.cs (4)
  • Error (12-17)
  • Error (19-24)
  • LogFunctionBegan (8-8)
  • LogException (10-10)
src/web/Jordnaer/Features/Authentication/CurrentUser.cs (1)
  • CurrentUser (7-25)
🔇 Additional comments (11)
src/web/Jordnaer/Features/Groups/GroupService.cs (6)

20-21: LGTM! Dependency injection for notification service added.

The constructor now includes IGroupMembershipNotificationService which is used for notifying admins of pending membership changes. This is a clean dependency injection pattern.


72-75: Good defensive guard against null user ID.

Early return with an empty list when currentUser.Id is null prevents potential null reference issues downstream and avoids unnecessary database queries.


81-82: LGTM! Correctly excludes Left memberships.

The enhanced filtering now excludes both Rejected and Left membership statuses, which makes sense for displaying the user's active group list.


139-161: LGTM! New method follows coding guidelines.

The implementation correctly uses AsNoTracking() for the read-only query and await using var context for the scoped DbContext instance, as per coding guidelines.


215-227: LGTM! Pending count query is straightforward and efficient.

The method correctly counts pending memberships for a specific group using AsNoTracking() and follows the coding guidelines.


331-337: No action required—notification service handles exceptions internally.

The NotifyAdminsOfPendingCountChangeAsync method in GroupMembershipNotificationService already wraps its logic in a try-catch block (lines 58-64) that logs errors but does not rethrow exceptions. This prevents notification failures from propagating to the caller, so the method returns a successful response even if the notification fails. No additional try-catch is needed in GroupService.

Likely an incorrect or invalid review comment.

src/web/Jordnaer/Features/Membership/MembershipService.cs (5)

27-28: LGTM! Notification service dependency added.

The constructor now includes IGroupMembershipNotificationService for broadcasting membership changes to admins. Clean dependency injection.


19-21: LGTM! New interface method for leaving groups.

The interface correctly declares the new LeaveMembership method with appropriate return types using OneOf for explicit error handling.


52-81: Excellent resilient notification handling!

The re-application logic correctly wraps both email and SignalR notifications in individual try-catch blocks, ensuring that notification failures don't cause the membership request to fail. This addresses the previous review concern about notification failures masking DB success.


100-118: LGTM! Consistent notification error handling.

The notification handling for new memberships follows the same fire-and-forget pattern with try-catch blocks, ensuring reliability and consistent user experience.


129-170: LeaveMembership implementation looks solid.

The method correctly:

  • Uses await using var context as per coding guidelines ✓
  • Validates membership existence and active status
  • Prevents owners from leaving (requiring ownership transfer first)
  • Implements soft delete by setting status to Left

One question: Should pending members be able to cancel their membership requests? Currently, only Active members can leave (line 148). If a user has a pending request (MembershipStatus.PendingApprovalFromGroup), they cannot cancel it using this method.

Is it intentional that users with pending membership requests cannot cancel them via LeaveMembership? If cancellation should be supported, you might want to either:

  1. Allow pending users to call this method (and notify admins of the pending count change), or
  2. Create a separate CancelMembershipRequest method

@NielsPilgaard NielsPilgaard merged commit 16e3aa1 into main Jan 2, 2026
4 of 5 checks passed
@github-project-automation github-project-automation Bot moved this from In Progress to Done in Jordnaer Community Website Jan 2, 2026
@NielsPilgaard NielsPilgaard deleted the feature/group-invite-display branch January 2, 2026 09:20

@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)
tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs (1)

62-63: Constructor update is correct; consider test coverage for new functionality.

The addition of IGroupMembershipNotificationService substitute correctly aligns with the updated GroupService constructor. The unconfigured mock is acceptable for these existing tests.

However, the AI summary indicates that GroupService now has new methods (GetPendingMembershipCountAsync, GetPendingMembershipCountsForUserAsync, GetGroupMembersWithRolesByPredicateAsync) and new notification behavior for pending membership changes, but there's no test coverage for these features in this file.

💡 Optional: Extract mock to field for better testability

Consider extracting the notification service mock to a private field to allow individual tests to configure or verify its behavior:

 public class GroupServiceTests : IAsyncLifetime
 {
 	private readonly GroupService _groupService;
 	private readonly IDbContextFactory<JordnaerDbContext> _contextFactory = Substitute.For<IDbContextFactory<JordnaerDbContext>>();
 	private readonly JordnaerDbContext _context;
+	private readonly IGroupMembershipNotificationService _notificationService = Substitute.For<IGroupMembershipNotificationService>();

 	private readonly string _userProfileId;
 	
 	// ... constructor ...
 	
 	_groupService = new GroupService(_contextFactory,
 		Substitute.For<ILogger<GroupService>>(),
 		Substitute.For<IDiagnosticContext>(),
 		new CurrentUser
 		{
 			User = new ClaimsPrincipal(
 				new ClaimsIdentity(
 					[new Claim(ClaimTypes.NameIdentifier, _userProfileId)]
 					))
 		},
-		Substitute.For<IGroupMembershipNotificationService>());
+		_notificationService);

This would enable future tests to verify notification calls.

Would you like me to generate test cases for the new GroupService methods and notification behavior, or open an issue to track this task?

📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3924be2 and 63373ec.

📒 Files selected for processing (1)
  • tests/web/Jordnaer.Tests/Groups/GroupServiceTests.cs

This was referenced Jan 2, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Feb 4, 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