Browse Source

bug-fixes

imwald
Silberengel 4 weeks ago
parent
commit
a68dec76e4
  1. 17
      src/PageManager.tsx
  2. 16
      src/components/NoteList/index.tsx
  3. 98
      src/components/Sidebar/SidebarCalendarWeekWidget.tsx
  4. 1
      src/constants.ts
  5. 9
      src/contexts/primary-page-context.tsx
  6. 31
      src/layouts/PrimaryPageLayout/index.tsx
  7. 94
      src/pages/primary/CalendarPrimaryPage.tsx
  8. 12
      src/services/note-stats.service.ts

17
src/PageManager.tsx

@ -15,6 +15,7 @@ import { ImwaldBrandBar } from '@/assets/Logo' @@ -15,6 +15,7 @@ import { ImwaldBrandBar } from '@/assets/Logo'
import LiveActivitiesStrip from '@/components/LiveActivitiesStrip'
import NoteDrawer from '@/components/NoteDrawer'
import client from '@/services/client.service'
import noteStatsService from '@/services/note-stats.service'
import { navigationEventStore } from '@/services/navigation-event-store'
import type { Event } from 'nostr-tools'
import { Sheet, SheetContent } from '@/components/ui/sheet'
@ -2126,19 +2127,31 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2126,19 +2127,31 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
setSinglePaneSheetOpen(shouldBeOpen)
}, [panelMode, isSmallScreen, secondaryStack.length, drawerOpen])
const primaryFrozen =
secondaryStack.length > 0 && (isSmallScreen || panelMode === 'double')
useEffect(() => {
noteStatsService.setBackgroundStatsPaused(primaryFrozen)
if (primaryFrozen) {
client.interruptBackgroundQueries()
}
}, [primaryFrozen])
const primaryPageContextValue = useMemo(
(): PrimaryPageContextValue => ({
navigate: navigatePrimaryPageStable,
current: currentPrimaryPage,
currentPageProps,
display: isSmallScreen ? secondaryStack.length === 0 : true
display: isSmallScreen ? secondaryStack.length === 0 : true,
frozen: primaryFrozen
}),
[
navigatePrimaryPageStable,
currentPrimaryPage,
currentPageProps,
isSmallScreen,
secondaryStack.length
secondaryStack.length,
primaryFrozen
]
)

16
src/components/NoteList/index.tsx

@ -1016,6 +1016,7 @@ const NoteList = forwardRef( @@ -1016,6 +1016,7 @@ const NoteList = forwardRef(
const primaryPageCtx = usePrimaryPageOptional()
const primaryPageCurrent = primaryPageCtx?.current ?? null
const primaryPanelFrozen = primaryPageCtx?.frozen ?? false
/** Clears text/author/time/full-search; does not change panel open state. */
const clearFeedClientSearchCriteria = useCallback(() => {
@ -1907,6 +1908,10 @@ const NoteList = forwardRef( @@ -1907,6 +1908,10 @@ const NoteList = forwardRef(
timelineEstablishedCloserRef.current?.()
timelineEstablishedCloserRef.current = null
if (primaryPanelFrozen) {
return () => {}
}
const currentSubRequests = subRequestsRef.current
if (!currentSubRequests.length) {
if (oneShotDebugLabel) {
@ -3127,11 +3132,17 @@ const NoteList = forwardRef( @@ -3127,11 +3132,17 @@ const NoteList = forwardRef(
mapLiveSubRequestsForTimeline,
progressiveWarmupQuery,
hostPrimaryPageName,
relayAuthoritativeFeedOnly
relayAuthoritativeFeedOnly,
primaryPanelFrozen
])
useEffect(() => {
if (oneShotFetch) return
if (primaryPanelFrozen) {
followingFeedDeltaCloserRef.current?.()
followingFeedDeltaCloserRef.current = null
return
}
const deltas = followingFeedDeltaSubRequests ?? []
if (deltas.length === 0) {
followingFeedDeltaCloserRef.current?.()
@ -3388,7 +3399,8 @@ const NoteList = forwardRef( @@ -3388,7 +3399,8 @@ const NoteList = forwardRef(
effectiveShowKinds,
showKind1OPs,
showKind1Replies,
showKind1111
showKind1111,
primaryPanelFrozen
])
const oneShotDebugPrevLoadingRef = useRef(false)

98
src/components/Sidebar/SidebarCalendarWeekWidget.tsx

@ -22,7 +22,7 @@ import indexedDb from '@/services/indexed-db.service' @@ -22,7 +22,7 @@ import indexedDb from '@/services/indexed-db.service'
import { CALENDAR_EVENT_KINDS, ExtendedKind } from '@/constants'
import { CalendarDays, ChevronLeft, ChevronRight } from 'lucide-react'
import { type Event } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useReducer, useState } from 'react'
import { useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { CalendarEventCoverImage } from '@/components/CalendarEventCoverImage'
import { Button } from '@/components/ui/button'
@ -103,36 +103,50 @@ export default function SidebarCalendarWeekWidget() { @@ -103,36 +103,50 @@ export default function SidebarCalendarWeekWidget() {
}
}, [rawEvents, weekOffset])
const fetchGenRef = useRef(0)
useEffect(() => {
const fetchGen = ++fetchGenRef.current
let cancelled = false
let lateMergeTimer: number | null = null
const { weekStartMs, weekEndExclusiveMs } = getLocalMondayWeekBounds(weekOffset)
const stale = () => cancelled || fetchGenRef.current !== fetchGen
const weekBounds = () => getLocalMondayWeekBounds(weekOffset)
const replacePool = (pool: Event[]) => {
if (stale()) return
const { weekStartMs, weekEndExclusiveMs } = weekBounds()
setRawEvents(dedupeCalendarEventsPreferringOccurrenceRange(pool, weekStartMs, weekEndExclusiveMs))
}
const mergeIntoPool = (incoming: Event[]) => {
if (stale()) return
const { weekStartMs, weekEndExclusiveMs } = weekBounds()
setRawEvents((prev) =>
dedupeCalendarEventsPreferringOccurrenceRange([...prev, ...incoming], weekStartMs, weekEndExclusiveMs)
)
}
const { weekStartMs, weekEndExclusiveMs } = weekBounds()
const fromSessionSync = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
const sessionOnly = dedupeCalendarEventsPreferringOccurrenceRange(
fromSessionSync,
weekStartMs,
weekEndExclusiveMs
replacePool(
dedupeCalendarEventsPreferringOccurrenceRange(fromSessionSync, weekStartMs, weekEndExclusiveMs)
)
setRawEvents(sessionOnly)
const scheduleLateSessionMerge = (mergeWithIdb: Event[]) => {
const scheduleLateSessionMerge = () => {
lateMergeTimer = window.setTimeout(() => {
lateMergeTimer = null
if (cancelled) return
const { weekStartMs: ws, weekEndExclusiveMs: we } = getLocalMondayWeekBounds(weekOffset)
if (stale()) return
const later = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents((prev) =>
dedupeCalendarEventsPreferringOccurrenceRange([...prev, ...later, ...mergeWithIdb], ws, we)
)
mergeIntoPool(later)
}, 2500)
}
@ -151,25 +165,18 @@ export default function SidebarCalendarWeekWidget() { @@ -151,25 +165,18 @@ export default function SidebarCalendarWeekWidget() {
)
.catch((): Event[] => [])
void idbP.then((localBaseline) => {
if (cancelled) return
const { weekStartMs: ws, weekEndExclusiveMs: we } = getLocalMondayWeekBounds(weekOffset)
const s2 = client.getSessionEventsMatchingSearch(
if (stale()) return
if (!relayUrls.length) {
const localBaseline = await idbP
if (stale()) return
const fromSession = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents(
dedupeCalendarEventsPreferringOccurrenceRange([...localBaseline, ...s2], ws, we)
)
})
if (cancelled) return
if (!relayUrls.length) {
void idbP.then((lb) => {
if (!cancelled) scheduleLateSessionMerge(lb)
})
replacePool([...localBaseline, ...fromSession])
scheduleLateSessionMerge()
return
}
@ -223,40 +230,19 @@ export default function SidebarCalendarWeekWidget() { @@ -223,40 +230,19 @@ export default function SidebarCalendarWeekWidget() {
.catch(() => ({ batch: [] as Event[], fromFollowing: [] as Event[] }))
const [{ batch, fromFollowing }, localBaseline] = await Promise.all([relayMergedP, idbP])
if (cancelled) return
if (stale()) return
const { weekStartMs: ws, weekEndExclusiveMs: we } = getLocalMondayWeekBounds(weekOffset)
const fromSessionAfterNet = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
if (!cancelled) {
setRawEvents(
dedupeCalendarEventsPreferringOccurrenceRange(
[...localBaseline, ...fromSessionAfterNet, ...batch, ...fromFollowing],
ws,
we
)
)
}
lateMergeTimer = window.setTimeout(() => {
lateMergeTimer = null
if (cancelled) return
const { weekStartMs: w2, weekEndExclusiveMs: w2e } = getLocalMondayWeekBounds(weekOffset)
const later = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents((prev) =>
dedupeCalendarEventsPreferringOccurrenceRange([...prev, ...later, ...localBaseline], w2, w2e)
)
}, 2500)
replacePool([...localBaseline, ...fromSessionAfterNet, ...batch, ...fromFollowing])
scheduleLateSessionMerge()
} catch {
if (!cancelled) {
if (!stale()) {
try {
const { weekStartMs: ws, weekEndExclusiveMs: we } = getLocalMondayWeekBounds(weekOffset)
const { weekStartMs: ws, weekEndExclusiveMs: we } = weekBounds()
const [idb, arc] = await Promise.all([
indexedDb.getCalendarEventsForOccurrenceWindow(ws, we),
indexedDb.getArchivedCalendarEventsOverlappingWindow(ws, we, 25_000, 400)
@ -267,9 +253,9 @@ export default function SidebarCalendarWeekWidget() { @@ -267,9 +253,9 @@ export default function SidebarCalendarWeekWidget() {
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents(dedupeCalendarEventsPreferringOccurrenceRange([...salvage, ...fromSession], ws, we))
replacePool([...salvage, ...fromSession])
} catch {
setRawEvents([])
if (!stale()) setRawEvents([])
}
}
}

1
src/constants.ts

@ -74,7 +74,6 @@ export const DESKTOP_APP_DOWNLOAD_URL_DEFAULT = @@ -74,7 +74,6 @@ export const DESKTOP_APP_DOWNLOAD_URL_DEFAULT =
export const DEFAULT_FAVORITE_RELAYS = [
'wss://theforest.nostr1.com',
'wss://nostr.land',
'wss://relays.land/spatianostra'
]
/**

9
src/contexts/primary-page-context.tsx

@ -12,7 +12,16 @@ export type PrimaryPageContextValue = { @@ -12,7 +12,16 @@ export type PrimaryPageContextValue = {
current: TPrimaryPageName | null
/** Props passed to the current primary page (e.g. `{ spell: 'discussions' }` for spells). */
currentPageProps: object | undefined
/**
* False on small screens while the secondary stack is open (primary feed unmounted).
* True on desktop double-pane so the left column stays visible.
*/
display: boolean
/**
* True while a secondary panel is open: pause primary feed timelines / background stats
* and preserve scroll position until the panel closes.
*/
frozen: boolean
}
export const PrimaryPageContext = createContext<PrimaryPageContextValue | undefined>(undefined)

31
src/layouts/PrimaryPageLayout/index.tsx

@ -44,7 +44,9 @@ const PrimaryPageLayout = forwardRef( @@ -44,7 +44,9 @@ const PrimaryPageLayout = forwardRef(
const smallScreenScrollAreaRef = useRef<HTMLDivElement>(null)
const smallScreenLastScrollTopRef = useRef(0)
const { isSmallScreen } = useScreenSize()
const { current, display } = usePrimaryPage()
const { current, display, frozen } = usePrimaryPage()
const savedScrollTopRef = useRef(0)
const wasFrozenRef = useRef(false)
useImperativeHandle(
ref,
@ -84,6 +86,28 @@ const PrimaryPageLayout = forwardRef( @@ -84,6 +86,28 @@ const PrimaryPageLayout = forwardRef(
}
}, [current, isSmallScreen, display])
useEffect(() => {
if (isSmallScreen) return
const el = scrollAreaRef.current
if (!el) return
if (frozen && !wasFrozenRef.current) {
savedScrollTopRef.current = el.scrollTop
wasFrozenRef.current = true
return
}
if (!frozen && wasFrozenRef.current) {
wasFrozenRef.current = false
const top = savedScrollTopRef.current
requestAnimationFrame(() => {
if (scrollAreaRef.current) {
scrollAreaRef.current.scrollTop = top
}
})
}
}, [frozen, isSmallScreen, pageName])
useEffect(() => {
if (isSmallScreen) return
if (current !== pageName || !display) return
@ -133,7 +157,10 @@ const PrimaryPageLayout = forwardRef( @@ -133,7 +157,10 @@ const PrimaryPageLayout = forwardRef(
}
return (
<DeepBrowsingProvider active={current === pageName && display} scrollAreaRef={scrollAreaRef}>
<DeepBrowsingProvider
active={current === pageName && display && !frozen}
scrollAreaRef={scrollAreaRef}
>
<div className="relative flex h-full min-h-0 min-w-0 flex-col">
{hasTitlebarRow ? (
<PrimaryPageTitlebar

94
src/pages/primary/CalendarPrimaryPage.tsx

@ -150,41 +150,52 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -150,41 +150,52 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
return { rangeStartMs: startMs - pad, rangeEndExclusiveMs: endExclusiveMs + pad }
}, [viewYear, viewMonth])
const calendarFetchGenRef = useRef(0)
useEffect(() => {
const fetchGen = ++calendarFetchGenRef.current
let cancelled = false
let lateMergeTimer: number | null = null
const stale = () => cancelled || calendarFetchGenRef.current !== fetchGen
const { rangeStartMs, rangeEndExclusiveMs } = paddedMonthRange
const replacePool = (pool: NostrEvent[]) => {
if (stale()) return
setRawEvents(dedupeCalendarEventsPreferringOccurrenceRange(pool, rangeStartMs, rangeEndExclusiveMs))
setLoading(false)
}
const mergeIntoPool = (incoming: NostrEvent[]) => {
if (stale()) return
setRawEvents((prev) =>
dedupeCalendarEventsPreferringOccurrenceRange(
[...prev, ...incoming],
rangeStartMs,
rangeEndExclusiveMs
)
)
}
/** Same-tick paint from in-memory session (no await) — IDB + relays merge in the async block below. */
const fromSessionSync = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
const sessionOnly = dedupeCalendarEventsPreferringOccurrenceRange(
fromSessionSync,
rangeStartMs,
rangeEndExclusiveMs
replacePool(
dedupeCalendarEventsPreferringOccurrenceRange(fromSessionSync, rangeStartMs, rangeEndExclusiveMs)
)
setRawEvents(sessionOnly)
setLoading(false)
const scheduleLateSessionMerge = (mergeWithIdb: NostrEvent[]) => {
const scheduleLateSessionMerge = () => {
lateMergeTimer = window.setTimeout(() => {
lateMergeTimer = null
if (cancelled) return
if (stale()) return
const later = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents((prev) =>
dedupeCalendarEventsPreferringOccurrenceRange(
[...prev, ...later, ...mergeWithIdb],
rangeStartMs,
rangeEndExclusiveMs
)
)
mergeIntoPool(later)
}, 2500)
}
@ -212,26 +223,18 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -212,26 +223,18 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
)
.catch((): NostrEvent[] => [])
void idbP.then((localBaseline) => {
if (cancelled) return
const s2 = client.getSessionEventsMatchingSearch(
if (stale()) return
if (!relayUrls.length) {
const localBaseline = await idbP
if (stale()) return
const fromSession = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents(
dedupeCalendarEventsPreferringOccurrenceRange(
[...localBaseline, ...s2],
rangeStartMs,
rangeEndExclusiveMs
)
)
})
if (!relayUrls.length) {
void idbP.then((lb) => {
if (!cancelled) scheduleLateSessionMerge(lb)
})
replacePool([...localBaseline, ...fromSession])
scheduleLateSessionMerge()
return
}
@ -288,38 +291,17 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct @@ -288,38 +291,17 @@ const CalendarPrimaryPage = forwardRef<TPageRef, CalendarPrimaryPageProps>(funct
.catch(() => ({ batch: [] as NostrEvent[], fromFollowing: [] as NostrEvent[] }))
const [{ batch, fromFollowing }, localBaseline] = await Promise.all([relayMergedP, idbP])
if (cancelled) return
if (stale()) return
const fromSession = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents(
dedupeCalendarEventsPreferringOccurrenceRange(
[...batch, ...fromFollowing, ...fromSession, ...localBaseline],
rangeStartMs,
rangeEndExclusiveMs
)
)
lateMergeTimer = window.setTimeout(() => {
lateMergeTimer = null
if (cancelled) return
const later = client.getSessionEventsMatchingSearch(
'',
SESSION_CALENDAR_MERGE_CAP,
[...CALENDAR_EVENT_KINDS]
)
setRawEvents((prev) =>
dedupeCalendarEventsPreferringOccurrenceRange(
[...prev, ...later, ...localBaseline],
rangeStartMs,
rangeEndExclusiveMs
)
)
}, 2500)
replacePool([...localBaseline, ...fromSession, ...batch, ...fromFollowing])
scheduleLateSessionMerge()
} catch {
if (!cancelled) {
if (!stale()) {
try {
const rs = rangeStartMs
const re = rangeEndExclusiveMs

12
src/services/note-stats.service.ts

@ -91,6 +91,8 @@ class NoteStatsService { @@ -91,6 +91,8 @@ class NoteStatsService {
private batchTimeout: NodeJS.Timeout | null = null
/** Prevents overlapping processBatch runs (reentrant calls corrupted pendingEvents). */
private processBatchRunning = false
/** When true (secondary panel open), skip background stats relay batches so the note panel is not starved. */
private backgroundStatsPaused = false
/** While greater than zero, {@link processBatch} defers so user publishes are not starved for WebSocket pool / bandwidth. */
private publishPriorityDepth = 0
private readonly BATCH_DELAY = 40
@ -153,8 +155,13 @@ class NoteStatsService { @@ -153,8 +155,13 @@ class NoteStatsService {
return out
}
setBackgroundStatsPaused(paused: boolean): void {
this.backgroundStatsPaused = paused
}
/** Coalesce scroll bursts; flush immediately when backlog is large or a foreground note was queued. */
private maybeFlushStatsBatch(foreground: boolean) {
if (!foreground && this.backgroundStatsPaused) return
if (this.processBatchRunning) {
return
}
@ -200,6 +207,11 @@ class NoteStatsService { @@ -200,6 +207,11 @@ class NoteStatsService {
}
}
if (!foreground && this.backgroundStatsPaused) {
rememberRoot()
return
}
if (this.pendingEvents.has(eventId) || this.pendingForeground.has(eventId)) {
this.mergeFavoriteRelaysIntoPending(eventId, favoriteRelays)
rememberRoot()

Loading…
Cancel
Save