diff --git a/src/PageManager.tsx b/src/PageManager.tsx index ca6e4129..d01e0611 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -17,7 +17,6 @@ import MuteListPage from '@/pages/secondary/MuteListPage' import OthersRelaySettingsPage from '@/pages/secondary/OthersRelaySettingsPage' import SecondaryRelayPage from '@/pages/secondary/RelayPage' import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider' -import { NotificationProvider } from '@/providers/NotificationProvider' // DEPRECATED: useUserPreferences removed - double-panel functionality disabled import { TPageRef } from '@/types' import { @@ -1540,7 +1539,6 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { }} > - - @@ -1668,7 +1665,6 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { }} > - - diff --git a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts index 8b7f03b7..5d11cec9 100644 --- a/src/pages/primary/SpellsPage/fauxSpellFeeds.ts +++ b/src/pages/primary/SpellsPage/fauxSpellFeeds.ts @@ -47,7 +47,7 @@ export function applyFauxSpellCapsToSubRequests(requests: TFeedSubRequest[]): TF } /** - * Mention/notification-shaped kinds only (aligned with `NotificationProvider`, plus zap receipts). + * Mention/notification-shaped kinds only (aligned with global notification-shaped kinds, plus zap receipts). * Not full {@link PROFILE_FEED_KINDS} — that asked relays for huge multi-kind slices per `#p`. */ export const NOTIFICATION_SPELL_KINDS = [ diff --git a/src/pages/primary/SpellsPage/index.tsx b/src/pages/primary/SpellsPage/index.tsx index 8d4e8397..26fd878c 100644 --- a/src/pages/primary/SpellsPage/index.tsx +++ b/src/pages/primary/SpellsPage/index.tsx @@ -65,6 +65,7 @@ import { CalendarDays, Check, ChevronDown, + ChevronLeft, Copy, FileText, Gift, @@ -885,6 +886,12 @@ const SpellsPage = forwardRef(function SpellsPage( [favoriteIds] ) + const spellsTitlebarTitle = useMemo(() => { + if (selectedFauxSpell) return t(fauxSpellLabelKey(selectedFauxSpell)) + if (selectedSpell) return spellMenuLabel(selectedSpell) + return t('Spells') + }, [selectedFauxSpell, selectedSpell, spellMenuLabel, t]) + const pickSpell = useCallback( (spell: Event | null) => { if (spell) { @@ -1124,7 +1131,12 @@ const SpellsPage = forwardRef(function SpellsPage( pageName="spells" titlebar={
-
{t('Spells')}
+
+ {spellsTitlebarTitle} +
+
+ ) : ( <> - {isSmallScreen ? ( + {/* Spell picker + actions above the feed */} +
<> - - - - - {t('Select a spell…')} - -
+
-
-
+ + {selectedSpell ? spellMenuLabel(selectedSpell) : t('Select a spell…')} + + + + + + + {t('Select a spell…')} + +
+ {spellPickerList} +
+
+
+ + ) : ( + + + {spellPickerTriggerButton} + + +
+ {t('Select a spell…')} +
+
+ {spellPickerList} +
+
+
+ )} - ) : ( - - - {spellPickerTriggerButton} - - -
- {t('Select a spell…')} -
-
- {spellPickerList} -
-
-
- )} - -
- - {selectedSpell && ( - <> +
- - - - - - {selectedSpellIsOwn ? ( - { - setSpellToClone(null) - setSpellToEdit(selectedSpell) - setCreateOpen(true) - }} - > - - {t('Edit spell')} - - ) : ( - { - setSpellToEdit(null) - setSpellToClone(selectedSpell) - setCreateOpen(true) - }} - > - - {t('Clone spell')} - - )} - setDefinitionSpell(selectedSpell)}> - - {t('View definition')} - - {selectedSpellIsOwn ? ( - <> - - handleDeleteSpell(selectedSpell)} - > - - {t('Delete')} + + + + + + {selectedSpellIsOwn ? ( + { + setSpellToClone(null) + setSpellToEdit(selectedSpell) + setCreateOpen(true) + }} + > + + {t('Edit spell')} + + ) : ( + { + setSpellToEdit(null) + setSpellToClone(selectedSpell) + setCreateOpen(true) + }} + > + + {t('Clone spell')} + + )} + setDefinitionSpell(selectedSpell)}> + + {t('View definition')} - - ) : null} - - - - )} -
-
+ {selectedSpellIsOwn ? ( + <> + + handleDeleteSpell(selectedSpell)} + > + + {t('Delete')} + + + ) : null} + + + + )} +
+
- {spellsCatalogSyncing ? ( -

{t('Loading spells from your relays…')}

- ) : null} + {spellsCatalogSyncing ? ( +

{t('Loading spells from your relays…')}

+ ) : null} - {spellsForSelect.length === 0 && !spellsCatalogSyncing && ( -

{t('No spells yet. Create one with the button above.')}

+ {spellsForSelect.length === 0 && !spellsCatalogSyncing && ( +

{t('No spells yet. Create one with the button above.')}

+ )} + )} {/* Feed — faux spells and kind-777 spells all use NoteList */} diff --git a/src/providers/NotificationProvider.tsx b/src/providers/NotificationProvider.tsx deleted file mode 100644 index 168920e2..00000000 --- a/src/providers/NotificationProvider.tsx +++ /dev/null @@ -1,275 +0,0 @@ -import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' -import { compareEvents } from '@/lib/event' -import logger from '@/lib/logger' -import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' -import client from '@/services/client.service' -import { kinds, NostrEvent } from 'nostr-tools' -import { SubCloser } from 'nostr-tools/abstract-pool' -import { useEffect, useRef, useMemo } from 'react' -import { useNostr } from './NostrProvider' - -/** - * Subscribes to live notifications and forwards new events via {@link client.emitNewEvent}. - * (Read/unread UI and cross-device “seen at” sync were removed.) - */ -export function NotificationProvider({ children }: { children: React.ReactNode }) { - const { pubkey, relayList } = useNostr() - const { favoriteRelays } = useFavoriteRelays() - const notificationBufferRef = useRef([]) - const retryCountRef = useRef(0) - const retryTimeoutIdRef = useRef(null) - - // Memoize relay URLs to prevent unnecessary re-subscriptions - // This creates stable references based on actual relay URLs, not object references - const userReadRelays = useMemo(() => { - const userRelayList = relayList || { read: [], write: [] } - return userRelayList.read || [] - }, [relayList?.read?.join(',')]) // Compare by stringified array, not object reference - - const userFavoriteRelays = useMemo(() => { - return favoriteRelays || [] - }, [favoriteRelays?.join(',')]) // Compare by stringified array, not array reference - - // Memoize the notification relays to prevent re-subscriptions when they haven't changed - const notificationRelays = useMemo(() => { - if (userReadRelays.length > 0) { - return userReadRelays.slice(0, 5) - } else if (userFavoriteRelays.length > 0) { - return userFavoriteRelays.slice(0, 5) - } else { - return FAST_READ_RELAY_URLS.slice(0, 5) - } - }, [userReadRelays, userFavoriteRelays]) - - useEffect(() => { - if (!pubkey) return - - const deferredReset = setTimeout(() => { - notificationBufferRef.current = [] - }, 0) - - const isMountedRef = { current: true } - const subCloserRef: { - current: SubCloser | null - } = { current: null } - const topicSubCloserRef: { - current: SubCloser | null - } = { current: null } - const MAX_RETRIES = 5 - // Reset retry count when effect runs (relays changed) - retryCountRef.current = 0 - - const subscribe = async () => { - // Clear any pending retries - if (retryTimeoutIdRef.current) { - clearTimeout(retryTimeoutIdRef.current) - retryTimeoutIdRef.current = null - } - - if (subCloserRef.current) { - subCloserRef.current.close() - subCloserRef.current = null - } - if (topicSubCloserRef.current) { - topicSubCloserRef.current.close() - topicSubCloserRef.current = null - } - if (!isMountedRef.current) return null - - try { - let eosed = false - // Reset retry count on successful subscription attempt - retryCountRef.current = 0 - - if (notificationRelays.length > 0) { - logger.component('NotificationProvider', 'Using notification relays', { - count: notificationRelays.length, - relays: notificationRelays.slice(0, 3) - }) - } - - let discussionEosed = false - let initialBufferFlushed = false - const flushBufferedIfReady = () => { - if ( - !eosed || - !discussionEosed || - !isMountedRef.current || - initialBufferFlushed - ) { - return - } - initialBufferFlushed = true - const buf = notificationBufferRef.current - if (buf.length === 0) return - const sorted = [...buf].sort((a, b) => compareEvents(b, a)) - notificationBufferRef.current = sorted.slice(0, 50) - for (const evt of sorted) { - client.emitNewEvent(evt) - } - } - - const discussionSubCloser = client.subscribe( - notificationRelays, - [ - { - kinds: [11], - limit: 20 - } - ], - { - oneose: (e) => { - if (e) { - discussionEosed = e - flushBufferedIfReady() - } - }, - onevent: (evt) => { - if (evt.pubkey !== pubkey) { - const prev = notificationBufferRef.current - if (!discussionEosed) { - // Before EOSE: just buffer events, limit size - if (prev.length < 100) { - notificationBufferRef.current = [evt, ...prev] - } - return - } - if (prev.length && compareEvents(prev[0], evt) >= 0) { - return - } - - // Limit buffer size to prevent memory issues - if (prev.length >= 50) { - notificationBufferRef.current = [evt, ...prev.slice(0, 49)] - } else { - notificationBufferRef.current = [evt, ...prev] - } - client.emitNewEvent(evt) - } - } - } - ) - topicSubCloserRef.current = discussionSubCloser - - const subCloser = client.subscribe( - notificationRelays, - [ - { - kinds: [ - kinds.ShortTextNote, - kinds.Repost, - kinds.Reaction, - kinds.Zap, - ExtendedKind.COMMENT, - ExtendedKind.POLL_RESPONSE, - ExtendedKind.VOICE_COMMENT, - ExtendedKind.POLL, - ExtendedKind.PUBLIC_MESSAGE - ], - '#p': [pubkey], - limit: 20 - } - ], - { - oneose: (e) => { - if (e) { - eosed = e - // Don't sort on every EOSE - sorting is expensive and buffer is already maintained in order - // Only sort if buffer is getting large and out of order - if (notificationBufferRef.current.length > 100) { - notificationBufferRef.current = [ - ...notificationBufferRef.current.sort((a, b) => compareEvents(b, a)) - ] - } - flushBufferedIfReady() - } - }, - onevent: (evt) => { - if (evt.pubkey !== pubkey) { - const prev = notificationBufferRef.current - if (!eosed) { - // Before EOSE: just buffer events, don't emit yet - // Limit buffer size to prevent memory issues - if (prev.length < 100) { - notificationBufferRef.current = [evt, ...prev] - } - return - } - // After EOSE: only emit if it's newer than the most recent event - if (prev.length && compareEvents(prev[0], evt) >= 0) { - return - } - - // Limit buffer size to prevent memory issues - if (prev.length >= 50) { - notificationBufferRef.current = [evt, ...prev.slice(0, 49)] - } else { - notificationBufferRef.current = [evt, ...prev] - } - client.emitNewEvent(evt) - } - }, - onAllClose: (reasons) => { - if (reasons.every((reason) => reason === 'closed by caller')) { - return - } - - if (isMountedRef.current && retryCountRef.current < MAX_RETRIES) { - retryCountRef.current++ - const delay = Math.min(15_000 * retryCountRef.current, 60_000) // Exponential backoff, max 60s - logger.debug(`[NotificationProvider] Reconnecting after close (attempt ${retryCountRef.current}/${MAX_RETRIES})...`) - retryTimeoutIdRef.current = setTimeout(() => { - if (isMountedRef.current) { - subscribe() - } - }, delay) - } else if (retryCountRef.current >= MAX_RETRIES) { - logger.error('[NotificationProvider] Max retries reached, stopping reconnection attempts') - } - } - } - ) - - subCloserRef.current = subCloser - return subCloser - } catch (error) { - logger.error('Subscription error', { error, retryCount: retryCountRef.current }) - - if (isMountedRef.current && retryCountRef.current < MAX_RETRIES) { - retryCountRef.current++ - const delay = Math.min(5_000 * retryCountRef.current, 30_000) // Exponential backoff, max 30s - retryTimeoutIdRef.current = setTimeout(() => { - if (isMountedRef.current) { - subscribe() - } - }, delay) - } else if (retryCountRef.current >= MAX_RETRIES) { - logger.error('[NotificationProvider] Max retries reached, stopping subscription attempts') - } - return null - } - } - - subscribe() - - return () => { - clearTimeout(deferredReset) - if (retryTimeoutIdRef.current) { - clearTimeout(retryTimeoutIdRef.current) - retryTimeoutIdRef.current = null - } - retryCountRef.current = 0 // Reset retry count on cleanup - isMountedRef.current = false - if (subCloserRef.current) { - subCloserRef.current.close() - subCloserRef.current = null - } - if (topicSubCloserRef.current) { - topicSubCloserRef.current.close() - topicSubCloserRef.current = null - } - } - }, [pubkey, notificationRelays.join(',')]) // Use memoized notificationRelays instead of relayList/favoriteRelays - - return <>{children} -}