Skip to content

feat: real-time notifications with GraphQL subscriptions#155

Open
0xDeon wants to merge 3 commits intoboundlessfi:mainfrom
0xDeon:feat/realtime-notifications-131
Open

feat: real-time notifications with GraphQL subscriptions#155
0xDeon wants to merge 3 commits intoboundlessfi:mainfrom
0xDeon:feat/realtime-notifications-131

Conversation

@0xDeon
Copy link
Contributor

@0xDeon 0xDeon commented Mar 26, 2026

Summary

Implements real-time in-app notifications using GraphQL subscriptions (graphql-ws), allowing users to receive instant updates when bounties are updated or new applications are submitted.

Changes

Modified

  • lib/graphql/subscriptions.ts - Added ON_BOUNTY_UPDATED_SUBSCRIPTION and ON_NEW_APPLICATION_SUBSCRIPTION subscription documents with typed response interfaces
  • hooks/use-graphql-subscription.ts - Added enabled parameter to conditionally activate subscriptions (guards against unauthenticated connections)
  • components/global-navbar.tsx - Integrated NotificationCenter component between rank badge and wallet actions

Created

  • hooks/use-notifications.ts - Client-side notification state management hook that subscribes to bounty updates and new applications, with deduplication, read/unread tracking, and auto-capping at 25 notifications
  • components/notifications/notification-center.tsx - Bell icon popover UI with unread badge, notification list with relative timestamps, mark-as-read functionality, loading skeleton, and empty state

Implementation Details

  • Subscriptions are only activated when a user session exists (prevents unauthenticated WebSocket connections)
  • Notifications are deduplicated by type:id key to prevent duplicates from reconnections
  • Incoming subscription events invalidate relevant React Query caches for immediate data freshness
  • UI uses existing Radix UI Popover + shadcn/ui patterns for consistency
  • No memory leaks: subscriptions properly clean up on unmount via useGraphQLSubscription lifecycle

Acceptance Criteria

  • Users receive real-time notifications for relevant events
  • Subscriptions are correctly initialized and cleaned up
  • Notification bell displays unread count
  • Notification center displays incoming notifications
  • No memory leaks or duplicate subscriptions

Closes #131

Summary by CodeRabbit

  • New Features
    • Notification center added to the navbar with unread badges, real-time bounty and submission alerts, relative timestamps, scrollable history, and individual/bulk "mark as read" actions.
  • Tests
    • Updated test fixtures to reflect submission handling changes.

@drips-wave
Copy link

drips-wave bot commented Mar 26, 2026

@0xDeon Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits.

You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀

Learn more about application limits

@coderabbitai
Copy link

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Adds a client-side real-time notification system: new useNotifications hook and NotificationCenter component, two GraphQL subscription documents/types, an enabled gate on useGraphQLSubscription, and integrates the notification bell into the global navbar.

Changes

Cohort / File(s) Summary
Notification UI
components/global-navbar.tsx, components/notifications/notification-center.tsx
Integrated NotificationCenter into the navbar. New client component renders bell button with unread badge, popover list, loading/empty states, per-item mark-as-read, and "Mark all read" action.
Notification State Management
hooks/use-notifications.ts
New hook managing NotificationItem[], unread count, normalization/dedup/upsert, cap to 25 items, subscribes to bounty/application events, exposes markAsRead and markAllAsRead, and isLoading.
GraphQL Subscription Infra
hooks/use-graphql-subscription.ts, lib/graphql/subscriptions.ts
useGraphQLSubscription gains optional enabled param to gate subscription setup. Added ON_BOUNTY_UPDATED_SUBSCRIPTION and ON_NEW_APPLICATION_SUBSCRIPTION documents and corresponding TypeScript payload types; minor formatting tweaks.
Tests
lib/store.test.ts, hooks/__tests__/use-bounty-subscription.test.ts
Test fixture trimmed two fields from a Submission in lib/store.test.ts. Test file reformatting and subscription-string assertion updated in use-bounty-subscription test.

Sequence Diagram(s)

sequenceDiagram
    participant User as User/Client
    participant NavBar as Global Navbar
    participant NotifCenter as NotificationCenter
    participant Hook as useNotifications
    participant WSClient as GraphQL WS Client
    participant Server as GraphQL Server

    User->>NavBar: load page / mount
    NavBar->>NotifCenter: render
    NotifCenter->>Hook: call useNotifications()
    Hook->>WSClient: subscribe ON_BOUNTY_UPDATED (enabled=true)
    Hook->>WSClient: subscribe ON_NEW_APPLICATION
    WSClient->>Server: open subscriptions
    Server-->>WSClient: send event (bounty/app)
    WSClient->>Hook: deliver payload
    Hook->>Hook: normalize & upsert NotificationItem
    Hook-->>NotifCenter: provide notifications + unreadCount
    NotifCenter->>User: show badge / popover
    User->>NotifCenter: click notification
    NotifCenter->>Hook: markAsRead(id, type)
    Hook->>Hook: set read=true, update unreadCount
    NotifCenter->>User: update UI
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • 0xdevcollins
  • Benjtalkshow

Poem

🐰
I hop on the branch to give a cheer,
Bells ring softly when new things appear,
Subscriptions hum, the updates draw near,
Badges glow bright — notifications clear,
Hooray — real-time brings everyone near!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 14.29% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The pull request title clearly and specifically describes the main change: implementing real-time notifications using GraphQL subscriptions, which is the primary objective of the changeset.
Linked Issues check ✅ Passed All core coding requirements from issue #131 are met: GraphQL subscriptions defined [lib/graphql/subscriptions.ts], notifications hook created [hooks/use-notifications.ts], notification UI component built [components/notifications/notification-center.tsx], global navbar integrated with notification bell and unread badge, and proper subscription lifecycle management with cleanup implemented.
Out of Scope Changes check ✅ Passed All changes directly support the notifications feature. Minor updates to hooks/use-graphql-subscription.ts (enabled parameter) and test files are necessary supporting changes for the subscription system, not out-of-scope modifications.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

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.

@vercel
Copy link

vercel bot commented Mar 26, 2026

@0xDeon is attempting to deploy a commit to the Threadflow Team on Vercel.

A member of the Team first needs to authorize it.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
lib/graphql/subscriptions.ts (1)

85-94: Avoid duplicate payload interfaces for bounty-updated events.

OnBountyUpdatedData duplicates BountyUpdatedData exactly. Consolidating to a type alias avoids future drift.

♻️ Suggested simplification
-export interface OnBountyUpdatedData {
-  bountyUpdated: {
-    id: string;
-    title: string;
-    status: string;
-    rewardAmount: number;
-    rewardCurrency: string;
-    updatedAt?: string | null;
-  };
-}
+export type OnBountyUpdatedData = BountyUpdatedData;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/graphql/subscriptions.ts` around lines 85 - 94, The interface
OnBountyUpdatedData duplicates BountyUpdatedData; to avoid drift, replace the
duplicate interface declaration (OnBountyUpdatedData) with a type alias to
BountyUpdatedData (e.g., type OnBountyUpdatedData = BountyUpdatedData) or remove
OnBountyUpdatedData and update any references to use BountyUpdatedData directly,
ensuring all places that referenced OnBountyUpdatedData now point to the single
canonical BountyUpdatedData type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@hooks/use-notifications.ts`:
- Around line 27-29: The normaliseTimestamp function can throw for invalid date
strings; update normaliseTimestamp to defensively validate/parses the incoming
value (e.g., using Date.parse or constructing a Date and checking
isNaN(date.getTime())) and fall back to new Date().toISOString() for
null/undefined or invalid inputs. Keep the function signature and ensure it
returns a valid ISO string in all cases (so callers of normaliseTimestamp won't
throw on bad payloads).
- Around line 51-57: The hook retains prior notifications across auth
transitions; inside useNotifications add an effect that clears notifications
whenever the authenticated user changes (e.g., useEffect(() =>
setNotifications([]), [session?.user?.id]) or at minimum [session]) so that
notifications/state are reset on logout/login; reference the useNotifications
function and the notifications/setNotifications state variables and update the
effect to run when session (or session.user.id) changes.

---

Nitpick comments:
In `@lib/graphql/subscriptions.ts`:
- Around line 85-94: The interface OnBountyUpdatedData duplicates
BountyUpdatedData; to avoid drift, replace the duplicate interface declaration
(OnBountyUpdatedData) with a type alias to BountyUpdatedData (e.g., type
OnBountyUpdatedData = BountyUpdatedData) or remove OnBountyUpdatedData and
update any references to use BountyUpdatedData directly, ensuring all places
that referenced OnBountyUpdatedData now point to the single canonical
BountyUpdatedData type.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d9bb5aea-39c9-468a-8907-d540ba93dca7

📥 Commits

Reviewing files that changed from the base of the PR and between 3aad250 and 53a9464.

📒 Files selected for processing (6)
  • components/global-navbar.tsx
  • components/notifications/notification-center.tsx
  • hooks/use-graphql-subscription.ts
  • hooks/use-notifications.ts
  • lib/graphql/subscriptions.ts
  • lib/store.test.ts
💤 Files with no reviewable changes (1)
  • lib/store.test.ts

@0xDeon 0xDeon changed the title feat: implement real-time notifications with graphql subscriptions feat: real-time notifications with GraphQL subscriptions Mar 26, 2026
- Replace duplicate OnBountyUpdatedData interface with type alias to BountyUpdatedData
- Harden normaliseTimestamp against invalid date strings (fallback to now)
- Clear notification state on auth user change to prevent cross-user leakage
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/graphql/subscriptions.ts`:
- Around line 19-32: Update the test assertion that looks for the GraphQL
operation name from 'subscription BountyUpdated' to 'subscription
OnBountyUpdated' to match the exported
ON_BOUNTY_UPDATED_SUBSCRIPTION/BOUNTY_UPDATED_SUBSCRIPTION operation name;
modify the check in use-bounty-subscription.test (the assertion that inspects
c[0].query.includes(...)) accordingly and also verify any server-side
subscription resolver names that reference the old "BountyUpdated" operation are
renamed or aliased to "OnBountyUpdated" so the client and server operation names
match.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: c9db2527-564e-4f38-bece-bb4142ab64c6

📥 Commits

Reviewing files that changed from the base of the PR and between 53a9464 and 686902e.

📒 Files selected for processing (2)
  • hooks/use-notifications.ts
  • lib/graphql/subscriptions.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • hooks/use-notifications.ts

Align test assertion with renamed OnBountyUpdated operation name
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
hooks/__tests__/use-bounty-subscription.test.ts (1)

145-149: Consider adding a guard for the subscription lookup to improve test failure messages.

If the subscription string doesn't match (e.g., if the operation name changes again), call will be undefined and call[1].next will throw a confusing TypeError instead of a clear assertion failure.

This same pattern exists at lines 115-118 and 174-177.

💡 Suggested improvement
     // Find the call for bountyUpdated and trigger its callback
     const call = mockSubscribe.mock.calls.find((c) =>
       c[0].query.includes("subscription OnBountyUpdated"),
     );
+    expect(call).toBeDefined();
     const callback = call[1].next;

Apply the same pattern to the other find() calls for BountyCreated and BountyDeleted.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@hooks/__tests__/use-bounty-subscription.test.ts` around lines 145 - 149, The
test currently assumes mockSubscribe.mock.calls.find(...) returns a match and
immediately accesses call[1].next, which causes a confusing TypeError when the
subscription name changes; update each lookup (the three uses that search for
"subscription OnBountyUpdated", "subscription OnBountyCreated" and "subscription
OnBountyDeleted") to assert the result exists before accessing its elements:
capture the result into a variable (e.g., call), add an explicit
expect(call).toBeDefined() or throw a clear message if undefined, then safely
extract callback = call[1].next; this will produce readable test failures when
the subscription lookup fails.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@hooks/__tests__/use-bounty-subscription.test.ts`:
- Around line 145-149: The test currently assumes
mockSubscribe.mock.calls.find(...) returns a match and immediately accesses
call[1].next, which causes a confusing TypeError when the subscription name
changes; update each lookup (the three uses that search for "subscription
OnBountyUpdated", "subscription OnBountyCreated" and "subscription
OnBountyDeleted") to assert the result exists before accessing its elements:
capture the result into a variable (e.g., call), add an explicit
expect(call).toBeDefined() or throw a clear message if undefined, then safely
extract callback = call[1].next; this will produce readable test failures when
the subscription lookup fails.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2e30f303-e4bc-4f9a-8294-bd349d6ffd80

📥 Commits

Reviewing files that changed from the base of the PR and between 686902e and 45ed3d9.

📒 Files selected for processing (1)
  • hooks/__tests__/use-bounty-subscription.test.ts

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Real-time Activity Feed and In-App Notifications

1 participant