Browse Source

discussion topic subscriptions

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

2
src/PageManager.tsx

@ -3,6 +3,7 @@ import { cn } from '@/lib/utils' @@ -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' @@ -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'

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

@ -26,13 +26,27 @@ export function NotificationItem({ @@ -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,

51
src/components/NotificationList/index.tsx

@ -56,7 +56,8 @@ const NotificationList = forwardRef((_, ref) => { @@ -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) => { @@ -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) => { @@ -121,21 +123,48 @@ const NotificationList = forwardRef((_, ref) => {
setLastReadTime(getNotificationsSeenAt())
const relayList = await client.fetchRelayList(pubkey)
const { closer, timelineKey } = await client.subscribeTimeline(
[
{
// 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: filterKinds,
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) => { @@ -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)
}
}

14
src/providers/InterestListProvider.tsx

@ -67,30 +67,42 @@ export function InterestListProvider({ children }: { children: React.ReactNode } @@ -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) {

34
src/providers/NostrProvider/index.tsx

@ -31,7 +31,7 @@ import { @@ -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 }) { @@ -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 }) { @@ -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
}
@ -618,6 +627,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -618,6 +627,11 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
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
if (minPow > 0) {
@ -645,21 +659,25 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -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

34
src/providers/NotificationProvider.tsx

@ -11,7 +11,7 @@ import { useContentPolicy } from './ContentPolicyProvider' @@ -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 } @@ -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<NostrEvent[]>([])
const [readNotificationIdSet, setReadNotificationIdSet] = useState<Set<string>>(new Set())
const filteredNewNotifications = useMemo(() => {
@ -109,30 +109,36 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -109,30 +109,36 @@ 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(
// 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
'#t': subscribedTopics,
limit: 10
limit: 20
}
],
{
oneose: (e) => {
if (e) {
topicEosed = 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 (!topicEosed) {
if (!discussionEosed) {
return [evt, ...prev]
}
if (prev.length && compareEvents(prev[0], evt) >= 0) {
@ -146,8 +152,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -146,8 +152,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
}
}
)
topicSubCloserRef.current = topicSubCloser
}
topicSubCloserRef.current = discussionSubCloser
// Regular notifications subscription
const subCloser = client.subscribe(
@ -180,7 +185,6 @@ export function NotificationProvider({ children }: { children: React.ReactNode } @@ -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 } @@ -243,7 +247,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
topicSubCloserRef.current = null
}
}
}, [pubkey, getSubscribedTopics])
}, [pubkey])
useEffect(() => {
const newNotificationCount = filteredNewNotifications.length

6
src/services/client.service.ts

@ -389,7 +389,11 @@ class ClientService extends EventTarget { @@ -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)

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

@ -43,7 +43,7 @@ class IndexedDbService { @@ -43,7 +43,7 @@ class IndexedDbService {
init(): Promise<void> {
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)

Loading…
Cancel
Save