From 2bf261efb2c362300b90440cd5dcbbfc817878ee Mon Sep 17 00:00:00 2001 From: Silberengel Date: Thu, 4 Jun 2026 20:13:39 +0200 Subject: [PATCH] bug-fix janky feeds --- package-lock.json | 4 +- package.json | 2 +- src/PageManager.tsx | 125 +++++++++++------- src/components/PostEditor/PostContent.tsx | 14 +- .../PostEditor/PostTextarea/index.tsx | 32 ++--- src/components/PostEditor/index.tsx | 25 +++- src/index.css | 6 + 7 files changed, 133 insertions(+), 75 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5198aff7..2faf67f1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "imwald", - "version": "23.19.0", + "version": "23.19.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "imwald", - "version": "23.19.0", + "version": "23.19.1", "license": "MIT", "dependencies": { "@asciidoctor/core": "^3.0.4", diff --git a/package.json b/package.json index 6a0be2e6..804d5b21 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "imwald", - "version": "23.19.0", + "version": "23.19.1", "description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery", "private": true, "type": "module", diff --git a/src/PageManager.tsx b/src/PageManager.tsx index 34c0ece4..0cea87cc 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -479,32 +479,8 @@ export function useSmartNoteNavigation() { } const { noteId } = parsed - navigationEventStore.clear() - if (event) { - navigationEventStore.setEvent(event, noteId) - client.addEventToCache(event) - await prefetchThreadContextForNavigation(event).then((prefetched) => { - for (const ev of prefetched) { - client.addEventToCache(ev) - navigationEventStore.setEvent(ev) - } - }) - } - // Pre-cache related events (parent, root) and nostr embeds so NotePage avoids skeletons. - if (relatedEvents?.length) { - for (const ev of relatedEvents) { - if (ev && ev !== event) { - client.addEventToCache(ev) - navigationEventStore.setEvent(ev) - } - } - } - if (event) { - client.prefetchEmbeddedEventsForParents( - [event, ...(relatedEvents ?? []).filter((ev) => ev && ev !== event)] - ) - } - + primeNoteNavigationCache(noteId, event, relatedEvents) + // Build contextual URL based on current page const contextualUrl = buildNoteUrl(noteId, currentPrimaryPage) @@ -552,25 +528,7 @@ export function useSmartNoteNavigationOptional() { return } const { noteId } = parsed - navigationEventStore.clear() - if (event) { - navigationEventStore.setEvent(event, noteId) - client.addEventToCache(event) - await prefetchThreadContextForNavigation(event).then((prefetched) => { - for (const ev of prefetched) { - client.addEventToCache(ev) - navigationEventStore.setEvent(ev) - } - }) - } - if (relatedEvents?.length) { - for (const ev of relatedEvents) { - if (ev && ev !== event) { - client.addEventToCache(ev) - navigationEventStore.setEvent(ev) - } - } - } + primeNoteNavigationCache(noteId, event, relatedEvents) const contextualUrl = buildNoteUrl(noteId, currentPrimaryPage) if (isSmallScreen) { push(contextualUrl) @@ -2010,6 +1968,14 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } if (isCurrentPage(secondaryStackRef.current, url)) { const top = secondaryStackRef.current[secondaryStackRef.current.length - 1] + if (top && !top.component) { + const restored = ensureStackItemComponent(top) + if (restored.component) { + const next = [...secondaryStackRef.current.slice(0, -1), restored] + secondaryStackRef.current = next + setSecondaryStack(next) + } + } if (isSmallScreen && top) { window.history.pushState({ index: top.index, url }, '', url) } @@ -2018,8 +1984,9 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } recentSecondaryPushRef.current = { url, at: now } - noteStatsService.setBackgroundStatsPaused(true) + // Mobile overlays the feed — keep stats/live updates on the visible timeline. if (!isSmallScreen) { + noteStatsService.setBackgroundStatsPaused(true) client.interruptBackgroundQueries() } @@ -2066,6 +2033,15 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { if (isCurrentPage(prevStack, url)) { const top = prevStack[prevStack.length - 1] + if (top && !top.component) { + const restored = ensureStackItemComponent(top) + if (restored.component) { + if (isSmallScreen) { + window.history.pushState({ index: restored.index, url }, '', url) + } + return [...prevStack.slice(0, -1), restored] + } + } if (isSmallScreen && top) { window.history.pushState({ index: top.index, url }, '', url) } @@ -2280,8 +2256,12 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { const mobileSecondaryOverlaysFeed = isSmallScreen && secondaryStack.length > 0 && primaryNoteView == null + const primaryFeedStillVisible = + panelMode === 'double' || !primaryObscured || mobileSecondaryOverlaysFeed + useLayoutEffect(() => { - noteStatsService.setBackgroundStatsPaused(primaryFrozen) + const pauseBackgroundStats = primaryObscured && !primaryFeedStillVisible + noteStatsService.setBackgroundStatsPaused(pauseBackgroundStats) if (primaryFrozen) { extendProfileNetworkDeferral(PROFILE_SECONDARY_PANEL_DEFER_MS) // Keep in-flight REQ on double-pane and mobile feed overlay; interrupt only when primary is unmounted. @@ -2290,7 +2270,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { client.interruptBackgroundQueries() } } - }, [primaryFrozen, isSmallScreen, panelMode, primaryNoteView]) + }, [primaryObscured, primaryFeedStillVisible, isSmallScreen, panelMode, primaryNoteView]) const primaryPageContextValue = useMemo( (): PrimaryPageContextValue => ({ @@ -2581,12 +2561,57 @@ export function SecondaryPageLink({ ) } +/** Re-mount a stack frame when LRU eviction cleared `component` (otherwise the panel is blank). */ +function ensureStackItemComponent(item: TStackItem): TStackItem { + if (item.component) return item + const { component, ref } = findAndCreateComponent(item.url, item.index) + if (!component) return item + return { ...item, component, ref } +} + +function primeNoteNavigationCache( + noteId: string, + event?: Event, + relatedEvents?: Event[] +): void { + navigationEventStore.clear() + if (event) { + navigationEventStore.setEvent(event, noteId) + client.addEventToCache(event) + void prefetchThreadContextForNavigation(event).then((prefetched) => { + for (const ev of prefetched) { + client.addEventToCache(ev) + navigationEventStore.setEvent(ev) + } + }) + } + if (relatedEvents?.length) { + for (const ev of relatedEvents) { + if (ev && ev !== event) { + client.addEventToCache(ev) + navigationEventStore.setEvent(ev) + } + } + } + if (event) { + void client.prefetchEmbeddedEventsForParents( + [event, ...(relatedEvents ?? []).filter((ev) => ev && ev !== event)] + ) + } +} + function isCurrentPage(stack: TStackItem[], url: string) { const currentPage = stack[stack.length - 1] if (!currentPage) return false - logger.component('PageManager', 'isCurrentPage check', { currentUrl: currentPage.url, newUrl: url, match: currentPage.url === url }) - return currentPage.url === url + const match = + currentPage.url === url || secondaryPanelUrlsMatch(currentPage.url, url) + logger.component('PageManager', 'isCurrentPage check', { + currentUrl: currentPage.url, + newUrl: url, + match + }) + return match } /** Route elements are `` — props must be applied to the lazy leaf, not Suspense. */ diff --git a/src/components/PostEditor/PostContent.tsx b/src/components/PostEditor/PostContent.tsx index 78aa4598..1cc6f11b 100644 --- a/src/components/PostEditor/PostContent.tsx +++ b/src/components/PostEditor/PostContent.tsx @@ -2538,8 +2538,10 @@ export default function PostContent({
{/* Dynamic Title based on mode */} @@ -3377,6 +3379,7 @@ export default function PostContent({
)} +
post()} className={cn( - isPoll ? 'min-h-20' : isSmallScreen ? 'min-h-36' : 'min-h-52', + isPoll + ? 'min-h-20' + : isSmallScreen + ? 'min-h-[min(42vh,20rem)]' + : 'min-h-52', isDiscussionThread && threadErrors.content && 'border-destructive' )} onUploadStart={handleUploadStart} @@ -3644,6 +3651,7 @@ export default function PostContent({ })() } /> +
{isDiscussionThread && !parentEvent && (
{threadErrors.content &&

{threadErrors.content}

} diff --git a/src/components/PostEditor/PostTextarea/index.tsx b/src/components/PostEditor/PostTextarea/index.tsx index 59dd0b66..64086152 100644 --- a/src/components/PostEditor/PostTextarea/index.tsx +++ b/src/components/PostEditor/PostTextarea/index.tsx @@ -226,18 +226,6 @@ const PostTextarea = forwardRef< }) }, [editor, editorSurfaceClass]) - useEffect(() => { - if (!editor || !isSmallScreen) return - const scrollEditorIntoView = () => { - requestAnimationFrame(() => { - editor.view.dom.scrollIntoView({ block: 'nearest', inline: 'nearest' }) - }) - } - editor.on('focus', scrollEditorIntoView) - return () => { - editor.off('focus', scrollEditorIntoView) - } - }, [editor, isSmallScreen]) useImperativeHandle(ref, () => ({ appendText: (text: string, addNewline = false) => { @@ -327,8 +315,14 @@ const PostTextarea = forwardRef< ) return ( - -
+ +
{t('Edit')} @@ -346,10 +340,16 @@ const PostTextarea = forwardRef< {editor ? ( - + ) : (
(null) + const wasOpenRef = useRef(false) + + useEffect(() => { + if (open && isSmallScreen && !wasOpenRef.current) { + const vh = window.visualViewport?.height ?? window.innerHeight + setMobileSheetHeightPx(Math.round(vh)) + } + if (!open) { + setMobileSheetHeightPx(null) + } + wasOpenRef.current = open + }, [open, isSmallScreen]) useEffect(() => { if (!open) return @@ -98,7 +112,12 @@ export default function PostEditor({ return ( { @@ -114,7 +133,7 @@ export default function PostEditor({ } }} > -
+
Post Editor Create a new post or reply diff --git a/src/index.css b/src/index.css index c3b4a968..939798b3 100644 --- a/src/index.css +++ b/src/index.css @@ -102,6 +102,12 @@ sans-serif; } + @media (max-width: 768px) { + .tiptap .ProseMirror { + min-height: min(42vh, 20rem); + } + } + .scrollbar-hide { -ms-overflow-style: none; /* Internet Explorer 10+ */ scrollbar-width: none; /* Firefox */