From 240d08b50f8964a0945ef4b6dc333da35827e564 Mon Sep 17 00:00:00 2001 From: Silberengel Date: Sat, 11 Oct 2025 00:10:17 +0200 Subject: [PATCH] discussion topic subscriptions --- src/App.tsx | 20 ++--- src/PageManager.tsx | 2 +- .../NotificationItem/index.tsx | 16 +++- src/components/NotificationList/index.tsx | 61 +++++++++++--- src/providers/InterestListProvider.tsx | 14 +++- src/providers/NostrProvider/index.tsx | 34 ++++++-- src/providers/NotificationProvider.tsx | 82 ++++++++++--------- src/services/client.service.ts | 6 +- src/services/indexed-db.service.ts | 2 +- 9 files changed, 162 insertions(+), 75 deletions(-) diff --git a/src/App.tsx b/src/App.tsx index a265c9a..2c8de70 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -38,16 +38,16 @@ export default function App(): JSX.Element { - - - - - - - - - - + + + + + + + + + + diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 966cd13..7c69c91 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -3,6 +3,7 @@ import { cn } from '@/lib/utils' import NoteListPage from '@/pages/primary/NoteListPage' import HomePage from '@/pages/secondary/HomePage' import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider' +import { NotificationProvider } from '@/providers/NotificationProvider' import { TPageRef } from '@/types' import { cloneElement, @@ -25,7 +26,6 @@ import ProfilePage from './pages/primary/ProfilePage' import RelayPage from './pages/primary/RelayPage' import SearchPage from './pages/primary/SearchPage' import DiscussionsPage from './pages/primary/DiscussionsPage' -import { NotificationProvider } from './providers/NotificationProvider' import { useScreenSize } from './providers/ScreenSizeProvider' import { routes } from './routes' import modalManager from './services/modal-manager.service' diff --git a/src/components/NotificationList/NotificationItem/index.tsx b/src/components/NotificationList/NotificationItem/index.tsx index 0bd8051..2a13484 100644 --- a/src/components/NotificationList/NotificationItem/index.tsx +++ b/src/components/NotificationList/NotificationItem/index.tsx @@ -26,13 +26,27 @@ export function NotificationItem({ const { hideContentMentioningMutedUsers } = useContentPolicy() const { hideUntrustedNotifications, isUserTrusted } = useUserTrust() const canShow = useMemo(() => { - return notificationFilter(notification, { + const result = notificationFilter(notification, { pubkey, mutePubkeySet, hideContentMentioningMutedUsers, hideUntrustedNotifications, isUserTrusted }) + + if (notification.kind === 11) { + console.log('🔍 Discussion notification filter result:', { + id: notification.id, + kind: notification.kind, + canShow: result, + pubkey: notification.pubkey, + isMuted: mutePubkeySet.has(notification.pubkey), + hideUntrusted: hideUntrustedNotifications, + isTrusted: isUserTrusted(notification.pubkey) + }) + } + + return result }, [ notification, mutePubkeySet, diff --git a/src/components/NotificationList/index.tsx b/src/components/NotificationList/index.tsx index c169cfe..b43235c 100644 --- a/src/components/NotificationList/index.tsx +++ b/src/components/NotificationList/index.tsx @@ -56,7 +56,8 @@ const NotificationList = forwardRef((_, ref) => { ExtendedKind.COMMENT, ExtendedKind.VOICE_COMMENT, ExtendedKind.POLL, - ExtendedKind.PUBLIC_MESSAGE + ExtendedKind.PUBLIC_MESSAGE, + 11 // Discussion threads ] case 'reactions': return [kinds.Reaction, kinds.Repost, ExtendedKind.POLL_RESPONSE] @@ -72,7 +73,8 @@ const NotificationList = forwardRef((_, ref) => { ExtendedKind.POLL_RESPONSE, ExtendedKind.VOICE_COMMENT, ExtendedKind.POLL, - ExtendedKind.PUBLIC_MESSAGE + ExtendedKind.PUBLIC_MESSAGE, + 11 // Discussion threads ] } }, [notificationType]) @@ -121,21 +123,48 @@ const NotificationList = forwardRef((_, ref) => { setLastReadTime(getNotificationsSeenAt()) const relayList = await client.fetchRelayList(pubkey) - const { closer, timelineKey } = await client.subscribeTimeline( - [ - { - urls: relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS, - filter: { - '#p': [pubkey], - kinds: filterKinds, - limit: LIMIT - } + // Create separate subscriptions for different notification types + const subscriptions = [] + + // Subscription for mentions (events where user is in p-tags) + const mentionKinds = filterKinds.filter(kind => kind !== 11) + if (mentionKinds.length > 0) { + subscriptions.push({ + urls: relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS, + filter: { + '#p': [pubkey], + kinds: mentionKinds, + limit: LIMIT + } + }) + } + + // Separate subscription for discussion notifications (kind 11) - no p-tag requirement + if (filterKinds.includes(11)) { + subscriptions.push({ + urls: relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS, + filter: { + kinds: [11], + limit: LIMIT } - ], + }) + } + + const { closer, timelineKey } = await client.subscribeTimeline( + subscriptions, { onEvents: (events, eosed) => { if (events.length > 0) { - setNotifications(events.filter((event) => event.pubkey !== pubkey)) + console.log('📋 NotificationList received events:', events.map(e => ({ + id: e.id, + kind: e.kind, + pubkey: e.pubkey, + content: e.content.substring(0, 30) + '...' + }))) + + const filteredEvents = events.filter((event) => event.pubkey !== pubkey) + console.log('📋 After filtering own events:', filteredEvents.length, 'events') + setNotifications(filteredEvents) } if (eosed) { setLoading(false) @@ -144,6 +173,12 @@ const NotificationList = forwardRef((_, ref) => { } }, onNew: (event) => { + console.log('📋 NotificationList onNew event:', { + id: event.id, + kind: event.kind, + pubkey: event.pubkey, + content: event.content.substring(0, 30) + '...' + }) handleNewEvent(event) } } diff --git a/src/providers/InterestListProvider.tsx b/src/providers/InterestListProvider.tsx index 9d406c4..3341686 100644 --- a/src/providers/InterestListProvider.tsx +++ b/src/providers/InterestListProvider.tsx @@ -67,30 +67,42 @@ export function InterestListProvider({ children }: { children: React.ReactNode } } const subscribe = async (topic: string) => { + console.log('[InterestListProvider] subscribe called:', { topic, accountPubkey, changing }) if (!accountPubkey || changing) return const normalizedTopic = normalizeTopic(topic) if (subscribedTopics.has(normalizedTopic)) { + console.log('[InterestListProvider] Already subscribed to topic') return } setChanging(true) try { + console.log('[InterestListProvider] Fetching existing interest list event') const interestListEvent = await client.fetchInterestListEvent(accountPubkey) + console.log('[InterestListProvider] Existing interest list event:', interestListEvent) + const currentTopics = interestListEvent ? interestListEvent.tags .filter(tag => tag[0] === 't' && tag[1]) .map(tag => normalizeTopic(tag[1])) : [] + console.log('[InterestListProvider] Current topics:', currentTopics) + if (currentTopics.includes(normalizedTopic)) { - // Already subscribed + console.log('[InterestListProvider] Already subscribed to topic (from event)') return } const newTopics = [...currentTopics, normalizedTopic] + console.log('[InterestListProvider] Creating new interest list with topics:', newTopics) + const newInterestListEvent = await publishNewInterestListEvent(newTopics) + console.log('[InterestListProvider] Published new interest list event:', newInterestListEvent) + await updateInterestListEvent(newInterestListEvent) + console.log('[InterestListProvider] Updated interest list event in state') toast.success(t('Subscribed to topic')) } catch (error) { diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index 0fdad10..275675d 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -31,7 +31,7 @@ import { } from '@/types' import { hexToBytes } from '@noble/hashes/utils' import dayjs from 'dayjs' -import { Event, kinds, VerifiedEvent } from 'nostr-tools' +import { Event, kinds, VerifiedEvent, validateEvent } from 'nostr-tools' import * as nip19 from 'nostr-tools/nip19' import * as nip49 from 'nostr-tools/nip49' import { createContext, useContext, useEffect, useState } from 'react' @@ -237,7 +237,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { } setRelayList(relayList) - const events = await client.fetchEvents(relayList.write.concat(BIG_RELAY_URLS).slice(0, 4), [ + const fetchRelays = relayList.write.concat(BIG_RELAY_URLS).slice(0, 4) + const events = await client.fetchEvents(fetchRelays, [ { kinds: [ kinds.Metadata, @@ -607,6 +608,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { if (!event) { throw new Error('sign event failed') } + + // Validate the event before publishing + const isValid = validateEvent(event) + if (!isValid) { + console.error('Event validation failed:', event) + throw new Error('Event validation failed - invalid signature or format. Please try logging in again.') + } + return event as VerifiedEvent } @@ -617,6 +626,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { if (!account || !signer || account.signerType === 'npub') { throw new Error('You need to login first') } + + // Validate account state before publishing + if (!account.pubkey || account.pubkey.length !== 64) { + throw new Error('Invalid account state - pubkey is missing or invalid') + } const draft = JSON.parse(JSON.stringify(draftEvent)) as TDraftEvent let event: VerifiedEvent @@ -645,21 +659,25 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { try { const publishResult = await client.publishEvent(relays, event) - console.log('Publish result:', publishResult) - // Store relay status for display if (publishResult.relayStatuses.length > 0) { - // We'll pass this to the UI components that need it (event as any).relayStatuses = publishResult.relayStatuses - console.log('Attached relay statuses to event:', (event as any).relayStatuses) } return event } catch (error) { - // Even if publishing fails, try to extract relay statuses from the error + // Check for authentication-related errors if (error instanceof AggregateError && (error as any).relayStatuses) { (event as any).relayStatuses = (error as any).relayStatuses - console.log('Attached relay statuses from error:', (event as any).relayStatuses) + + // Check if any relay returned an "invalid key" error + const invalidKeyErrors = (error as any).relayStatuses.filter( + (status: any) => status.error && status.error.includes('invalid key') + ) + + if (invalidKeyErrors.length > 0) { + throw new Error('Authentication failed - invalid key. Please try logging out and logging in again.') + } } // Re-throw the error so the UI can handle it appropriately diff --git a/src/providers/NotificationProvider.tsx b/src/providers/NotificationProvider.tsx index a4b8039..96bbea6 100644 --- a/src/providers/NotificationProvider.tsx +++ b/src/providers/NotificationProvider.tsx @@ -11,7 +11,7 @@ import { useContentPolicy } from './ContentPolicyProvider' import { useMuteList } from './MuteListProvider' import { useNostr } from './NostrProvider' import { useUserTrust } from './UserTrustProvider' -import { useInterestList } from './InterestListProvider' +// import { useInterestList } from './InterestListProvider' // No longer needed type TNotificationContext = { hasNewNotification: boolean @@ -37,7 +37,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } const { hideUntrustedNotifications, isUserTrusted } = useUserTrust() const { mutePubkeySet } = useMuteList() const { hideContentMentioningMutedUsers } = useContentPolicy() - const { getSubscribedTopics } = useInterestList() + // 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(() => { @@ -109,45 +109,50 @@ export function NotificationProvider({ children }: { children: React.ReactNode } const relayList = await client.fetchRelayList(pubkey) const notificationRelays = relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS - // Subscribe to subscribed topics (kind 11 discussions) - const subscribedTopics = getSubscribedTopics() - if (subscribedTopics.length > 0) { - let topicEosed = false - const topicSubCloser = client.subscribe( - notificationRelays, - [ - { - kinds: [11], // Discussion threads - '#t': subscribedTopics, - limit: 10 - } - ], + // Subscribe to discussion notifications (kind 11) + // Subscribe to all discussions, not just subscribed topics + let discussionEosed = false + const discussionSubCloser = client.subscribe( + notificationRelays, + [ { - oneose: (e) => { - if (e) { - topicEosed = e - } - }, - onevent: (evt) => { - // Don't notify about our own threads - if (evt.pubkey !== pubkey) { - setNewNotifications((prev) => { - if (!topicEosed) { - return [evt, ...prev] - } - if (prev.length && compareEvents(prev[0], evt) >= 0) { - return prev - } - - client.emitNewEvent(evt) + kinds: [11], // Discussion threads + limit: 20 + } + ], + { + oneose: (e) => { + if (e) { + discussionEosed = e + } + }, + onevent: (evt) => { + // Don't notify about our own threads + if (evt.pubkey !== pubkey) { + console.log('📢 Discussion notification received:', { + id: evt.id, + pubkey: evt.pubkey, + kind: evt.kind, + content: evt.content.substring(0, 50) + '...', + topics: evt.tags.filter(tag => tag[0] === 't').map(tag => tag[1]) + }) + + setNewNotifications((prev) => { + if (!discussionEosed) { return [evt, ...prev] - }) - } + } + if (prev.length && compareEvents(prev[0], evt) >= 0) { + return prev + } + + client.emitNewEvent(evt) + return [evt, ...prev] + }) } } - ) - topicSubCloserRef.current = topicSubCloser - } + } + ) + topicSubCloserRef.current = discussionSubCloser // Regular notifications subscription const subCloser = client.subscribe( @@ -180,7 +185,6 @@ export function NotificationProvider({ children }: { children: React.ReactNode } }, onevent: (evt) => { if (evt.pubkey !== pubkey) { - setNewNotifications((prev) => { if (!eosed) { return [evt, ...prev] @@ -243,7 +247,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } topicSubCloserRef.current = null } } - }, [pubkey, getSubscribedTopics]) + }, [pubkey]) useEffect(() => { const newNotificationCount = filteredNewNotifications.length diff --git a/src/services/client.service.ts b/src/services/client.service.ts index cb720b3..b2a47ad 100644 --- a/src/services/client.service.ts +++ b/src/services/client.service.ts @@ -389,7 +389,11 @@ class ClientService extends EventTarget { await this.throttleRequest(url) const relay = await this.pool.ensureRelay(url) - await relay.auth((authEvt: EventTemplate) => that.signer!.signEvent(authEvt)) + await relay.auth((authEvt: EventTemplate) => { + // Ensure the auth event has the correct pubkey + const authEventWithPubkey = { ...authEvt, pubkey: that.pubkey } + return that.signer!.signEvent(authEventWithPubkey) + }) await relay.publish(event) this.trackEventSeenOn(event.id, relay) this.recordSuccess(url) diff --git a/src/services/indexed-db.service.ts b/src/services/indexed-db.service.ts index adb961f..72c6a3e 100644 --- a/src/services/indexed-db.service.ts +++ b/src/services/indexed-db.service.ts @@ -43,7 +43,7 @@ class IndexedDbService { init(): Promise { if (!this.initPromise) { this.initPromise = new Promise((resolve, reject) => { - const request = window.indexedDB.open('jumble', 8) + const request = window.indexedDB.open('jumble', 9) request.onerror = (event) => { reject(event)