feat(core): center peek open doc should only load doc when idle (#10023)

fix AF-2183
This commit is contained in:
pengx17
2025-02-11 04:56:23 +00:00
parent b1d7128e2b
commit d89d4a71dd
5 changed files with 113 additions and 47 deletions

View File

@@ -64,6 +64,14 @@ export class DocsService extends Service {
super();
}
loaded(docId: string) {
const exists = this.pool.get(docId);
if (exists) {
return { doc: exists.obj, release: exists.release };
}
return null;
}
open(docId: string) {
const docRecord = this.list.doc$(docId).value;
if (!docRecord) {

View File

@@ -179,7 +179,13 @@ function DocPeekPreviewEditor({
);
}
export function DocPeekPreview({ docRef }: { docRef: DocReferenceInfo }) {
export function DocPeekPreview({
docRef,
animating,
}: {
docRef: DocReferenceInfo;
animating?: boolean;
}) {
const {
docId,
blockIds,
@@ -204,7 +210,8 @@ export function DocPeekPreview({ docRef }: { docRef: DocReferenceInfo }) {
databaseRowId,
type: 'database',
}
: undefined
: undefined,
!animating
);
// if sync engine has been synced and the page is null, show 404 page.

View File

@@ -55,7 +55,7 @@ export type PeekViewModalContainerProps = PropsWithChildren<{
target?: HTMLElement;
controls?: React.ReactNode;
onAnimationStart?: () => void;
onAnimateEnd?: () => void;
onAnimationEnd?: () => void;
mode?: PeekViewMode;
animation?: PeekViewAnimation;
testId?: string;
@@ -76,7 +76,7 @@ export const PeekViewModalContainer = forwardRef<
controls,
children,
onAnimationStart,
onAnimateEnd,
onAnimationEnd,
animation = 'zoom',
mode = 'fit',
dialogFrame = true,
@@ -84,9 +84,7 @@ export const PeekViewModalContainer = forwardRef<
ref
) {
const [vtOpen, setVtOpen] = useState(open);
const [animeState, setAnimeState] = useState<'idle' | 'ready' | 'animating'>(
'idle'
);
const [animeState, setAnimeState] = useState<'idle' | 'animating'>('idle');
const contentClipRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null);
const overlayRef = useRef<HTMLDivElement>(null);
@@ -143,6 +141,7 @@ export const PeekViewModalContainer = forwardRef<
if (!contentClip || !content || !target || !overlay) {
resolve();
setAnimeState('idle');
onAnimationEnd?.();
return;
}
const targets = contentClip;
@@ -196,6 +195,7 @@ export const PeekViewModalContainer = forwardRef<
complete: (ins: AnimeInstance) => {
paramsMap?.contentWrapper?.complete?.(ins);
setAnimeState('idle');
onAnimationEnd?.();
overlay.style.pointerEvents = '';
if (zoomIn) {
Object.assign(targets.style, {
@@ -238,6 +238,7 @@ export const PeekViewModalContainer = forwardRef<
*/
const animateZoomIn = useCallback(() => {
setAnimeState('animating');
onAnimationStart?.();
setVtOpen(true);
setTimeout(() => {
zoomAnimate(true, {
@@ -257,9 +258,10 @@ export const PeekViewModalContainer = forwardRef<
// controls delay: to make sure the time interval for animations of dialog and controls is 150ms.
400 - 230 + 150
);
}, [animateControls, zoomAnimate]);
}, [animateControls, onAnimationStart, zoomAnimate]);
const animateZoomOut = useCallback(() => {
setAnimeState('animating');
onAnimationStart?.();
animateControls(false);
zoomAnimate(false, {
contentWrapper: {
@@ -275,33 +277,38 @@ export const PeekViewModalContainer = forwardRef<
})
.then(() => setVtOpen(false))
.catch(console.error);
}, [animateControls, zoomAnimate]);
}, [animateControls, onAnimationStart, zoomAnimate]);
const animateFade = useCallback((animateIn: boolean) => {
setAnimeState('animating');
return new Promise<void>(resolve => {
if (animateIn) setVtOpen(true);
setTimeout(() => {
const overlay = overlayRef.current;
const contentClip = contentClipRef.current;
if (!overlay || !contentClip) {
resolve();
return;
}
anime({
targets: [overlay, contentClip],
opacity: animateIn ? [0, 1] : [1, 0],
easing: 'easeOutQuad',
duration: 230,
complete: () => {
if (!animateIn) setVtOpen(false);
setAnimeState('idle');
const animateFade = useCallback(
(animateIn: boolean) => {
setAnimeState('animating');
onAnimationStart?.();
return new Promise<void>(resolve => {
if (animateIn) setVtOpen(true);
setTimeout(() => {
const overlay = overlayRef.current;
const contentClip = contentClipRef.current;
if (!overlay || !contentClip) {
resolve();
},
return;
}
anime({
targets: [overlay, contentClip],
opacity: animateIn ? [0, 1] : [1, 0],
easing: 'easeOutQuad',
duration: 230,
complete: () => {
if (!animateIn) setVtOpen(false);
setAnimeState('idle');
onAnimationEnd?.();
resolve();
},
});
});
});
});
}, []);
},
[onAnimationEnd, onAnimationStart]
);
useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => {
@@ -332,8 +339,6 @@ export const PeekViewModalContainer = forwardRef<
<PeekViewModalOverlay
ref={overlayRef}
className={styles.modalOverlay}
onAnimationStart={onAnimationStart}
onAnimationEnd={onAnimateEnd}
data-anime-state={animeState}
/>
<div

View File

@@ -2,7 +2,7 @@ import { toReactNode } from '@affine/component';
import { AIChatBlockPeekViewTemplate } from '@affine/core/blocksuite/presets/ai';
import { BlockComponent } from '@blocksuite/affine/block-std';
import { useLiveData, useService } from '@toeverything/infra';
import { useEffect, useMemo } from 'react';
import { useCallback, useEffect, useMemo, useState } from 'react';
import type { ActivePeekView } from '../entities/peek-view';
import { PeekViewService } from '../services/peek-view';
@@ -19,12 +19,12 @@ import {
DocPeekViewControls,
} from './peek-view-controls';
function renderPeekView({ info }: ActivePeekView) {
function renderPeekView({ info }: ActivePeekView, animating?: boolean) {
if (info.type === 'template') {
return toReactNode(info.template);
}
if (info.type === 'doc') {
return <DocPeekPreview docRef={info.docRef} />;
return <DocPeekPreview docRef={info.docRef} animating={animating} />;
}
if (info.type === 'attachment' && info.docRef.blockIds?.[0]) {
@@ -77,13 +77,14 @@ const getMode = (info: ActivePeekView['info']) => {
};
const getRendererProps = (
activePeekView?: ActivePeekView
activePeekView?: ActivePeekView,
animating?: boolean
): Partial<PeekViewModalContainerProps> | undefined => {
if (!activePeekView) {
return;
}
const preview = renderPeekView(activePeekView);
const preview = renderPeekView(activePeekView, animating);
const controls = renderControls(activePeekView);
return {
children: preview,
@@ -106,12 +107,24 @@ export const PeekViewManagerModal = () => {
const activePeekView = useLiveData(peekViewEntity.active$);
const show = useLiveData(peekViewEntity.show$);
const [animating, setAnimating] = useState(false);
const onAnimationStart = useCallback(() => {
console.log('onAnimationStart');
setAnimating(true);
}, []);
const onAnimationEnd = useCallback(() => {
console.log('onAnimationEnd');
setAnimating(false);
}, []);
const renderProps = useMemo(() => {
if (!activePeekView) {
return;
}
return getRendererProps(activePeekView);
}, [activePeekView]);
return getRendererProps(activePeekView, animating);
}, [activePeekView, animating]);
useEffect(() => {
const subscription = peekViewEntity.show$.subscribe(() => {
@@ -135,6 +148,8 @@ export const PeekViewManagerModal = () => {
peekViewEntity.close();
}
}}
onAnimationStart={onAnimationStart}
onAnimationEnd={onAnimationEnd}
>
{renderProps?.children}
</PeekViewModalContainer>

View File

@@ -12,11 +12,13 @@ export const useEditor = (
pageId: string,
preferMode?: DocMode,
preferSelector?: EditorSelector,
defaultOpenProperty?: DefaultOpenProperty
defaultOpenProperty?: DefaultOpenProperty,
canLoad?: boolean
) => {
const currentWorkspace = useService(WorkspaceService).workspace;
const docsService = useService(DocsService);
const docRecordList = docsService.list;
const [loading, setLoading] = useState(false);
const docListReady = useLiveData(docRecordList.isReady$);
const docRecord = docRecordList.doc$(pageId).value;
const preferModeRef = useRef(preferMode);
@@ -25,16 +27,40 @@ export const useEditor = (
const [doc, setDoc] = useState<Doc | null>(null);
const [editor, setEditor] = useState<Editor | null>(null);
useLayoutEffect(() => {
useEffect(() => {
if (!docRecord) {
return;
}
const { doc: opened, release } = docsService.open(pageId);
setDoc(opened);
let canceled = false;
let release: () => void;
setLoading(true);
const loaded = docsService.loaded(pageId);
if (loaded) {
setDoc(loaded.doc);
release = loaded.release;
setLoading(false);
} else if (canLoad) {
requestIdleCallback(
() => {
if (canceled) {
return;
}
const { doc: opened, release: _release } = docsService.open(pageId);
setDoc(opened);
release = _release;
setLoading(false);
},
{
timeout: 1000,
}
);
}
return () => {
release();
canceled = true;
release?.();
setLoading(false);
};
}, [docRecord, docsService, pageId]);
}, [canLoad, docRecord, docsService, pageId]);
useLayoutEffect(() => {
if (!doc) {
@@ -55,5 +81,10 @@ export const useEditor = (
return currentWorkspace.engine.doc.addPriority(pageId, 10);
}, [currentWorkspace, pageId]);
return { doc, editor, workspace: currentWorkspace, loading: !docListReady };
return {
doc,
editor,
workspace: currentWorkspace,
loading: !docListReady || loading,
};
};