Browse Source

speed up app

imwald
Silberengel 22 hours ago
parent
commit
5df1d0b346
  1. 32
      package-lock.json
  2. 3
      package.json
  3. 82
      src/PageManager.tsx
  4. 156
      src/components/NoteList/VirtualizedFeedRows.tsx
  5. 42
      src/components/NoteList/index.tsx
  6. 11
      src/hooks/use-event-callback.ts
  7. 11
      src/providers/DeepBrowsingProvider.tsx
  8. 153
      src/providers/NostrProvider/index.tsx
  9. 23
      src/services/client.service.ts
  10. 24
      src/services/indexed-db.service.ts

32
package-lock.json generated

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
{
"name": "imwald",
"version": "23.3.1",
"version": "23.4.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "imwald",
"version": "23.3.1",
"version": "23.4.0",
"license": "MIT",
"dependencies": {
"@asciidoctor/core": "^3.0.4",
@ -47,6 +47,7 @@ @@ -47,6 +47,7 @@
"@radix-ui/react-tabs": "^1.1.2",
"@scure/base": "^2.0.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-virtual": "^3.13.24",
"@tiptap/core": "^2.12.0",
"@tiptap/extension-document": "^2.12.0",
"@tiptap/extension-emoji": "^2.26.1",
@ -5707,6 +5708,33 @@ @@ -5707,6 +5708,33 @@
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.24",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.24.tgz",
"integrity": "sha512-aIJvz5OSkhNIhZIpYivrxrPTKYsjW9Uzy+sP/mx0S3sev2HyvPb7xmjbYvokzEpfgYHy/HjzJ2zFAETuUfgCpg==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.14.0"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.14.0",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.14.0.tgz",
"integrity": "sha512-JLANqGy/D6k4Ujmh8Tr25lGimuOXNiaVyXaCAZS0W+1390sADdGnyUdSWNIfd49gebtIxGMij4IktRVzrdr12Q==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@testing-library/dom": {
"version": "10.4.1",
"resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.1.tgz",

3
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "imwald",
"version": "23.3.1",
"version": "23.4.0",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true,
"type": "module",
@ -77,6 +77,7 @@ @@ -77,6 +77,7 @@
"@radix-ui/react-tabs": "^1.1.2",
"@scure/base": "^2.0.0",
"@tailwindcss/typography": "^0.5.16",
"@tanstack/react-virtual": "^3.13.24",
"@tiptap/core": "^2.12.0",
"@tiptap/extension-document": "^2.12.0",
"@tiptap/extension-emoji": "^2.26.1",

82
src/PageManager.tsx

@ -37,6 +37,7 @@ import { @@ -37,6 +37,7 @@ import {
useRef,
useState
} from 'react'
import { useEventCallback } from '@/hooks/use-event-callback'
import { flushSync } from 'react-dom'
import { useTranslation } from 'react-i18next'
import { KeyboardShortcutsHelpProvider } from '@/components/KeyboardShortcutsHelp'
@ -233,6 +234,27 @@ function mergePrimaryPageEntry( @@ -233,6 +234,27 @@ function mergePrimaryPageEntry(
return [...prev, { name: entry.name, element, props: entry.props }]
}
function renderActivePrimaryPageContent(
primaryPages: TPrimaryPageStateEntry[],
currentPrimaryPage: TPrimaryPageName
): ReactNode {
const entry =
primaryPages.find((p) => p.name === currentPrimaryPage) ??
(primaryPages.length > 0 ? primaryPages[0] : undefined)
if (!entry) return null
try {
logger.debug(`Rendering active primary page: ${entry.name}`)
return entry.props ? applyPrimaryPageProps(entry.element, entry.props) : entry.element
} catch (error) {
logger.error(`Error rendering ${entry.name} component:`, error)
return (
<div>
Error rendering {entry.name}: {error instanceof Error ? error.message : String(error)}
</div>
)
}
}
export { PrimaryPageContext, usePrimaryPage }
export { useSecondaryPage, useSecondaryPageOptional }
@ -987,30 +1009,9 @@ function MainContentArea({ @@ -987,30 +1009,9 @@ function MainContentArea({
</div>
</div>
) : (
// Show normal primary pages
primaryPages.map(({ name, element, props }) => {
const isCurrentPage = currentPrimaryPage === name
logger.debug(`Primary page ${name}:`, { isCurrentPage, currentPrimaryPage })
return (
<div
key={name}
className={cn(
'flex h-full min-h-0 w-full min-w-0 flex-col',
isCurrentPage ? 'flex' : 'hidden'
)}
>
{(() => {
try {
logger.debug(`Rendering ${name} component`)
return props ? applyPrimaryPageProps(element, props) : element
} catch (error) {
logger.error(`Error rendering ${name} component:`, error)
return <div>Error rendering {name}: {error instanceof Error ? error.message : String(error)}</div>
}
})()}
<div className="flex h-full min-h-0 w-full min-w-0 flex-col">
{renderActivePrimaryPageContent(primaryPages, currentPrimaryPage)}
</div>
)
})
)}
</div>
{/* DEPRECATED: Secondary panel removed - double-panel functionality disabled */}
@ -1820,6 +1821,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -1820,6 +1821,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
// NEVER scroll to top - feed should maintain scroll position at all times
}
const navigatePrimaryPageStable = useEventCallback(navigatePrimaryPage)
const goBack = () => {
if (primaryViewType === 'settings-sub') {
navigatePrimaryPage('settings')
@ -2031,12 +2034,21 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2031,12 +2034,21 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
setSinglePaneSheetOpen(shouldBeOpen)
}, [panelMode, isSmallScreen, secondaryStack.length, drawerOpen])
const primaryPageContextValue: PrimaryPageContextValue = {
navigate: navigatePrimaryPage,
const primaryPageContextValue = useMemo(
(): PrimaryPageContextValue => ({
navigate: navigatePrimaryPageStable,
current: currentPrimaryPage,
currentPageProps,
display: isSmallScreen ? secondaryStack.length === 0 : true
}
}),
[
navigatePrimaryPageStable,
currentPrimaryPage,
currentPageProps,
isSmallScreen,
secondaryStack.length
]
)
return (
<PrimaryPageContext.Provider value={primaryPageContextValue}>
@ -2049,7 +2061,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2049,7 +2061,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
currentIndex: secondaryStack.length
? secondaryStack[secondaryStack.length - 1].index
: 0,
navigateToPrimaryPage: navigatePrimaryPage
navigateToPrimaryPage: navigatePrimaryPageStable
}}
>
<CurrentRelaysProvider>
@ -2119,17 +2131,11 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2119,17 +2131,11 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
</div>
)
})}
{primaryPages.map(({ name, element, props }) => (
<div
key={name}
style={{
display:
secondaryStack.length === 0 && currentPrimaryPage === name ? 'block' : 'none'
}}
>
{props ? applyPrimaryPageProps(element, props) : element}
{secondaryStack.length === 0 ? (
<div className="block h-full min-h-0 min-w-0">
{renderActivePrimaryPageContent(primaryPages, currentPrimaryPage)}
</div>
))}
) : null}
</>
)}
</div>
@ -2165,7 +2171,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -2165,7 +2171,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
push: pushSecondaryPage,
pop: popSecondaryPage,
currentIndex: secondaryStack.length ? secondaryStack[secondaryStack.length - 1].index : 0,
navigateToPrimaryPage: navigatePrimaryPage
navigateToPrimaryPage: navigatePrimaryPageStable
}}
>
<CurrentRelaysProvider>

156
src/components/NoteList/VirtualizedFeedRows.tsx

@ -0,0 +1,156 @@ @@ -0,0 +1,156 @@
import NoteCard from '@/components/NoteCard'
import MediaGridItem from '@/components/MediaGridItem'
import { useVirtualizer, useWindowVirtualizer } from '@tanstack/react-virtual'
import type { Event } from 'nostr-tools'
import { memo } from 'react'
const ESTIMATE_NOTE_ROW_PX = 280
const ESTIMATE_GRID_ROW_PX = 120
const VIRTUAL_OVERSCAN = 10
export type VirtualizedFeedRowsProps = {
events: Event[]
gridLayout: boolean
filterMutedNotes: boolean
eventReasonLabelMap: Map<string, string>
/** When true, list scrolls with `window`; otherwise `scrollElement` must be set. */
useWindowScroll: boolean
scrollElement: HTMLElement | null
/** Document offset of the list root (window virtualizer scroll margin). */
scrollMarginTop: number
}
const WindowRows = memo(function WindowRows({
events,
gridLayout,
filterMutedNotes,
eventReasonLabelMap,
scrollMarginTop
}: Omit<VirtualizedFeedRowsProps, 'useWindowScroll' | 'scrollElement'>) {
const rowCount = gridLayout ? Math.ceil(events.length / 3) : events.length
const virtualizer = useWindowVirtualizer({
count: rowCount,
estimateSize: () => (gridLayout ? ESTIMATE_GRID_ROW_PX : ESTIMATE_NOTE_ROW_PX),
overscan: VIRTUAL_OVERSCAN,
scrollMargin: scrollMarginTop,
getItemKey: (index) => (gridLayout ? `grid-${index}` : (events[index]?.id ?? `i-${index}`))
})
return (
<div className="relative w-full" style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((vi) => (
<div
key={vi.key}
data-index={vi.index}
ref={virtualizer.measureElement}
className="absolute left-0 top-0 w-full"
style={{ transform: `translateY(${vi.start}px)` }}
>
{gridLayout ? (
<div className="grid grid-cols-3 gap-0.5 pr-4">
{events.slice(vi.index * 3, vi.index * 3 + 3).map((event) => (
<MediaGridItem key={event.id} event={event} />
))}
</div>
) : (
<NoteCard
className="w-full"
event={events[vi.index]!}
filterMutedNotes={filterMutedNotes}
bottomNoteLabel={eventReasonLabelMap.get(events[vi.index]!.id)}
/>
)}
</div>
))}
</div>
)
})
const ElementRows = memo(function ElementRows({
events,
gridLayout,
filterMutedNotes,
eventReasonLabelMap,
scrollElement
}: Omit<VirtualizedFeedRowsProps, 'useWindowScroll' | 'scrollMarginTop'> & {
scrollElement: HTMLElement
}) {
const rowCount = gridLayout ? Math.ceil(events.length / 3) : events.length
const virtualizer = useVirtualizer({
count: rowCount,
getScrollElement: () => scrollElement,
estimateSize: () => (gridLayout ? ESTIMATE_GRID_ROW_PX : ESTIMATE_NOTE_ROW_PX),
overscan: VIRTUAL_OVERSCAN,
getItemKey: (index) => (gridLayout ? `grid-${index}` : (events[index]?.id ?? `i-${index}`))
})
return (
<div className="relative w-full" style={{ height: virtualizer.getTotalSize() }}>
{virtualizer.getVirtualItems().map((vi) => (
<div
key={vi.key}
data-index={vi.index}
ref={virtualizer.measureElement}
className="absolute left-0 top-0 w-full"
style={{ transform: `translateY(${vi.start}px)` }}
>
{gridLayout ? (
<div className="grid grid-cols-3 gap-0.5 pr-4">
{events.slice(vi.index * 3, vi.index * 3 + 3).map((event) => (
<MediaGridItem key={event.id} event={event} />
))}
</div>
) : (
<NoteCard
className="w-full"
event={events[vi.index]!}
filterMutedNotes={filterMutedNotes}
bottomNoteLabel={eventReasonLabelMap.get(events[vi.index]!.id)}
/>
)}
</div>
))}
</div>
)
})
/** Window- or element-scrolling virtual list for feed rows (and 3-column media grid by row). */
export default memo(function VirtualizedFeedRows({
events,
gridLayout,
filterMutedNotes,
eventReasonLabelMap,
useWindowScroll,
scrollElement,
scrollMarginTop
}: VirtualizedFeedRowsProps) {
if (events.length === 0) {
return null
}
if (useWindowScroll) {
return (
<WindowRows
events={events}
gridLayout={gridLayout}
filterMutedNotes={filterMutedNotes}
eventReasonLabelMap={eventReasonLabelMap}
scrollMarginTop={scrollMarginTop}
/>
)
}
if (!scrollElement) {
return null
}
return (
<ElementRows
events={events}
gridLayout={gridLayout}
filterMutedNotes={filterMutedNotes}
eventReasonLabelMap={eventReasonLabelMap}
scrollElement={scrollElement}
/>
)
})

42
src/components/NoteList/index.tsx

@ -77,8 +77,8 @@ import { @@ -77,8 +77,8 @@ import {
SelectTrigger,
SelectValue
} from '@/components/ui/select'
import NoteCard, { NoteCardLoadingSkeleton } from '../NoteCard'
import MediaGridItem from '../MediaGridItem'
import { NoteCardLoadingSkeleton } from '../NoteCard'
import VirtualizedFeedRows from './VirtualizedFeedRows'
const LIMIT = 150 // Per-shard REQ limit for timeline + loadMore (larger batches = fewer round-trips)
const ALGO_LIMIT = 200 // Increased from 500 for algorithm feeds
@ -846,6 +846,7 @@ const NoteList = forwardRef( @@ -846,6 +846,7 @@ const NoteList = forwardRef(
}, [hostPrimaryPageName, primaryPageCurrent, resetFeedClientFilterState])
const timelineSubscriptionKey = feedSubscriptionKey ?? subRequestsKey
const prevSubRequestsKeyForTimelineRef = useRef<string | null>(null)
const feedTimelineScopePrevRef = useRef<string | undefined>(undefined)
/** Detect pull-to-refresh so preserve-mode feeds still clear; unrelated dep changes must not clear. */
@ -1258,6 +1259,19 @@ const NoteList = forwardRef( @@ -1258,6 +1259,19 @@ const NoteList = forwardRef(
[showFeedClientFilter, applyClientFeedFilter, filteredEvents]
)
const [feedVirtualScrollParent, setFeedVirtualScrollParent] = useState<HTMLElement | null>(null)
const [feedVirtualScrollMarginTop, setFeedVirtualScrollMarginTop] = useState(0)
useLayoutEffect(() => {
const root = feedRootRef.current
if (!root) {
setFeedVirtualScrollParent(null)
setFeedVirtualScrollMarginTop(0)
return
}
setFeedVirtualScrollParent(getNearestScrollableAncestor(root))
setFeedVirtualScrollMarginTop(root.offsetTop)
}, [timelineSubscriptionKey, refreshCount, clientFilteredEvents.length])
const clientFilteredNewEvents = useMemo(
() =>
showFeedClientFilter ? applyClientFeedFilter(filteredNewEvents) : filteredNewEvents,
@ -3371,23 +3385,17 @@ const NoteList = forwardRef( @@ -3371,23 +3385,17 @@ const NoteList = forwardRef(
{t('Feed full search empty')}
</div>
) : null}
{gridLayout ? (
<div className="grid grid-cols-3 gap-0.5 pr-4">
{clientFilteredEvents.map((event) => (
<MediaGridItem key={event.id} event={event} />
))}
</div>
) : (
clientFilteredEvents.map((event) => (
<NoteCard
key={event.id}
className="w-full"
event={event}
{clientFilteredEvents.length > 0 ? (
<VirtualizedFeedRows
events={clientFilteredEvents}
gridLayout={gridLayout}
filterMutedNotes={filterMutedNotes}
bottomNoteLabel={eventReasonLabelMap.get(event.id)}
eventReasonLabelMap={eventReasonLabelMap}
useWindowScroll={feedVirtualScrollParent === null}
scrollElement={feedVirtualScrollParent}
scrollMarginTop={feedVirtualScrollMarginTop}
/>
))
)}
) : null}
{listSourceEvents.length === 0 &&
!feedFullSearchActive &&
(loading || (subRequests.length > 0 && !feedTimelineEmptyUiReady)) ? (

11
src/hooks/use-event-callback.ts

@ -0,0 +1,11 @@ @@ -0,0 +1,11 @@
import { useCallback, useRef } from 'react'
/**
* Stable callback identity with always-fresh implementation (React `useEvent` pattern).
* Avoids stale closures without forcing unrelated consumers to re-render.
*/
export function useEventCallback<A extends readonly unknown[], R>(fn: (...args: A) => R): (...args: A) => R {
const ref = useRef(fn)
ref.current = fn
return useCallback((...args: A) => ref.current(...args), [])
}

11
src/providers/DeepBrowsingProvider.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { createContext, useContext, useEffect, useRef, useState } from 'react'
import { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react'
type TDeepBrowsingContext = {
deepBrowsing: boolean
@ -69,9 +69,10 @@ export function DeepBrowsingProvider({ @@ -69,9 +69,10 @@ export function DeepBrowsingProvider({
}
}, [active, scrollAreaRef])
return (
<DeepBrowsingContext.Provider value={{ deepBrowsing, lastScrollTop }}>
{children}
</DeepBrowsingContext.Provider>
const value = useMemo(
() => ({ deepBrowsing, lastScrollTop }),
[deepBrowsing, lastScrollTop]
)
return <DeepBrowsingContext.Provider value={value}>{children}</DeepBrowsingContext.Provider>
}

153
src/providers/NostrProvider/index.tsx

@ -49,8 +49,9 @@ import dayjs from 'dayjs' @@ -49,8 +49,9 @@ import dayjs from 'dayjs'
import { Event, kinds, VerifiedEvent, getEventHash, validateEvent } from 'nostr-tools'
import * as nip19 from 'nostr-tools/nip19'
import * as nip49 from 'nostr-tools/nip49'
import { NostrContext } from '@/providers/nostr-context'
import { useCallback, useEffect, useRef, useState } from 'react'
import { NostrContext, type TNostrContext } from '@/providers/nostr-context'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useEventCallback } from '@/hooks/use-event-callback'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { BunkerSigner } from './bunker.signer'
@ -1573,9 +1574,38 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1573,9 +1574,38 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
})
}, [account])
return (
<NostrContext.Provider
value={{
const startLogin = useCallback(() => setOpenLoginDialog(true), [])
const removeAccountStable = useEventCallback(removeAccount)
const switchAccountStable = useEventCallback(switchAccount)
const nsecLoginStable = useEventCallback(nsecLogin)
const ncryptsecLoginStable = useEventCallback(ncryptsecLogin)
const npubLoginStable = useEventCallback(npubLogin)
const nip07LoginStable = useEventCallback(nip07Login)
const bunkerLoginStable = useEventCallback(bunkerLogin)
const nostrConnectionLoginStable = useEventCallback(nostrConnectionLogin)
const publishStable = useEventCallback(publish)
const attemptDeleteStable = useEventCallback(attemptDelete)
const signHttpAuthStable = useEventCallback(signHttpAuth)
const nip04EncryptStable = useEventCallback(nip04Encrypt)
const nip04DecryptStable = useEventCallback(nip04Decrypt)
const checkLoginStable = useEventCallback(checkLogin)
const signEventStable = useEventCallback(signEvent)
const updateRelayListEventStable = useEventCallback(updateRelayListEvent)
const updateCacheRelayListEventStable = useEventCallback(updateCacheRelayListEvent)
const updateHttpRelayListEventStable = useEventCallback(updateHttpRelayListEvent)
const updateProfileEventStable = useEventCallback(updateProfileEvent)
const updateFollowListEventStable = useEventCallback(updateFollowListEvent)
const updateMuteListEventStable = useEventCallback(updateMuteListEvent)
const updateBookmarkListEventStable = useEventCallback(updateBookmarkListEvent)
const updateInterestListEventStable = useEventCallback(updateInterestListEvent)
const updateUserEmojiListEventStable = useEventCallback(updateUserEmojiListEvent)
const updateFavoriteRelaysEventStable = useEventCallback(updateFavoriteRelaysEvent)
const updateBlockedRelaysEventStable = useEventCallback(updateBlockedRelaysEvent)
const updateRssFeedListEventStable = useEventCallback(updateRssFeedListEvent)
const nostrContextValue = useMemo(
(): TNostrContext => ({
isInitialized,
isAccountSessionHydrating,
pubkey: account?.pubkey ?? null,
@ -1596,37 +1626,90 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { @@ -1596,37 +1626,90 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
accounts,
nsec,
ncryptsec,
switchAccount,
nsecLogin,
ncryptsecLogin,
nip07Login,
bunkerLogin,
nostrConnectionLogin,
npubLogin,
removeAccount,
publish,
attemptDelete,
signHttpAuth,
nip04Encrypt,
nip04Decrypt,
startLogin: () => setOpenLoginDialog(true),
checkLogin,
signEvent,
updateRelayListEvent,
updateCacheRelayListEvent,
updateHttpRelayListEvent,
updateProfileEvent,
updateFollowListEvent,
updateMuteListEvent,
updateBookmarkListEvent,
updateInterestListEvent,
updateUserEmojiListEvent,
updateFavoriteRelaysEvent,
updateBlockedRelaysEvent,
updateRssFeedListEvent,
switchAccount: switchAccountStable,
nsecLogin: nsecLoginStable,
ncryptsecLogin: ncryptsecLoginStable,
nip07Login: nip07LoginStable,
bunkerLogin: bunkerLoginStable,
nostrConnectionLogin: nostrConnectionLoginStable,
npubLogin: npubLoginStable,
removeAccount: removeAccountStable,
publish: publishStable,
attemptDelete: attemptDeleteStable,
signHttpAuth: signHttpAuthStable,
nip04Encrypt: nip04EncryptStable,
nip04Decrypt: nip04DecryptStable,
startLogin,
checkLogin: checkLoginStable,
signEvent: signEventStable,
updateRelayListEvent: updateRelayListEventStable,
updateCacheRelayListEvent: updateCacheRelayListEventStable,
updateHttpRelayListEvent: updateHttpRelayListEventStable,
updateProfileEvent: updateProfileEventStable,
updateFollowListEvent: updateFollowListEventStable,
updateMuteListEvent: updateMuteListEventStable,
updateBookmarkListEvent: updateBookmarkListEventStable,
updateInterestListEvent: updateInterestListEventStable,
updateUserEmojiListEvent: updateUserEmojiListEventStable,
updateFavoriteRelaysEvent: updateFavoriteRelaysEventStable,
updateBlockedRelaysEvent: updateBlockedRelaysEventStable,
updateRssFeedListEvent: updateRssFeedListEventStable,
requestAccountNetworkHydrate
}}
>
}),
[
isInitialized,
isAccountSessionHydrating,
account,
accounts,
attemptDeleteStable,
blockedRelaysEvent,
bookmarkListEvent,
bunkerLoginStable,
cacheRelayListEvent,
checkLoginStable,
favoriteRelaysEvent,
followListEvent,
httpRelayListEvent,
interestListEvent,
muteListEvent,
ncryptsec,
ncryptsecLoginStable,
nip04DecryptStable,
nip04EncryptStable,
nip07LoginStable,
nostrConnectionLoginStable,
npubLoginStable,
nsec,
nsecLoginStable,
profile,
profileEvent,
publishStable,
relayList,
removeAccountStable,
requestAccountNetworkHydrate,
rssFeedListEvent,
signEventStable,
signHttpAuthStable,
startLogin,
switchAccountStable,
updateBlockedRelaysEventStable,
updateBookmarkListEventStable,
updateCacheRelayListEventStable,
updateFavoriteRelaysEventStable,
updateFollowListEventStable,
updateHttpRelayListEventStable,
updateInterestListEventStable,
updateMuteListEventStable,
updateProfileEventStable,
updateRelayListEventStable,
updateRssFeedListEventStable,
updateUserEmojiListEventStable,
userEmojiListEvent
]
)
return (
<NostrContext.Provider value={nostrContextValue}>
{children}
<LoginDialog open={openLoginDialog} setOpen={setOpenLoginDialog} />
<NcryptsecPasswordPrompt open={ncryptsecPasswordOpen} onResult={finishNcryptsecPasswordPrompt} />

23
src/services/client.service.ts

@ -2470,12 +2470,13 @@ class ClientService extends EventTarget { @@ -2470,12 +2470,13 @@ class ClientService extends EventTarget {
/**
* Stream matching events to the UI immediately. Initial completion is either aggregate `oneose` from all
* relays, or {@link firstRelayResultGraceMs} after the first event (whichever comes first).
* While still before EOSE, coalesce bursts onto one rAF so we do not sort the full buffer on every microtask.
*/
let streamFlushMicrotask = false
let streamFlushRafId: number | null = null
const flushStreamingSnapshot = () => {
if (eosedAt) return
const emit = () => {
streamFlushMicrotask = false
streamFlushRafId = null
if (eosedAt) return
if (needSort) {
const sorted = [...events].sort((a, b) => b.created_at - a.created_at).slice(0, filter.limit)
@ -2485,13 +2486,15 @@ class ClientService extends EventTarget { @@ -2485,13 +2486,15 @@ class ClientService extends EventTarget {
}
}
if (events.length <= 1) {
streamFlushMicrotask = false
if (streamFlushRafId != null) {
cancelAnimationFrame(streamFlushRafId)
streamFlushRafId = null
}
emit()
return
}
if (!streamFlushMicrotask) {
streamFlushMicrotask = true
queueMicrotask(emit)
if (streamFlushRafId == null) {
streamFlushRafId = requestAnimationFrame(emit)
}
}
@ -2627,6 +2630,10 @@ class ClientService extends EventTarget { @@ -2627,6 +2630,10 @@ class ClientService extends EventTarget {
if (eosedAt != null) return
clearFirstResultGraceTimer()
if (streamFlushRafId != null) {
cancelAnimationFrame(streamFlushRafId)
streamFlushRafId = null
}
eosedAt = dayjs().unix()
@ -2713,6 +2720,10 @@ class ClientService extends EventTarget { @@ -2713,6 +2720,10 @@ class ClientService extends EventTarget {
timelineKey: key,
closer: () => {
clearFirstResultGraceTimer()
if (streamFlushRafId != null) {
cancelAnimationFrame(streamFlushRafId)
streamFlushRafId = null
}
clearHttpTimelinePoll()
onEvents = () => {}
onNew = () => {}

24
src/services/indexed-db.service.ts

@ -2798,27 +2798,31 @@ class IndexedDbService { @@ -2798,27 +2798,31 @@ class IndexedDbService {
if (uniq.length === 0) return []
await this.initPromise
if (!this.db?.objectStoreNames.contains(StoreNames.EVENT_ARCHIVE)) return []
return new Promise((resolve, reject) => {
const out: Event[] = []
await Promise.all(
uniq.map(
(id) =>
new Promise<void>((resolve, reject) => {
const tx = this.db!.transaction(StoreNames.EVENT_ARCHIVE, 'readonly')
const get = tx.objectStore(StoreNames.EVENT_ARCHIVE).get(id)
const store = tx.objectStore(StoreNames.EVENT_ARCHIVE)
let pending = uniq.length
const doneOne = () => {
pending -= 1
if (pending === 0) {
tx.commit()
resolve(out)
}
}
for (const id of uniq) {
const get = store.get(id)
get.onsuccess = () => {
const row = get.result as TArchivedEventRow | undefined
if (row?.value) out.push(row.value)
tx.commit()
resolve()
doneOne()
}
get.onerror = (e) => {
tx.commit()
reject(idbEventToError(e))
}
}
})
)
)
return out
}
async deleteArchivedEvent(eventId: string): Promise<void> {

Loading…
Cancel
Save