diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx index ed2ed46258ab..414425294f75 100644 --- a/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx +++ b/packages/frontend/core/src/components/blocksuite/block-suite-editor/blocksuite-editor-container.tsx @@ -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 { AffineEditorContainer, @@ -7,7 +8,7 @@ import type { } from '@blocksuite/presets'; import type { Doc } 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 type React from 'react'; import type { RefObject } from 'react'; @@ -102,10 +103,11 @@ export const BlocksuiteEditorContainer = forwardRef< { page, mode, className, style, defaultSelectedBlockId, referenceRenderer }, ref ) { - const [scrolled, setScrolled] = useState(false); + const scrolledRef = useRef(false); const rootRef = useRef(null); const docRef = useRef(null); const edgelessRef = useRef(null); + const renderStartRef = useRef(Date.now()); const slots: BlocksuiteEditorContainerRef['slots'] = useMemo(() => { return { @@ -208,39 +210,18 @@ export const BlocksuiteEditorContainer = forwardRef< }, [affineEditorContainerProxy, ref]); const blockElement = useBlockElementById(rootRef, defaultSelectedBlockId); + const currentView = useServiceOptional(ViewService)?.view; useEffect(() => { - let disposable: Disposable | undefined = undefined; - - // 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 + let canceled = false; const handleScrollToBlock = (blockElement: BlockElement) => { - if (mode === 'page') { - blockElement.scrollIntoView({ - behavior: 'smooth', - block: 'center', - }); + if (!mode || !blockElement) { + return; } + blockElement.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); const selectManager = affineEditorContainerProxy.host?.selection; if (!blockElement.path.length || !selectManager) { return; @@ -249,22 +230,65 @@ export const BlocksuiteEditorContainer = forwardRef< blockId: blockElement.blockId, }); selectManager.set([newSelection]); - setScrolled(true); }; - affineEditorContainerProxy.updateComplete .then(() => { - if (blockElement && !scrolled) { + if (blockElement && !scrolledRef.current && !canceled) { 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); return () => { + canceled = true; disposable?.dispose(); }; - }, [blockElement, affineEditorContainerProxy, mode, scrolled]); + }, [affineEditorContainerProxy, currentView]); return (
(); 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( (el: PageEditor) => { docRef.current = el; @@ -98,13 +104,14 @@ export const BlocksuiteDocEditor = forwardRef< if (docPage) { setDocPage(docPage); } - if (titleRef.current) { + if (titleRef.current && !hash) { const richText = titleRef.current.querySelector('rich-text'); richText?.inlineEditor?.focusEnd(); } else { docPage?.focusFirstParagraph(); } }); + // eslint-disable-next-line react-hooks/exhaustive-deps }, []); return ( diff --git a/packages/frontend/core/src/hooks/affine/use-share-url.ts b/packages/frontend/core/src/hooks/affine/use-share-url.ts index dfa0fc707f01..0cf6d333826d 100644 --- a/packages/frontend/core/src/hooks/affine/use-share-url.ts +++ b/packages/frontend/core/src/hooks/affine/use-share-url.ts @@ -1,8 +1,10 @@ -import { toast } from '@affine/component'; +import { notify } from '@affine/component'; import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; +import { WorkbenchService } from '@affine/core/modules/workbench'; import { mixpanel } from '@affine/core/utils'; 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'; @@ -19,28 +21,18 @@ const useGenerateUrl = ({ workspaceId, pageId, urlType }: UseSharingUrl) => { // to generate a public url like https://app.affine.app/share/123/456 // or https://app.affine.app/share/123/456?mode=edgeless - const [hash, setHash] = useState(window.location.hash); - - useEffect(() => { - const handleLocationChange = () => { - setHash(window.location.hash); - }; - window.addEventListener('hashchange-custom', handleLocationChange); - - return () => { - window.removeEventListener('hashchange-custom', handleLocationChange); - }; - }, [setHash]); + const workbench = useService(WorkbenchService).workbench; + const activeView = useLiveData(workbench.activeView$); + const hash = useLiveData(activeView.location$).hash; const baseUrl = getAffineCloudBaseUrl(); - const url = useMemo(() => { // baseUrl is null when running in electron and without network if (!baseUrl) return null; try { return new URL( - `${baseUrl}/${urlType}/${workspaceId}/${pageId}${urlType === 'workspace' ? `${hash}` : ''}` + `${baseUrl}/${urlType}/${workspaceId}/${pageId}${urlType === 'workspace' && hash ? `${hash}` : ''}` ).toString(); } catch (e) { return null; @@ -63,7 +55,9 @@ export const useSharingUrl = ({ navigator.clipboard .writeText(sharingUrl) .then(() => { - toast(t['Copied link to clipboard']()); + notify.success({ + title: t['Copied link to clipboard'](), + }); }) .catch(err => { console.error(err); @@ -73,7 +67,9 @@ export const useSharingUrl = ({ type: 'link', }); } else { - toast('Network not available'); + notify.error({ + title: 'Network not available', + }); } }, [sharingUrl, t, urlType]); diff --git a/packages/frontend/i18n/src/resources/index.ts b/packages/frontend/i18n/src/resources/index.ts index 7209ad2f822a..8216fe156bc1 100644 --- a/packages/frontend/i18n/src/resources/index.ts +++ b/packages/frontend/i18n/src/resources/index.ts @@ -66,7 +66,7 @@ export const LOCALES = [ originalName: '简体中文', flagEmoji: '🇨🇳', base: false, - completeRate: 1, + completeRate: 0.99, res: zh_Hans, }, {