Browse Source

bug-fixes

imwald
Silberengel 3 weeks ago
parent
commit
0ed194873c
  1. 5
      src/PageManager.tsx
  2. 3
      src/components/ConnectedRelays/ConnectedRelaysSidebarStrip.tsx
  3. 44
      src/components/NoteList/index.tsx
  4. 6
      src/components/Relay/index.tsx
  5. 90
      src/hooks/useRelayConnectionRows.ts
  6. 5
      src/services/client.service.ts

5
src/PageManager.tsx

@ -2277,9 +2277,12 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2277,9 +2277,12 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
noteStatsService.setBackgroundStatsPaused(primaryFrozen)
if (primaryFrozen) {
extendProfileNetworkDeferral(PROFILE_SECONDARY_PANEL_DEFER_MS)
// Double-pane: keep the left feed's in-flight REQ alive; interrupt only when primary is hidden.
if (isSmallScreen || panelMode === 'single') {
client.interruptBackgroundQueries()
}
}, [primaryFrozen])
}
}, [primaryFrozen, isSmallScreen, panelMode])
const primaryPageContextValue = useMemo(
(): PrimaryPageContextValue => ({

3
src/components/ConnectedRelays/ConnectedRelaysSidebarStrip.tsx

@ -32,7 +32,8 @@ function rowTitle(url: string, connected: boolean, t: (k: string) => string) { @@ -32,7 +32,8 @@ function rowTitle(url: string, connected: boolean, t: (k: string) => string) {
}
/**
* Desktop sidebar: relay avatars for favorites + defaults + inbox; muted when the pool socket is down.
* Desktop sidebar: relay avatars for favorites, inbox, cache, HTTP index, and defaults;
* muted when the WebSocket is down (HTTP index relays count as active when configured).
*/
export function ConnectedRelaysSidebarStrip({ className }: { className?: string }) {
const { t } = useTranslation()

44
src/components/NoteList/index.tsx

@ -944,6 +944,9 @@ const NoteList = forwardRef( @@ -944,6 +944,9 @@ const NoteList = forwardRef(
}>(() => ({ profiles: new Map(), pending: new Set(), version: 0 }))
const feedProfileLoadedRef = useRef<Set<string>>(new Set())
const feedProfileBatchGenRef = useRef(0)
/** Dedupes layout-time pending sync so a new `events` array reference alone cannot loop setState. */
const lastProfilePrefetchPubkeysKeyRef = useRef('')
const clientFilteredVisibleCountRef = useRef(0)
const noteFeedProfileContextValue = useMemo<NoteFeedProfileContextValue>(
() => ({
@ -1074,13 +1077,16 @@ const NoteList = forwardRef( @@ -1074,13 +1077,16 @@ const NoteList = forwardRef(
useLayoutEffect(() => {
publicReadFallbackAttemptedRef.current = false
if (!pauseTimelineForPrimaryFreeze) {
setFeedTimelineEmptyUiReady(false)
setFeedSubscribeRelayOutcomes([])
}, [timelineSubscriptionKey, subRequestsKey, refreshCount])
}
}, [timelineSubscriptionKey, subRequestsKey, refreshCount, pauseTimelineForPrimaryFreeze])
useEffect(() => {
feedProfileBatchGenRef.current += 1
feedProfileLoadedRef.current.clear()
lastProfilePrefetchPubkeysKeyRef.current = ''
setFeedProfileBatch({ profiles: new Map(), pending: new Set(), version: 0 })
}, [timelineSubscriptionKey, refreshCount])
@ -1093,22 +1099,30 @@ const NoteList = forwardRef( @@ -1093,22 +1099,30 @@ const NoteList = forwardRef(
for (const e of newEvents) {
collectProfilePrefetchPubkeysFromEvent(e, candidates)
}
const pubkeysKey = [...candidates].sort().join('\n')
if (pubkeysKey === lastProfilePrefetchPubkeysKeyRef.current) return
lastProfilePrefetchPubkeysKeyRef.current = pubkeysKey
setFeedProfileBatch((prev) => {
const pending = new Set(prev.pending)
let changed = false
for (const pk of candidates) {
if (!prev.profiles.has(pk) && !pending.has(pk)) {
if (
prev.profiles.has(pk) ||
pending.has(pk) ||
feedProfileLoadedRef.current.has(pk)
) {
continue
}
pending.add(pk)
changed = true
}
}
if (!changed) return prev
// Do not bump `version` here — only the debounced batch + profile merges should notify
// `useFetchProfile` (via profiles map / pending membership), not every pending-key sync.
return { ...prev, pending }
})
}, [timelineEventsForFilter, newEvents])
})
const subRequestsRef = useRef(subRequests)
subRequestsRef.current = subRequests
@ -1508,6 +1522,10 @@ const NoteList = forwardRef( @@ -1508,6 +1522,10 @@ const NoteList = forwardRef(
[showFeedClientFilter, applyClientFeedFilter, filteredEvents]
)
useEffect(() => {
clientFilteredVisibleCountRef.current = clientFilteredEvents.length
}, [clientFilteredEvents.length])
const visibleNoteIdsForStatsPrefetchKey = useMemo(
() =>
clientFilteredEvents
@ -1929,6 +1947,10 @@ const NoteList = forwardRef( @@ -1929,6 +1947,10 @@ const NoteList = forwardRef(
timelineEstablishedCloserRef.current = null
if (pauseTimelineForPrimaryFreeze) {
setLoading(false)
if (eventsRef.current.length > 0) {
setFeedTimelineEmptyUiReady(true)
}
return () => {}
}
@ -3896,11 +3918,15 @@ const NoteList = forwardRef( @@ -3896,11 +3918,15 @@ const NoteList = forwardRef(
const remaining = currentEvents.length - currentShowCount
const step = revealBatchSize ?? REVEAL_BATCH_STEP
const increment = Math.min(step, remaining)
const exhausted = bufferExhaustedForVisibleQuotaRef.current
const noVisibleRowsYet = clientFilteredVisibleCountRef.current === 0
// Revealing more raw buffer rows cannot surface visible cards (aggressive filters / seen-on gate).
if (!(exhausted && noVisibleRowsYet)) {
setShowCount((prev) => prev + increment)
}
// `showCount` is a *visible-row quota*, not an offset into the raw merged timeline. Skipping relay
// fetch when `events.length - showCount` is large breaks sparse feeds (e.g. only zap receipts): the
// buffer can hold many raw events while every visible row is already shown — we must still REQ.
const exhausted = bufferExhaustedForVisibleQuotaRef.current
if (
!exhausted &&
currentEvents.length >= 50 &&
@ -4188,6 +4214,14 @@ const NoteList = forwardRef( @@ -4188,6 +4214,14 @@ const NoteList = forwardRef(
const ev = eventsRef.current
const sc = showCountRef.current
if (sc < ev.length || hasMoreRef.current) {
if (
sc < ev.length &&
!hasMoreRef.current &&
bufferExhaustedForVisibleQuotaRef.current &&
clientFilteredVisibleCountRef.current === 0
) {
return
}
loadMore()
}
}, options)

6
src/components/Relay/index.tsx

@ -15,6 +15,7 @@ import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'r @@ -15,6 +15,7 @@ import { forwardRef, useCallback, useEffect, useMemo, useRef, useState } from 'r
import { useTranslation } from 'react-i18next'
import { AlexandriaEventsSearchEmptyCta } from '@/components/AlexandriaEventsSearchEmptyCta'
import { buildAlexandriaEventsSearchUrlFromNotesQuery } from '@/lib/alexandria-events-search-url'
import { stableFeedKindKey } from '@/features/feed/descriptor'
import NotFound from '../NotFound'
const Relay = forwardRef<
@ -81,9 +82,10 @@ const Relay = forwardRef< @@ -81,9 +82,10 @@ const Relay = forwardRef<
}, [normalizedUrl, noteListRef])
/** Default browse: explicit kinds (many strfry / small relays never return a useful kindless global REQ). */
const relayBrowseKindsKey = useMemo(() => stableFeedKindKey(showKinds), [showKinds])
const relayBrowseKinds = useMemo(
() => (showKinds.length > 0 ? showKinds : [kinds.ShortTextNote]),
[showKinds]
[relayBrowseKindsKey, showKinds]
)
const relayFeedSubRequests = useMemo<TFeedSubRequest[]>(() => {
@ -103,7 +105,7 @@ const Relay = forwardRef< @@ -103,7 +105,7 @@ const Relay = forwardRef<
filter: { kinds: [...relayBrowseKinds], limit: SINGLE_RELAY_KINDLESS_REQ_LIMIT }
}
]
}, [normalizedUrl, debouncedInput, relayBrowseKinds])
}, [normalizedUrl, debouncedInput, relayBrowseKindsKey])
const allowKindlessRelayExplore = debouncedInput.trim().length > 0

90
src/hooks/useRelayConnectionRows.ts

@ -1,5 +1,14 @@ @@ -1,5 +1,14 @@
import { DEFAULT_FAVORITE_RELAYS, FAST_READ_RELAY_URLS } from '@/constants'
import { normalizeAnyRelayUrl } from '@/lib/url'
import {
getHttpRelayListFromEvent,
getRelayListReadFromEventNoFastFallback
} from '@/lib/event-metadata'
import {
canonicalRelaySessionKey,
normalizeAnyRelayUrl,
normalizeHttpRelayUrl,
urlMatchesConfiguredHttpIndexRelay
} from '@/lib/url'
import { useNostr } from '@/providers/NostrProvider'
import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import client from '@/services/client.service'
@ -7,8 +16,14 @@ import { useEffect, useMemo, useState } from 'react' @@ -7,8 +16,14 @@ import { useEffect, useMemo, useState } from 'react'
const POLL_MS = 1500
function canon(url: string): string {
return (normalizeAnyRelayUrl(url) || url).trim().toLowerCase()
function normalizeRelayRowUrl(raw: string): string {
const t = raw.trim()
if (/^https?:\/\//i.test(t)) return normalizeHttpRelayUrl(t) || t
return normalizeAnyRelayUrl(t) || t
}
function rowCanon(url: string): string {
return (canonicalRelaySessionKey(url) || normalizeRelayRowUrl(url)).trim().toLowerCase()
}
function mergeUniquePreserveOrder(...lists: (readonly string[] | undefined)[]): string[] {
@ -17,8 +32,8 @@ function mergeUniquePreserveOrder(...lists: (readonly string[] | undefined)[]): @@ -17,8 +32,8 @@ function mergeUniquePreserveOrder(...lists: (readonly string[] | undefined)[]):
for (const list of lists) {
if (!list?.length) continue
for (const raw of list) {
const n = normalizeAnyRelayUrl(raw) || raw
const k = canon(n)
const n = normalizeRelayRowUrl(raw)
const k = rowCanon(n)
if (!k || seen.has(k)) continue
seen.add(k)
out.push(n)
@ -29,58 +44,89 @@ function mergeUniquePreserveOrder(...lists: (readonly string[] | undefined)[]): @@ -29,58 +44,89 @@ function mergeUniquePreserveOrder(...lists: (readonly string[] | undefined)[]):
export type TRelayConnectionRow = {
url: string
/** WebSocket in the pool is open. */
/** WebSocket open in the pool, or HTTP index relay in use for the viewer. */
connected: boolean
}
/**
* Relays to show in active relays UI: favorites + NIP-65 read/write + defaults + fast-read,
* then any pool-connected URL not already listed. {@link row.connected} reflects the live WebSocket.
* Relays for active relays UI: favorites + NIP-65 read/write + kind 10432 cache + kind 10243 HTTP index
* + defaults + fast-read, then any pool-connected URL not already listed.
*/
export function useRelayConnectionRows(): {
rows: TRelayConnectionRow[]
/** Relays that currently have an open WebSocket connection. */
/** Relays counted as active (open WebSocket or configured HTTP index). */
connectedCount: number
} {
const { relayList } = useNostr()
const { favoriteRelays } = useFavoriteRelays()
const { relayList, cacheRelayListEvent, httpRelayListEvent } = useNostr()
const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [connectedCanon, setConnectedCanon] = useState<Set<string>>(() =>
new Set(client.getConnectedRelayUrls().map(canon))
new Set(client.getConnectedRelayUrls().map(rowCanon))
)
const [httpIndexBases, setHttpIndexBases] = useState<readonly string[]>(() =>
client.getViewerHttpIndexRelayBases()
)
useEffect(() => {
const tick = () => setConnectedCanon(new Set(client.getConnectedRelayUrls().map(canon)))
const tick = () => {
setConnectedCanon(new Set(client.getConnectedRelayUrls().map(rowCanon)))
setHttpIndexBases(client.getViewerHttpIndexRelayBases())
}
tick()
const id = window.setInterval(tick, POLL_MS)
return () => clearInterval(id)
}, [])
const cacheRelayUrls = useMemo(() => {
if (!cacheRelayListEvent) return []
return getRelayListReadFromEventNoFastFallback(cacheRelayListEvent, blockedRelays)
}, [cacheRelayListEvent, blockedRelays])
const httpIndexRelayUrls = useMemo(() => {
const out: string[] = [...(relayList?.httpRead ?? []), ...(relayList?.httpWrite ?? [])]
if (httpRelayListEvent) {
const http = getHttpRelayListFromEvent(httpRelayListEvent, blockedRelays)
out.push(...http.httpRead, ...http.httpWrite)
}
return out
}, [relayList?.httpRead, relayList?.httpWrite, httpRelayListEvent, blockedRelays])
return useMemo(() => {
const inbox = [...(relayList?.read ?? []), ...(relayList?.write ?? [])]
const base = mergeUniquePreserveOrder(
favoriteRelays,
inbox,
cacheRelayUrls,
httpIndexRelayUrls,
DEFAULT_FAVORITE_RELAYS,
FAST_READ_RELAY_URLS
)
const baseCanon = new Set(base.map(canon))
const baseCanon = new Set(base.map(rowCanon))
const isConnected = (url: string) =>
urlMatchesConfiguredHttpIndexRelay(url, httpIndexBases) || connectedCanon.has(rowCanon(url))
const rowFor = (url: string, socketConnected: boolean): TRelayConnectionRow => ({
const rowFor = (url: string): TRelayConnectionRow => ({
url,
connected: socketConnected
connected: isConnected(url)
})
const rows: TRelayConnectionRow[] = base.map((url) =>
rowFor(url, connectedCanon.has(canon(url)))
)
const rows: TRelayConnectionRow[] = base.map((url) => rowFor(url))
for (const url of client.getConnectedRelayUrls()) {
const k = canon(url)
const k = rowCanon(url)
if (baseCanon.has(k)) continue
rows.push(rowFor(url, true))
rows.push(rowFor(url))
}
const connectedCount = rows.filter((r) => r.connected).length
return { rows, connectedCount }
}, [favoriteRelays, relayList?.read, relayList?.write, connectedCanon])
}, [
favoriteRelays,
relayList?.read,
relayList?.write,
cacheRelayUrls,
httpIndexRelayUrls,
connectedCanon,
httpIndexBases
])
}

5
src/services/client.service.ts

@ -3294,6 +3294,11 @@ class ClientService extends EventTarget { @@ -3294,6 +3294,11 @@ class ClientService extends EventTarget {
return [...new Set(out)].sort((a, b) => a.localeCompare(b))
}
/** Kind 10243 HTTP index bases for the logged-in viewer (read + write). */
getViewerHttpIndexRelayBases(): readonly string[] {
return this.viewerHttpIndexRelayBases
}
trackEventSeenOn(eventId: string, relay: AbstractRelay) {
const key = canonicalSeenOnEventId(eventId)
let set = this.pool.seenOn.get(key)

Loading…
Cancel
Save