fix(desktop): show due-reminder count in the Inbox nav badge (#1191)

Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Co-authored-by: npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7 <dcfd242e557282d7a1e2cf2e6877522682f1e5c6156dc92ca7d90eaedd3b0f95@sprout-oss.stage.blox.sqprod.co>
This commit is contained in:
Will Pfleger
2026-06-23 10:46:18 -04:00
committed by GitHub
co-authored by npub1mn7jgtj4w2pd0g0zeuhxsa6jy6p0rewxz4kujt98my82ahfmp72sxjexk7
parent 45eae7c0ec
commit c0858dac12
5 changed files with 92 additions and 3 deletions
+4
View File
@@ -44,6 +44,10 @@ const overrides = new Map([
// overage from load-bearing per-message plumbing, not generic debt growth.
// Approved override; still queued to split with the rest of this list.
["src/features/messages/ui/MessageThreadPanel.tsx", 1006],
// useDueReminderBadgeCount hook call + sum to wire due-reminder count into
// the Inbox nav badge — a small overage from load-bearing badge plumbing,
// not generic debt growth. Approved override; still queued to split.
["src/app/AppShell.tsx", 1007],
]);
await runFileSizeCheck({
+8 -1
View File
@@ -69,6 +69,7 @@ import {
isSettingsSection,
} from "@/features/settings/ui/SettingsPanels";
import { HuddleBar, HuddleProvider } from "@/features/huddle";
import { useDueReminderBadgeCount } from "@/features/reminders/hooks";
import { RemindMeLaterProvider } from "@/features/reminders/ui/RemindMeLaterProvider";
import { useReminderNotifications } from "@/features/reminders/useReminderNotifications";
import { AppSidebar } from "@/features/sidebar/ui/AppSidebar";
@@ -411,6 +412,12 @@ export function AppShell() {
getThreadReadAt,
);
// Raw add to the in-app nav badge, mirroring the inbox filter badge; gated by
// homeBadgeEnabled to match every other badge contribution.
const dueReminderBadge = useDueReminderBadgeCount(
identityQuery.data?.pubkey,
notificationSettings.settings.homeBadgeEnabled,
);
const isNotifiedForThread = React.useCallback(
(rootId: string) =>
!mutedRootIds.has(rootId) &&
@@ -822,7 +829,7 @@ export function AppShell() {
currentPubkey={identityQuery.data?.pubkey}
errorMessage={channelsErrorMessage}
fallbackDisplayName={identityQuery.data?.displayName}
homeBadgeCount={homeBadgeCount}
homeBadgeCount={homeBadgeCount + dueReminderBadge}
isAddWorkspaceOpen={isAddWorkspaceOpen}
isCreatingChannel={createChannelMutation.isPending}
isCreatingForum={createForumMutation.isPending}
+21
View File
@@ -33,6 +33,27 @@ export function useRemindersQuery(pubkey: string | undefined) {
});
}
/**
* The due-reminder contribution to the in-app Inbox nav badge. Reminders are a
* separate stream from the feed badge machinery, so the count is summed in at
* the AppShell wiring point rather than threaded through homeBadge.ts. Reads
* the shared query above, so the useReminderNotifications poll's invalidate
* keeps it live and countDue re-evaluates as reminders cross due. The caller
* adds this raw (no isHomeActive suppression), mirroring the inbox filter
* badge, which persists while the Inbox is open.
*
* `enabled` mirrors the homeBadgeEnabled contract: when the home badge toggle is
* off, the feed contribution returns 0, so the reminder add must too — otherwise
* a disabled badge would still show a reminder `(1)`.
*/
export function useDueReminderBadgeCount(
pubkey: string | undefined,
enabled: boolean,
): number {
const remindersQuery = useRemindersQuery(pubkey);
return enabled ? countDueReminders(remindersQuery.data ?? []) : 0;
}
/**
* Wraps every reminder write so the shared query is invalidated on success —
* the consistency spine the panel/badge/overlay all depend on. A mutation that
@@ -1,6 +1,10 @@
import * as React from "react";
import { useQueryClient } from "@tanstack/react-query";
import { useRemindersQuery } from "@/features/reminders/hooks";
import {
remindersQueryKey,
useRemindersQuery,
} from "@/features/reminders/hooks";
import { dueSince } from "@/features/reminders/lib/reminderFilters";
import type { Reminder } from "@/features/reminders/lib/reminderTypes";
import {
@@ -51,6 +55,7 @@ export function useReminderNotifications(
settings: NotificationSettings,
): void {
const reminders = useRemindersQuery(pubkey).data;
const queryClient = useQueryClient();
const remindersRef = React.useRef<Reminder[]>([]);
remindersRef.current = reminders ?? [];
const settingsRef = React.useRef(settings);
@@ -109,10 +114,18 @@ export function useReminderNotifications(
// not backlog-replay reminders that came due while muted — same no-replay
// rationale as seed-to-now. Suppressed reminders still show in panel/badge.
window.localStorage.setItem(watermarkStorageKey(pubkey), String(now));
// Liveness tick: re-render every countDue consumer (inbox nav badge,
// HomeView filter, panel) so a reminder that crossed notBefore while the
// app sat idle surfaces within the poll interval. Safe to run after the
// watermark advance — the toast check() fires on this hook's own
// setInterval, not on query-data change, so the refetch cannot re-fire it.
void queryClient.invalidateQueries({
queryKey: remindersQueryKey(pubkey),
});
};
check();
const interval = window.setInterval(check, POLL_INTERVAL_MS);
return () => window.clearInterval(interval);
}, [pubkey]);
}, [pubkey, queryClient]);
}
@@ -270,6 +270,50 @@ function aliceReminderContent() {
});
}
// Nav badge — the surface unit tests can't prove: a due reminder driving the
// Inbox nav item's `(1)` count. The count is gated behind `homeBadgeEnabled`,
// so seed that setting on before installMockBridge (addInitScript runs at
// document start, ahead of the app reading localStorage).
const NAVBADGE_SHOTS = "test-results/reminders-navbadge";
const NOTIFICATION_SETTINGS_KEY = `buzz-notification-settings.v2:${MOCK_PUBKEY}`;
test.describe("reminders nav badge", () => {
test.beforeEach(async ({ page }) => {
await page.addInitScript(
({ key }) => {
localStorage.setItem(key, JSON.stringify({ homeBadgeEnabled: true }));
},
{ key: NOTIFICATION_SETTINGS_KEY },
);
await installMockBridge(page);
});
test("09 — Inbox nav badge counts a due reminder", async ({ page }) => {
await gotoInboxHome(page);
// One overdue pending reminder (notBefore in the past) is the sole badge
// contributor; the feed baseline is 0, so an exact "1" proves the reminder
// — not feed noise — drives the nav count.
const pastTimestamp = Math.floor(Date.now() / 1000) - 7200;
await seedReminders(page, [
mockReminderEvent({
id: "reminder-navbadge-01",
dTag: "rem-navbadge-01",
content: aliceReminderContent(),
notBefore: pastTimestamp,
}),
]);
await expect(page.getByTestId("sidebar-home-count")).toHaveText("1");
await waitForAnimations(page);
await page.screenshot({
path: `${NAVBADGE_SHOTS}/01-inbox-nav-badge-due-reminder.png`,
clip: { x: 0, y: 0, width: 900, height: 720 },
});
});
});
test.describe("reminders phase 2 — author, source, navigation", () => {
test.beforeEach(async ({ page }) => {
await installMockBridge(page);