mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-14 21:27:20 +00:00
feat(core): center peek open doc should only load doc when idle (#10023)
fix AF-2183
This commit is contained in:
@@ -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) {
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user