Browse Source

bug-fixes

imwald
Silberengel 1 month ago
parent
commit
ceb9fd0d9e
  1. 2
      src/components/BottomNavigationBar/NotificationsButton.tsx
  2. 2
      src/components/NotificationList/NotificationItem/Notification.tsx
  3. 2
      src/components/NotificationList/index.tsx
  4. 5
      src/components/PostEditor/PostTextarea/Mention/suggestion.ts
  5. 2
      src/components/Sidebar/NotificationButton.tsx
  6. 6
      src/components/TextareaWithMentionAutocomplete/index.tsx
  7. 9
      src/providers/NostrProvider/index.tsx
  8. 18
      src/providers/NotificationContext.tsx
  9. 69
      src/providers/NotificationProvider.tsx
  10. 13
      src/services/client.service.ts

2
src/components/BottomNavigationBar/NotificationsButton.tsx

@ -1,6 +1,6 @@
import { usePrimaryPage } from '@/PageManager' import { usePrimaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNotification } from '@/providers/NotificationProvider' import { useNotification } from '@/providers/NotificationContext'
import { Bell } from 'lucide-react' import { Bell } from 'lucide-react'
import BottomNavigationBarItem from './BottomNavigationBarItem' import BottomNavigationBarItem from './BottomNavigationBarItem'

2
src/components/NotificationList/NotificationItem/Notification.tsx

@ -9,7 +9,7 @@ import { toNote, toProfile } from '@/lib/link'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useSmartNoteNavigation, useSecondaryPage } from '@/PageManager' import { useSmartNoteNavigation, useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNotification } from '@/providers/NotificationProvider' import { useNotification } from '@/providers/NotificationContext'
import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { NostrEvent } from 'nostr-tools' import { NostrEvent } from 'nostr-tools'
import { useMemo } from 'react' import { useMemo } from 'react'

2
src/components/NotificationList/index.tsx

@ -3,7 +3,7 @@ import { compareEvents } from '@/lib/event'
import logger from '@/lib/logger' import logger from '@/lib/logger'
import { usePrimaryPage } from '@/PageManager' import { usePrimaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNotification } from '@/providers/NotificationProvider' import { useNotification } from '@/providers/NotificationContext'
import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import client from '@/services/client.service' import client from '@/services/client.service'

5
src/components/PostEditor/PostTextarea/Mention/suggestion.ts

@ -8,6 +8,11 @@ import MentionList, { MentionListHandle, MentionListProps } from './MentionList'
const suggestion = { const suggestion = {
items: async ({ query }: { query: string }) => { items: async ({ query }: { query: string }) => {
const q = query.trim().toLowerCase()
// Reserved for future nevent/naddr picker; don't treat as npub handle
if (q === 'nevent' || q === 'naddr' || q.startsWith('nevent') || q.startsWith('naddr')) {
return []
}
const result = await client.searchNpubsFromLocal(query, 20) const result = await client.searchNpubsFromLocal(query, 20)
return result ?? [] return result ?? []
}, },

2
src/components/Sidebar/NotificationButton.tsx

@ -1,6 +1,6 @@
import { usePrimaryPage, usePrimaryNoteView } from '@/PageManager' import { usePrimaryPage, usePrimaryNoteView } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { useNotification } from '@/providers/NotificationProvider' import { useNotification } from '@/providers/NotificationContext'
import { Bell } from 'lucide-react' import { Bell } from 'lucide-react'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'

6
src/components/TextareaWithMentionAutocomplete/index.tsx

@ -64,6 +64,12 @@ const TextareaWithMentionAutocomplete = forwardRef<HTMLTextAreaElement, Textarea
setMentionOpen(false) setMentionOpen(false)
return return
} }
const q = mentionQuery.trim().toLowerCase()
if (q === 'nevent' || q === 'naddr' || q.startsWith('nevent') || q.startsWith('naddr')) {
setMentionItems([])
setMentionOpen(false)
return
}
if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current) if (searchTimeoutRef.current) clearTimeout(searchTimeoutRef.current)
searchTimeoutRef.current = setTimeout(() => { searchTimeoutRef.current = setTimeout(() => {
client client

9
src/providers/NostrProvider/index.tsx

@ -1123,6 +1123,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
setRssFeedListEvent(newRssFeedListEvent) setRssFeedListEvent(newRssFeedListEvent)
} }
/** Updates local last read time and optionally publishes kind 30078 (notification seen-at) for cross-device sync.
* Relay list: users 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) => { const updateNotificationsSeenAt = async (skipPublish = false) => {
if (!account) return if (!account) return
@ -1140,8 +1142,15 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
(lastPublishedSeenNotificationsAtEventAt < 0 || (lastPublishedSeenNotificationsAtEventAt < 0 ||
now - lastPublishedSeenNotificationsAtEventAt > 10 * 60) // 10 minutes now - lastPublishedSeenNotificationsAtEventAt > 10 * 60) // 10 minutes
) { ) {
try {
await publish(createSeenNotificationsAtDraftEvent()) await publish(createSeenNotificationsAtDraftEvent())
lastPublishedSeenNotificationsAtEventAtMap.set(account.pubkey, now) 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)
})
}
} }
} }

18
src/providers/NotificationContext.tsx

@ -0,0 +1,18 @@
import { createContext, useContext } from 'react'
export type TNotificationContext = {
hasNewNotification: boolean
getNotificationsSeenAt: () => number
isNotificationRead: (id: string) => boolean
markNotificationAsRead: (id: string) => void
}
export const NotificationContext = createContext<TNotificationContext | undefined>(undefined)
export const useNotification = () => {
const context = useContext(NotificationContext)
if (!context) {
throw new Error('useNotification must be used within a NotificationProvider')
}
return context
}

69
src/providers/NotificationProvider.tsx

@ -8,29 +8,12 @@ import client from '@/services/client.service'
import storage from '@/services/local-storage.service' import storage from '@/services/local-storage.service'
import { kinds, NostrEvent } from 'nostr-tools' import { kinds, NostrEvent } from 'nostr-tools'
import { SubCloser } from 'nostr-tools/abstract-pool' import { SubCloser } from 'nostr-tools/abstract-pool'
import { createContext, useContext, useEffect, useMemo, useState } from 'react' import { useCallback, useEffect, useMemo, useState } from 'react'
import { useContentPolicy } from './ContentPolicyProvider' 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' // No longer needed import { NotificationContext } from './NotificationContext'
type TNotificationContext = {
hasNewNotification: boolean
getNotificationsSeenAt: () => number
isNotificationRead: (id: string) => boolean
markNotificationAsRead: (id: string) => void
}
const NotificationContext = createContext<TNotificationContext | undefined>(undefined)
export const useNotification = () => {
const context = useContext(NotificationContext)
if (!context) {
throw new Error('useNotification must be used within a NotificationProvider')
}
return context
}
export function NotificationProvider({ children }: { children: React.ReactNode }) { export function NotificationProvider({ children }: { children: React.ReactNode }) {
const { current } = usePrimaryPage() const { current } = usePrimaryPage()
@ -76,16 +59,28 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
active active
]) ])
// Defer so we don't trigger state updates during the same commit as consumer renders (avoids "Cannot update NotificationList while rendering NotificationProvider")
useEffect(() => { useEffect(() => {
let t2: ReturnType<typeof setTimeout> | null = null
const t = setTimeout(() => {
setNewNotifications([]) setNewNotifications([])
t2 = setTimeout(() => {
updateNotificationsSeenAt(!active) updateNotificationsSeenAt(!active)
}, [active]) }, 0)
}, 0)
return () => {
clearTimeout(t)
if (t2 !== null) clearTimeout(t2)
}
}, [active, updateNotificationsSeenAt])
useEffect(() => { useEffect(() => {
if (!pubkey) return if (!pubkey) return
const deferredReset = setTimeout(() => {
setNewNotifications([]) setNewNotifications([])
setReadNotificationIdSet(new Set()) setReadNotificationIdSet(new Set())
}, 0)
// Track if component is mounted // Track if component is mounted
const isMountedRef = { current: true } const isMountedRef = { current: true }
@ -262,6 +257,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
// Cleanup function // Cleanup function
return () => { return () => {
clearTimeout(deferredReset)
isMountedRef.current = false isMountedRef.current = false
if (subCloserRef.current) { if (subCloserRef.current) {
subCloserRef.current.close() subCloserRef.current.close()
@ -322,7 +318,7 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
} }
}, [filteredNewNotifications]) }, [filteredNewNotifications])
const getNotificationsSeenAt = () => { const getNotificationsSeenAt = useCallback(() => {
if (notificationsSeenAt >= 0) { if (notificationsSeenAt >= 0) {
return notificationsSeenAt return notificationsSeenAt
} }
@ -330,25 +326,34 @@ export function NotificationProvider({ children }: { children: React.ReactNode }
return storage.getLastReadNotificationTime(pubkey) return storage.getLastReadNotificationTime(pubkey)
} }
return 0 return 0
} }, [notificationsSeenAt, pubkey])
const isNotificationRead = (notificationId: string): boolean => { const isNotificationRead = useCallback(
return readNotificationIdSet.has(notificationId) (notificationId: string): boolean => readNotificationIdSet.has(notificationId),
} [readNotificationIdSet]
)
const markNotificationAsRead = (notificationId: string): void => { const markNotificationAsRead = useCallback((notificationId: string) => {
setReadNotificationIdSet((prev) => new Set([...prev, notificationId])) setReadNotificationIdSet((prev) => new Set([...prev, notificationId]))
} }, [])
return ( const value = useMemo(
<NotificationContext.Provider () => ({
value={{
hasNewNotification: filteredNewNotifications.length > 0, hasNewNotification: filteredNewNotifications.length > 0,
getNotificationsSeenAt, getNotificationsSeenAt,
isNotificationRead, isNotificationRead,
markNotificationAsRead markNotificationAsRead
}} }),
> [
filteredNewNotifications.length,
getNotificationsSeenAt,
isNotificationRead,
markNotificationAsRead
]
)
return (
<NotificationContext.Provider value={value}>
{children} {children}
</NotificationContext.Provider> </NotificationContext.Provider>
) )

13
src/services/client.service.ts

@ -269,6 +269,19 @@ class ClientService extends EventTarget {
return relays.length > 0 ? relays : [...FAST_WRITE_RELAY_URLS] 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[] let relays: string[]
if (specifiedRelayUrls?.length) { if (specifiedRelayUrls?.length) {
relays = specifiedRelayUrls relays = specifiedRelayUrls

Loading…
Cancel
Save