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. 13
      src/providers/NostrProvider/index.tsx
  8. 18
      src/providers/NotificationContext.tsx
  9. 83
      src/providers/NotificationProvider.tsx
  10. 13
      src/services/client.service.ts

2
src/components/BottomNavigationBar/NotificationsButton.tsx

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

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

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

2
src/components/NotificationList/index.tsx

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

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

@ -8,6 +8,11 @@ import MentionList, { MentionListHandle, MentionListProps } from './MentionList' @@ -8,6 +8,11 @@ import MentionList, { MentionListHandle, MentionListProps } from './MentionList'
const suggestion = {
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)
return result ?? []
},

2
src/components/Sidebar/NotificationButton.tsx

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

6
src/components/TextareaWithMentionAutocomplete/index.tsx

@ -64,6 +64,12 @@ const TextareaWithMentionAutocomplete = forwardRef<HTMLTextAreaElement, Textarea @@ -64,6 +64,12 @@ const TextareaWithMentionAutocomplete = forwardRef<HTMLTextAreaElement, Textarea
setMentionOpen(false)
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)
searchTimeoutRef.current = setTimeout(() => {
client

13
src/providers/NostrProvider/index.tsx

@ -1123,6 +1123,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1123,6 +1123,8 @@ 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: 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) => {
if (!account) return
@ -1140,8 +1142,15 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1140,8 +1142,15 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
(lastPublishedSeenNotificationsAtEventAt < 0 ||
now - lastPublishedSeenNotificationsAtEventAt > 10 * 60) // 10 minutes
) {
await publish(createSeenNotificationsAtDraftEvent())
lastPublishedSeenNotificationsAtEventAtMap.set(account.pubkey, now)
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)
})
}
}
}

18
src/providers/NotificationContext.tsx

@ -0,0 +1,18 @@ @@ -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
}

83
src/providers/NotificationProvider.tsx

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

13
src/services/client.service.ts

@ -269,6 +269,19 @@ class ClientService extends EventTarget { @@ -269,6 +269,19 @@ 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

Loading…
Cancel
Save