-
- {t['com.affine.share-menu.ShareMode']()}
-
-
- {}}
- >
-
- {t['com.affine.pageMode.page']()}
-
-
- {t['com.affine.pageMode.edgeless']()}
-
-
-
+
+
+ {t['com.affine.share-menu.ShareMode']()}
- ) : null}
- {isPublic ? (
+
+
+
+ {t['com.affine.pageMode.page']()}
+
+
+ {t['com.affine.pageMode.edgeless']()}
+
+
+
+
+ {isSharedPage ? (
<>
{runtimeConfig.enableEnhanceShareMode && (
<>
diff --git a/packages/frontend/component/src/components/share-menu/use-share-url.ts b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts
similarity index 67%
rename from packages/frontend/component/src/components/share-menu/use-share-url.ts
rename to packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts
index a1eaea3d66..d349235f6f 100644
--- a/packages/frontend/component/src/components/share-menu/use-share-url.ts
+++ b/packages/frontend/core/src/components/affine/share-page-modal/share-menu/use-share-url.ts
@@ -1,8 +1,7 @@
+import { toast } from '@affine/component';
import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useCallback, useMemo } from 'react';
-import { toast } from '../../ui/toast';
-
type UrlType = 'share' | 'workspace';
type UseSharingUrl = {
@@ -16,7 +15,14 @@ export const generateUrl = ({
pageId,
urlType,
}: UseSharingUrl) => {
- return `${runtimeConfig.serverUrlPrefix}/${urlType}/${workspaceId}/${pageId}`;
+ // to generate a private url like https://affine.app/workspace/123/456
+ // to generate a public url like https://affine.app/share/123/456
+ // or https://affine.app/share/123/456?mode=edgeless
+
+ const url = new URL(
+ `${runtimeConfig.serverUrlPrefix}/${urlType}/${workspaceId}/${pageId}`
+ );
+ return url.toString();
};
export const useSharingUrl = ({
@@ -27,7 +33,7 @@ export const useSharingUrl = ({
const t = useAFFiNEI18N();
const sharingUrl = useMemo(
() => generateUrl({ workspaceId, pageId, urlType }),
- [urlType, workspaceId, pageId]
+ [workspaceId, pageId, urlType]
);
const onClickCopyLink = useCallback(() => {
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-header-title/index.tsx
index 68d02cb88b..cdbbd3bc8b 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-header-title/index.tsx
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-header-title/index.tsx
@@ -1,5 +1,4 @@
import type { AffineOfficialWorkspace } from '@affine/env/workspace';
-import { WorkspaceFlavour } from '@affine/env/workspace';
import {
useBlockSuitePageMeta,
usePageMetaHelper,
@@ -13,6 +12,7 @@ import {
useState,
} from 'react';
+import type { PageMode } from '../../../atoms';
import { EditorModeSwitch } from '../block-suite-mode-switch';
import { PageMenu } from './operation-menu';
import * as styles from './styles.css';
@@ -20,6 +20,8 @@ import * as styles from './styles.css';
export interface BlockSuiteHeaderTitleProps {
workspace: AffineOfficialWorkspace;
pageId: string;
+ isPublic?: boolean;
+ publicMode?: PageMode;
}
const EditableTitle = ({
@@ -54,6 +56,8 @@ const StableTitle = ({
workspace,
pageId,
onRename,
+ isPublic,
+ publicMode,
}: BlockSuiteHeaderTitleProps & {
onRename?: () => void;
}) => {
@@ -64,11 +68,19 @@ const StableTitle = ({
const title = pageMeta?.title;
+ const handleRename = useCallback(() => {
+ if (!isPublic && onRename) {
+ onRename();
+ }
+ }, [isPublic, onRename]);
+
return (
{title || 'Untitled'}
-
+ {isPublic ? null : }
);
};
@@ -139,7 +151,7 @@ const BlockSuiteTitleWithRename = (props: BlockSuiteHeaderTitleProps) => {
};
export const BlockSuiteHeaderTitle = (props: BlockSuiteHeaderTitleProps) => {
- if (props.workspace.flavour === WorkspaceFlavour.AFFINE_PUBLIC) {
+ if (props.isPublic) {
return
;
}
return
;
diff --git a/packages/frontend/core/src/components/blocksuite/block-suite-mode-switch/index.tsx b/packages/frontend/core/src/components/blocksuite/block-suite-mode-switch/index.tsx
index 7cc1f7a348..477fa4be2c 100644
--- a/packages/frontend/core/src/components/blocksuite/block-suite-mode-switch/index.tsx
+++ b/packages/frontend/core/src/components/blocksuite/block-suite-mode-switch/index.tsx
@@ -6,6 +6,7 @@ import { useAtomValue } from 'jotai';
import type { CSSProperties } from 'react';
import { useCallback, useEffect } from 'react';
+import type { PageMode } from '../../../atoms';
import { currentModeAtom } from '../../../atoms/mode';
import { useBlockSuiteMetaHelper } from '../../../hooks/affine/use-block-suite-meta-helper';
import type { BlockSuiteWorkspace } from '../../../shared';
@@ -18,6 +19,8 @@ export type EditorModeSwitchProps = {
blockSuiteWorkspace: BlockSuiteWorkspace;
pageId: string;
style?: CSSProperties;
+ isPublic?: boolean;
+ publicMode?: PageMode;
};
const TooltipContent = () => {
const t = useAFFiNEI18N();
@@ -34,6 +37,8 @@ export const EditorModeSwitch = ({
style,
blockSuiteWorkspace,
pageId,
+ isPublic,
+ publicMode,
}: EditorModeSwitchProps) => {
const t = useAFFiNEI18N();
const pageMeta = useBlockSuitePageMeta(blockSuiteWorkspace).find(
@@ -47,7 +52,7 @@ export const EditorModeSwitch = ({
const currentMode = useAtomValue(currentModeAtom);
useEffect(() => {
- if (trash) {
+ if (trash || isPublic) {
return;
}
const keydown = (e: KeyboardEvent) => {
@@ -64,41 +69,58 @@ export const EditorModeSwitch = ({
document.addEventListener('keydown', keydown, { capture: true });
return () =>
document.removeEventListener('keydown', keydown, { capture: true });
- }, [currentMode, pageId, t, togglePageMode, trash]);
+ }, [currentMode, isPublic, pageId, t, togglePageMode, trash]);
const onSwitchToPageMode = useCallback(() => {
- if (currentMode === 'page') {
+ if (currentMode === 'page' || isPublic) {
return;
}
switchToPageMode(pageId);
toast(t['com.affine.toastMessage.pageMode']());
- }, [currentMode, pageId, switchToPageMode, t]);
+ }, [currentMode, isPublic, pageId, switchToPageMode, t]);
+
const onSwitchToEdgelessMode = useCallback(() => {
- if (currentMode === 'edgeless') {
+ if (currentMode === 'edgeless' || isPublic) {
return;
}
switchToEdgelessMode(pageId);
toast(t['com.affine.toastMessage.edgelessMode']());
- }, [currentMode, pageId, switchToEdgelessMode, t]);
+ }, [currentMode, isPublic, pageId, switchToEdgelessMode, t]);
+
+ const shouldHide = useCallback(
+ (mode: PageMode) =>
+ (trash && currentMode !== mode) || (isPublic && publicMode !== mode),
+ [currentMode, isPublic, publicMode, trash]
+ );
+
+ const shouldActive = useCallback(
+ (mode: PageMode) => (isPublic ? false : currentMode === mode),
+ [currentMode, isPublic]
+ );
return (
-
}>
+
}
+ options={{
+ hidden: isPublic || trash,
+ }}
+ >
diff --git a/packages/frontend/core/src/components/cloud/share-header-left-item/index.tsx b/packages/frontend/core/src/components/cloud/share-header-left-item/index.tsx
new file mode 100644
index 0000000000..7a674c2ff1
--- /dev/null
+++ b/packages/frontend/core/src/components/cloud/share-header-left-item/index.tsx
@@ -0,0 +1,26 @@
+import { Logo1Icon } from '@blocksuite/icons';
+
+import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
+import * as styles from './styles.css';
+import { PublishPageUserAvatar } from './user-avatar';
+
+const ShareHeaderLeftItem = () => {
+ const loginStatus = useCurrentLoginStatus();
+ if (loginStatus === 'authenticated') {
+ return ;
+ }
+
+ return (
+
+
+
+ );
+};
+
+export default ShareHeaderLeftItem;
diff --git a/packages/frontend/core/src/components/cloud/share-header-left-item/styles.css.ts b/packages/frontend/core/src/components/cloud/share-header-left-item/styles.css.ts
new file mode 100644
index 0000000000..3000c5d257
--- /dev/null
+++ b/packages/frontend/core/src/components/cloud/share-header-left-item/styles.css.ts
@@ -0,0 +1,15 @@
+import { style } from '@vanilla-extract/css';
+
+export const iconWrapper = style({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ fontSize: '24px',
+ cursor: 'pointer',
+ color: 'var(--affine-text-primary-color)',
+ selectors: {
+ '&:visited': {
+ color: 'var(--affine-text-primary-color)',
+ },
+ },
+});
diff --git a/packages/frontend/core/src/components/cloud/share-header-left-item/user-avatar.tsx b/packages/frontend/core/src/components/cloud/share-header-left-item/user-avatar.tsx
new file mode 100644
index 0000000000..43558bdd03
--- /dev/null
+++ b/packages/frontend/core/src/components/cloud/share-header-left-item/user-avatar.tsx
@@ -0,0 +1,45 @@
+import { useAFFiNEI18N } from '@affine/i18n/hooks';
+import { SignOutIcon } from '@blocksuite/icons';
+import { Avatar } from '@toeverything/components/avatar';
+import { Menu, MenuIcon, MenuItem } from '@toeverything/components/menu';
+import { useAsyncCallback } from '@toeverything/hooks/affine-async-hooks';
+import { useMemo } from 'react';
+import { useLocation } from 'react-router-dom';
+
+import { useCurrentUser } from '../../../hooks/affine/use-current-user';
+import { signOutCloud } from '../../../utils/cloud-utils';
+import * as styles from './styles.css';
+
+export const PublishPageUserAvatar = () => {
+ const user = useCurrentUser();
+ const t = useAFFiNEI18N();
+ const location = useLocation();
+
+ const handleSignOut = useAsyncCallback(async () => {
+ await signOutCloud({ callbackUrl: location.pathname });
+ }, [location.pathname]);
+
+ const menuItem = useMemo(() => {
+ return (
+
+ );
+ }, [handleSignOut, t]);
+
+ return (
+
+ );
+};
diff --git a/packages/frontend/core/src/components/cloud/share-header-right-item/authenticated-item.tsx b/packages/frontend/core/src/components/cloud/share-header-right-item/authenticated-item.tsx
new file mode 100644
index 0000000000..f927723365
--- /dev/null
+++ b/packages/frontend/core/src/components/cloud/share-header-right-item/authenticated-item.tsx
@@ -0,0 +1,30 @@
+import { useAFFiNEI18N } from '@affine/i18n/hooks';
+import { Button } from '@toeverything/components/button';
+
+import { useCurrentUser } from '../../../hooks/affine/use-current-user';
+import { useMembers } from '../../../hooks/affine/use-members';
+import { useNavigateHelper } from '../../../hooks/use-navigate-helper';
+import type { ShareHeaderRightItemProps } from '.';
+
+export const AuthenticatedItem = ({ ...props }: ShareHeaderRightItemProps) => {
+ const { workspaceId, pageId } = props;
+ const user = useCurrentUser();
+ const members = useMembers(workspaceId, 0);
+ const isMember = members.some(m => m.id === user.id);
+ const t = useAFFiNEI18N();
+ const { jumpToPage } = useNavigateHelper();
+
+ if (isMember) {
+ return (
+
+ );
+ }
+
+ return null;
+};
diff --git a/packages/frontend/core/src/components/cloud/share-header-right-item/index.tsx b/packages/frontend/core/src/components/cloud/share-header-right-item/index.tsx
new file mode 100644
index 0000000000..564b657917
--- /dev/null
+++ b/packages/frontend/core/src/components/cloud/share-header-right-item/index.tsx
@@ -0,0 +1,18 @@
+import { useCurrentLoginStatus } from '../../../hooks/affine/use-current-login-status';
+import { AuthenticatedItem } from './authenticated-item';
+
+export type ShareHeaderRightItemProps = {
+ workspaceId: string;
+ pageId: string;
+};
+
+const ShareHeaderRightItem = ({ ...props }: ShareHeaderRightItemProps) => {
+ const loginStatus = useCurrentLoginStatus();
+ if (loginStatus === 'authenticated') {
+ return ;
+ }
+ // TODO: Add TOC
+ return null;
+};
+
+export default ShareHeaderRightItem;
diff --git a/packages/frontend/core/src/components/cloud/share-header-right-item/styles.css.ts b/packages/frontend/core/src/components/cloud/share-header-right-item/styles.css.ts
new file mode 100644
index 0000000000..3000c5d257
--- /dev/null
+++ b/packages/frontend/core/src/components/cloud/share-header-right-item/styles.css.ts
@@ -0,0 +1,15 @@
+import { style } from '@vanilla-extract/css';
+
+export const iconWrapper = style({
+ display: 'flex',
+ justifyContent: 'center',
+ alignItems: 'center',
+ fontSize: '24px',
+ cursor: 'pointer',
+ color: 'var(--affine-text-primary-color)',
+ selectors: {
+ '&:visited': {
+ color: 'var(--affine-text-primary-color)',
+ },
+ },
+});
diff --git a/packages/frontend/core/src/components/page-detail-editor.tsx b/packages/frontend/core/src/components/page-detail-editor.tsx
index 084843a2bc..5cb4c47eac 100644
--- a/packages/frontend/core/src/components/page-detail-editor.tsx
+++ b/packages/frontend/core/src/components/page-detail-editor.tsx
@@ -31,7 +31,7 @@ import {
import { Panel, PanelGroup, PanelResizeHandle } from 'react-resizable-panels';
import { useLocation } from 'react-router-dom';
-import { pageSettingFamily } from '../atoms';
+import { type PageMode, pageSettingFamily } from '../atoms';
import { fontStyleOptions } from '../atoms/settings';
import { useAppSettingHelper } from '../hooks/affine/use-app-setting-helper';
import { useBlockSuiteMetaHelper } from '../hooks/affine/use-block-suite-meta-helper';
@@ -50,6 +50,7 @@ export type OnLoadEditor = (page: Page, editor: EditorContainer) => () => void;
export interface PageDetailEditorProps {
isPublic?: boolean;
+ publishMode?: PageMode;
workspace: Workspace;
pageId: string;
onLoad?: OnLoadEditor;
@@ -91,6 +92,7 @@ const EditorWrapper = memo(function EditorWrapper({
pageId,
onLoad,
isPublic,
+ publishMode,
}: PageDetailEditorProps) {
const page = useBlockSuiteWorkspacePage(workspace, pageId);
if (!page) {
@@ -105,7 +107,16 @@ const EditorWrapper = memo(function EditorWrapper({
const pageSettingAtom = pageSettingFamily(pageId);
const pageSetting = useAtomValue(pageSettingAtom);
- const currentMode = pageSetting?.mode ?? 'page';
+
+ const mode = useMemo(() => {
+ const currentMode = pageSetting.mode;
+ const shareMode = publishMode || currentMode;
+
+ if (isPublic) {
+ return shareMode;
+ }
+ return currentMode;
+ }, [isPublic, publishMode, pageSetting.mode]);
const { appSettings } = useAppSettingHelper();
@@ -120,13 +131,16 @@ const EditorWrapper = memo(function EditorWrapper({
const setEditorMode = useCallback(
(mode: 'page' | 'edgeless') => {
+ if (isPublic) {
+ return;
+ }
if (mode === 'edgeless') {
switchToEdgelessMode(pageId);
} else {
switchToPageMode(pageId);
}
},
- [switchToEdgelessMode, switchToPageMode, pageId]
+ [isPublic, switchToEdgelessMode, pageId, switchToPageMode]
);
const [editor, setEditor] = useState();
@@ -191,7 +205,7 @@ const EditorWrapper = memo(function EditorWrapper({
'--affine-font-family': value,
} as CSSProperties
}
- mode={isPublic ? 'page' : currentMode}
+ mode={mode}
page={page}
onModeChange={setEditorMode}
defaultSelectedBlockId={blockId}
diff --git a/packages/frontend/core/src/components/share-header.tsx b/packages/frontend/core/src/components/share-header.tsx
new file mode 100644
index 0000000000..20f6c6ab64
--- /dev/null
+++ b/packages/frontend/core/src/components/share-header.tsx
@@ -0,0 +1,44 @@
+import type { Workspace } from '@blocksuite/store';
+import { useSetAtom } from 'jotai/react';
+
+import type { PageMode } from '../atoms';
+import { appHeaderAtom, mainContainerAtom } from '../atoms/element';
+import { useWorkspace } from '../hooks/use-workspace';
+import { BlockSuiteHeaderTitle } from './blocksuite/block-suite-header-title';
+import ShareHeaderLeftItem from './cloud/share-header-left-item';
+import ShareHeaderRightItem from './cloud/share-header-right-item';
+import { Header } from './pure/header';
+
+export function ShareHeader({
+ workspace,
+ pageId,
+ publishMode,
+}: {
+ workspace: Workspace;
+ pageId: string;
+ publishMode: PageMode;
+}) {
+ const setAppHeader = useSetAtom(appHeaderAtom);
+
+ const currentWorkspace = useWorkspace(workspace.id);
+
+ return (
+ }
+ center={
+
+ }
+ right={
+
+ }
+ bottomBorder
+ />
+ );
+}
diff --git a/packages/frontend/core/src/components/share-page-not-found-error.css.ts b/packages/frontend/core/src/components/share-page-not-found-error.css.ts
new file mode 100644
index 0000000000..bfd01d03f4
--- /dev/null
+++ b/packages/frontend/core/src/components/share-page-not-found-error.css.ts
@@ -0,0 +1,15 @@
+import { style } from '@vanilla-extract/css';
+
+export const iconWrapper = style({
+ position: 'absolute',
+ top: '16px',
+ left: '16px',
+ fontSize: '24px',
+ cursor: 'pointer',
+ color: 'var(--affine-text-primary-color)',
+ selectors: {
+ '&:visited': {
+ color: 'var(--affine-text-primary-color)',
+ },
+ },
+});
diff --git a/packages/frontend/core/src/hooks/affine/use-is-shared-page.ts b/packages/frontend/core/src/hooks/affine/use-is-shared-page.ts
index 5f8cba129c..49f6cc110b 100644
--- a/packages/frontend/core/src/hooks/affine/use-is-shared-page.ts
+++ b/packages/frontend/core/src/hooks/affine/use-is-shared-page.ts
@@ -1,57 +1,190 @@
+import { pushNotificationAtom } from '@affine/component/notification-center';
import {
- getWorkspaceSharedPagesQuery,
- revokePageMutation,
- sharePageMutation,
+ getWorkspacePublicPagesQuery,
+ PublicPageMode,
+ publishPageMutation,
+ revokePublicPageMutation,
} from '@affine/graphql';
+import { useAFFiNEI18N } from '@affine/i18n/hooks';
import { useMutation, useQuery } from '@affine/workspace/affine/gql';
+import { useSetAtom } from 'jotai';
import { useCallback, useMemo } from 'react';
+import type { PageMode } from '../../atoms';
+
+type NoParametersKeys = {
+ [K in keyof T]: T[K] extends () => any ? K : never;
+}[keyof T];
+
+type i18nKey = NoParametersKeys>;
+
+type NotificationKey =
+ | 'enableSuccessTitle'
+ | 'enableSuccessMessage'
+ | 'enableErrorTitle'
+ | 'enableErrorMessage'
+ | 'changeSuccessTitle'
+ | 'changeErrorTitle'
+ | 'changeErrorMessage'
+ | 'disableSuccessTitle'
+ | 'disableSuccessMessage'
+ | 'disableErrorTitle'
+ | 'disableErrorMessage';
+
+const notificationToI18nKey: Record = {
+ enableSuccessTitle:
+ 'com.affine.share-menu.create-public-link.notification.success.title',
+ enableSuccessMessage:
+ 'com.affine.share-menu.create-public-link.notification.success.message',
+ enableErrorTitle:
+ 'com.affine.share-menu.create-public-link.notification.fail.title',
+ enableErrorMessage:
+ 'com.affine.share-menu.create-public-link.notification.fail.message',
+ changeSuccessTitle:
+ 'com.affine.share-menu.confirm-modify-mode.notification.success.title',
+ changeErrorTitle:
+ 'com.affine.share-menu.confirm-modify-mode.notification.fail.title',
+ changeErrorMessage:
+ 'com.affine.share-menu.confirm-modify-mode.notification.fail.message',
+ disableSuccessTitle:
+ 'com.affine.share-menu.disable-publish-link.notification.success.title',
+ disableSuccessMessage:
+ 'com.affine.share-menu.disable-publish-link.notification.success.message',
+ disableErrorTitle:
+ 'com.affine.share-menu.disable-publish-link.notification.fail.title',
+ disableErrorMessage:
+ 'com.affine.share-menu.disable-publish-link.notification.fail.message',
+};
+
export function useIsSharedPage(
workspaceId: string,
pageId: string
-): [isSharedPage: boolean, setSharedPage: (enable: boolean) => void] {
+): {
+ isSharedPage: boolean;
+ changeShare: (mode: PageMode) => void;
+ disableShare: () => void;
+ currentShareMode: PageMode;
+ enableShare: (mode: PageMode) => void;
+} {
+ const t = useAFFiNEI18N();
+ const pushNotification = useSetAtom(pushNotificationAtom);
const { data, mutate } = useQuery({
- query: getWorkspaceSharedPagesQuery,
+ query: getWorkspacePublicPagesQuery,
variables: {
workspaceId,
},
});
+
const { trigger: enableSharePage } = useMutation({
- mutation: sharePageMutation,
+ mutation: publishPageMutation,
});
const { trigger: disableSharePage } = useMutation({
- mutation: revokePageMutation,
+ mutation: revokePublicPageMutation,
});
- return [
- useMemo(
- () => data.workspace.sharedPages.some(id => id === pageId),
- [data.workspace.sharedPages, pageId]
- ),
- useCallback(
- (enable: boolean) => {
- // todo: push notification
- if (enable) {
- enableSharePage({
- workspaceId,
- pageId,
- })
- .then(() => {
- return mutate();
- })
- .catch(console.error);
- } else {
- disableSharePage({
- workspaceId,
- pageId,
- })
- .then(() => {
- return mutate();
- })
- .catch(console.error);
- }
- mutate().catch(console.error);
- },
- [disableSharePage, enableSharePage, mutate, pageId, workspaceId]
- ),
- ];
+
+ const [isSharedPage, currentShareMode] = useMemo(() => {
+ const publicPage = data?.workspace.publicPages.find(
+ publicPage => publicPage.id === pageId
+ );
+ const isPageShared = !!publicPage;
+
+ const currentShareMode: PageMode =
+ publicPage?.mode === PublicPageMode.Edgeless ? 'edgeless' : 'page';
+
+ return [isPageShared, currentShareMode];
+ }, [data?.workspace.publicPages, pageId]);
+
+ const enableShare = useCallback(
+ (mode: PageMode) => {
+ const publishMode =
+ mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page;
+
+ enableSharePage({ workspaceId, pageId, mode: publishMode })
+ .then(() => {
+ pushNotification({
+ title: t[notificationToI18nKey['enableSuccessTitle']](),
+ message: t[notificationToI18nKey['enableSuccessMessage']](),
+ type: 'success',
+ theme: 'default',
+ });
+ return mutate();
+ })
+ .catch(e => {
+ pushNotification({
+ title: t[notificationToI18nKey['enableErrorTitle']](),
+ message: t[notificationToI18nKey['enableErrorMessage']](),
+ type: 'error',
+ });
+ console.error(e);
+ });
+ },
+ [enableSharePage, mutate, pageId, pushNotification, t, workspaceId]
+ );
+
+ const changeShare = useCallback(
+ (mode: PageMode) => {
+ const publishMode =
+ mode === 'edgeless' ? PublicPageMode.Edgeless : PublicPageMode.Page;
+
+ enableSharePage({ workspaceId, pageId, mode: publishMode })
+ .then(() => {
+ pushNotification({
+ title: t[notificationToI18nKey['changeSuccessTitle']](),
+ message: t[
+ 'com.affine.share-menu.confirm-modify-mode.notification.success.message'
+ ]({
+ preMode:
+ publishMode === PublicPageMode.Edgeless
+ ? PublicPageMode.Page
+ : PublicPageMode.Edgeless,
+ currentMode: publishMode,
+ }),
+ type: 'success',
+ theme: 'default',
+ });
+ return mutate();
+ })
+ .catch(e => {
+ pushNotification({
+ title: t[notificationToI18nKey['changeErrorTitle']](),
+ message: t[notificationToI18nKey['changeErrorMessage']](),
+ type: 'error',
+ });
+ console.error(e);
+ });
+ },
+ [enableSharePage, mutate, pageId, pushNotification, t, workspaceId]
+ );
+
+ const disableShare = useCallback(() => {
+ disableSharePage({ workspaceId, pageId })
+ .then(() => {
+ pushNotification({
+ title: t[notificationToI18nKey['disableSuccessTitle']](),
+ message: t[notificationToI18nKey['disableSuccessMessage']](),
+ type: 'success',
+ theme: 'default',
+ });
+ return mutate();
+ })
+ .catch(e => {
+ pushNotification({
+ title: t[notificationToI18nKey['disableErrorTitle']](),
+ message: t[notificationToI18nKey['disableErrorMessage']](),
+ type: 'error',
+ });
+ console.error(e);
+ });
+ }, [disableSharePage, mutate, pageId, pushNotification, t, workspaceId]);
+
+ return useMemo(
+ () => ({
+ isSharedPage,
+ currentShareMode,
+ enableShare,
+ disableShare,
+ changeShare,
+ }),
+ [isSharedPage, currentShareMode, enableShare, disableShare, changeShare]
+ );
}
diff --git a/packages/frontend/core/src/pages/share/detail-page.tsx b/packages/frontend/core/src/pages/share/detail-page.tsx
index ab9bb26a58..07ce82d3a8 100644
--- a/packages/frontend/core/src/pages/share/detail-page.tsx
+++ b/packages/frontend/core/src/pages/share/detail-page.tsx
@@ -3,6 +3,7 @@ import { DebugLogger } from '@affine/debug';
import { WorkspaceFlavour } from '@affine/env/workspace';
import { getOrCreateWorkspace } from '@affine/workspace/manager';
import { downloadBinaryFromCloud } from '@affine/workspace/providers';
+import type { CloudDoc } from '@affine/workspace/providers/cloud';
import { assertExists } from '@blocksuite/global/utils';
import type { Page } from '@blocksuite/store';
import { noop } from 'foxact/noop';
@@ -18,12 +19,25 @@ import {
import { applyUpdate } from 'yjs';
import { PageDetailEditor } from '../../adapters/shared';
+import type { PageMode } from '../../atoms';
import { AppContainer } from '../../components/affine/app-container';
+import { ShareHeader } from '../../components/share-header';
import { SharePageNotFoundError } from '../../components/share-page-not-found-error';
-function assertArrayBuffer(value: unknown): asserts value is ArrayBuffer {
- if (!(value instanceof ArrayBuffer)) {
- throw new Error('value is not ArrayBuffer');
+type LoaderData = {
+ page: Page;
+ publishMode: PageMode;
+};
+
+function assertDownloadResponse(
+ value: CloudDoc | boolean
+): asserts value is CloudDoc {
+ if (
+ !value ||
+ !((value as CloudDoc).arrayBuffer instanceof ArrayBuffer) ||
+ typeof (value as CloudDoc).publishMode !== 'string'
+ ) {
+ throw new Error('value is not a valid download response');
}
}
@@ -41,33 +55,42 @@ export const loader: LoaderFunction = async ({ params }) => {
);
// download root workspace
{
- const buffer = await downloadBinaryFromCloud(workspaceId, workspaceId);
- assertArrayBuffer(buffer);
- applyUpdate(workspace.doc, new Uint8Array(buffer));
+ const response = await downloadBinaryFromCloud(workspaceId, workspaceId);
+ assertDownloadResponse(response);
+ const { arrayBuffer } = response;
+ applyUpdate(workspace.doc, new Uint8Array(arrayBuffer));
}
const page = workspace.getPage(pageId);
assertExists(page, 'cannot find page');
// download page
- {
- const buffer = await downloadBinaryFromCloud(
- workspaceId,
- page.spaceDoc.guid
- );
- assertArrayBuffer(buffer);
- applyUpdate(page.spaceDoc, new Uint8Array(buffer));
- }
+
+ const response = await downloadBinaryFromCloud(
+ workspaceId,
+ page.spaceDoc.guid
+ );
+ assertDownloadResponse(response);
+ const { arrayBuffer, publishMode } = response;
+
+ applyUpdate(page.spaceDoc, new Uint8Array(arrayBuffer));
+
logger.info('workspace', workspace);
workspace.awarenessStore.setReadonly(page, true);
- return page;
+ return { page, publishMode };
};
export const Component = (): ReactElement => {
- const page = useLoaderData() as Page;
+ const { page, publishMode } = useLoaderData() as LoaderData;
return (
+
noop, [])}
diff --git a/packages/frontend/graphql/src/graphql/get-workspace-public-pages.gql b/packages/frontend/graphql/src/graphql/get-workspace-public-pages.gql
new file mode 100644
index 0000000000..fcfd7fc6e7
--- /dev/null
+++ b/packages/frontend/graphql/src/graphql/get-workspace-public-pages.gql
@@ -0,0 +1,8 @@
+query getWorkspacePublicPages($workspaceId: String!) {
+ workspace(id: $workspaceId) {
+ publicPages {
+ id
+ mode
+ }
+ }
+}
diff --git a/packages/frontend/graphql/src/graphql/get-workspace-shared-pages.gql b/packages/frontend/graphql/src/graphql/get-workspace-shared-pages.gql
deleted file mode 100644
index 3d94e0a739..0000000000
--- a/packages/frontend/graphql/src/graphql/get-workspace-shared-pages.gql
+++ /dev/null
@@ -1,5 +0,0 @@
-query getWorkspaceSharedPages($workspaceId: String!) {
- workspace(id: $workspaceId) {
- sharedPages
- }
-}
diff --git a/packages/frontend/graphql/src/graphql/index.ts b/packages/frontend/graphql/src/graphql/index.ts
index 5de29e1419..4fd2c04148 100644
--- a/packages/frontend/graphql/src/graphql/index.ts
+++ b/packages/frontend/graphql/src/graphql/index.ts
@@ -320,15 +320,18 @@ query getWorkspacePublicById($id: String!) {
}`,
};
-export const getWorkspaceSharedPagesQuery = {
- id: 'getWorkspaceSharedPagesQuery' as const,
- operationName: 'getWorkspaceSharedPages',
+export const getWorkspacePublicPagesQuery = {
+ id: 'getWorkspacePublicPagesQuery' as const,
+ operationName: 'getWorkspacePublicPages',
definitionName: 'workspace',
containsFile: false,
query: `
-query getWorkspaceSharedPages($workspaceId: String!) {
+query getWorkspacePublicPages($workspaceId: String!) {
workspace(id: $workspaceId) {
- sharedPages
+ publicPages {
+ id
+ mode
+ }
}
}`,
};
@@ -428,6 +431,20 @@ query prices {
}`,
};
+export const publishPageMutation = {
+ id: 'publishPageMutation' as const,
+ operationName: 'publishPage',
+ definitionName: 'publishPage',
+ containsFile: false,
+ query: `
+mutation publishPage($workspaceId: String!, $pageId: String!, $mode: PublicPageMode = Page) {
+ publishPage(workspaceId: $workspaceId, pageId: $pageId, mode: $mode) {
+ id
+ mode
+ }
+}`,
+};
+
export const removeAvatarMutation = {
id: 'removeAvatarMutation' as const,
operationName: 'removeAvatar',
@@ -469,14 +486,18 @@ mutation revokeMemberPermission($workspaceId: String!, $userId: String!) {
}`,
};
-export const revokePageMutation = {
- id: 'revokePageMutation' as const,
- operationName: 'revokePage',
- definitionName: 'revokePage',
+export const revokePublicPageMutation = {
+ id: 'revokePublicPageMutation' as const,
+ operationName: 'revokePublicPage',
+ definitionName: 'revokePublicPage',
containsFile: false,
query: `
-mutation revokePage($workspaceId: String!, $pageId: String!) {
- revokePage(workspaceId: $workspaceId, pageId: $pageId)
+mutation revokePublicPage($workspaceId: String!, $pageId: String!) {
+ revokePublicPage(workspaceId: $workspaceId, pageId: $pageId) {
+ id
+ mode
+ public
+ }
}`,
};
@@ -537,17 +558,6 @@ mutation setWorkspacePublicById($id: ID!, $public: Boolean!) {
}`,
};
-export const sharePageMutation = {
- id: 'sharePageMutation' as const,
- operationName: 'sharePage',
- definitionName: 'sharePage',
- containsFile: false,
- query: `
-mutation sharePage($workspaceId: String!, $pageId: String!) {
- sharePage(workspaceId: $workspaceId, pageId: $pageId)
-}`,
-};
-
export const signInMutation = {
id: 'signInMutation' as const,
operationName: 'signIn',
diff --git a/packages/frontend/graphql/src/graphql/public-page.gql b/packages/frontend/graphql/src/graphql/public-page.gql
new file mode 100644
index 0000000000..7074bc153a
--- /dev/null
+++ b/packages/frontend/graphql/src/graphql/public-page.gql
@@ -0,0 +1,10 @@
+mutation publishPage(
+ $workspaceId: String!
+ $pageId: String!
+ $mode: PublicPageMode = Page
+) {
+ publishPage(workspaceId: $workspaceId, pageId: $pageId, mode: $mode) {
+ id
+ mode
+ }
+}
diff --git a/packages/frontend/graphql/src/graphql/revoke-page.gql b/packages/frontend/graphql/src/graphql/revoke-page.gql
deleted file mode 100644
index df8de2aba0..0000000000
--- a/packages/frontend/graphql/src/graphql/revoke-page.gql
+++ /dev/null
@@ -1,3 +0,0 @@
-mutation revokePage($workspaceId: String!, $pageId: String!) {
- revokePage(workspaceId: $workspaceId, pageId: $pageId)
-}
diff --git a/packages/frontend/graphql/src/graphql/revoke-public-page.gql b/packages/frontend/graphql/src/graphql/revoke-public-page.gql
new file mode 100644
index 0000000000..1515b3d501
--- /dev/null
+++ b/packages/frontend/graphql/src/graphql/revoke-public-page.gql
@@ -0,0 +1,7 @@
+mutation revokePublicPage($workspaceId: String!, $pageId: String!) {
+ revokePublicPage(workspaceId: $workspaceId, pageId: $pageId) {
+ id
+ mode
+ public
+ }
+}
diff --git a/packages/frontend/graphql/src/graphql/share-page.gql b/packages/frontend/graphql/src/graphql/share-page.gql
deleted file mode 100644
index 26a4259412..0000000000
--- a/packages/frontend/graphql/src/graphql/share-page.gql
+++ /dev/null
@@ -1,3 +0,0 @@
-mutation sharePage($workspaceId: String!, $pageId: String!) {
- sharePage(workspaceId: $workspaceId, pageId: $pageId)
-}
diff --git a/packages/frontend/graphql/src/schema.ts b/packages/frontend/graphql/src/schema.ts
index f026ab0f8d..99b6282ca3 100644
--- a/packages/frontend/graphql/src/schema.ts
+++ b/packages/frontend/graphql/src/schema.ts
@@ -340,13 +340,20 @@ export type GetWorkspacePublicByIdQuery = {
workspace: { __typename?: 'WorkspaceType'; public: boolean };
};
-export type GetWorkspaceSharedPagesQueryVariables = Exact<{
+export type GetWorkspacePublicPagesQueryVariables = Exact<{
workspaceId: Scalars['String']['input'];
}>;
-export type GetWorkspaceSharedPagesQuery = {
+export type GetWorkspacePublicPagesQuery = {
__typename?: 'Query';
- workspace: { __typename?: 'WorkspaceType'; sharedPages: Array };
+ workspace: {
+ __typename?: 'WorkspaceType';
+ publicPages: Array<{
+ __typename?: 'WorkspacePage';
+ id: string;
+ mode: PublicPageMode;
+ }>;
+ };
};
export type GetWorkspaceQueryVariables = Exact<{
@@ -422,6 +429,21 @@ export type PricesQuery = {
}>;
};
+export type PublishPageMutationVariables = Exact<{
+ workspaceId: Scalars['String']['input'];
+ pageId: Scalars['String']['input'];
+ mode?: InputMaybe;
+}>;
+
+export type PublishPageMutation = {
+ __typename?: 'Mutation';
+ publishPage: {
+ __typename?: 'WorkspacePage';
+ id: string;
+ mode: PublicPageMode;
+ };
+};
+
export type RemoveAvatarMutationVariables = Exact<{ [key: string]: never }>;
export type RemoveAvatarMutation = {
@@ -455,14 +477,19 @@ export type RevokeMemberPermissionMutation = {
revoke: boolean;
};
-export type RevokePageMutationVariables = Exact<{
+export type RevokePublicPageMutationVariables = Exact<{
workspaceId: Scalars['String']['input'];
pageId: Scalars['String']['input'];
}>;
-export type RevokePageMutation = {
+export type RevokePublicPageMutation = {
__typename?: 'Mutation';
- revokePage: boolean;
+ revokePublicPage: {
+ __typename?: 'WorkspacePage';
+ id: string;
+ mode: PublicPageMode;
+ public: boolean;
+ };
};
export type SendChangeEmailMutationVariables = Exact<{
@@ -516,13 +543,6 @@ export type SetWorkspacePublicByIdMutation = {
updateWorkspace: { __typename?: 'WorkspaceType'; id: string };
};
-export type SharePageMutationVariables = Exact<{
- workspaceId: Scalars['String']['input'];
- pageId: Scalars['String']['input'];
-}>;
-
-export type SharePageMutation = { __typename?: 'Mutation'; sharePage: boolean };
-
export type SignInMutationVariables = Exact<{
email: Scalars['String']['input'];
password: Scalars['String']['input'];
@@ -683,9 +703,9 @@ export type Queries =
response: GetWorkspacePublicByIdQuery;
}
| {
- name: 'getWorkspaceSharedPagesQuery';
- variables: GetWorkspaceSharedPagesQueryVariables;
- response: GetWorkspaceSharedPagesQuery;
+ name: 'getWorkspacePublicPagesQuery';
+ variables: GetWorkspacePublicPagesQueryVariables;
+ response: GetWorkspacePublicPagesQuery;
}
| {
name: 'getWorkspaceQuery';
@@ -774,6 +794,11 @@ export type Mutations =
variables: LeaveWorkspaceMutationVariables;
response: LeaveWorkspaceMutation;
}
+ | {
+ name: 'publishPageMutation';
+ variables: PublishPageMutationVariables;
+ response: PublishPageMutation;
+ }
| {
name: 'removeAvatarMutation';
variables: RemoveAvatarMutationVariables;
@@ -790,9 +815,9 @@ export type Mutations =
response: RevokeMemberPermissionMutation;
}
| {
- name: 'revokePageMutation';
- variables: RevokePageMutationVariables;
- response: RevokePageMutation;
+ name: 'revokePublicPageMutation';
+ variables: RevokePublicPageMutationVariables;
+ response: RevokePublicPageMutation;
}
| {
name: 'sendChangeEmailMutation';
@@ -819,11 +844,6 @@ export type Mutations =
variables: SetWorkspacePublicByIdMutationVariables;
response: SetWorkspacePublicByIdMutation;
}
- | {
- name: 'sharePageMutation';
- variables: SharePageMutationVariables;
- response: SharePageMutation;
- }
| {
name: 'signInMutation';
variables: SignInMutationVariables;
diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json
index bc73c4fdf4..0d2e5f1cb3 100644
--- a/packages/frontend/i18n/src/resources/en.json
+++ b/packages/frontend/i18n/src/resources/en.json
@@ -334,6 +334,21 @@
"com.affine.share-menu.ShareViaExportDescription": "Download a static copy of your page to share with others.",
"com.affine.share-menu.ShareWithLink": "Share with link",
"com.affine.share-menu.ShareWithLinkDescription": "Create a link you can easily share with anyone. The visitors will open your page in the form od a document",
+ "com.affine.share-menu.confirm-modify-mode.title": "Modify the sharing method?",
+ "com.affine.share-menu.confirm-modify-mode.description": "Once modified, new public link will be created. Please share it with others again.",
+ "com.affine.share-menu.confirm-modify-mode.confirm-button": "Modify",
+ "com.affine.share-menu.confirm-modify-mode.notification.success.title": "Modified successfully",
+ "com.affine.share-menu.confirm-modify-mode.notification.success.message": "You have changed the public link from {{preMode}} Mode to {{currentMode}} Mode.",
+ "com.affine.share-menu.confirm-modify-mode.notification.fail.title": "Failed to modify",
+ "com.affine.share-menu.confirm-modify-mode.notification.fail.message": "Please try again later.",
+ "com.affine.share-menu.create-public-link.notification.success.title": "Public link created",
+ "com.affine.share-menu.create-public-link.notification.success.message": "You can share this document with link.",
+ "com.affine.share-menu.create-public-link.notification.fail.title": "Failed to create public link",
+ "com.affine.share-menu.create-public-link.notification.fail.message": "Please try again later.",
+ "com.affine.share-menu.disable-publish-link.notification.success.title": "Public link disabled",
+ "com.affine.share-menu.disable-publish-link.notification.success.message": "This page is no longer shared publicly.",
+ "com.affine.share-menu.disable-publish-link.notification.fail.title": "Failed to disable public link",
+ "com.affine.share-menu.disable-publish-link.notification.fail.message": "Please try again later.",
"com.affine.shortcutsTitle.edgeless": "Edgeless",
"com.affine.shortcutsTitle.general": "General",
"com.affine.shortcutsTitle.markdownSyntax": "Markdown Syntax",
diff --git a/packages/frontend/workspace/src/providers/cloud/index.ts b/packages/frontend/workspace/src/providers/cloud/index.ts
index b374451d5e..a5dd71c4b6 100644
--- a/packages/frontend/workspace/src/providers/cloud/index.ts
+++ b/packages/frontend/workspace/src/providers/cloud/index.ts
@@ -10,10 +10,17 @@ const logger = new DebugLogger('affine:cloud');
const hashMap = new Map();
+type DocPublishMode = 'edgeless' | 'page';
+
+export type CloudDoc = {
+ arrayBuffer: ArrayBuffer;
+ publishMode: DocPublishMode;
+};
+
export async function downloadBinaryFromCloud(
rootGuid: string,
pageGuid: string
-): Promise {
+): Promise {
if (hashMap.has(`${rootGuid}/${pageGuid}`)) {
return true;
}
@@ -25,17 +32,22 @@ export async function downloadBinaryFromCloud(
}
);
if (response.ok) {
+ const publishMode = (response.headers.get('publish-mode') ||
+ 'page') as DocPublishMode;
const arrayBuffer = await response.arrayBuffer();
hashMap.set(`${rootGuid}/${pageGuid}`, arrayBuffer);
- return arrayBuffer;
+
+ // return both arrayBuffer and publish mode
+ return { arrayBuffer, publishMode };
}
return false;
}
async function downloadBinary(rootGuid: string, doc: Doc) {
- const buffer = await downloadBinaryFromCloud(rootGuid, doc.guid);
- if (typeof buffer !== 'boolean') {
- Y.applyUpdate(doc, new Uint8Array(buffer), 'affine-cloud');
+ const response = await downloadBinaryFromCloud(rootGuid, doc.guid);
+ if (typeof response !== 'boolean') {
+ const { arrayBuffer } = response;
+ Y.applyUpdate(doc, new Uint8Array(arrayBuffer), 'affine-cloud');
}
}
diff --git a/tests/affine-cloud/e2e/collaboration.spec.ts b/tests/affine-cloud/e2e/collaboration.spec.ts
index a97af04e28..2be5db6571 100644
--- a/tests/affine-cloud/e2e/collaboration.spec.ts
+++ b/tests/affine-cloud/e2e/collaboration.spec.ts
@@ -7,6 +7,7 @@ import {
loginUser,
} from '@affine-test/kit/utils/cloud';
import { dropFile } from '@affine-test/kit/utils/drop-file';
+import { clickEdgelessModeButton } from '@affine-test/kit/utils/editor';
import {
clickNewPageButton,
getBlockSuiteEditorTitle,
@@ -78,6 +79,52 @@ test.describe('collaboration', () => {
}
});
+ test('share page with default edgeless', async ({ page, browser }) => {
+ await page.reload();
+ await waitForEditorLoad(page);
+ await createLocalWorkspace(
+ {
+ name: 'test',
+ },
+ page
+ );
+ await enableCloudWorkspaceFromShareButton(page);
+ const title = getBlockSuiteEditorTitle(page);
+ await title.pressSequentially('TEST TITLE', {
+ delay: 50,
+ });
+ await page.keyboard.press('Enter', { delay: 50 });
+ await page.keyboard.type('TEST CONTENT', { delay: 50 });
+ await clickEdgelessModeButton(page);
+ await expect(page.locator('affine-edgeless-page')).toBeVisible({
+ timeout: 1000,
+ });
+ await page.getByTestId('cloud-share-menu-button').click();
+ await page.getByTestId('share-menu-create-link-button').click();
+ await page.getByTestId('share-menu-copy-link-button').click();
+
+ // check share page is accessible
+ {
+ const context = await browser.newContext();
+ const url: string = await page.evaluate(() =>
+ navigator.clipboard.readText()
+ );
+ const page2 = await context.newPage();
+ await page2.goto(url);
+ await waitForEditorLoad(page2);
+ await expect(page.locator('affine-edgeless-page')).toBeVisible({
+ timeout: 1000,
+ });
+ expect(await page2.textContent('affine-paragraph')).toContain(
+ 'TEST CONTENT'
+ );
+ const logo = page2.getByTestId('share-page-logo');
+ const editButton = page2.getByTestId('share-page-edit-button');
+ await expect(editButton).not.toBeVisible();
+ await expect(logo).toBeVisible();
+ }
+ });
+
test('can collaborate with other user and name should display when editing', async ({
page,
browser,
diff --git a/tests/storybook/src/stories/share-menu.stories.tsx b/tests/storybook/src/stories/share-menu.stories.tsx
index b711e0c2e4..7bafe3c318 100644
--- a/tests/storybook/src/stories/share-menu.stories.tsx
+++ b/tests/storybook/src/stories/share-menu.stories.tsx
@@ -1,9 +1,6 @@
import { toast } from '@affine/component';
-import {
- PublicLinkDisableModal,
- StyledDisableButton,
-} from '@affine/component/share-menu';
-import { ShareMenu } from '@affine/component/share-menu';
+import { PublicLinkDisableModal } from '@affine/component/disable-public-link';
+import { ShareMenu } from '@affine/core/components/affine/share-page-modal/share-menu';
import type {
AffineCloudWorkspace,
LocalWorkspace,
@@ -24,20 +21,6 @@ export default {
},
} satisfies Meta;
-const sharePageMap = new Map([]);
-// todo: use a real hook
-const useIsSharedPage = (
- _workspaceId: string,
- pageId: string
-): [isSharePage: boolean, setIsSharePage: (enable: boolean) => void] => {
- const [isShared, setIsShared] = useState(sharePageMap.get(pageId) ?? false);
- const togglePagePublic = (enable: boolean) => {
- setIsShared(enable);
- sharePageMap.set(pageId, enable);
- };
- return [isShared, togglePagePublic];
-};
-
async function initPage(page: Page) {
await page.waitForLoaded();
// Add page block and surface block at root level
@@ -88,11 +71,8 @@ export const Basic: StoryFn = () => {
return (
);
};
@@ -119,11 +99,8 @@ export const AffineBasic: StoryFn = () => {
return (
);
};
@@ -133,9 +110,7 @@ export const DisableModal: StoryFn = () => {
use(promise);
return (
<>
- setOpen(!open)}>
- Disable Public Link
-
+ setOpen(!open)}>Disable Public Link
{