From 966044e13d42d787b5c5e90e2072f2ebe9d98d68 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 19 Mar 2026 06:35:26 +0100 Subject: [PATCH] remove read receipts --- .../NotificationsButton.tsx | 9 +- .../DiscussionNotification.tsx | 10 +- .../NotificationItem/MentionNotification.tsx | 10 +- .../NotificationItem/Notification.tsx | 53 +--- .../PollResponseNotification.tsx | 10 +- .../PublicMessageNotification.tsx | 10 +- .../NotificationItem/ReactionNotification.tsx | 10 +- .../NotificationItem/RepostNotification.tsx | 10 +- .../NotificationItem/ZapNotification.tsx | 10 +- .../NotificationItem/index.tsx | 25 +- src/components/NotificationList/index.tsx | 10 +- src/components/Sidebar/NotificationButton.tsx | 9 +- src/constants.ts | 5 - src/lib/draft-event.ts | 11 +- src/main.tsx | 5 +- src/providers/NostrProvider/index.tsx | 71 +---- src/providers/NotificationContext.tsx | 18 -- src/providers/NotificationProvider.tsx | 259 ++++-------------- src/services/client.service.ts | 13 - src/services/local-storage.service.ts | 20 -- 20 files changed, 83 insertions(+), 495 deletions(-) delete mode 100644 src/providers/NotificationContext.tsx diff --git a/src/components/BottomNavigationBar/NotificationsButton.tsx b/src/components/BottomNavigationBar/NotificationsButton.tsx index 3b273d7f..21ca8089 100644 --- a/src/components/BottomNavigationBar/NotificationsButton.tsx +++ b/src/components/BottomNavigationBar/NotificationsButton.tsx @@ -1,25 +1,18 @@ import { usePrimaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' -import { useNotification } from '@/providers/NotificationContext' import { Bell } from 'lucide-react' import BottomNavigationBarItem from './BottomNavigationBarItem' export default function NotificationsButton() { const { checkLogin } = useNostr() const { navigate, current, display } = usePrimaryPage() - const { hasNewNotification } = useNotification() return ( checkLogin(() => navigate('notifications'))} > -
- - {hasNewNotification && ( -
- )} -
+ ) } diff --git a/src/components/NotificationList/NotificationItem/DiscussionNotification.tsx b/src/components/NotificationList/NotificationItem/DiscussionNotification.tsx index c7466e71..92fa624a 100644 --- a/src/components/NotificationList/NotificationItem/DiscussionNotification.tsx +++ b/src/components/NotificationList/NotificationItem/DiscussionNotification.tsx @@ -3,13 +3,7 @@ import { Event } from 'nostr-tools' import { useTranslation } from 'react-i18next' import Notification from './Notification' -export function DiscussionNotification({ - notification, - isNew = false -}: { - notification: Event - isNew?: boolean -}) { +export function DiscussionNotification({ notification }: { notification: Event }) { const { t } = useTranslation() // Get the topic from t-tags @@ -19,13 +13,11 @@ export function DiscussionNotification({ return ( } targetEvent={notification} - isNew={isNew} showStats={false} /> ) diff --git a/src/components/NotificationList/NotificationItem/MentionNotification.tsx b/src/components/NotificationList/NotificationItem/MentionNotification.tsx index ac298b5f..c93211cf 100644 --- a/src/components/NotificationList/NotificationItem/MentionNotification.tsx +++ b/src/components/NotificationList/NotificationItem/MentionNotification.tsx @@ -11,13 +11,7 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Notification from './Notification' -export function MentionNotification({ - notification, - isNew = false -}: { - notification: Event - isNew?: boolean -}) { +export function MentionNotification({ notification }: { notification: Event }) { const { t } = useTranslation() const { navigateToNote } = useSmartNoteNavigation() const { pubkey } = useNostr() @@ -31,7 +25,6 @@ export function MentionNotification({ return ( @@ -60,7 +53,6 @@ export function MentionNotification({ description={ isMention ? t('mentioned you in a note') : parentEventId ? '' : t('quoted your note') } - isNew={isNew} showStats /> ) diff --git a/src/components/NotificationList/NotificationItem/Notification.tsx b/src/components/NotificationList/NotificationItem/Notification.tsx index 3012a54b..c96885d4 100644 --- a/src/components/NotificationList/NotificationItem/Notification.tsx +++ b/src/components/NotificationList/NotificationItem/Notification.tsx @@ -10,68 +10,48 @@ import client from '@/services/client.service' import { cn } from '@/lib/utils' import { useSmartNoteNavigation, useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' -import { useNotification } from '@/providers/NotificationContext' import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { NostrEvent } from 'nostr-tools' -import { useMemo } from 'react' -import { useTranslation } from 'react-i18next' export default function Notification({ icon, - notificationId, sender, sentAt, description, middle = null, targetEvent, - isNew = false, showStats = false, rightAction = null }: { icon: React.ReactNode - notificationId: string sender: string sentAt: number description: string middle?: React.ReactNode targetEvent?: NostrEvent - isNew?: boolean showStats?: boolean rightAction?: React.ReactNode }) { - const { t } = useTranslation() const { navigateToNote } = useSmartNoteNavigation() const { push } = useSecondaryPage() const { pubkey } = useNostr() - const { isNotificationRead, markNotificationAsRead } = useNotification() const { notificationListStyle } = useUserPreferences() - const unread = useMemo( - () => isNew && !isNotificationRead(notificationId), - [isNew, isNotificationRead, notificationId] - ) const handleClick = (e: React.MouseEvent) => { - // Don't navigate if clicking on interactive elements (buttons, links, etc.) const target = e.target as HTMLElement if (target.closest('button') || target.closest('[role="button"]') || target.closest('a')) { return } - - // Don't navigate if clicking within NoteStats (which contains the ReplyButton) - // NoteStats is rendered inside the notification, so we need to check for it + if (target.closest('[data-note-stats]')) { return } - - // Don't navigate if a modal/dialog/sheet is currently open - // Check for Radix UI dialog/sheet elements in the DOM - // Radix UI uses data-radix-dialog-content for the dialog content + const hasOpenModal = document.querySelector('[data-radix-dialog-content][data-state="open"]') if (hasOpenModal) { return } - - markNotificationAsRead(notificationId) + if (targetEvent) { client.addEventToCache(targetEvent) navigateToNote(toNote(targetEvent.id)) @@ -91,13 +71,7 @@ export default function Notification({ {icon} {middle} {targetEvent && ( - + )}
@@ -126,26 +100,11 @@ export default function Notification({ />
{description}
-
- {rightAction} - {unread && ( -
+
{rightAction}
{middle} {targetEvent && ( - + )} {showStats && targetEvent && } diff --git a/src/components/NotificationList/NotificationItem/PollResponseNotification.tsx b/src/components/NotificationList/NotificationItem/PollResponseNotification.tsx index cbb4068f..98a117f6 100644 --- a/src/components/NotificationList/NotificationItem/PollResponseNotification.tsx +++ b/src/components/NotificationList/NotificationItem/PollResponseNotification.tsx @@ -6,13 +6,7 @@ import { useMemo } from 'react' import Notification from './Notification' import { useTranslation } from 'react-i18next' -export function PollResponseNotification({ - notification, - isNew = false -}: { - notification: Event - isNew?: boolean -}) { +export function PollResponseNotification({ notification }: { notification: Event }) { const { t } = useTranslation() const eventId = useMemo(() => { const eTag = notification.tags.find(tagNameEquals('e')) @@ -26,13 +20,11 @@ export function PollResponseNotification({ return ( } sender={notification.pubkey} sentAt={notification.created_at} targetEvent={pollEvent} description={t('voted in your poll')} - isNew={isNew} /> ) } diff --git a/src/components/NotificationList/NotificationItem/PublicMessageNotification.tsx b/src/components/NotificationList/NotificationItem/PublicMessageNotification.tsx index bd0ede20..6b345f79 100644 --- a/src/components/NotificationList/NotificationItem/PublicMessageNotification.tsx +++ b/src/components/NotificationList/NotificationItem/PublicMessageNotification.tsx @@ -5,13 +5,7 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Notification from './Notification' -export function PublicMessageNotification({ - notification, - isNew = false -}: { - notification: Event - isNew?: boolean -}) { +export function PublicMessageNotification({ notification }: { notification: Event }) { const { t } = useTranslation() const { pubkey } = useNostr() @@ -43,13 +37,11 @@ export function PublicMessageNotification({ return ( } sender={notification.pubkey} sentAt={notification.created_at} targetEvent={notification} description={description} - isNew={isNew} showStats /> ) diff --git a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx index 13218be6..3f495155 100644 --- a/src/components/NotificationList/NotificationItem/ReactionNotification.tsx +++ b/src/components/NotificationList/NotificationItem/ReactionNotification.tsx @@ -8,13 +8,7 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Notification from './Notification' -export function ReactionNotification({ - notification, - isNew = false -}: { - notification: Event - isNew?: boolean -}) { +export function ReactionNotification({ notification }: { notification: Event }) { const { t } = useTranslation() const { pubkey } = useNostr() const eventId = useMemo(() => { @@ -56,13 +50,11 @@ export function ReactionNotification({ return ( {reaction}} sender={notification.pubkey} sentAt={notification.created_at} targetEvent={event} description={t('reacted to your note')} - isNew={isNew} /> ) } diff --git a/src/components/NotificationList/NotificationItem/RepostNotification.tsx b/src/components/NotificationList/NotificationItem/RepostNotification.tsx index d4e45ea9..6cf6c9f7 100644 --- a/src/components/NotificationList/NotificationItem/RepostNotification.tsx +++ b/src/components/NotificationList/NotificationItem/RepostNotification.tsx @@ -5,13 +5,7 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Notification from './Notification' -export function RepostNotification({ - notification, - isNew = false -}: { - notification: Event - isNew?: boolean -}) { +export function RepostNotification({ notification }: { notification: Event }) { const { t } = useTranslation() const event = useMemo(() => { try { @@ -28,13 +22,11 @@ export function RepostNotification({ return ( } sender={notification.pubkey} sentAt={notification.created_at} targetEvent={event} description={t('reposted your note')} - isNew={isNew} /> ) } diff --git a/src/components/NotificationList/NotificationItem/ZapNotification.tsx b/src/components/NotificationList/NotificationItem/ZapNotification.tsx index 0bb6fc89..288085ed 100644 --- a/src/components/NotificationList/NotificationItem/ZapNotification.tsx +++ b/src/components/NotificationList/NotificationItem/ZapNotification.tsx @@ -7,13 +7,7 @@ import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import Notification from './Notification' -export function ZapNotification({ - notification, - isNew = false -}: { - notification: Event - isNew?: boolean -}) { +export function ZapNotification({ notification }: { notification: Event }) { const { t } = useTranslation() const { senderPubkey, eventId, amount, comment } = useMemo( () => getZapInfoFromEvent(notification) ?? ({} as any), @@ -25,7 +19,6 @@ export function ZapNotification({ return ( } sender={senderPubkey} sentAt={notification.created_at} @@ -36,7 +29,6 @@ export function ZapNotification({ } description={event ? t('zapped your note') : t('zapped you')} - isNew={isNew} /> ) } diff --git a/src/components/NotificationList/NotificationItem/index.tsx b/src/components/NotificationList/NotificationItem/index.tsx index f929b820..352b89cd 100644 --- a/src/components/NotificationList/NotificationItem/index.tsx +++ b/src/components/NotificationList/NotificationItem/index.tsx @@ -14,13 +14,7 @@ import { ReactionNotification } from './ReactionNotification' import { RepostNotification } from './RepostNotification' import { ZapNotification } from './ZapNotification' -export function NotificationItem({ - notification, - isNew = false -}: { - notification: Event - isNew?: boolean -}) { +export function NotificationItem({ notification }: { notification: Event }) { const { pubkey } = useNostr() const { mutePubkeySet } = useMuteList() const { hideContentMentioningMutedUsers } = useContentPolicy() @@ -33,8 +27,7 @@ export function NotificationItem({ hideUntrustedNotifications, isUserTrusted }) - - + return result }, [ notification, @@ -46,13 +39,13 @@ export function NotificationItem({ if (!canShow) return null if (notification.kind === 11) { - return + return } if (notification.kind === kinds.Reaction) { - return + return } if (notification.kind === ExtendedKind.PUBLIC_MESSAGE) { - return + return } if ( notification.kind === kinds.ShortTextNote || @@ -60,16 +53,16 @@ export function NotificationItem({ notification.kind === ExtendedKind.VOICE_COMMENT || notification.kind === ExtendedKind.POLL ) { - return + return } if (notification.kind === kinds.Repost) { - return + return } if (notification.kind === kinds.Zap) { - return + return } if (notification.kind === ExtendedKind.POLL_RESPONSE) { - return + return } return null } diff --git a/src/components/NotificationList/index.tsx b/src/components/NotificationList/index.tsx index 64074654..c821cde0 100644 --- a/src/components/NotificationList/index.tsx +++ b/src/components/NotificationList/index.tsx @@ -3,7 +3,6 @@ import { compareEvents } from '@/lib/event' import logger from '@/lib/logger' import { usePrimaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' -import { useNotification } from '@/providers/NotificationContext' import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import client from '@/services/client.service' @@ -41,10 +40,8 @@ const NotificationList = forwardRef( const { current, display } = usePrimaryPage() const active = useMemo(() => current === 'notifications' && display, [current, display]) const { pubkey, relayList } = useNostr() - const { getNotificationsSeenAt } = useNotification() const { notificationListStyle } = useUserPreferences() const { favoriteRelays } = useFavoriteRelays() - const [lastReadTime, setLastReadTime] = useState(0) const [refreshCount, setRefreshCount] = useState(0) const [timelineKey, setTimelineKey] = useState(undefined) const [loading, setLoading] = useState(true) @@ -132,7 +129,6 @@ const NotificationList = forwardRef( setLoading(true) setNotifications([]) setShowCount(SHOW_COUNT) - setLastReadTime(getNotificationsSeenAt()) // Use proper fallback hierarchy: user's read/inbox relays → favorite relays → fast read relays const userRelayList = relayList || { read: [], write: [] } const userReadRelays = userRelayList.read || [] @@ -295,11 +291,7 @@ const NotificationList = forwardRef( const list = (
{visibleNotifications.map((notification) => ( - lastReadTime} - /> + ))}
{until || loading ? ( diff --git a/src/components/Sidebar/NotificationButton.tsx b/src/components/Sidebar/NotificationButton.tsx index 5f1392b9..63b36187 100644 --- a/src/components/Sidebar/NotificationButton.tsx +++ b/src/components/Sidebar/NotificationButton.tsx @@ -1,6 +1,5 @@ import { usePrimaryPage, usePrimaryNoteView } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' -import { useNotification } from '@/providers/NotificationContext' import { Bell } from 'lucide-react' import SidebarItem from './SidebarItem' @@ -8,7 +7,6 @@ export default function NotificationsButton() { const { checkLogin } = useNostr() const { navigate, current, display } = usePrimaryPage() const { primaryViewType } = usePrimaryNoteView() - const { hasNewNotification } = useNotification() return ( checkLogin(() => navigate('notifications'))} active={display && current === 'notifications' && primaryViewType === null} > -
- - {hasNewNotification && ( -
- )} -
+ ) } diff --git a/src/constants.ts b/src/constants.ts index 428fbb08..b5a3c7c7 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -38,7 +38,6 @@ export const StorageKey = { DEFAULT_ZAP_COMMENT: 'defaultZapComment', QUICK_ZAP: 'quickZap', ZAP_REPLY_THRESHOLD: 'zapReplyThreshold', - LAST_READ_NOTIFICATION_TIME_MAP: 'lastReadNotificationTimeMap', ACCOUNT_FEED_INFO_MAP: 'accountFeedInfoMap', AUTOPLAY: 'autoplay', HIDE_UNTRUSTED_INTERACTIONS: 'hideUntrustedInteractions', @@ -85,10 +84,6 @@ export const FONT_SIZE = { LARGE: 'large' } as const -export const ApplicationDataKey = { - NOTIFICATIONS_SEEN_AT: 'seen_notifications_at' -} - export const BIG_RELAY_URLS = [ 'wss://theforest.nostr1.com', 'wss://orly-relay.imwald.eu', diff --git a/src/lib/draft-event.ts b/src/lib/draft-event.ts index a880822c..445890c9 100644 --- a/src/lib/draft-event.ts +++ b/src/lib/draft-event.ts @@ -1,4 +1,4 @@ -import { ApplicationDataKey, EMBEDDED_EVENT_REGEX, ExtendedKind, POLL_TYPE } from '@/constants' +import { EMBEDDED_EVENT_REGEX, ExtendedKind, POLL_TYPE } from '@/constants' import client from '@/services/client.service' import customEmojiService from '@/services/custom-emoji.service' import mediaUpload from '@/services/media-upload.service' @@ -737,15 +737,6 @@ export function createBlockedRelaysDraftEvent(blockedRelays: string[]): TDraftEv } } -export function createSeenNotificationsAtDraftEvent(): TDraftEvent { - return { - kind: kinds.Application, - content: 'Records read time to sync notification status across devices.', - tags: [buildDTag(ApplicationDataKey.NOTIFICATIONS_SEEN_AT)], - created_at: dayjs().unix() - } -} - export function createBookmarkDraftEvent(tags: string[][], content = ''): TDraftEvent { return { kind: kinds.BookmarkList, diff --git a/src/main.tsx b/src/main.tsx index f46c4ff2..91e1ca45 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -28,11 +28,12 @@ setVh() const SESSION_STORAGE_KEY = 'jumble:session' async function bootstrap() { + // Always defined: fetch does not throw on 4xx/5xx, so non-OK responses must not leave this unset. + window.__RUNTIME_CONFIG__ = {} try { const r = await fetch('/config.json') if (r.ok) { - const config = (await r.json()) as { NIP66_MONITOR_NPUB?: string } - window.__RUNTIME_CONFIG__ = config + window.__RUNTIME_CONFIG__ = (await r.json()) as { NIP66_MONITOR_NPUB?: string } } } catch { window.__RUNTIME_CONFIG__ = {} diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index bb545f38..dde1d730 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -1,19 +1,14 @@ import LoginDialog from '@/components/LoginDialog' -import { ApplicationDataKey, BIG_RELAY_URLS, ExtendedKind, FAST_WRITE_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants' +import { BIG_RELAY_URLS, ExtendedKind, FAST_WRITE_RELAY_URLS, PROFILE_FETCH_RELAY_URLS, PROFILE_RELAY_URLS } from '@/constants' import { buildAltTag, buildClientTag, createDeletionRequestDraftEvent, createFollowListDraftEvent, createMuteListDraftEvent, - createRelayListDraftEvent, - createSeenNotificationsAtDraftEvent + createRelayListDraftEvent } from '@/lib/draft-event' -import { - getLatestEvent, - getReplaceableEventIdentifier, - minePow -} from '@/lib/event' +import { getLatestEvent, minePow } from '@/lib/event' import { getProfileFromEvent, getRelayListFromEvent } from '@/lib/event-metadata' import logger from '@/lib/logger' import { normalizeUrl } from '@/lib/url' @@ -63,7 +58,6 @@ type TNostrContext = { blockedRelaysEvent: Event | null userEmojiListEvent: Event | null rssFeedListEvent: Event | null - notificationsSeenAt: number account: TAccountPointer | null accounts: TAccountPointer[] nsec: string | null @@ -97,13 +91,10 @@ type TNostrContext = { updateFavoriteRelaysEvent: (favoriteRelaysEvent: Event) => Promise updateBlockedRelaysEvent: (blockedRelaysEvent: Event) => Promise updateRssFeedListEvent: (rssFeedListEvent: Event) => Promise - updateNotificationsSeenAt: (skipPublish?: boolean) => Promise } const NostrContext = createContext(undefined) -const lastPublishedSeenNotificationsAtEventAtMap = new Map() - export const useNostr = () => { const context = useContext(NostrContext) if (!context) { @@ -172,7 +163,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { const [blockedRelaysEvent, setBlockedRelaysEvent] = useState(null) const [userEmojiListEvent, setUserEmojiListEvent] = useState(null) const [rssFeedListEvent, setRssFeedListEvent] = useState(null) - const [notificationsSeenAt, setNotificationsSeenAt] = useState(-1) const [isInitialized, setIsInitialized] = useState(false) useEffect(() => { @@ -215,7 +205,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { setMuteListEvent(null) setBookmarkListEvent(null) setRssFeedListEvent(null) - setNotificationsSeenAt(-1) if (!account) { return } @@ -234,8 +223,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { setNcryptsec(null) } - const storedNotificationsSeenAt = storage.getLastReadNotificationTime(account.pubkey) - const [ storedRelayListEvent, storedCacheRelayListEvent, @@ -434,11 +421,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { kinds.UserEmojiList ], authors: [account.pubkey] - }, - { - kinds: [kinds.Application], - authors: [account.pubkey], - '#d': [ApplicationDataKey.NOTIFICATIONS_SEEN_AT] } ]) const sortedEvents = events.sort((a, b) => b.created_at - a.created_at) @@ -453,11 +435,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { (e) => e.kind === ExtendedKind.BLOSSOM_SERVER_LIST ) const userEmojiListEvent = sortedEvents.find((e) => e.kind === kinds.UserEmojiList) - const notificationsSeenAtEvent = sortedEvents.find( - (e) => - e.kind === kinds.Application && - getReplaceableEventIdentifier(e) === ApplicationDataKey.NOTIFICATIONS_SEEN_AT - ) if (profileEvent) { const updatedProfileEvent = await indexedDb.putReplaceableEvent(profileEvent) if (updatedProfileEvent.id === profileEvent.id) { @@ -534,13 +511,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { } } - const notificationsSeenAt = Math.max( - notificationsSeenAtEvent?.created_at ?? 0, - storedNotificationsSeenAt - ) - setNotificationsSeenAt(notificationsSeenAt) - storage.setLastReadNotificationTime(account.pubkey, notificationsSeenAt) - client.initUserIndexFromFollowings(account.pubkey, controller.signal) return controller } @@ -1123,37 +1093,6 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { setRssFeedListEvent(newRssFeedListEvent) } - /** Updates local “last read” time and optionally publishes kind 30078 (notification seen-at) for cross-device sync. - * Relay list: user’s write relays (first 5) or FAST_WRITE_RELAY_URLS; read-only relays are excluded (see client.determineTargetRelays for kind 30078). */ - const updateNotificationsSeenAt = async (skipPublish = false) => { - if (!account) return - - const now = dayjs().unix() - storage.setLastReadNotificationTime(account.pubkey, now) - setTimeout(() => { - setNotificationsSeenAt(now) - }, 5_000) - - // Prevent too frequent requests for signing seen notifications events - const lastPublishedSeenNotificationsAtEventAt = - lastPublishedSeenNotificationsAtEventAtMap.get(account.pubkey) ?? -1 - if ( - !skipPublish && - (lastPublishedSeenNotificationsAtEventAt < 0 || - now - lastPublishedSeenNotificationsAtEventAt > 10 * 60) // 10 minutes - ) { - try { - await publish(createSeenNotificationsAtDraftEvent()) - lastPublishedSeenNotificationsAtEventAtMap.set(account.pubkey, now) - } catch (err) { - // Notification seen-at sync is best-effort; local state already updated above - logger.warn('[updateNotificationsSeenAt] Publish failed (sync across devices may be delayed)', { - error: err instanceof Error ? err.message : String(err) - }) - } - } - } - return ( {children} diff --git a/src/providers/NotificationContext.tsx b/src/providers/NotificationContext.tsx deleted file mode 100644 index ea2e78a7..00000000 --- a/src/providers/NotificationContext.tsx +++ /dev/null @@ -1,18 +0,0 @@ -import { createContext, useContext } from 'react' - -export type TNotificationContext = { - hasNewNotification: boolean - getNotificationsSeenAt: () => number - isNotificationRead: (id: string) => boolean - markNotificationAsRead: (id: string) => void -} - -export const NotificationContext = createContext(undefined) - -export const useNotification = () => { - const context = useContext(NotificationContext) - if (!context) { - throw new Error('useNotification must be used within a NotificationProvider') - } - return context -} diff --git a/src/providers/NotificationProvider.tsx b/src/providers/NotificationProvider.tsx index 57a8f6f6..55a9ea7a 100644 --- a/src/providers/NotificationProvider.tsx +++ b/src/providers/NotificationProvider.tsx @@ -1,88 +1,29 @@ import { ExtendedKind, FAST_READ_RELAY_URLS } from '@/constants' import { compareEvents } from '@/lib/event' import logger from '@/lib/logger' -import { notificationFilter } from '@/lib/notification' -import { usePrimaryPage } from '@/PageManager' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import client from '@/services/client.service' -import storage from '@/services/local-storage.service' import { kinds, NostrEvent } from 'nostr-tools' import { SubCloser } from 'nostr-tools/abstract-pool' -import { useCallback, useEffect, useMemo, useState } from 'react' -import { useContentPolicy } from './ContentPolicyProvider' -import { useMuteList } from './MuteListProvider' +import { useEffect, useRef } from 'react' import { useNostr } from './NostrProvider' -import { useUserTrust } from './UserTrustProvider' -import { NotificationContext } from './NotificationContext' +/** + * 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 { current } = usePrimaryPage() - const active = useMemo(() => current === 'notifications', [current]) - const { pubkey, relayList, notificationsSeenAt, updateNotificationsSeenAt } = useNostr() + const { pubkey, relayList } = useNostr() const { favoriteRelays } = useFavoriteRelays() - const { hideUntrustedNotifications, isUserTrusted } = useUserTrust() - const { mutePubkeySet } = useMuteList() - const { hideContentMentioningMutedUsers } = useContentPolicy() - // const { getSubscribedTopics } = useInterestList() // No longer needed since we subscribe to all discussions - const [newNotifications, setNewNotifications] = useState([]) - const [readNotificationIdSet, setReadNotificationIdSet] = useState>(new Set()) - const filteredNewNotifications = useMemo(() => { - if (active || notificationsSeenAt < 0) { - return [] - } - const filtered: NostrEvent[] = [] - for (const notification of newNotifications) { - if (notification.created_at <= notificationsSeenAt || filtered.length >= 10) { - break - } - if ( - !notificationFilter(notification, { - pubkey, - mutePubkeySet, - hideContentMentioningMutedUsers, - hideUntrustedNotifications, - isUserTrusted - }) - ) { - continue - } - filtered.push(notification) - } - return filtered - }, [ - newNotifications, - notificationsSeenAt, - mutePubkeySet, - hideContentMentioningMutedUsers, - hideUntrustedNotifications, - isUserTrusted, - active - ]) - - // Defer so we don't trigger state updates during the same commit as consumer renders (avoids "Cannot update NotificationList while rendering NotificationProvider") - useEffect(() => { - let t2: ReturnType | null = null - const t = setTimeout(() => { - setNewNotifications([]) - t2 = setTimeout(() => { - updateNotificationsSeenAt(!active) - }, 0) - }, 0) - return () => { - clearTimeout(t) - if (t2 !== null) clearTimeout(t2) - } - }, [active, updateNotificationsSeenAt]) + const notificationBufferRef = useRef([]) useEffect(() => { if (!pubkey) return const deferredReset = setTimeout(() => { - setNewNotifications([]) - setReadNotificationIdSet(new Set()) + notificationBufferRef.current = [] }, 0) - // Track if component is mounted const isMountedRef = { current: true } const subCloserRef: { current: SubCloser | null @@ -104,45 +45,38 @@ export function NotificationProvider({ children }: { children: React.ReactNode } try { let eosed = false - // Use proper fallback hierarchy: user's read/inbox relays → favorite relays → fast read relays const userRelayList = relayList || { read: [], write: [] } const userReadRelays = userRelayList.read || [] const userFavoriteRelays = favoriteRelays || [] - - // Build relay list with proper fallback hierarchy + let notificationRelays: string[] = [] - + if (userReadRelays.length > 0) { - // Priority 1: User's read/inbox relays (kind 10002) notificationRelays = userReadRelays.slice(0, 5) - logger.component('NotificationProvider', 'Using user read relays', { - count: notificationRelays.length, - relays: notificationRelays.slice(0, 3) // Show first 3 for brevity + logger.component('NotificationProvider', 'Using user read relays', { + count: notificationRelays.length, + relays: notificationRelays.slice(0, 3) }) } else if (userFavoriteRelays.length > 0) { - // Priority 2: User's favorite relays (kind 10012) notificationRelays = userFavoriteRelays.slice(0, 5) - logger.component('NotificationProvider', 'Using user favorite relays', { - count: notificationRelays.length, - relays: notificationRelays.slice(0, 3) // Show first 3 for brevity + logger.component('NotificationProvider', 'Using user favorite relays', { + count: notificationRelays.length, + relays: notificationRelays.slice(0, 3) }) } else { - // Priority 3: Fast read relays (reliable defaults) notificationRelays = FAST_READ_RELAY_URLS.slice(0, 5) - logger.component('NotificationProvider', 'Using fast read relays fallback', { - count: notificationRelays.length, - relays: notificationRelays.slice(0, 3) // Show first 3 for brevity + logger.component('NotificationProvider', 'Using fast read relays fallback', { + count: notificationRelays.length, + relays: notificationRelays.slice(0, 3) }) } - - // Subscribe to discussion notifications (kind 11) - // Subscribe to all discussions, not just subscribed topics + let discussionEosed = false const discussionSubCloser = client.subscribe( notificationRelays, [ { - kinds: [11], // Discussion threads + kinds: [11], limit: 20 } ], @@ -153,26 +87,24 @@ export function NotificationProvider({ children }: { children: React.ReactNode } } }, onevent: (evt) => { - // Don't notify about our own threads if (evt.pubkey !== pubkey) { - setNewNotifications((prev) => { - if (!discussionEosed) { - return [evt, ...prev] - } - if (prev.length && compareEvents(prev[0], evt) >= 0) { - return prev - } - - client.emitNewEvent(evt) - return [evt, ...prev] - }) + const prev = notificationBufferRef.current + if (!discussionEosed) { + notificationBufferRef.current = [evt, ...prev] + return + } + if (prev.length && compareEvents(prev[0], evt) >= 0) { + return + } + + client.emitNewEvent(evt) + notificationBufferRef.current = [evt, ...prev] } } } ) topicSubCloserRef.current = discussionSubCloser - - // Regular notifications subscription + const subCloser = client.subscribe( notificationRelays, [ @@ -196,24 +128,24 @@ export function NotificationProvider({ children }: { children: React.ReactNode } oneose: (e) => { if (e) { eosed = e - setNewNotifications((prev) => { - return [...prev.sort((a, b) => compareEvents(b, a))] - }) + notificationBufferRef.current = [ + ...notificationBufferRef.current.sort((a, b) => compareEvents(b, a)) + ] } }, onevent: (evt) => { if (evt.pubkey !== pubkey) { - setNewNotifications((prev) => { - if (!eosed) { - return [evt, ...prev] - } - if (prev.length && compareEvents(prev[0], evt) >= 0) { - return prev - } - - client.emitNewEvent(evt) - return [evt, ...prev] - }) + const prev = notificationBufferRef.current + if (!eosed) { + notificationBufferRef.current = [evt, ...prev] + return + } + if (prev.length && compareEvents(prev[0], evt) >= 0) { + return + } + + client.emitNewEvent(evt) + notificationBufferRef.current = [evt, ...prev] } }, onAllClose: (reasons) => { @@ -221,15 +153,13 @@ export function NotificationProvider({ children }: { children: React.ReactNode } return } - // Only reconnect if still mounted and not a manual close - // Increase timeout to prevent rapid reconnection loops if (isMountedRef.current) { setTimeout(() => { if (isMountedRef.current) { logger.debug('[NotificationProvider] Reconnecting after close...') subscribe() } - }, 15_000) // Increased from 5s to 15s + }, 15_000) } } } @@ -240,7 +170,6 @@ export function NotificationProvider({ children }: { children: React.ReactNode } } catch (error) { logger.error('Subscription error', { error }) - // Retry on error if still mounted if (isMountedRef.current) { setTimeout(() => { if (isMountedRef.current) { @@ -252,10 +181,8 @@ export function NotificationProvider({ children }: { children: React.ReactNode } } } - // Initial subscription subscribe() - // Cleanup function return () => { clearTimeout(deferredReset) isMountedRef.current = false @@ -268,93 +195,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } topicSubCloserRef.current = null } } - }, [pubkey]) - - useEffect(() => { - const newNotificationCount = filteredNewNotifications.length - - // Update title - if (newNotificationCount > 0) { - document.title = `(${newNotificationCount >= 10 ? '9+' : newNotificationCount}) Jumble` - } else { - document.title = 'Jumble' - } - - // Update favicons - const favicons = document.querySelectorAll("link[rel*='icon']") - if (!favicons.length) return - - const treeFavicon = "data:image/svg+xml,🌲" - - if (newNotificationCount === 0) { - favicons.forEach((favicon) => { - favicon.href = treeFavicon - }) - } else { - // Create a canvas with the tree emoji and a notification badge - const canvas = document.createElement('canvas') - const size = 64 - canvas.width = size - canvas.height = size - const ctx = canvas.getContext('2d', { willReadFrequently: true }) // Optimize for frequent readback operations - if (!ctx) return - - // Draw tree emoji as text - ctx.font = `${size * 0.9}px Arial` - ctx.textBaseline = 'middle' - ctx.textAlign = 'center' - ctx.fillText('🌲', size / 2, size / 2) - - // Draw red notification badge - const r = size * 0.16 - ctx.beginPath() - ctx.arc(size - r - 6, r + 6, r, 0, 2 * Math.PI) - ctx.fillStyle = '#FF0000' - ctx.fill() - - favicons.forEach((favicon) => { - favicon.href = canvas.toDataURL('image/png') - }) - } - }, [filteredNewNotifications]) - - const getNotificationsSeenAt = useCallback(() => { - if (notificationsSeenAt >= 0) { - return notificationsSeenAt - } - if (pubkey) { - return storage.getLastReadNotificationTime(pubkey) - } - return 0 - }, [notificationsSeenAt, pubkey]) - - const isNotificationRead = useCallback( - (notificationId: string): boolean => readNotificationIdSet.has(notificationId), - [readNotificationIdSet] - ) - - const markNotificationAsRead = useCallback((notificationId: string) => { - setReadNotificationIdSet((prev) => new Set([...prev, notificationId])) - }, []) - - const value = useMemo( - () => ({ - hasNewNotification: filteredNewNotifications.length > 0, - getNotificationsSeenAt, - isNotificationRead, - markNotificationAsRead - }), - [ - filteredNewNotifications.length, - getNotificationsSeenAt, - isNotificationRead, - markNotificationAsRead - ] - ) + }, [pubkey, relayList, favoriteRelays]) - return ( - - {children} - - ) + return <>{children} } diff --git a/src/services/client.service.ts b/src/services/client.service.ts index 42c32902..aefbaa25 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -269,19 +269,6 @@ class ClientService extends EventTarget { return relays.length > 0 ? relays : [...FAST_WRITE_RELAY_URLS] } - // Notification seen-at (kind 30078): use only user write relays to avoid paid/slow relays - if (event.kind === kinds.Application) { - const dTag = event.tags.find((t) => t[0] === 'd')?.[1] - if (dTag === 'seen_notifications_at') { - const relayList = await this.fetchRelayList(event.pubkey).catch(() => ({ write: [] as string[], read: [] as string[] })) - const userWrite = (relayList?.write ?? []).slice(0, 5).map((url) => normalizeUrl(url)).filter(Boolean) as string[] - const list = userWrite.length > 0 ? userWrite : [...FAST_WRITE_RELAY_URLS] - const readOnlySet = new Set(READ_ONLY_RELAY_URLS.map((u) => normalizeUrl(u) || u)) - const filtered = list.filter((url) => !readOnlySet.has(normalizeUrl(url) || url)) - return filtered.length > 0 ? filtered : [...FAST_WRITE_RELAY_URLS] - } - } - let relays: string[] if (specifiedRelayUrls?.length) { relays = specifiedRelayUrls diff --git a/src/services/local-storage.service.ts b/src/services/local-storage.service.ts index a60970b6..b3311f4b 100644 --- a/src/services/local-storage.service.ts +++ b/src/services/local-storage.service.ts @@ -38,7 +38,6 @@ const SETTINGS_KEYS = [ StorageKey.DEFAULT_ZAP_COMMENT, StorageKey.QUICK_ZAP, StorageKey.ZAP_REPLY_THRESHOLD, - StorageKey.LAST_READ_NOTIFICATION_TIME_MAP, StorageKey.ACCOUNT_FEED_INFO_MAP, StorageKey.AUTOPLAY, StorageKey.HIDE_UNTRUSTED_INTERACTIONS, @@ -79,7 +78,6 @@ class LocalStorageService { private accounts: TAccount[] = [] private currentAccount: TAccount | null = null private noteListMode: TNoteListMode = 'posts' - private lastReadNotificationTimeMap: Record = {} private defaultZapSats: number = 21 private defaultZapComment: string = 'Zap!' private quickZap: boolean = false @@ -138,10 +136,6 @@ class LocalStorageService { noteListModeStr && ['posts', 'postsAndReplies', 'pictures'].includes(noteListModeStr) ? (noteListModeStr as TNoteListMode) : 'posts' - const lastReadNotificationTimeMapStr = - window.localStorage.getItem(StorageKey.LAST_READ_NOTIFICATION_TIME_MAP) ?? '{}' - this.lastReadNotificationTimeMap = JSON.parse(lastReadNotificationTimeMapStr) - const relaySetsStr = window.localStorage.getItem(StorageKey.RELAY_SETS) if (!relaySetsStr) { let relaySets: TRelaySet[] = [] @@ -462,8 +456,6 @@ class LocalStorageService { if (accountsStr != null) this.accounts = JSON.parse(accountsStr) as TAccount[] const currentAccountStr = get(StorageKey.CURRENT_ACCOUNT) if (currentAccountStr != null) this.currentAccount = JSON.parse(currentAccountStr) as TAccount | null - const lastReadStr = get(StorageKey.LAST_READ_NOTIFICATION_TIME_MAP) - if (lastReadStr != null) this.lastReadNotificationTimeMap = JSON.parse(lastReadStr) as Record const relaySetsStr = get(StorageKey.RELAY_SETS) if (relaySetsStr != null) this.relaySets = JSON.parse(relaySetsStr) as TRelaySet[] const defaultZapSatsStr = get(StorageKey.DEFAULT_ZAP_SATS) @@ -676,18 +668,6 @@ class LocalStorageService { this.persistSetting(StorageKey.ZAP_REPLY_THRESHOLD, sats.toString()) } - getLastReadNotificationTime(pubkey: string) { - return this.lastReadNotificationTimeMap[pubkey] ?? 0 - } - - setLastReadNotificationTime(pubkey: string, time: number) { - this.lastReadNotificationTimeMap[pubkey] = time - this.persistSetting( - StorageKey.LAST_READ_NOTIFICATION_TIME_MAP, - JSON.stringify(this.lastReadNotificationTimeMap) - ) - } - getFeedInfo(pubkey: string) { return this.accountFeedInfoMap[pubkey] }