Skip to content

REP/NIC Notifications + Push Notifications Settings + Drag to refresh for mobile#1764

Merged
prxt6529 merged 17 commits intomainfrom
cic-rep-notifications
Jan 20, 2026
Merged

REP/NIC Notifications + Push Notifications Settings + Drag to refresh for mobile#1764
prxt6529 merged 17 commits intomainfrom
cic-rep-notifications

Conversation

@prxt6529
Copy link
Copy Markdown
Collaborator

@prxt6529 prxt6529 commented Jan 20, 2026

Summary by CodeRabbit

  • New Features

    • Identity rating notifications (REP/NIC) UI and a generic notification renderer.
    • Per‑device push notification settings UI + API endpoints.
    • Native pull‑to‑refresh gesture and a global refresh mechanism; "Push Notifications" added to user menu.
  • Refactor

    • Combined identity-related filters into an "Identity" filter and made the filter bar scrollable.
    • Streamlined notification type unions and priority/sorting behavior.
  • Tests

    • Added tests for identity rating notifications.
  • Style

    • Minor formatting and class tidy-ups.

✏️ Tip: You can customize this high-level summary in your review settings.

… for mobile

Signed-off-by: prxt6529 <prxt@6529.io>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Jan 20, 2026

📝 Walkthrough

Walkthrough

Adds identity-rating notification rendering and a generic notification fallback; introduces push notification device/settings API and UI; adds native pull-to-refresh and a global refresh context; refactors notification types and query key/handling; and makes filter bar scrollable with identity composite filter. (50 words)

Changes

Cohort / File(s) Summary
Identity rating & routing
components/brain/notifications/identity-rating/NotificationIdentityRating.tsx, components/brain/notifications/NotificationItem.tsx, __tests__/components/brain/notifications/item/NotificationItem.test.tsx
New NotificationIdentityRating component; NotificationItem now dispatches IdentityRep/IdentityNic to it; tests/mocks added for identity-rating rendering.
Generic notification fallback
components/brain/notifications/generic/NotificationGeneric.tsx
New NotificationGeneric component to render unknown or unmapped notification causes with formatted context and optional related identity header.
Notification types & typing
types/feed.types.ts
Introduces shared NotificationBase/WithDrops, new types INotificationIdentityRep, INotificationIdentityNic, and fallback INotificationGeneric; updates TypedNotification union.
Filter bar & controller
components/brain/notifications/NotificationsCauseFilter.tsx, components/brain/notifications/hooks/useNotificationsController.ts
Replaces single "Follows" with composite "Identity" filter (IdentitySubscribed, IdentityRep, IdentityNic); adds horizontal scroll controls, resize observer, smooth scroll buttons; passes null for empty causes to controller.
Query/key handling
hooks/useNotificationsQuery.tsx, components/brain/notifications/hooks/useNotificationsController.ts
useNotificationsQuery now accepts a props object and returns items + isInitialQueryDone; query key serializes cause as sorted comma string or null; controller aligns to use null for empty causes.
Push notifications UI & API
components/header/PushNotificationSettings.tsx, components/header/AppUserConnect.tsx, openapi.yaml, scripts/refresh-api.sh
New PushNotificationSettings dialog, AppUserConnect integration and button; OpenAPI adds push devices/settings endpoints and IDENTITY_REP/IDENTITY_NIC enum values; script now fetches via GitHub Contents API.
Native pull-to-refresh & global refresh
components/providers/PullToRefresh.tsx, contexts/RefreshContext.tsx, components/providers/LayoutWrapper.tsx, components/providers/Providers.tsx
New PullToRefresh for Capacitor native platforms; added RefreshProvider and useGlobalRefresh; LayoutWrapper conditionally renders PullToRefresh and uses refreshKey for ErrorBoundary resets.
Notifications context redirect
components/notifications/NotificationsContext.tsx
redirectConfig.profile now accepts optional subroute and validates it; NotificationData now carries optional subroute.
Minor UI & formatting tweaks
components/nft-image/renderers/NFTHTMLRenderer.tsx, components/providers/CapacitorSetup.tsx, components/user/user-page-header/UserPageHeaderClient.tsx, components/brain/notifications/NotificationItems.tsx
Small formatting/import/class-order/layout adjustments; minor padding tweak in NotificationItems.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant AppUserConnect
    participant PushNotificationSettings
    participant API
    participant Toast

    User->>AppUserConnect: Click "Push Notifications"
    AppUserConnect->>PushNotificationSettings: Open modal (isOpen=true)

    rect rgba(100, 150, 200, 0.5)
    Note over PushNotificationSettings: Load Phase
    PushNotificationSettings->>PushNotificationSettings: Generate device ID
    PushNotificationSettings->>API: GET /push-notifications/settings/{device_id}
    API-->>PushNotificationSettings: Return current settings
    end

    rect rgba(150, 150, 100, 0.5)
    Note over PushNotificationSettings: Edit Phase
    User->>PushNotificationSettings: Toggle setting
    PushNotificationSettings->>PushNotificationSettings: Update currentSettings
    end

    rect rgba(100, 200, 100, 0.5)
    Note over PushNotificationSettings: Save Phase
    User->>PushNotificationSettings: Click "Save Changes"
    PushNotificationSettings->>API: PUT /push-notifications/settings/{device_id}
    API-->>PushNotificationSettings: Settings saved
    PushNotificationSettings->>Toast: Show success
    end

    PushNotificationSettings->>AppUserConnect: Close modal
Loading
sequenceDiagram
    actor User
    participant PullToRefresh
    participant ContentElement
    participant Router
    participant Spinner

    User->>PullToRefresh: Touch and drag down
    PullToRefresh->>PullToRefresh: Verify at top & scrollable parent

    loop On touchMove
        PullToRefresh->>PullToRefresh: Calculate pullDistance
        PullToRefresh->>ContentElement: Apply translateY transform
    end

    alt pullDistance >= PULL_THRESHOLD
        rect rgba(100, 200, 100, 0.5)
        PullToRefresh->>Spinner: Show spinner
        PullToRefresh->>Router: Call router.refresh() / invalidateAll()
        Router-->>PullToRefresh: Refresh complete
        end
        PullToRefresh->>PullToRefresh: Reset transform and state
    else pullDistance < PULL_THRESHOLD
        PullToRefresh->>ContentElement: Snap back (reset transform)
    end

    User->>PullToRefresh: Touch ends
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

Suggested reviewers

  • simo6529

"🐰 New identities bloom with purple and green,
Pull down the page for a refresh so clean,
Settings saved swift, notifications sing true,
REP and NIC now show their colorful hue,
A tiny rabbit hops — release notes, woohoo! ✨"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the three main features being added: REP/NIC notification support, push notification settings UI, and pull-to-refresh functionality for mobile.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Copy link
Copy Markdown

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

Caution

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

⚠️ Outside diff range comments (1)
openapi.yaml (1)

7550-7571: Align unminted nullability with edition_size.

The schema marks edition_size as nullable but keeps unminted as non-nullable. Since unminted is logically derived from edition_size (remaining unminted count), it should also be nullable when edition_size is null to prevent schema contract mismatches for clients.

Suggested fix
         unminted:
           type: number
           format: int64
+          nullable: true
🤖 Fix all issues with AI agents
In `@components/header/PushNotificationSettings.tsx`:
- Around line 125-144: The save mutation (useMutation in
PushNotificationSettings handling mutationFn -> commonApiPut) lacks an onError
handler so failures give no user feedback; add an onError callback to the
useMutation config (alongside onSuccess) that calls setToast with an error
message (and optionally the error details) and ensures any UI state like
isSaving or onClose is handled appropriately so users see a failure notification
when saveSettings fails.
- Around line 88-98: The catch block in PushNotificationSettings currently
treats any error from commonApiFetch as a “not found” case and falls back to
DEFAULT_SETTINGS; change this to only use DEFAULT_SETTINGS when the API
indicates a 404/not-found and otherwise propagate or surface the error (e.g.,
rethrow or call processLogger/error handler) so real network/server errors
aren’t swallowed. Locate the call to commonApiFetch<ApiPushNotificationSettings>
and the catch that calls setOriginalSettings(DEFAULT_SETTINGS) /
setCurrentSettings(DEFAULT_SETTINGS), detect the error status (e.g., check
error.response?.status or the ApiError type/message) and handle 404 by setting
defaults but rethrow or log non-404 errors instead.

In `@components/providers/PullToRefresh.tsx`:
- Around line 110-128: The setTimeout used after triggering the refresh (when
pullDistance >= PULL_THRESHOLD) can fire after the component unmounts and cause
state updates; capture the timeout id(s) returned by setTimeout in variables
(for the block using pullDistance/PULL_THRESHOLD/isRefreshing and the other
similar block around the 147-155 range), store them in a ref (e.g.,
refreshTimeoutRef) and clear them in the cleanup of the effect/hook
(clearTimeout(refreshTimeoutRef.current)) to prevent setIsRefreshing,
setPullDistance or DOM style updates after teardown; ensure you also reset the
ref after clearing.
- Around line 83-102: When handling touch moves (the onTouchMove handler that
reads touchStartY.current and computes diff), ensure you reset pull state when
diff is <= 0: call setPullDistance(0) and clear any transform/transition on
contentRef.current so the indicator doesn't stick; also avoid calling
e.preventDefault() in that branch. Update the block that currently only handles
diff > 0 to include an else (or early-return) that resets pullDistance and
contentRef.current.style.transform/transition when the finger moves above the
start point (diff <= 0).
🧹 Nitpick comments (2)
types/feed.types.ts (1)

138-171: Add INotificationGeneric to the TypedNotification union for type-safe fallback handling.

Currently, INotificationGeneric is defined but excluded from TypedNotification, forcing NotificationItem to use an unsafe cast in the default case (notification as unknown as INotificationGeneric). This should be part of the union to enable type-safe handling of unknown notification causes.

♻️ Suggested type adjustment
 export type TypedNotification =
   | INotificationIdentitySubscribed
   | INotificationIdentityMentioned
   | INotificationIdentityRep
   | INotificationIdentityNic
   | INotificationDropVoted
   | INotificationDropReacted
   | INotificationDropBoosted
   | INotificationDropQuoted
   | INotificationDropReplied
   | INotificationWaveCreated
   | INotificationAllDrops
-  | INotificationPriorityAlert;
+  | INotificationPriorityAlert
+  | INotificationGeneric;
components/brain/notifications/NotificationItem.tsx (1)

107-112: Move notification type normalization to the data layer to eliminate the unsafe double cast.

The as unknown as INotificationGeneric cast bypasses type safety. Since INotificationGeneric is designed as a fallback for unknown notification causes, consider mapping unrecognized causes to this type in useNotificationsQuery rather than casting in the component. This could widen the query's return type to (TypedNotification | INotificationGeneric)[], allowing NotificationItem to receive properly typed notifications without unsafe casts.

Comment thread components/header/PushNotificationSettings.tsx
Comment thread components/header/PushNotificationSettings.tsx
Comment thread components/providers/PullToRefresh.tsx
Comment thread components/providers/PullToRefresh.tsx Outdated
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Copy link
Copy Markdown

@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

🤖 Fix all issues with AI agents
In `@components/brain/notifications/NotificationsCauseFilter.tsx`:
- Around line 218-247: The two chevron buttons in NotificationsCauseFilter (the
left and right scroll buttons that call scrollLeft and scrollRight) are missing
type attributes and will default to type="submit" inside a form; update both
<button> elements that render the FontAwesomeIcon (the button with
onClick={scrollLeft} and the button with onClick={scrollRight}) to include
type="button" so they do not submit parent forms.
🧹 Nitpick comments (4)
components/providers/LayoutWrapper.tsx (1)

92-95: Minor redundancy: refreshKey as both key and in resetKeys.

Using refreshKey as the key prop will unmount and remount the entire ErrorBoundary when it changes, making its presence in resetKeys redundant. The current approach is harmless but slightly over-engineered.

Consider keeping only one mechanism:

♻️ Option: Remove from key prop (preferred for smoother UX)
       <ErrorBoundary
-        key={refreshKey}
         FallbackComponent={LayoutErrorFallback}
         resetKeys={[pathname, refreshKey]}
       >
components/providers/PullToRefresh.tsx (1)

149-171: Event listeners churning on every pullDistance change.

Since handleTouchEnd depends on pullDistance, it's recreated on every state update during a pull gesture. This causes the useEffect to remove and re-add all three event listeners repeatedly during the interaction, which can impact performance and potentially cause missed events.

Consider using a ref to hold the latest callback values:

♻️ Suggested pattern using stable listeners
+  const handlersRef = useRef({ handleTouchStart, handleTouchMove, handleTouchEnd });
+  useEffect(() => {
+    handlersRef.current = { handleTouchStart, handleTouchMove, handleTouchEnd };
+  });
+
   useEffect(() => {
     if (!isNativeApp) return;
 
+    const onTouchStart = (e: TouchEvent) => handlersRef.current.handleTouchStart(e);
+    const onTouchMove = (e: TouchEvent) => handlersRef.current.handleTouchMove(e);
+    const onTouchEnd = () => handlersRef.current.handleTouchEnd();
+
-    document.addEventListener("touchstart", handleTouchStart, {
+    document.addEventListener("touchstart", onTouchStart, {
       passive: true,
     });
-    document.addEventListener("touchmove", handleTouchMove, { passive: false });
-    document.addEventListener("touchend", handleTouchEnd, { passive: true });
+    document.addEventListener("touchmove", onTouchMove, { passive: false });
+    document.addEventListener("touchend", onTouchEnd, { passive: true });
 
     return () => {
-      document.removeEventListener("touchstart", handleTouchStart);
-      document.removeEventListener("touchmove", handleTouchMove);
-      document.removeEventListener("touchend", handleTouchEnd);
+      document.removeEventListener("touchstart", onTouchStart);
+      document.removeEventListener("touchmove", onTouchMove);
+      document.removeEventListener("touchend", onTouchEnd);
       // ... rest of cleanup
     };
-  }, [isNativeApp, handleTouchStart, handleTouchMove, handleTouchEnd]);
+  }, [isNativeApp]);
components/header/PushNotificationSettings.tsx (1)

78-117: Missing setToast in dependency array and potential stale state on error.

Two issues:

  1. setToast is used inside the effect but not listed in the dependency array (line 117). This violates React's exhaustive-deps rule.

  2. On non-404 errors, the toast is shown but originalSettings/currentSettings are not reset. If the dialog was previously opened successfully, users will see stale settings instead of the error state.

Suggested fix
     loadSettings();
-  }, [isOpen]);
+  }, [isOpen, setToast]);

And to handle the stale state issue, reset settings before loading:

   useEffect(() => {
     if (!isOpen) return;

     const loadSettings = async () => {
       setIsLoading(true);
+      setOriginalSettings(null);
+      setCurrentSettings(null);
       try {
types/feed.types.ts (1)

140-166: Consider including INotificationGeneric in the union if it flows through typed APIs.
If unknown causes are meant to be handled by the generic renderer, adding it to TypedNotification (and therefore TypedNotificationsResponse) avoids casts and keeps the type surface honest.

💡 Suggested tweak
 export type TypedNotification =
   | INotificationIdentitySubscribed
   | INotificationIdentityMentioned
   | INotificationIdentityRep
   | INotificationIdentityNic
   | INotificationDropVoted
   | INotificationDropReacted
   | INotificationDropBoosted
   | INotificationDropQuoted
   | INotificationDropReplied
   | INotificationWaveCreated
   | INotificationAllDrops
-  | INotificationPriorityAlert;
+  | INotificationPriorityAlert
+  | INotificationGeneric;

Comment thread components/brain/notifications/NotificationsCauseFilter.tsx
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
Copy link
Copy Markdown

@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

Caution

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

⚠️ Outside diff range comments (1)
components/brain/notifications/NotificationsCauseFilter.tsx (1)

267-274: Missing tw- prefix on z-10 class.

Line 270 uses z-10 without the tw- prefix, which is inconsistent with the rest of the Tailwind classes in this codebase that use the tw- prefix.

✅ Suggested fix
      `tw-border-none tw-bg-transparent tw-no-underline tw-flex tw-justify-center tw-items-center
       tw-px-3 tw-py-2 tw-gap-2 tw-flex-1 tw-h-8 tw-rounded-lg tw-transition-colors tw-duration-300
-      tw-ease-in-out tw-relative z-10 ${
+      tw-ease-in-out tw-relative tw-z-10 ${
        isActive
          ? "tw-text-iron-300"
          : "tw-text-iron-400 desktop-hover:hover:tw-text-iron-300"
      }`;
🤖 Fix all issues with AI agents
In `@components/providers/PullToRefresh.tsx`:
- Around line 50-68: handleTouchStart can start a new pull while a refresh is
active which later clears pullDistance; add an early return at the top of
handleTouchStart that checks isRefreshingRef.current and exits if true so no new
pull is initiated during refresh. Update the handleTouchStart callback
(referenced by isAtTop, getScrollableParent) to check isRefreshingRef.current
before setting isPulling.current, touchStartY.current, or contentRef.current.
- Around line 154-176: The effect registers touch event listeners but misses
touchcancel, which can leave the content transform stuck; add a touchcancel
listener (e.g., document.addEventListener("touchcancel", handleTouchEnd, {
passive: true }) or create a dedicated handleTouchCancel that mirrors
handleTouchEnd/reset logic) and remove it in the cleanup; ensure the handler
clears refreshTimeoutRef.current and resets
contentRef.current.style.transform/transition so interrupted gestures don't
leave the UI translated.
♻️ Duplicate comments (3)
components/brain/notifications/NotificationsCauseFilter.tsx (1)

218-249: Past issue resolved: chevron buttons now include type="button".

Both scroll buttons correctly specify type="button" (lines 222 and 238), preventing unintended form submissions.

components/header/PushNotificationSettings.tsx (2)

94-108: Error handling properly distinguishes 404 from other errors.

The implementation now correctly treats only 404 responses as "settings not found" while surfacing other errors to the user via toast notification. This addresses the previous review feedback.


135-161: Error handling for save mutation is now in place.

The onError callback properly surfaces failures to users via toast notification, addressing the previous review feedback.

🧹 Nitpick comments (3)
components/brain/notifications/NotificationsCauseFilter.tsx (2)

146-165: Inconsistent ResizeObserver instantiation without browser support check.

Unlike lines 89-94 which check typeof ResizeObserver !== "undefined", this useLayoutEffect creates a ResizeObserver directly. While modern browsers support it, this could cause issues in older environments.

♻️ Suggested fix
     // Handle layout shifts (resize, font load, etc.)
     const container = containerRef.current;
     if (!container) return;
+    if (typeof ResizeObserver === "undefined") return;
     const ro = new ResizeObserver(() =>
       updateHighlightPosition(activeIndexRef.current)
     );
     ro.observe(container);
     return () => ro.disconnect();

276-285: Add type="button" to the filter button.

The NotificationCauseFilterButton component's button is missing the type="button" attribute. Without it, the button defaults to type="submit" inside a form context, which could cause unintended behavior.

✅ Suggested fix
   return (
     <button
+      type="button"
       className={getLinkClasses()}
       onClick={onClick}
       onMouseEnter={onMouseEnter}
       ref={buttonRef}
     >
components/header/PushNotificationSettings.tsx (1)

78-117: Missing setToast in dependency array.

The effect uses setToast but it's not listed in the dependency array. While setToast is likely stable from the auth context, ESLint's exhaustive-deps rule would flag this.

Suggested fix
     loadSettings();
-  }, [isOpen]);
+  }, [isOpen, setToast]);

Comment thread components/providers/PullToRefresh.tsx
Comment thread components/providers/PullToRefresh.tsx Outdated
Signed-off-by: prxt6529 <prxt@6529.io>
Signed-off-by: prxt6529 <prxt@6529.io>
@sonarqubecloud
Copy link
Copy Markdown

@prxt6529 prxt6529 merged commit c5e3233 into main Jan 20, 2026
7 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Mar 2, 2026
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.

2 participants