feat(mobile/nav): avoid flicker caused by preview rendering

This commit is contained in:
Elian Doran
2026-04-20 23:50:48 +03:00
parent 1714adc4e2
commit 24469b32ea
3 changed files with 61 additions and 24 deletions

View File

@@ -207,19 +207,22 @@ function NoteAttributes({ note }: { note: FNote }) {
return <span className="note-list-attributes" ref={ref} />;
}
export function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes, showTextRepresentation }: {
export function NoteContent({ note, trim, noChildrenList, highlightedTokens, includeArchivedNotes, showTextRepresentation, onReady }: {
note: FNote;
trim?: boolean;
noChildrenList?: boolean;
highlightedTokens: string[] | null | undefined;
includeArchivedNotes: boolean;
showTextRepresentation?: boolean;
onReady?: () => void;
}) {
const contentRef = useRef<HTMLDivElement>(null);
const highlightSearch = useImperativeSearchHighlighlighting(highlightedTokens);
const [ready, setReady] = useState(false);
const [noteType, setNoteType] = useState<string>("none");
const onReadyRef = useRef(onReady);
onReadyRef.current = onReady;
useEffect(() => {
const contentElement = contentRef.current;
@@ -233,6 +236,7 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc
}, []);
useEffect(() => {
setReady(false);
content_renderer.getRenderedContent(note, {
trim,
noChildrenList,
@@ -250,12 +254,14 @@ export function NoteContent({ note, trim, noChildrenList, highlightedTokens, inc
highlightSearch(contentRef.current);
setNoteType(type);
setReady(true);
onReadyRef.current?.();
})
.catch(e => {
console.warn(`Caught error while rendering note '${note.noteId}' of type '${note.type}'`);
console.error(e);
contentRef.current?.replaceChildren(t("collections.rendering_error"));
setReady(true);
onReadyRef.current?.();
});
}, [ note, highlightedTokens ]);

View File

@@ -25,6 +25,7 @@
}
.mobile-navigator-scroll {
position: relative;
flex: 1 1 auto;
overflow-y: auto;
overflow-x: hidden;
@@ -34,6 +35,22 @@
padding-bottom: env(safe-area-inset-bottom, 0);
}
.mobile-navigator-scroll.is-pending > .mobile-navigator-current-tile,
.mobile-navigator-scroll.is-pending > .mobile-navigator-list {
visibility: hidden;
}
.mobile-navigator-placeholder {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.8em;
color: var(--muted-text-color, var(--main-text-color));
pointer-events: none;
}
.mobile-navigator-current-tile {
display: flex;
flex-direction: column;

View File

@@ -79,6 +79,16 @@ export default function MobileNoteNavigator() {
const children = useNavigatorChildren(parentNote, hideArchived, isLoaded);
const canGoBack = stack.length > 1;
// Gate the visible body on the content preview being ready to avoid a layout shift
// when the preview finishes rendering. Tied to the parent's noteId so a stale "ready"
// flag from the previous column can't leak into the new one.
const [readyForNoteId, setReadyForNoteId] = useState<string | null>(null);
const previewReady = !!parentNote && readyForNoteId === parentNote.noteId;
const bodyVisible = previewReady && isLoaded;
const onPreviewReady = useCallback(() => {
if (parentNote) setReadyForNoteId(parentNote.noteId);
}, [parentNote]);
const goBack = useCallback(() => {
manualStackRef.current = true;
setStack((prev) => (prev.length > 1 ? prev.slice(0, -1) : prev));
@@ -117,40 +127,38 @@ export default function MobileNoteNavigator() {
/>
</div>
<div className="mobile-navigator-scroll">
<div
className={clsx("mobile-navigator-current-tile", {
"is-active": isCurrentActive,
"is-archived": parentNote?.isArchived
})}
role="button"
tabIndex={0}
onClick={openCurrent}
>
<div className="mobile-navigator-current-header">
<Icon icon={parentIcon ?? "bx bx-folder"} className="mobile-navigator-current-icon" />
<span className="mobile-navigator-current-title">
{parentNote?.title ?? t("mobile_note_navigator.loading")}
</span>
<Icon icon="bx bx-link-external" className="mobile-navigator-current-open" />
</div>
{parentNote && (
<div className={clsx("mobile-navigator-scroll", !bodyVisible && "is-pending")}>
{parentNote && (
<div
className={clsx("mobile-navigator-current-tile", {
"is-active": isCurrentActive,
"is-archived": parentNote.isArchived
})}
role="button"
tabIndex={0}
onClick={openCurrent}
>
<div className="mobile-navigator-current-header">
<Icon icon={parentIcon ?? "bx bx-folder"} className="mobile-navigator-current-icon" />
<span className="mobile-navigator-current-title">{parentNote.title}</span>
<Icon icon="bx bx-link-external" className="mobile-navigator-current-open" />
</div>
<div className="mobile-navigator-current-preview">
<NoteContent
key={parentNote.noteId}
note={parentNote}
trim
noChildrenList
highlightedTokens={null}
includeArchivedNotes={!hideArchived}
onReady={onPreviewReady}
/>
</div>
)}
</div>
</div>
)}
<div className="mobile-navigator-list">
{!isLoaded ? (
<NoItems icon="bx bx-loader" text={t("mobile_note_navigator.loading")} />
) : children.length === 0 ? (
{!isLoaded ? null : children.length === 0 ? (
<NoItems icon="bx bx-folder-open" text={t("mobile_note_navigator.empty")} />
) : (
children.map((child) => (
@@ -166,6 +174,12 @@ export default function MobileNoteNavigator() {
))
)}
</div>
{!bodyVisible && (
<div className="mobile-navigator-placeholder">
<span className="bx bx-loader bx-spin" />
</div>
)}
</div>
</div>
);