Browse Source

fix feeds

imwald
Silberengel 2 weeks ago
parent
commit
fb7f469ea9
  1. 84
      src/components/NoteList/index.tsx
  2. 28
      src/lib/spell-feed-request-identity.test.ts
  3. 18
      src/lib/spell-feed-request-identity.ts
  4. 9
      src/pages/primary/SpellsPage/index.tsx

84
src/components/NoteList/index.tsx

@ -24,6 +24,7 @@ import { prefetchAuthorNip30EmojisForPubkeys } from '@/lib/nip30-author-emojis'
import { shouldFilterEvent } from '@/lib/event-filtering' import { shouldFilterEvent } from '@/lib/event-filtering'
import { import {
isRelayUrlStrictSupersetIdentityKey, isRelayUrlStrictSupersetIdentityKey,
isSpellSubRequestsFilterSuperset,
isSpellSubRequestsSameFiltersDifferentRelays isSpellSubRequestsSameFiltersDifferentRelays
} from '@/lib/spell-feed-request-identity' } from '@/lib/spell-feed-request-identity'
import logger from '@/lib/logger' import logger from '@/lib/logger'
@ -748,6 +749,12 @@ const NoteList = forwardRef(
* Use a larger value for slow feeds (e.g. notifications `#p` across many relays). * Use a larger value for slow feeds (e.g. notifications `#p` across many relays).
*/ */
timelineLoadingSafetyTimeoutMs, timelineLoadingSafetyTimeoutMs,
/**
* When true, live `onNew` events merge into the visible timeline immediately (home feed behavior).
* Default false on Spells faux feeds: new rows go to {@link NewNotesButton} until the user scrolls near the top.
* Enable for notifications so mentions/replies appear without tapping Show n new notes.
*/
mergeLiveEventsImmediately = false,
/** /**
* With {@link useFilterAsIs}: omit relay `kinds` when the subrequest filter has none. Kindless relay feeds * With {@link useFilterAsIs}: omit relay `kinds` when the subrequest filter has none. Kindless relay feeds
* merge the full batch; {@link withKindFilter} + {@link showAllKinds} control whether {@link showKinds} * merge the full batch; {@link withKindFilter} + {@link showAllKinds} control whether {@link showKinds}
@ -865,6 +872,7 @@ const NoteList = forwardRef(
spellFeedInstrumentToken?: number spellFeedInstrumentToken?: number
onSpellFeedFirstPaint?: (detail: { eventCount: number; firstEventId: string }) => void onSpellFeedFirstPaint?: (detail: { eventCount: number; firstEventId: string }) => void
timelineLoadingSafetyTimeoutMs?: number timelineLoadingSafetyTimeoutMs?: number
mergeLiveEventsImmediately?: boolean
clientSideKindFilter?: boolean clientSideKindFilter?: boolean
oneShotFetch?: boolean oneShotFetch?: boolean
oneShotMergedCap?: number oneShotMergedCap?: number
@ -1333,6 +1341,8 @@ const NoteList = forwardRef(
withKindFilterRef.current = withKindFilter withKindFilterRef.current = withKindFilter
const hostPrimaryPageNameRef = useRef(hostPrimaryPageName) const hostPrimaryPageNameRef = useRef(hostPrimaryPageName)
hostPrimaryPageNameRef.current = hostPrimaryPageName hostPrimaryPageNameRef.current = hostPrimaryPageName
const mergeLiveEventsImmediatelyRef = useRef(mergeLiveEventsImmediately)
mergeLiveEventsImmediatelyRef.current = mergeLiveEventsImmediately
const gridLayoutRef = useRef(gridLayout) const gridLayoutRef = useRef(gridLayout)
gridLayoutRef.current = gridLayout gridLayoutRef.current = gridLayout
@ -1444,6 +1454,61 @@ const NoteList = forwardRef(
shouldHideEventRef.current = shouldHideEvent shouldHideEventRef.current = shouldHideEvent
}, [shouldHideEvent]) }, [shouldHideEvent])
/** Paint the author's own publishes into the open feed without waiting for relay echo or "new notes". */
useEffect(() => {
const onOwnPublish = (data: globalThis.Event) => {
const evt = (data as CustomEvent<Event>).detail
if (!evt?.id || !pubkey || evt.pubkey !== pubkey) return
if (shouldHideEventRef.current(evt)) return
const mapped = stripNostrLandAggrFromTimelineSubRequests(
feedSubscriptionKey,
mapLiveSubRequestsForTimelineRef.current(subRequestsRef.current)
).filter((req) => req.urls.length > 0)
if (mapped.length === 0) return
if (
!mapped.some(({ filter }) =>
eventMatchesSubRequestFilterWithWindow(evt, filter as Filter)
)
) {
return
}
const narrowed = narrowLiveBatchUsingRefs([evt])
if (narrowed.length === 0) return
if (eventsRef.current.some((e) => e.id === evt.id)) return
const cap = allowKindlessRelayExplore
? RELAY_EXPLORE_LIMIT
: areAlgoRelays
? ALGO_LIMIT
: LIMIT
setEvents((oldEvents) => {
if (oldEvents.some((e) => e.id === evt.id)) return oldEvents
const next = collapseDuplicateNip18RepostTimelineRows(
mergeEventBatchesById(oldEvents, narrowed, cap, areAlgoRelays)
)
lastEventsForTimelinePrefetchRef.current = next
return next
})
setNewEvents((pending) => pending.filter((e) => e.id !== evt.id))
setLoading(false)
client.prefetchEmbeddedEventsForParents(narrowed, {
relayHintsOnly: relayAuthoritativeFeedOnlyRef.current
})
}
client.addEventListener('newEvent', onOwnPublish)
return () => client.removeEventListener('newEvent', onOwnPublish)
}, [
pubkey,
feedSubscriptionKey,
allowKindlessRelayExplore,
areAlgoRelays,
shouldHideEvent
])
const { items: filteredEvents, bufferExhaustedForVisibleQuota } = useMemo(() => { const { items: filteredEvents, bufferExhaustedForVisibleQuota } = useMemo(() => {
const idSet = new Set<string>() const idSet = new Set<string>()
const out: Event[] = [] const out: Event[] = []
@ -2256,7 +2321,8 @@ const NoteList = forwardRef(
!feedScopeChanged && !feedScopeChanged &&
prevSubKey != null && prevSubKey != null &&
(isRelayUrlStrictSupersetIdentityKey(prevSubKey, subRequestsKey) || (isRelayUrlStrictSupersetIdentityKey(prevSubKey, subRequestsKey) ||
isSpellSubRequestsSameFiltersDifferentRelays(prevSubKey, subRequestsKey)) isSpellSubRequestsSameFiltersDifferentRelays(prevSubKey, subRequestsKey) ||
isSpellSubRequestsFilterSuperset(prevSubKey, subRequestsKey))
const keepExistingTimelineEvents = const keepExistingTimelineEvents =
preserveTimelineOnSubRequestsChange && preserveTimelineOnSubRequestsChange &&
@ -2266,7 +2332,8 @@ const NoteList = forwardRef(
(prevSubKey === subRequestsKey || (prevSubKey === subRequestsKey ||
isRelayUrlStrictSupersetIdentityKey(prevSubKey, subRequestsKey) || isRelayUrlStrictSupersetIdentityKey(prevSubKey, subRequestsKey) ||
(mergeTimelineWhenSubRequestFiltersMatch && (mergeTimelineWhenSubRequestFiltersMatch &&
isSpellSubRequestsSameFiltersDifferentRelays(prevSubKey, subRequestsKey))) (isSpellSubRequestsSameFiltersDifferentRelays(prevSubKey, subRequestsKey) ||
isSpellSubRequestsFilterSuperset(prevSubKey, subRequestsKey))))
prevSubRequestsKeyForTimelineRef.current = subRequestsKey prevSubRequestsKeyForTimelineRef.current = subRequestsKey
/** False after cleanup so stale timeline callbacks cannot overwrite state after switching feeds (e.g. Spells discussions → notifications). */ /** False after cleanup so stale timeline callbacks cannot overwrite state after switching feeds (e.g. Spells discussions → notifications). */
@ -3516,12 +3583,15 @@ const NoteList = forwardRef(
} }
} }
if (shouldHideEventRef.current(event)) return if (shouldHideEventRef.current(event)) return
const isOwnPublish = Boolean(pubkey && event.pubkey === pubkey)
const route: 'profile' | 'home' | 'pending' = const route: 'profile' | 'home' | 'pending' =
(pubkey && event.pubkey === pubkey) || eventMatchesProfileTimelineRequest(event) mergeLiveEventsImmediatelyRef.current || isOwnPublish
? 'profile' ? 'home'
: hostPrimaryPageNameRef.current === 'feed' : eventMatchesProfileTimelineRequest(event)
? 'home' ? 'profile'
: 'pending' : hostPrimaryPageNameRef.current === 'feed'
? 'home'
: 'pending'
liveOnNewPendingRef.current.push({ event, route }) liveOnNewPendingRef.current.push({ event, route })
scheduleLiveOnNewFlush() scheduleLiveOnNewFlush()
}, },

28
src/lib/spell-feed-request-identity.test.ts

@ -0,0 +1,28 @@
import { describe, expect, it } from 'vitest'
import {
computeSpellSubRequestsIdentityKey,
isSpellSubRequestsFilterSuperset
} from './spell-feed-request-identity'
import type { TFeedSubRequest } from '@/types'
describe('isSpellSubRequestsFilterSuperset', () => {
it('detects when new shards add thread-watch filters', () => {
const base: TFeedSubRequest[] = [
{
urls: ['wss://relay.example/'],
filter: { limit: 200, '#p': ['abc'.repeat(32)] }
}
]
const expanded: TFeedSubRequest[] = [
...base,
{
urls: ['wss://relay.example/'],
filter: { kinds: [1], limit: 200, '#e': ['d'.repeat(64)] }
}
]
const prevKey = computeSpellSubRequestsIdentityKey(base)
const nextKey = computeSpellSubRequestsIdentityKey(expanded)
expect(isSpellSubRequestsFilterSuperset(prevKey, nextKey)).toBe(true)
expect(isSpellSubRequestsFilterSuperset(nextKey, prevKey)).toBe(false)
})
})

18
src/lib/spell-feed-request-identity.ts

@ -95,3 +95,21 @@ export function isSpellSubRequestsSameFiltersDifferentRelays(
return false return false
} }
} }
/**
* True when `nextKey` keeps every prior REQ filter and adds more shards (e.g. notifications spell gains
* `#e` / `#a` subrequests after thread-watch lists load from relays).
*/
export function isSpellSubRequestsFilterSuperset(prevKey: string | null, nextKey: string): boolean {
if (!prevKey || prevKey === nextKey) return false
try {
type Item = { urls: string[]; filter: string }
const prev = JSON.parse(prevKey) as Item[]
const next = JSON.parse(nextKey) as Item[]
if (!Array.isArray(prev) || !Array.isArray(next) || next.length < prev.length) return false
const nextFilters = new Set(next.map((item) => item.filter))
return prev.every((item) => nextFilters.has(item.filter))
} catch {
return false
}
}

9
src/pages/primary/SpellsPage/index.tsx

@ -1063,8 +1063,13 @@ const SpellsPage = forwardRef<TPageRef>(function SpellsPage(
subRequests={subRequests} subRequests={subRequests}
feedSubscriptionKey={spellFeedSubscriptionKey} feedSubscriptionKey={spellFeedSubscriptionKey}
hostPrimaryPageName="spells" hostPrimaryPageName="spells"
preserveTimelineOnSubRequestsChange={spellFauxMergeTimeline} preserveTimelineOnSubRequestsChange={
mergeTimelineWhenSubRequestFiltersMatch={spellFauxMergeTimeline} spellFauxMergeTimeline || selectedFauxSpell === 'notifications'
}
mergeTimelineWhenSubRequestFiltersMatch={
spellFauxMergeTimeline || selectedFauxSpell === 'notifications'
}
mergeLiveEventsImmediately={selectedFauxSpell === 'notifications'}
showKinds={ showKinds={
selectedFauxSpell === 'notifications' ? NOTIFICATION_SPELL_KINDS : showKinds selectedFauxSpell === 'notifications' ? NOTIFICATION_SPELL_KINDS : showKinds
} }

Loading…
Cancel
Save