Browse Source

discussion topic subscriptions

imwald
Silberengel 5 months ago
parent
commit
240d08b50f
  1. 20
      src/App.tsx
  2. 2
      src/PageManager.tsx
  3. 16
      src/components/NotificationList/NotificationItem/index.tsx
  4. 61
      src/components/NotificationList/index.tsx
  5. 14
      src/providers/InterestListProvider.tsx
  6. 34
      src/providers/NostrProvider/index.tsx
  7. 80
      src/providers/NotificationProvider.tsx
  8. 6
      src/services/client.service.ts
  9. 2
      src/services/indexed-db.service.ts

20
src/App.tsx

@ -38,16 +38,16 @@ export default function App(): JSX.Element {
<UserTrustProvider> <UserTrustProvider>
<BookmarksProvider> <BookmarksProvider>
<FeedProvider> <FeedProvider>
<ReplyProvider> <ReplyProvider>
<MediaUploadServiceProvider> <MediaUploadServiceProvider>
<KindFilterProvider> <KindFilterProvider>
<UserPreferencesProvider> <UserPreferencesProvider>
<PageManager /> <PageManager />
<Toaster /> <Toaster />
</UserPreferencesProvider> </UserPreferencesProvider>
</KindFilterProvider> </KindFilterProvider>
</MediaUploadServiceProvider> </MediaUploadServiceProvider>
</ReplyProvider> </ReplyProvider>
</FeedProvider> </FeedProvider>
</BookmarksProvider> </BookmarksProvider>
</UserTrustProvider> </UserTrustProvider>

2
src/PageManager.tsx

@ -3,6 +3,7 @@ import { cn } from '@/lib/utils'
import NoteListPage from '@/pages/primary/NoteListPage' import NoteListPage from '@/pages/primary/NoteListPage'
import HomePage from '@/pages/secondary/HomePage' import HomePage from '@/pages/secondary/HomePage'
import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider' import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider'
import { NotificationProvider } from '@/providers/NotificationProvider'
import { TPageRef } from '@/types' import { TPageRef } from '@/types'
import { import {
cloneElement, cloneElement,
@ -25,7 +26,6 @@ import ProfilePage from './pages/primary/ProfilePage'
import RelayPage from './pages/primary/RelayPage' import RelayPage from './pages/primary/RelayPage'
import SearchPage from './pages/primary/SearchPage' import SearchPage from './pages/primary/SearchPage'
import DiscussionsPage from './pages/primary/DiscussionsPage' import DiscussionsPage from './pages/primary/DiscussionsPage'
import { NotificationProvider } from './providers/NotificationProvider'
import { useScreenSize } from './providers/ScreenSizeProvider' import { useScreenSize } from './providers/ScreenSizeProvider'
import { routes } from './routes' import { routes } from './routes'
import modalManager from './services/modal-manager.service' import modalManager from './services/modal-manager.service'

16
src/components/NotificationList/NotificationItem/index.tsx

@ -26,13 +26,27 @@ export function NotificationItem({
const { hideContentMentioningMutedUsers } = useContentPolicy() const { hideContentMentioningMutedUsers } = useContentPolicy()
const { hideUntrustedNotifications, isUserTrusted } = useUserTrust() const { hideUntrustedNotifications, isUserTrusted } = useUserTrust()
const canShow = useMemo(() => { const canShow = useMemo(() => {
return notificationFilter(notification, { const result = notificationFilter(notification, {
pubkey, pubkey,
mutePubkeySet, mutePubkeySet,
hideContentMentioningMutedUsers, hideContentMentioningMutedUsers,
hideUntrustedNotifications, hideUntrustedNotifications,
isUserTrusted 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, notification,
mutePubkeySet, mutePubkeySet,

61
src/components/NotificationList/index.tsx

@ -56,7 +56,8 @@ const NotificationList = forwardRef((_, ref) => {
ExtendedKind.COMMENT, ExtendedKind.COMMENT,
ExtendedKind.VOICE_COMMENT, ExtendedKind.VOICE_COMMENT,
ExtendedKind.POLL, ExtendedKind.POLL,
ExtendedKind.PUBLIC_MESSAGE ExtendedKind.PUBLIC_MESSAGE,
11 // Discussion threads
] ]
case 'reactions': case 'reactions':
return [kinds.Reaction, kinds.Repost, ExtendedKind.POLL_RESPONSE] return [kinds.Reaction, kinds.Repost, ExtendedKind.POLL_RESPONSE]
@ -72,7 +73,8 @@ const NotificationList = forwardRef((_, ref) => {
ExtendedKind.POLL_RESPONSE, ExtendedKind.POLL_RESPONSE,
ExtendedKind.VOICE_COMMENT, ExtendedKind.VOICE_COMMENT,
ExtendedKind.POLL, ExtendedKind.POLL,
ExtendedKind.PUBLIC_MESSAGE ExtendedKind.PUBLIC_MESSAGE,
11 // Discussion threads
] ]
} }
}, [notificationType]) }, [notificationType])
@ -121,21 +123,48 @@ const NotificationList = forwardRef((_, ref) => {
setLastReadTime(getNotificationsSeenAt()) setLastReadTime(getNotificationsSeenAt())
const relayList = await client.fetchRelayList(pubkey) const relayList = await client.fetchRelayList(pubkey)
const { closer, timelineKey } = await client.subscribeTimeline( // Create separate subscriptions for different notification types
[ const subscriptions = []
{
urls: relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS, // Subscription for mentions (events where user is in p-tags)
filter: { const mentionKinds = filterKinds.filter(kind => kind !== 11)
'#p': [pubkey], if (mentionKinds.length > 0) {
kinds: filterKinds, subscriptions.push({
limit: LIMIT 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) => { onEvents: (events, eosed) => {
if (events.length > 0) { 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) { if (eosed) {
setLoading(false) setLoading(false)
@ -144,6 +173,12 @@ const NotificationList = forwardRef((_, ref) => {
} }
}, },
onNew: (event) => { onNew: (event) => {
console.log('📋 NotificationList onNew event:', {
id: event.id,
kind: event.kind,
pubkey: event.pubkey,
content: event.content.substring(0, 30) + '...'
})
handleNewEvent(event) handleNewEvent(event)
} }
} }

14
src/providers/InterestListProvider.tsx

@ -67,30 +67,42 @@ export function InterestListProvider({ children }: { children: React.ReactNode }
} }
const subscribe = async (topic: string) => { const subscribe = async (topic: string) => {
console.log('[InterestListProvider] subscribe called:', { topic, accountPubkey, changing })
if (!accountPubkey || changing) return if (!accountPubkey || changing) return
const normalizedTopic = normalizeTopic(topic) const normalizedTopic = normalizeTopic(topic)
if (subscribedTopics.has(normalizedTopic)) { if (subscribedTopics.has(normalizedTopic)) {
console.log('[InterestListProvider] Already subscribed to topic')
return return
} }
setChanging(true) setChanging(true)
try { try {
console.log('[InterestListProvider] Fetching existing interest list event')
const interestListEvent = await client.fetchInterestListEvent(accountPubkey) const interestListEvent = await client.fetchInterestListEvent(accountPubkey)
console.log('[InterestListProvider] Existing interest list event:', interestListEvent)
const currentTopics = interestListEvent const currentTopics = interestListEvent
? interestListEvent.tags ? interestListEvent.tags
.filter(tag => tag[0] === 't' && tag[1]) .filter(tag => tag[0] === 't' && tag[1])
.map(tag => normalizeTopic(tag[1])) .map(tag => normalizeTopic(tag[1]))
: [] : []
console.log('[InterestListProvider] Current topics:', currentTopics)
if (currentTopics.includes(normalizedTopic)) { if (currentTopics.includes(normalizedTopic)) {
// Already subscribed console.log('[InterestListProvider] Already subscribed to topic (from event)')
return return
} }
const newTopics = [...currentTopics, normalizedTopic] const newTopics = [...currentTopics, normalizedTopic]
console.log('[InterestListProvider] Creating new interest list with topics:', newTopics)
const newInterestListEvent = await publishNewInterestListEvent(newTopics) const newInterestListEvent = await publishNewInterestListEvent(newTopics)
console.log('[InterestListProvider] Published new interest list event:', newInterestListEvent)
await updateInterestListEvent(newInterestListEvent) await updateInterestListEvent(newInterestListEvent)
console.log('[InterestListProvider] Updated interest list event in state')
toast.success(t('Subscribed to topic')) toast.success(t('Subscribed to topic'))
} catch (error) { } catch (error) {

34
src/providers/NostrProvider/index.tsx

@ -31,7 +31,7 @@ import {
} from '@/types' } from '@/types'
import { hexToBytes } from '@noble/hashes/utils' import { hexToBytes } from '@noble/hashes/utils'
import dayjs from 'dayjs' 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 nip19 from 'nostr-tools/nip19'
import * as nip49 from 'nostr-tools/nip49' import * as nip49 from 'nostr-tools/nip49'
import { createContext, useContext, useEffect, useState } from 'react' import { createContext, useContext, useEffect, useState } from 'react'
@ -237,7 +237,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
} }
setRelayList(relayList) 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: [
kinds.Metadata, kinds.Metadata,
@ -607,6 +608,14 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
if (!event) { if (!event) {
throw new Error('sign event failed') 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 return event as VerifiedEvent
} }
@ -618,6 +627,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
throw new Error('You need to login first') 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 const draft = JSON.parse(JSON.stringify(draftEvent)) as TDraftEvent
let event: VerifiedEvent let event: VerifiedEvent
if (minPow > 0) { if (minPow > 0) {
@ -645,21 +659,25 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
try { try {
const publishResult = await client.publishEvent(relays, event) const publishResult = await client.publishEvent(relays, event)
console.log('Publish result:', publishResult)
// Store relay status for display // Store relay status for display
if (publishResult.relayStatuses.length > 0) { if (publishResult.relayStatuses.length > 0) {
// We'll pass this to the UI components that need it
(event as any).relayStatuses = publishResult.relayStatuses (event as any).relayStatuses = publishResult.relayStatuses
console.log('Attached relay statuses to event:', (event as any).relayStatuses)
} }
return event return event
} catch (error) { } 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) { if (error instanceof AggregateError && (error as any).relayStatuses) {
(event as any).relayStatuses = (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 // Re-throw the error so the UI can handle it appropriately

80
src/providers/NotificationProvider.tsx

@ -11,7 +11,7 @@ import { useContentPolicy } from './ContentPolicyProvider'
import { useMuteList } from './MuteListProvider' import { useMuteList } from './MuteListProvider'
import { useNostr } from './NostrProvider' import { useNostr } from './NostrProvider'
import { useUserTrust } from './UserTrustProvider' import { useUserTrust } from './UserTrustProvider'
import { useInterestList } from './InterestListProvider' // import { useInterestList } from './InterestListProvider' // No longer needed
type TNotificationContext = { type TNotificationContext = {
hasNewNotification: boolean hasNewNotification: boolean
@ -37,7 +37,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
const { hideUntrustedNotifications, isUserTrusted } = useUserTrust() const { hideUntrustedNotifications, isUserTrusted } = useUserTrust()
const { mutePubkeySet } = useMuteList() const { mutePubkeySet } = useMuteList()
const { hideContentMentioningMutedUsers } = useContentPolicy() const { hideContentMentioningMutedUsers } = useContentPolicy()
const { getSubscribedTopics } = useInterestList() // const { getSubscribedTopics } = useInterestList() // No longer needed since we subscribe to all discussions
const [newNotifications, setNewNotifications] = useState<NostrEvent[]>([]) const [newNotifications, setNewNotifications] = useState<NostrEvent[]>([])
const [readNotificationIdSet, setReadNotificationIdSet] = useState<Set<string>>(new Set()) const [readNotificationIdSet, setReadNotificationIdSet] = useState<Set<string>>(new Set())
const filteredNewNotifications = useMemo(() => { const filteredNewNotifications = useMemo(() => {
@ -109,45 +109,50 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
const relayList = await client.fetchRelayList(pubkey) const relayList = await client.fetchRelayList(pubkey)
const notificationRelays = relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS const notificationRelays = relayList.read.length > 0 ? relayList.read.slice(0, 5) : BIG_RELAY_URLS
// Subscribe to subscribed topics (kind 11 discussions) // Subscribe to discussion notifications (kind 11)
const subscribedTopics = getSubscribedTopics() // Subscribe to all discussions, not just subscribed topics
if (subscribedTopics.length > 0) { let discussionEosed = false
let topicEosed = false const discussionSubCloser = client.subscribe(
const topicSubCloser = client.subscribe( notificationRelays,
notificationRelays, [
[
{
kinds: [11], // Discussion threads
'#t': subscribedTopics,
limit: 10
}
],
{ {
oneose: (e) => { kinds: [11], // Discussion threads
if (e) { limit: 20
topicEosed = e }
} ],
}, {
onevent: (evt) => { oneose: (e) => {
// Don't notify about our own threads if (e) {
if (evt.pubkey !== pubkey) { discussionEosed = e
setNewNotifications((prev) => { }
if (!topicEosed) { },
return [evt, ...prev] onevent: (evt) => {
} // Don't notify about our own threads
if (prev.length && compareEvents(prev[0], evt) >= 0) { if (evt.pubkey !== pubkey) {
return prev 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])
})
client.emitNewEvent(evt) setNewNotifications((prev) => {
if (!discussionEosed) {
return [evt, ...prev] 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 // Regular notifications subscription
const subCloser = client.subscribe( const subCloser = client.subscribe(
@ -180,7 +185,6 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
}, },
onevent: (evt) => { onevent: (evt) => {
if (evt.pubkey !== pubkey) { if (evt.pubkey !== pubkey) {
setNewNotifications((prev) => { setNewNotifications((prev) => {
if (!eosed) { if (!eosed) {
return [evt, ...prev] return [evt, ...prev]
@ -243,7 +247,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
topicSubCloserRef.current = null topicSubCloserRef.current = null
} }
} }
}, [pubkey, getSubscribedTopics]) }, [pubkey])
useEffect(() => { useEffect(() => {
const newNotificationCount = filteredNewNotifications.length const newNotificationCount = filteredNewNotifications.length

6
src/services/client.service.ts

@ -389,7 +389,11 @@ class ClientService extends EventTarget {
await this.throttleRequest(url) await this.throttleRequest(url)
const relay = await this.pool.ensureRelay(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) await relay.publish(event)
this.trackEventSeenOn(event.id, relay) this.trackEventSeenOn(event.id, relay)
this.recordSuccess(url) this.recordSuccess(url)

2
src/services/indexed-db.service.ts

@ -43,7 +43,7 @@ class IndexedDbService {
init(): Promise<void> { init(): Promise<void> {
if (!this.initPromise) { if (!this.initPromise) {
this.initPromise = new Promise((resolve, reject) => { this.initPromise = new Promise((resolve, reject) => {
const request = window.indexedDB.open('jumble', 8) const request = window.indexedDB.open('jumble', 9)
request.onerror = (event) => { request.onerror = (event) => {
reject(event) reject(event)

Loading…
Cancel
Save