refactor(core): replace history to ViewService.history (#6972)

upstream: #6966
This commit is contained in:
JimmFly
2024-05-22 04:01:33 +00:00
parent 3b8345ea5a
commit 28effc19e4
4 changed files with 84 additions and 57 deletions
@@ -1,4 +1,5 @@
import type { BlockElement } from '@blocksuite/block-std'; import { ViewService } from '@affine/core/modules/workbench/services/view';
import type { BaseSelection, BlockElement } from '@blocksuite/block-std';
import type { Disposable } from '@blocksuite/global/utils'; import type { Disposable } from '@blocksuite/global/utils';
import type { import type {
AffineEditorContainer, AffineEditorContainer,
@@ -7,7 +8,7 @@ import type {
} from '@blocksuite/presets'; } from '@blocksuite/presets';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import { Slot } from '@blocksuite/store'; import { Slot } from '@blocksuite/store';
import type { DocMode } from '@toeverything/infra'; import { type DocMode, useServiceOptional } from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import type React from 'react'; import type React from 'react';
import type { RefObject } from 'react'; import type { RefObject } from 'react';
@@ -102,10 +103,11 @@ export const BlocksuiteEditorContainer = forwardRef<
{ page, mode, className, style, defaultSelectedBlockId, referenceRenderer }, { page, mode, className, style, defaultSelectedBlockId, referenceRenderer },
ref ref
) { ) {
const [scrolled, setScrolled] = useState(false); const scrolledRef = useRef(false);
const rootRef = useRef<HTMLDivElement>(null); const rootRef = useRef<HTMLDivElement>(null);
const docRef = useRef<PageEditor>(null); const docRef = useRef<PageEditor>(null);
const edgelessRef = useRef<EdgelessEditor>(null); const edgelessRef = useRef<EdgelessEditor>(null);
const renderStartRef = useRef<number>(Date.now());
const slots: BlocksuiteEditorContainerRef['slots'] = useMemo(() => { const slots: BlocksuiteEditorContainerRef['slots'] = useMemo(() => {
return { return {
@@ -208,39 +210,18 @@ export const BlocksuiteEditorContainer = forwardRef<
}, [affineEditorContainerProxy, ref]); }, [affineEditorContainerProxy, ref]);
const blockElement = useBlockElementById(rootRef, defaultSelectedBlockId); const blockElement = useBlockElementById(rootRef, defaultSelectedBlockId);
const currentView = useServiceOptional(ViewService)?.view;
useEffect(() => { useEffect(() => {
let disposable: Disposable | undefined = undefined; let canceled = false;
// update the hash when the block is selected
const handleUpdateComplete = () => {
const selectManager = affineEditorContainerProxy?.host?.selection;
if (!selectManager) return;
disposable = selectManager.slots.changed.on(() => {
const selectedBlock = selectManager.find('block');
const selectedId = selectedBlock?.blockId;
const newHash = selectedId ? `#${selectedId}` : '';
//TODO: use activeView.history which is in workbench instead of history.replaceState
history.replaceState(null, '', `${window.location.pathname}${newHash}`);
// Dispatch a custom event to notify the hash change
const hashChangeEvent = new CustomEvent('hashchange-custom', {
detail: { hash: newHash },
});
window.dispatchEvent(hashChangeEvent);
});
};
// scroll to the block element when the block id is provided and the page is first loaded
const handleScrollToBlock = (blockElement: BlockElement) => { const handleScrollToBlock = (blockElement: BlockElement) => {
if (mode === 'page') { if (!mode || !blockElement) {
blockElement.scrollIntoView({ return;
behavior: 'smooth',
block: 'center',
});
} }
blockElement.scrollIntoView({
behavior: 'smooth',
block: 'center',
});
const selectManager = affineEditorContainerProxy.host?.selection; const selectManager = affineEditorContainerProxy.host?.selection;
if (!blockElement.path.length || !selectManager) { if (!blockElement.path.length || !selectManager) {
return; return;
@@ -249,22 +230,65 @@ export const BlocksuiteEditorContainer = forwardRef<
blockId: blockElement.blockId, blockId: blockElement.blockId,
}); });
selectManager.set([newSelection]); selectManager.set([newSelection]);
setScrolled(true);
}; };
affineEditorContainerProxy.updateComplete affineEditorContainerProxy.updateComplete
.then(() => { .then(() => {
if (blockElement && !scrolled) { if (blockElement && !scrolledRef.current && !canceled) {
handleScrollToBlock(blockElement); handleScrollToBlock(blockElement);
scrolledRef.current = true;
} }
handleUpdateComplete(); })
.catch(console.error);
return () => {
canceled = true;
};
}, [blockElement, affineEditorContainerProxy, mode]);
useEffect(() => {
let disposable: Disposable | null = null;
let canceled = false;
// Function to handle block selection change
const handleSelectionChange = (selection: BaseSelection[]) => {
const viewLocation = currentView?.location$.value;
const currentPath = viewLocation?.pathname;
const locationHash = viewLocation?.hash;
if (
!currentView ||
!currentPath ||
// do not update the hash during the initial render
renderStartRef.current > Date.now() - 1000
) {
return;
}
if (selection[0]?.type !== 'block') {
return currentView.history.replace(currentPath);
}
const selectedId = selection[0]?.blockId;
if (!selectedId) {
return;
}
const newHash = `#${selectedId}`;
// Only update the hash if it has changed
if (locationHash !== newHash) {
currentView.history.replace(currentPath + newHash);
}
};
affineEditorContainerProxy.updateComplete
.then(() => {
const selectManager = affineEditorContainerProxy.host?.selection;
if (!selectManager || canceled) return;
// Set up the new disposable listener
disposable = selectManager.slots.changed.on(handleSelectionChange);
}) })
.catch(console.error); .catch(console.error);
return () => { return () => {
canceled = true;
disposable?.dispose(); disposable?.dispose();
}; };
}, [blockElement, affineEditorContainerProxy, mode, scrolled]); }, [affineEditorContainerProxy, currentView]);
return ( return (
<div <div
@@ -3,6 +3,7 @@ import {
useLitPortalFactory, useLitPortalFactory,
} from '@affine/component'; } from '@affine/component';
import { useJournalInfoHelper } from '@affine/core/hooks/use-journal'; import { useJournalInfoHelper } from '@affine/core/hooks/use-journal';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { import {
BiDirectionalLinkPanel, BiDirectionalLinkPanel,
DocMetaTags, DocMetaTags,
@@ -11,6 +12,7 @@ import {
PageEditor, PageEditor,
} from '@blocksuite/presets'; } from '@blocksuite/presets';
import type { Doc } from '@blocksuite/store'; import type { Doc } from '@blocksuite/store';
import { useLiveData, useService } from '@toeverything/infra';
import React, { import React, {
forwardRef, forwardRef,
Fragment, Fragment,
@@ -70,6 +72,10 @@ export const BlocksuiteDocEditor = forwardRef<
useState<HTMLElementTagNameMap['affine-page-root']>(); useState<HTMLElementTagNameMap['affine-page-root']>();
const { isJournal } = useJournalInfoHelper(page.collection, page.id); const { isJournal } = useJournalInfoHelper(page.collection, page.id);
const workbench = useService(WorkbenchService).workbench;
const activeView = useLiveData(workbench.activeView$);
const hash = useLiveData(activeView.location$).hash;
const onDocRef = useCallback( const onDocRef = useCallback(
(el: PageEditor) => { (el: PageEditor) => {
docRef.current = el; docRef.current = el;
@@ -98,13 +104,14 @@ export const BlocksuiteDocEditor = forwardRef<
if (docPage) { if (docPage) {
setDocPage(docPage); setDocPage(docPage);
} }
if (titleRef.current) { if (titleRef.current && !hash) {
const richText = titleRef.current.querySelector('rich-text'); const richText = titleRef.current.querySelector('rich-text');
richText?.inlineEditor?.focusEnd(); richText?.inlineEditor?.focusEnd();
} else { } else {
docPage?.focusFirstParagraph(); docPage?.focusFirstParagraph();
} }
}); });
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
return ( return (
@@ -1,8 +1,10 @@
import { toast } from '@affine/component'; import { notify } from '@affine/component';
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
import { WorkbenchService } from '@affine/core/modules/workbench';
import { mixpanel } from '@affine/core/utils'; import { mixpanel } from '@affine/core/utils';
import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback, useMemo } from 'react';
type UrlType = 'share' | 'workspace'; type UrlType = 'share' | 'workspace';
@@ -19,28 +21,18 @@ const useGenerateUrl = ({ workspaceId, pageId, urlType }: UseSharingUrl) => {
// to generate a public url like https://app.affine.app/share/123/456 // to generate a public url like https://app.affine.app/share/123/456
// or https://app.affine.app/share/123/456?mode=edgeless // or https://app.affine.app/share/123/456?mode=edgeless
const [hash, setHash] = useState(window.location.hash); const workbench = useService(WorkbenchService).workbench;
const activeView = useLiveData(workbench.activeView$);
useEffect(() => { const hash = useLiveData(activeView.location$).hash;
const handleLocationChange = () => {
setHash(window.location.hash);
};
window.addEventListener('hashchange-custom', handleLocationChange);
return () => {
window.removeEventListener('hashchange-custom', handleLocationChange);
};
}, [setHash]);
const baseUrl = getAffineCloudBaseUrl(); const baseUrl = getAffineCloudBaseUrl();
const url = useMemo(() => { const url = useMemo(() => {
// baseUrl is null when running in electron and without network // baseUrl is null when running in electron and without network
if (!baseUrl) return null; if (!baseUrl) return null;
try { try {
return new URL( return new URL(
`${baseUrl}/${urlType}/${workspaceId}/${pageId}${urlType === 'workspace' ? `${hash}` : ''}` `${baseUrl}/${urlType}/${workspaceId}/${pageId}${urlType === 'workspace' && hash ? `${hash}` : ''}`
).toString(); ).toString();
} catch (e) { } catch (e) {
return null; return null;
@@ -63,7 +55,9 @@ export const useSharingUrl = ({
navigator.clipboard navigator.clipboard
.writeText(sharingUrl) .writeText(sharingUrl)
.then(() => { .then(() => {
toast(t['Copied link to clipboard']()); notify.success({
title: t['Copied link to clipboard'](),
});
}) })
.catch(err => { .catch(err => {
console.error(err); console.error(err);
@@ -73,7 +67,9 @@ export const useSharingUrl = ({
type: 'link', type: 'link',
}); });
} else { } else {
toast('Network not available'); notify.error({
title: 'Network not available',
});
} }
}, [sharingUrl, t, urlType]); }, [sharingUrl, t, urlType]);
@@ -66,7 +66,7 @@ export const LOCALES = [
originalName: '简体中文', originalName: '简体中文',
flagEmoji: '🇨🇳', flagEmoji: '🇨🇳',
base: false, base: false,
completeRate: 1, completeRate: 0.99,
res: zh_Hans, res: zh_Hans,
}, },
{ {