mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-12 04:18:54 +00:00
@@ -142,3 +142,8 @@ export const journalShareButton = style({
|
|||||||
height: 32,
|
height: 32,
|
||||||
padding: '0px 8px',
|
padding: '0px 8px',
|
||||||
});
|
});
|
||||||
|
export const shortcutStyle = style({
|
||||||
|
fontSize: cssVar('fontXs'),
|
||||||
|
color: cssVar('textSecondaryColor'),
|
||||||
|
fontWeight: 400,
|
||||||
|
});
|
||||||
|
|||||||
@@ -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 { Divider } from '@affine/component/ui/divider';
|
||||||
import { ExportMenuItems } from '@affine/core/components/page-list';
|
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 { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { LinkIcon } from '@blocksuite/icons';
|
import { CopyIcon } from '@blocksuite/icons';
|
||||||
import { DocService, useLiveData, useService } from '@toeverything/infra';
|
import { DocService, useLiveData, useService } from '@toeverything/infra';
|
||||||
|
|
||||||
import { useExportPage } from '../../../../hooks/affine/use-export-page';
|
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import type { ShareMenuProps } from './share-menu';
|
import type { ShareMenuProps } from './share-menu';
|
||||||
import { useSharingUrl } from './use-share-url';
|
|
||||||
|
|
||||||
export const ShareExport = ({
|
export const ShareExport = ({
|
||||||
workspaceMetadata: workspace,
|
workspaceMetadata: workspace,
|
||||||
@@ -26,6 +26,7 @@ export const ShareExport = ({
|
|||||||
});
|
});
|
||||||
const exportHandler = useExportPage(currentPage);
|
const exportHandler = useExportPage(currentPage);
|
||||||
const currentMode = useLiveData(doc.mode$);
|
const currentMode = useLiveData(doc.mode$);
|
||||||
|
const isMac = environment.isBrowser && environment.isMacOs;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -52,15 +53,24 @@ export const ShareExport = ({
|
|||||||
{t['com.affine.share-menu.share-privately.description']()}
|
{t['com.affine.share-menu.share-privately.description']()}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<MenuItem
|
||||||
className={styles.shareLinkStyle}
|
className={styles.shareLinkStyle}
|
||||||
onClick={onClickCopyLink}
|
onSelect={onClickCopyLink}
|
||||||
icon={<LinkIcon />}
|
block
|
||||||
type="plain"
|
|
||||||
disabled={!sharingUrl}
|
disabled={!sharingUrl}
|
||||||
|
preFix={
|
||||||
|
<MenuIcon>
|
||||||
|
<CopyIcon fontSize={16} />
|
||||||
|
</MenuIcon>
|
||||||
|
}
|
||||||
|
endFix={
|
||||||
|
<div className={styles.shortcutStyle}>
|
||||||
|
{isMac ? '⌘ + ⌥ + C' : 'Ctrl + Shift + C'}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
>
|
>
|
||||||
{t['com.affine.share-menu.copy-private-link']()}
|
{t['com.affine.share-menu.copy-private-link']()}
|
||||||
</Button>
|
</MenuItem>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
) : null}
|
) : null}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { Divider } from '@affine/component/ui/divider';
|
import { Divider } from '@affine/component/ui/divider';
|
||||||
import { Menu } from '@affine/component/ui/menu';
|
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 { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
import { useAFFiNEI18N } from '@affine/i18n/hooks';
|
||||||
import { WebIcon } from '@blocksuite/icons';
|
import { WebIcon } from '@blocksuite/icons';
|
||||||
@@ -65,6 +67,13 @@ const LocalShareMenu = (props: ShareMenuProps) => {
|
|||||||
const CloudShareMenu = (props: ShareMenuProps) => {
|
const CloudShareMenu = (props: ShareMenuProps) => {
|
||||||
const t = useAFFiNEI18N();
|
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 (
|
return (
|
||||||
<Menu
|
<Menu
|
||||||
items={<ShareMenuContent {...props} />}
|
items={<ShareMenuContent {...props} />}
|
||||||
|
|||||||
@@ -9,7 +9,9 @@ import {
|
|||||||
import { PublicLinkDisableModal } from '@affine/component/disable-public-link';
|
import { PublicLinkDisableModal } from '@affine/component/disable-public-link';
|
||||||
import { Button } from '@affine/component/ui/button';
|
import { Button } from '@affine/component/ui/button';
|
||||||
import { Menu, MenuItem, MenuTrigger } from '@affine/component/ui/menu';
|
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 { useAsyncCallback } from '@affine/core/hooks/affine-async-hooks';
|
||||||
|
import { ServerConfigService } from '@affine/core/modules/cloud';
|
||||||
import { ShareService } from '@affine/core/modules/share-doc';
|
import { ShareService } from '@affine/core/modules/share-doc';
|
||||||
import { mixpanel } from '@affine/core/utils';
|
import { mixpanel } from '@affine/core/utils';
|
||||||
import { WorkspaceFlavour } from '@affine/env/workspace';
|
import { WorkspaceFlavour } from '@affine/env/workspace';
|
||||||
@@ -29,11 +31,9 @@ import { cssVar } from '@toeverything/theme';
|
|||||||
import { Suspense, useEffect, useMemo, useState } from 'react';
|
import { Suspense, useEffect, useMemo, useState } from 'react';
|
||||||
import { ErrorBoundary } from 'react-error-boundary';
|
import { ErrorBoundary } from 'react-error-boundary';
|
||||||
|
|
||||||
import { ServerConfigService } from '../../../../modules/cloud';
|
|
||||||
import { CloudSvg } from '../cloud-svg';
|
import { CloudSvg } from '../cloud-svg';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
import type { ShareMenuProps } from './share-menu';
|
import type { ShareMenuProps } from './share-menu';
|
||||||
import { useSharingUrl } from './use-share-url';
|
|
||||||
|
|
||||||
export const LocalSharePage = (props: ShareMenuProps) => {
|
export const LocalSharePage = (props: ShareMenuProps) => {
|
||||||
const t = useAFFiNEI18N();
|
const t = useAFFiNEI18N();
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import type { BlockElement } from '@blocksuite/block-std';
|
import type { BlockElement } from '@blocksuite/block-std';
|
||||||
|
import type { Disposable } from '@blocksuite/global/utils';
|
||||||
import type {
|
import type {
|
||||||
AffineEditorContainer,
|
AffineEditorContainer,
|
||||||
EdgelessEditor,
|
EdgelessEditor,
|
||||||
@@ -101,6 +102,7 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
{ page, mode, className, style, defaultSelectedBlockId, customRenderers },
|
{ page, mode, className, style, defaultSelectedBlockId, customRenderers },
|
||||||
ref
|
ref
|
||||||
) {
|
) {
|
||||||
|
const [scrolled, setScrolled] = useState(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);
|
||||||
@@ -208,27 +210,61 @@ export const BlocksuiteEditorContainer = forwardRef<
|
|||||||
const blockElement = useBlockElementById(rootRef, defaultSelectedBlockId);
|
const blockElement = useBlockElementById(rootRef, defaultSelectedBlockId);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (blockElement) {
|
let disposable: Disposable | undefined = undefined;
|
||||||
affineEditorContainerProxy.updateComplete
|
|
||||||
.then(() => {
|
// update the hash when the block is selected
|
||||||
if (mode === 'page') {
|
const handleUpdateComplete = () => {
|
||||||
blockElement.scrollIntoView({
|
const selectManager = affineEditorContainerProxy?.host?.selection;
|
||||||
behavior: 'smooth',
|
if (!selectManager) return;
|
||||||
block: 'center',
|
|
||||||
});
|
disposable = selectManager.slots.changed.on(() => {
|
||||||
}
|
const selectedBlock = selectManager.find('block');
|
||||||
const selectManager = affineEditorContainerProxy.host?.selection;
|
const selectedId = selectedBlock?.blockId;
|
||||||
if (!blockElement.path.length || !selectManager) {
|
|
||||||
return;
|
const newHash = selectedId ? `#${selectedId}` : '';
|
||||||
}
|
//TODO: use activeView.history which is in workbench instead of history.replaceState
|
||||||
const newSelection = selectManager.create('block', {
|
history.replaceState(null, '', `${window.location.pathname}${newHash}`);
|
||||||
path: blockElement.path,
|
|
||||||
});
|
// Dispatch a custom event to notify the hash change
|
||||||
selectManager.set([newSelection]);
|
const hashChangeEvent = new CustomEvent('hashchange-custom', {
|
||||||
})
|
detail: { hash: newHash },
|
||||||
.catch(console.error);
|
});
|
||||||
}
|
window.dispatchEvent(hashChangeEvent);
|
||||||
}, [blockElement, affineEditorContainerProxy, mode]);
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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 (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -0,0 +1,42 @@
|
|||||||
|
import { useSharingUrl } from '@affine/core/hooks/affine/use-share-url';
|
||||||
|
import { registerAffineCommand } from '@toeverything/infra';
|
||||||
|
import { useEffect } from 'react';
|
||||||
|
|
||||||
|
export function useRegisterCopyLinkCommands({
|
||||||
|
workspaceId,
|
||||||
|
docId,
|
||||||
|
isActiveView,
|
||||||
|
}: {
|
||||||
|
workspaceId: string;
|
||||||
|
docId: string;
|
||||||
|
isActiveView: boolean;
|
||||||
|
}) {
|
||||||
|
const { onClickCopyLink } = useSharingUrl({
|
||||||
|
workspaceId,
|
||||||
|
pageId: docId,
|
||||||
|
urlType: 'workspace',
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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]);
|
||||||
|
}
|
||||||
@@ -2,7 +2,7 @@ import { toast } from '@affine/component';
|
|||||||
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
import { getAffineCloudBaseUrl } from '@affine/core/modules/cloud/services/fetch';
|
||||||
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, useMemo } from 'react';
|
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
|
|
||||||
type UrlType = 'share' | 'workspace';
|
type UrlType = 'share' | 'workspace';
|
||||||
|
|
||||||
@@ -14,9 +14,24 @@ type UseSharingUrl = {
|
|||||||
|
|
||||||
const useGenerateUrl = ({ workspaceId, pageId, urlType }: UseSharingUrl) => {
|
const useGenerateUrl = ({ workspaceId, pageId, urlType }: UseSharingUrl) => {
|
||||||
// to generate a private url like https://app.affine.app/workspace/123/456
|
// 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
|
// 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);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
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(() => {
|
||||||
@@ -25,12 +40,12 @@ const useGenerateUrl = ({ workspaceId, pageId, urlType }: UseSharingUrl) => {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
return new URL(
|
return new URL(
|
||||||
`${baseUrl}/${urlType}/${workspaceId}/${pageId}`
|
`${baseUrl}/${urlType}/${workspaceId}/${pageId}${urlType === 'workspace' ? `${hash}` : ''}`
|
||||||
).toString();
|
).toString();
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}, [baseUrl, pageId, urlType, workspaceId]);
|
}, [baseUrl, hash, pageId, urlType, workspaceId]);
|
||||||
|
|
||||||
return url;
|
return url;
|
||||||
};
|
};
|
||||||
@@ -42,7 +42,8 @@ type KeyboardShortcutsI18NKeys =
|
|||||||
| 'groupDatabase'
|
| 'groupDatabase'
|
||||||
| 'moveUp'
|
| 'moveUp'
|
||||||
| 'moveDown'
|
| 'moveDown'
|
||||||
| 'divider';
|
| 'divider'
|
||||||
|
| 'copy-private-link';
|
||||||
|
|
||||||
// TODO(550): remove this hook after 'useAFFiNEI18N' support scoped i18n
|
// TODO(550): remove this hook after 'useAFFiNEI18N' support scoped i18n
|
||||||
const useKeyboardShortcutsI18N = () => {
|
const useKeyboardShortcutsI18N = () => {
|
||||||
@@ -81,8 +82,9 @@ export const useWinGeneralKeyboardShortcuts = (): ShortcutMap => {
|
|||||||
// not implement yet
|
// not implement yet
|
||||||
// [t('appendDailyNote')]: 'Ctrl + Alt + A',
|
// [t('appendDailyNote')]: 'Ctrl + Alt + A',
|
||||||
[t('expandOrCollapseSidebar')]: ['Ctrl', '/'],
|
[t('expandOrCollapseSidebar')]: ['Ctrl', '/'],
|
||||||
[t('goBack')]: ['Ctrl + ['],
|
[t('goBack')]: ['Ctrl', '['],
|
||||||
[t('goForward')]: ['Ctrl + ]'],
|
[t('goForward')]: ['Ctrl', ']'],
|
||||||
|
[t('copy-private-link')]: ['⌘', '⇧', 'C'],
|
||||||
}),
|
}),
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
@@ -97,8 +99,9 @@ export const useMacGeneralKeyboardShortcuts = (): ShortcutMap => {
|
|||||||
// not implement yet
|
// not implement yet
|
||||||
// [t('appendDailyNote')]: '⌘ + ⌥ + A',
|
// [t('appendDailyNote')]: '⌘ + ⌥ + A',
|
||||||
[t('expandOrCollapseSidebar')]: ['⌘', '/'],
|
[t('expandOrCollapseSidebar')]: ['⌘', '/'],
|
||||||
[t('goBack')]: ['⌘ + ['],
|
[t('goBack')]: ['⌘ ', '['],
|
||||||
[t('goForward')]: ['⌘ + ]'],
|
[t('goForward')]: ['⌘ ', ']'],
|
||||||
|
[t('copy-private-link')]: ['⌘', '⇧', 'C'],
|
||||||
}),
|
}),
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1322,5 +1322,6 @@
|
|||||||
"will be moved to Trash": "{{title}} will be moved to Trash",
|
"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.get-started": "Get Started",
|
||||||
"com.affine.ai-onboarding.edgeless.purchase": "Upgrade to Unlimited Usage",
|
"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"
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user