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(); super();
} }
loaded(docId: string) {
const exists = this.pool.get(docId);
if (exists) {
return { doc: exists.obj, release: exists.release };
}
return null;
}
open(docId: string) { open(docId: string) {
const docRecord = this.list.doc$(docId).value; const docRecord = this.list.doc$(docId).value;
if (!docRecord) { 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 { const {
docId, docId,
blockIds, blockIds,
@@ -204,7 +210,8 @@ export function DocPeekPreview({ docRef }: { docRef: DocReferenceInfo }) {
databaseRowId, databaseRowId,
type: 'database', type: 'database',
} }
: undefined : undefined,
!animating
); );
// if sync engine has been synced and the page is null, show 404 page. // 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; target?: HTMLElement;
controls?: React.ReactNode; controls?: React.ReactNode;
onAnimationStart?: () => void; onAnimationStart?: () => void;
onAnimateEnd?: () => void; onAnimationEnd?: () => void;
mode?: PeekViewMode; mode?: PeekViewMode;
animation?: PeekViewAnimation; animation?: PeekViewAnimation;
testId?: string; testId?: string;
@@ -76,7 +76,7 @@ export const PeekViewModalContainer = forwardRef<
controls, controls,
children, children,
onAnimationStart, onAnimationStart,
onAnimateEnd, onAnimationEnd,
animation = 'zoom', animation = 'zoom',
mode = 'fit', mode = 'fit',
dialogFrame = true, dialogFrame = true,
@@ -84,9 +84,7 @@ export const PeekViewModalContainer = forwardRef<
ref ref
) { ) {
const [vtOpen, setVtOpen] = useState(open); const [vtOpen, setVtOpen] = useState(open);
const [animeState, setAnimeState] = useState<'idle' | 'ready' | 'animating'>( const [animeState, setAnimeState] = useState<'idle' | 'animating'>('idle');
'idle'
);
const contentClipRef = useRef<HTMLDivElement>(null); const contentClipRef = useRef<HTMLDivElement>(null);
const contentRef = useRef<HTMLDivElement>(null); const contentRef = useRef<HTMLDivElement>(null);
const overlayRef = useRef<HTMLDivElement>(null); const overlayRef = useRef<HTMLDivElement>(null);
@@ -143,6 +141,7 @@ export const PeekViewModalContainer = forwardRef<
if (!contentClip || !content || !target || !overlay) { if (!contentClip || !content || !target || !overlay) {
resolve(); resolve();
setAnimeState('idle'); setAnimeState('idle');
onAnimationEnd?.();
return; return;
} }
const targets = contentClip; const targets = contentClip;
@@ -196,6 +195,7 @@ export const PeekViewModalContainer = forwardRef<
complete: (ins: AnimeInstance) => { complete: (ins: AnimeInstance) => {
paramsMap?.contentWrapper?.complete?.(ins); paramsMap?.contentWrapper?.complete?.(ins);
setAnimeState('idle'); setAnimeState('idle');
onAnimationEnd?.();
overlay.style.pointerEvents = ''; overlay.style.pointerEvents = '';
if (zoomIn) { if (zoomIn) {
Object.assign(targets.style, { Object.assign(targets.style, {
@@ -238,6 +238,7 @@ export const PeekViewModalContainer = forwardRef<
*/ */
const animateZoomIn = useCallback(() => { const animateZoomIn = useCallback(() => {
setAnimeState('animating'); setAnimeState('animating');
onAnimationStart?.();
setVtOpen(true); setVtOpen(true);
setTimeout(() => { setTimeout(() => {
zoomAnimate(true, { 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. // controls delay: to make sure the time interval for animations of dialog and controls is 150ms.
400 - 230 + 150 400 - 230 + 150
); );
}, [animateControls, zoomAnimate]); }, [animateControls, onAnimationStart, zoomAnimate]);
const animateZoomOut = useCallback(() => { const animateZoomOut = useCallback(() => {
setAnimeState('animating'); setAnimeState('animating');
onAnimationStart?.();
animateControls(false); animateControls(false);
zoomAnimate(false, { zoomAnimate(false, {
contentWrapper: { contentWrapper: {
@@ -275,33 +277,38 @@ export const PeekViewModalContainer = forwardRef<
}) })
.then(() => setVtOpen(false)) .then(() => setVtOpen(false))
.catch(console.error); .catch(console.error);
}, [animateControls, zoomAnimate]); }, [animateControls, onAnimationStart, zoomAnimate]);
const animateFade = useCallback((animateIn: boolean) => { const animateFade = useCallback(
setAnimeState('animating'); (animateIn: boolean) => {
return new Promise<void>(resolve => { setAnimeState('animating');
if (animateIn) setVtOpen(true); onAnimationStart?.();
setTimeout(() => { return new Promise<void>(resolve => {
const overlay = overlayRef.current; if (animateIn) setVtOpen(true);
const contentClip = contentClipRef.current; setTimeout(() => {
if (!overlay || !contentClip) { const overlay = overlayRef.current;
resolve(); const contentClip = contentClipRef.current;
return; if (!overlay || !contentClip) {
}
anime({
targets: [overlay, contentClip],
opacity: animateIn ? [0, 1] : [1, 0],
easing: 'easeOutQuad',
duration: 230,
complete: () => {
if (!animateIn) setVtOpen(false);
setAnimeState('idle');
resolve(); 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(() => { useEffect(() => {
const onKeyDown = (e: KeyboardEvent) => { const onKeyDown = (e: KeyboardEvent) => {
@@ -332,8 +339,6 @@ export const PeekViewModalContainer = forwardRef<
<PeekViewModalOverlay <PeekViewModalOverlay
ref={overlayRef} ref={overlayRef}
className={styles.modalOverlay} className={styles.modalOverlay}
onAnimationStart={onAnimationStart}
onAnimationEnd={onAnimateEnd}
data-anime-state={animeState} data-anime-state={animeState}
/> />
<div <div

View File

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

View File

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