From 579b1e533b6f913af809acf5c420a1627515fd76 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sun, 29 Mar 2026 17:10:45 +0200 Subject: [PATCH] make the notifications page more advanced --- src/i18n/locales/de.ts | 5 ++ src/i18n/locales/en.ts | 5 ++ src/pages/primary/SpellsPage/index.tsx | 108 ++++++++++++++++++++++--- src/providers/NostrProvider/index.tsx | 6 +- src/providers/nostr-context.tsx | 3 +- 5 files changed, 113 insertions(+), 14 deletions(-) diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 8ee54f92..315977a2 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -596,6 +596,11 @@ export default { Pay: 'Bezahlen', interactions: 'Interaktionen', notifications: 'Benachrichtigungen', + notificationsViewAsAccount: 'Anzeigen als', + notificationsViewAsAccountAria: + 'Gespeichertes Konto wechseln. Benachrichtigungen, Antworten, Zaps, Reaktionen und Boosts nutzen dieses Konto.', + notificationsSwitchAccountFailed: + 'Kontowechsel fehlgeschlagen. Signierer, Passwort oder Erweiterung prüfen und erneut versuchen.', 'Show untrusted {type}': 'Untrusted {{type}} anzeigen', 'Hide untrusted {type}': 'Untrusted {{type}} ausblenden', 'Currently hiding {type} from untrusted users.': diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 23604e97..c5685fa5 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -617,6 +617,11 @@ export default { Pay: 'Pay', interactions: 'interactions', notifications: 'notifications', + notificationsViewAsAccount: 'View as', + notificationsViewAsAccountAria: + 'Switch stored account. Notifications, replies, zaps, reactions, and boosts use this account.', + notificationsSwitchAccountFailed: + 'Could not switch account. Check your signer, password, or extension and try again.', 'Show untrusted {type}': 'Show untrusted {{type}}', 'Hide untrusted {type}': 'Hide untrusted {{type}}', 'Currently hiding {type} from untrusted users.': diff --git a/src/pages/primary/SpellsPage/index.tsx b/src/pages/primary/SpellsPage/index.tsx index 5a0dbdfc..dd1ac6e1 100644 --- a/src/pages/primary/SpellsPage/index.tsx +++ b/src/pages/primary/SpellsPage/index.tsx @@ -15,6 +15,13 @@ import { DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue +} from '@/components/ui/select' import { Separator } from '@/components/ui/separator' import { Drawer, DrawerContent, DrawerHeader, DrawerTitle } from '@/components/ui/drawer' import UserAvatar from '@/components/UserAvatar' @@ -50,7 +57,7 @@ import { FIRST_RELAY_RESULT_GRACE_MS, } from '@/constants' import { filterEventsExcludingTombstones, isUserInEventMentions } from '@/lib/event' -import { formatPubkey } from '@/lib/pubkey' +import { formatPubkey, hexPubkeysEqual, normalizeHexPubkey } from '@/lib/pubkey' import { augmentSubRequestsWithFavoritesFastReadAndInbox, getRelayUrlsWithFavoritesFastReadAndInbox @@ -98,6 +105,7 @@ import type { Event } from 'nostr-tools' import { kinds as nostrKinds, verifyEvent } from 'nostr-tools' import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' +import { toast } from 'sonner' import CreateSpellDialog from './CreateSpellDialog' import { appendCuratedReadOnlyRelays, @@ -310,8 +318,17 @@ const SpellsPage = forwardRef(function SpellsPage( ) { const { t } = useTranslation() const { navigate: navigatePrimary } = usePrimaryPage() - const { pubkey, account, relayList, attemptDelete, bookmarkListEvent, interestListEvent } = - useNostr() + const { + pubkey, + account, + accounts, + relayList, + attemptDelete, + bookmarkListEvent, + interestListEvent, + switchAccount, + isAccountSessionHydrating + } = useNostr() const { addBookmark, removeBookmark } = useBookmarks() const { hideUntrustedNotifications } = useUserTrust() const { isSmallScreen } = useScreenSize() @@ -352,6 +369,43 @@ const SpellsPage = forwardRef(function SpellsPage( const spellFeedInstrLabelRef = useRef('') const [spellFeedInstrumentToken, setSpellFeedInstrumentToken] = useState(0) const [followSetManualRefreshKey, setFollowSetManualRefreshKey] = useState(0) + /** Notifications `#p` + mention filter track the active session; changing the dropdown calls {@link switchAccount}. */ + const notificationsFeedPubkey = useMemo(() => { + const cur = pubkey?.trim() + return cur ? normalizeHexPubkey(cur) : null + }, [pubkey]) + + const storedAccountPubkeys = useMemo(() => { + const seen = new Set() + const out: string[] = [] + for (const a of accounts) { + const raw = a.pubkey?.trim() + if (!raw) continue + const p = normalizeHexPubkey(raw) + if (!seen.has(p)) { + seen.add(p) + out.push(p) + } + } + return out + }, [accounts]) + + const handleNotificationsAccountPick = useCallback( + async (v: string) => { + const target = normalizeHexPubkey(v) + if (pubkey && hexPubkeysEqual(target, pubkey)) return + const nextAccount = accounts.find((a) => hexPubkeysEqual(a.pubkey, target)) + if (!nextAccount) { + toast.error(t('notificationsSwitchAccountFailed')) + return + } + const switched = await switchAccount(nextAccount) + if (!switched || !hexPubkeysEqual(normalizeHexPubkey(switched), target)) { + toast.error(t('notificationsSwitchAccountFailed')) + } + }, + [pubkey, accounts, switchAccount, t] + ) const logSpellFeedPickerSelection = useCallback((label: string, extra?: Record) => { spellFeedInstrT0Ref.current = performance.now() @@ -835,8 +889,8 @@ const SpellsPage = forwardRef(function SpellsPage( ) if (selectedFauxSpell === 'notifications') { - if (!pubkey || !feedUrls.length) return [] - return buildNotificationsSpellSubRequests(feedUrls, pubkey) + if (!notificationsFeedPubkey || !feedUrls.length) return [] + return buildNotificationsSpellSubRequests(feedUrls, notificationsFeedPubkey) } if (selectedFauxSpell === 'discussions') { // Read-only prepended in appendCuratedReadOnlyRelays so FAUX_SPELL_MAX_RELAYS still includes aggr. @@ -876,7 +930,7 @@ const SpellsPage = forwardRef(function SpellsPage( ] } return [] - }, [selectedFauxSpell, pubkey, fauxFeedRelaysDepsKey, relayMailboxStableKey]) + }, [selectedFauxSpell, pubkey, notificationsFeedPubkey, fauxFeedRelaysDepsKey, relayMailboxStableKey]) const fauxSubRequests = useMemo(() => { const base = isFollowFeedFauxSpellId(selectedFauxSpell ?? '') @@ -1205,8 +1259,9 @@ const SpellsPage = forwardRef(function SpellsPage( }, [selectedFauxSpell]) const notificationsMentionExtraHide = useCallback( - (evt: Event) => (pubkey ? !isUserInEventMentions(evt, pubkey) : false), - [pubkey] + (evt: Event) => + notificationsFeedPubkey ? !isUserInEventMentions(evt, notificationsFeedPubkey) : false, + [notificationsFeedPubkey] ) const fauxFeedEmptyMessage = useMemo(() => { @@ -1670,7 +1725,40 @@ const SpellsPage = forwardRef(function SpellsPage( ) : selectedFauxSpell && fauxSubRequests.length > 0 ? ( <> {selectedFauxSpell === 'notifications' ? ( -
+
+ {storedAccountPubkeys.length > 1 && notificationsFeedPubkey ? ( +
+ + {t('notificationsViewAsAccount')} + + +
+ ) : null}
) : null} @@ -1712,7 +1800,7 @@ const SpellsPage = forwardRef(function SpellsPage( : false } extraShouldHideEvent={ - selectedFauxSpell === 'notifications' && pubkey + selectedFauxSpell === 'notifications' && notificationsFeedPubkey ? notificationsMentionExtraHide : undefined } diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 07e247a6..054d5d77 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -840,14 +840,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { } } - const switchAccount = async (act: TAccountPointer | null) => { + const switchAccount = async (act: TAccountPointer | null): Promise => { if (!act) { storage.switchAccount(null) setAccount(null) setSigner(null) - return + return null } - await loginWithAccountPointer(act) + return await loginWithAccountPointer(act) } const finishNcryptsecPasswordPrompt = useCallback((password: string | null) => { diff --git a/src/providers/nostr-context.tsx b/src/providers/nostr-context.tsx index a3fa7b16..5b6b8179 100644 --- a/src/providers/nostr-context.tsx +++ b/src/providers/nostr-context.tsx @@ -35,7 +35,8 @@ export type TNostrContext = { accounts: TAccountPointer[] nsec: string | null ncryptsec: string | null - switchAccount: (account: TAccountPointer | null) => Promise + /** Returns the new session pubkey on success, or `null` if logout / switch failed. */ + switchAccount: (account: TAccountPointer | null) => Promise nsecLogin: (nsec: string, password?: string, needSetup?: boolean) => Promise ncryptsecLogin: (ncryptsec: string) => Promise nip07Login: () => Promise