Skip to content

Commit

Permalink
feat(core): add private anchor link for sharing (#6966)
Browse files Browse the repository at this point in the history
close AFF-1085
  • Loading branch information
JimmFly committed May 16, 2024
1 parent 3799b65 commit 10015c5
Show file tree
Hide file tree
Showing 9 changed files with 162 additions and 41 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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,
});
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -26,6 +26,7 @@ export const ShareExport = ({
});
const exportHandler = useExportPage(currentPage);
const currentMode = useLiveData(doc.mode$);
const isMac = environment.isBrowser && environment.isMacOs;

return (
<>
Expand All @@ -52,15 +53,24 @@ export const ShareExport = ({
{t['com.affine.share-menu.share-privately.description']()}
</div>
<div>
<Button
<MenuItem
className={styles.shareLinkStyle}
onClick={onClickCopyLink}
icon={<LinkIcon />}
type="plain"
onSelect={onClickCopyLink}
block
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']()}
</Button>
</MenuItem>
</div>
</div>
) : null}
Expand Down
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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 (
<Menu
items={<ShareMenuContent {...props} />}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { BlockElement } from '@blocksuite/block-std';
import type { Disposable } from '@blocksuite/global/utils';
import type {
AffineEditorContainer,
EdgelessEditor,
Expand Down Expand Up @@ -101,6 +102,7 @@ export const BlocksuiteEditorContainer = forwardRef<
{ page, mode, className, style, defaultSelectedBlockId, customRenderers },
ref
) {
const [scrolled, setScrolled] = useState(false);
const rootRef = useRef<HTMLDivElement>(null);
const docRef = useRef<PageEditor>(null);
const edgelessRef = useRef<EdgelessEditor>(null);
Expand Down Expand Up @@ -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 (
<div
Expand Down
Original file line number Diff line number Diff line change
@@ -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]);
}
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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(() => {
Expand All @@ -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;
};
Expand Down
13 changes: 8 additions & 5 deletions packages/frontend/core/src/hooks/affine/use-shortcuts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => {
Expand Down Expand Up @@ -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]
);
Expand All @@ -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]
);
Expand Down
3 changes: 2 additions & 1 deletion packages/frontend/i18n/src/resources/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}

0 comments on commit 10015c5

Please sign in to comment.