diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts index b99511257b..9aa22527ed 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/index.css.ts @@ -142,3 +142,8 @@ export const journalShareButton = style({ height: 32, padding: '0px 8px', }); +export const shortcutStyle = style({ + fontSize: cssVar('fontXs'), + color: cssVar('textSecondaryColor'), + fontWeight: 400, +}); diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx index db37e6aa6d..8a26f5f24a 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-export.tsx @@ -1,15 +1,15 @@ -import { Button } from '@affine/component/ui/button'; +import { MenuIcon, MenuItem } from '@affine/component'; import { Divider } from '@affine/component/ui/divider'; import { ExportMenuItems } from '@affine/core/components/page-list'; +import { useExportPage } from '@affine/core/hooks/affine/use-export-page'; +import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { LinkIcon } from '@blocksuite/icons'; +import { CopyIcon } from '@blocksuite/icons'; import { DocService, useLiveData, useService } from '@toeverything/infra'; -import { useExportPage } from '../../../../hooks/affine/use-export-page'; import * as styles from './index.css'; import type { ShareMenuProps } from './share-menu'; -import { useSharingUrl } from './use-share-url'; export const ShareExport = ({ workspaceMetadata: workspace, @@ -26,6 +26,7 @@ export const ShareExport = ({ }); const exportHandler = useExportPage(currentPage); const currentMode = useLiveData(doc.mode$); + const isMac = environment.isBrowser && environment.isMacOs; return ( <> @@ -52,15 +53,24 @@ export const ShareExport = ({ {t['com.affine.share-menu.share-privately.description']()}
- +
) : null} diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx index 135cf006e5..bb9fde300e 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-menu.tsx @@ -1,6 +1,8 @@ import { Button } from '@affine/component/ui/button'; import { Divider } from '@affine/component/ui/divider'; import { Menu } from '@affine/component/ui/menu'; +import { useRegisterCopyLinkCommands } from '@affine/core/hooks/affine/use-register-copy-link-commands'; +import { useIsActiveView } from '@affine/core/modules/workbench'; import { WorkspaceFlavour } from '@affine/env/workspace'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { WebIcon } from '@blocksuite/icons'; @@ -65,6 +67,13 @@ const LocalShareMenu = (props: ShareMenuProps) => { const CloudShareMenu = (props: ShareMenuProps) => { const t = useAFFiNEI18N(); + // only enable copy link commands when the view is active and the workspace is cloud + const isActiveView = useIsActiveView(); + useRegisterCopyLinkCommands({ + workspaceId: props.workspaceMetadata.id, + docId: props.currentPage.id, + isActiveView, + }); return ( } diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx index 84dabcecb5..a12a259c51 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx +++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/share-page.tsx @@ -9,7 +9,9 @@ import { import { PublicLinkDisableModal } from '@affine/component/disable-public-link'; import { Button } from '@affine/component/ui/button'; import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu'; +import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url'; import { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks'; +import { ServerConfigService } from '@affine/core/modules/cloud'; import { ShareService } from '@affine/core/modules/share-doc'; import { mixpanel } from '@affine/core/utils'; import { WorkspaceFlavour } from '@affine/env/workspace'; @@ -29,11 +31,9 @@ import { cssVar } from '@toeverything/theme'; import { Suspense, useEffect, useMemo, useState } from 'react'; import { ErrorBoundary } from 'react-error-boundary'; -import { ServerConfigService } from '../../../../modules/cloud'; import { CloudSvg } from '../cloud-svg'; import * as styles from './index.css'; import type { ShareMenuProps } from './share-menu'; -import { useSharingUrl } from './use-share-url'; export const LocalSharePage = (props: ShareMenuProps) => { const t = useAFFiNEI18N(); 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 ee9a6da6a2..a5b99aa970 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 type { Disposable } from '@blocksuite/global/utils'; import type { AffineEditorContainer, EdgelessEditor, @@ -101,6 +102,7 @@ export const BlocksuiteEditorContainer = forwardRef< { page, mode, className, style, defaultSelectedBlockId, customRenderers }, ref ) { + const [scrolled, setScrolled] = useState(false); const rootRef = useRef(null); const docRef = useRef(null); const edgelessRef = useRef(null); @@ -208,27 +210,61 @@ export const BlocksuiteEditorContainer = forwardRef< const blockElement = useBlockElementById(rootRef, defaultSelectedBlockId); useEffect(() => { - if (blockElement) { - affineEditorContainerProxy.updateComplete - .then(() => { - if (mode === 'page') { - blockElement.scrollIntoView({ - behavior: 'smooth', - block: 'center', - }); - } - const selectManager = affineEditorContainerProxy.host?.selection; - if (!blockElement.path.length || !selectManager) { - return; - } - const newSelection = selectManager.create('block', { - path: blockElement.path, - }); - selectManager.set([newSelection]); - }) - .catch(console.error); - } - }, [blockElement, affineEditorContainerProxy, mode]); + 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 + const handleScrollToBlock = (blockElement: BlockElement) => { + if (mode === 'page') { + blockElement.scrollIntoView({ + behavior: 'smooth', + block: 'center', + }); + } + const selectManager = affineEditorContainerProxy.host?.selection; + if (!blockElement.path.length || !selectManager) { + return; + } + const newSelection = selectManager.create('block', { + path: blockElement.path, + }); + selectManager.set([newSelection]); + setScrolled(true); + }; + + affineEditorContainerProxy.updateComplete + .then(() => { + if (blockElement && !scrolled) { + handleScrollToBlock(blockElement); + } + handleUpdateComplete(); + }) + .catch(console.error); + + return () => { + disposable?.dispose(); + }; + }, [blockElement, affineEditorContainerProxy, mode, scrolled]); return (
{ + const unsubs: Array<() => void> = []; + + unsubs.push( + registerAffineCommand({ + id: `affine:share-private-link:${docId}`, + category: 'affine:general', + preconditionStrategy: () => isActiveView, + keyBinding: { + binding: '$mod+Shift+c', + }, + label: '', + icon: null, + run() { + isActiveView && onClickCopyLink(); + }, + }) + ); + return () => { + unsubs.forEach(unsub => unsub()); + }; + }, [docId, isActiveView, onClickCopyLink]); +} diff --git a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts b/packages/frontend/core/src/hooks/affine/use-share-url.ts similarity index 72% rename from packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts rename to packages/frontend/core/src/hooks/affine/use-share-url.ts index 83db694965..dfa0fc707f 100644 --- a/packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts +++ b/packages/frontend/core/src/hooks/affine/use-share-url.ts @@ -2,7 +2,7 @@ import { toast } from '@affine/component'; import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch'; import { mixpanel } from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { useCallback, useMemo } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; type UrlType = 'share' | 'workspace'; @@ -14,9 +14,24 @@ type UseSharingUrl = { const useGenerateUrl = ({ workspaceId, pageId, urlType }: UseSharingUrl) => { // to generate a private url like https://app.affine.app/workspace/123/456 + // or https://app.affine.app/workspace/123/456#block-123 + // 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 baseUrl = getAffineCloudBaseUrl(); const url = useMemo(() => { @@ -25,12 +40,12 @@ const useGenerateUrl = ({ workspaceId, pageId, urlType }: UseSharingUrl) => { try { return new URL( - `${baseUrl}/${urlType}/${workspaceId}/${pageId}` + `${baseUrl}/${urlType}/${workspaceId}/${pageId}${urlType === 'workspace' ? `${hash}` : ''}` ).toString(); } catch (e) { return null; } - }, [baseUrl, pageId, urlType, workspaceId]); + }, [baseUrl, hash, pageId, urlType, workspaceId]); return url; }; diff --git a/packages/frontend/core/src/hooks/affine/use-shortcuts.ts b/packages/frontend/core/src/hooks/affine/use-shortcuts.ts index c0e73761a3..0ed473d3b6 100644 --- a/packages/frontend/core/src/hooks/affine/use-shortcuts.ts +++ b/packages/frontend/core/src/hooks/affine/use-shortcuts.ts @@ -42,7 +42,8 @@ type KeyboardShortcutsI18NKeys = | 'groupDatabase' | 'moveUp' | 'moveDown' - | 'divider'; + | 'divider' + | 'copy-private-link'; // TODO(550): remove this hook after 'useAFFiNEI18N' support scoped i18n const useKeyboardShortcutsI18N = () => { @@ -81,8 +82,9 @@ export const useWinGeneralKeyboardShortcuts = (): ShortcutMap => { // not implement yet // [t('appendDailyNote')]: 'Ctrl + Alt + A', [t('expandOrCollapseSidebar')]: ['Ctrl', '/'], - [t('goBack')]: ['Ctrl + ['], - [t('goForward')]: ['Ctrl + ]'], + [t('goBack')]: ['Ctrl', '['], + [t('goForward')]: ['Ctrl', ']'], + [t('copy-private-link')]: ['⌘', '⇧', 'C'], }), [t] ); @@ -97,8 +99,9 @@ export const useMacGeneralKeyboardShortcuts = (): ShortcutMap => { // not implement yet // [t('appendDailyNote')]: '⌘ + ⌥ + A', [t('expandOrCollapseSidebar')]: ['⌘', '/'], - [t('goBack')]: ['⌘ + ['], - [t('goForward')]: ['⌘ + ]'], + [t('goBack')]: ['⌘ ', '['], + [t('goForward')]: ['⌘ ', ']'], + [t('copy-private-link')]: ['⌘', '⇧', 'C'], }), [t] ); diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 77c41eded6..22570febc3 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1322,5 +1322,6 @@ "will be moved to Trash": "{{title}} will be moved to Trash", "com.affine.ai-onboarding.edgeless.get-started": "Get Started", "com.affine.ai-onboarding.edgeless.purchase": "Upgrade to Unlimited Usage", - "will delete member": "will delete member" + "will delete member": "will delete member", + "com.affine.keyboardShortcuts.copy-private-link": "Copy Private Link" }