diff --git a/packages/frontend/component/src/components/affine-banner/download-client.tsx b/packages/frontend/component/src/components/affine-banner/download-client.tsx deleted file mode 100644 index 593721af5c..0000000000 --- a/packages/frontend/component/src/components/affine-banner/download-client.tsx +++ /dev/null @@ -1,47 +0,0 @@ -import { Trans } from '@affine/i18n'; -import { CloseIcon, Logo1Icon } from '@blocksuite/icons'; - -import { - downloadCloseButtonStyle, - downloadMessageStyle, - downloadTipContainerStyle, - downloadTipIconStyle, - downloadTipStyle, - linkStyle, -} from './index.css'; - -export const DownloadTips = ({ onClose }: { onClose: () => void }) => { - return ( -
-
- -
- - This demo is limited. - - Download the AFFiNE Client - - for the latest features and Performance. - -
-
-
- -
-
- ); -}; - -export default DownloadTips; diff --git a/packages/frontend/component/src/components/affine-banner/index.css.ts b/packages/frontend/component/src/components/affine-banner/index.css.ts index 515f15c760..345f9eb30f 100644 --- a/packages/frontend/component/src/components/affine-banner/index.css.ts +++ b/packages/frontend/component/src/components/affine-banner/index.css.ts @@ -1,13 +1,4 @@ -import { keyframes, style } from '@vanilla-extract/css'; - -const slideDown = keyframes({ - '0%': { - height: '0px', - }, - '100%': { - height: '44px', - }, -}); +import { style } from '@vanilla-extract/css'; export const browserWarningStyle = style({ backgroundColor: 'var(--affine-background-warning-color)', @@ -36,52 +27,31 @@ export const closeIconStyle = style({ position: 'relative', zIndex: 1, }); -export const downloadTipContainerStyle = style({ - backgroundColor: 'var(--affine-primary-color)', - color: 'var(--affine-white)', +export const tipsContainer = style({ + backgroundColor: 'var(--affine-background-error-color)', + color: 'var(--affine-error-color)', width: '100%', - height: '44px', - fontSize: 'var(--affine-font-base)', + fontSize: 'var(--affine-font-sm)', + fontWeight: '700', display: 'flex', - justifyContent: 'center', + justifyContent: 'space-between', alignItems: 'center', - position: 'relative', - animation: `${slideDown} .3s ease-in-out forwards`, + padding: '12px 16px', + position: 'sticky', + gap: '16px', + containerType: 'inline-size', }); -export const downloadTipStyle = style({ + +export const tipsMessage = style({ + color: 'var(--affine-error-color)', + flexGrow: 1, + flexShrink: 1, +}); + +export const tipsRightItem = style({ display: 'flex', - justifyContent: 'center', + flexShrink: 0, + justifyContent: 'space-between', alignItems: 'center', -}); -export const downloadTipIconStyle = style({ - color: 'var(--affine-white)', - width: '24px', - height: '24px', - fontSize: '24px', - position: 'relative', - zIndex: 1, -}); -export const downloadCloseButtonStyle = style({ - color: 'var(--affine-white)', - cursor: 'pointer', - display: 'flex', - justifyContent: 'center', - alignItems: 'center', - position: 'absolute', - right: '24px', -}); -export const downloadMessageStyle = style({ - color: 'var(--affine-white)', - marginLeft: '8px', -}); -export const linkStyle = style({ - color: 'var(--affine-white)', - textDecoration: 'underline', - ':hover': { - textDecoration: 'underline', - }, - ':visited': { - color: 'var(--affine-white)', - textDecoration: 'underline', - }, + gap: '16px', }); diff --git a/packages/frontend/component/src/components/affine-banner/index.tsx b/packages/frontend/component/src/components/affine-banner/index.tsx index 24c34dfbfb..fae16ec0f7 100644 --- a/packages/frontend/component/src/components/affine-banner/index.tsx +++ b/packages/frontend/component/src/components/affine-banner/index.tsx @@ -1,2 +1,2 @@ export * from './browser-warning'; -export * from './download-client'; +export * from './local-demo-tips'; diff --git a/packages/frontend/component/src/components/affine-banner/local-demo-tips.tsx b/packages/frontend/component/src/components/affine-banner/local-demo-tips.tsx new file mode 100644 index 0000000000..3a9eea2ef2 --- /dev/null +++ b/packages/frontend/component/src/components/affine-banner/local-demo-tips.tsx @@ -0,0 +1,54 @@ +import { CloseIcon } from '@blocksuite/icons'; +import { Button, IconButton } from '@toeverything/components/button'; +import { useCallback } from 'react'; + +import * as styles from './index.css'; + +type LocalDemoTipsProps = { + isLoggedIn: boolean; + onLogin: () => void; + onEnableCloud: () => void; + onClose: () => void; +}; + +export const LocalDemoTips = ({ + onClose, + isLoggedIn, + onLogin, + onEnableCloud, +}: LocalDemoTipsProps) => { + const content = isLoggedIn + ? 'This is a local demo workspace, and the data is stored locally. We recommend enabling AFFiNE Cloud.' + : 'This is a local demo workspace, and the data is stored locally in the browser. We recommend Enabling AFFiNE Cloud or downloading the client for a better experience.'; + + const buttonLabel = isLoggedIn + ? 'Enable AFFiNE Cloud' + : 'Sign in with AFFiNE Cloud'; + + const handleClick = useCallback(() => { + if (isLoggedIn) { + return onEnableCloud(); + } + return onLogin(); + }, [isLoggedIn, onEnableCloud, onLogin]); + + return ( +
+
{content}
+ +
+
+ +
+ + + +
+
+ ); +}; + +export default LocalDemoTips; diff --git a/packages/frontend/component/src/components/app-sidebar/app-download-button/index.css.ts b/packages/frontend/component/src/components/app-sidebar/app-download-button/index.css.ts new file mode 100644 index 0000000000..22ccf82a88 --- /dev/null +++ b/packages/frontend/component/src/components/app-sidebar/app-download-button/index.css.ts @@ -0,0 +1,22 @@ +import { style } from '@vanilla-extract/css'; +export { + closeIcon, + ellipsisTextOverflow, + halo, + icon, + particles, + root, +} from '../app-updater-button/index.css'; + +export const rootPadding = style({ + padding: '0 24px', +}); + +export const label = style({ + display: 'flex', + alignItems: 'center', + width: '100%', + height: '100%', + fontSize: 'var(--affine-font-sm)', + whiteSpace: 'nowrap', +}); diff --git a/packages/frontend/component/src/components/app-sidebar/app-download-button/index.tsx b/packages/frontend/component/src/components/app-sidebar/app-download-button/index.tsx new file mode 100644 index 0000000000..8ec414aa9d --- /dev/null +++ b/packages/frontend/component/src/components/app-sidebar/app-download-button/index.tsx @@ -0,0 +1,53 @@ +import { CloseIcon, DownloadIcon } from '@blocksuite/icons'; +import clsx from 'clsx'; +import { useCallback, useState } from 'react'; + +import * as styles from './index.css'; + +// Although it is called an input, it is actually a button. +export function AppDownloadButton({ + className, + style, +}: { + className?: string; + style?: React.CSSProperties; +}) { + const [show, setShow] = useState(true); + + const handleClose = useCallback(() => { + setShow(false); + }, []); + + // TODO: unify this type of literal value. + const handleClick = useCallback(() => { + const url = `https://affine.pro/download?channel=stable`; + open(url, '_blank'); + }, []); + + if (!show) { + return null; + } + return ( + + ); +} diff --git a/packages/frontend/component/src/components/app-sidebar/index.tsx b/packages/frontend/component/src/components/app-sidebar/index.tsx index 8b2dc59557..69bc06f419 100644 --- a/packages/frontend/component/src/components/app-sidebar/index.tsx +++ b/packages/frontend/component/src/components/app-sidebar/index.tsx @@ -159,6 +159,7 @@ export const AppSidebarFallback = (): ReactElement | null => { }; export * from './add-page-button'; +export * from './app-download-button'; export * from './app-updater-button'; export * from './category-divider'; export * from './index.css'; diff --git a/packages/frontend/core/src/components/pure/header/index.tsx b/packages/frontend/core/src/components/pure/header/index.tsx index 90997094ed..ac1633582c 100644 --- a/packages/frontend/core/src/components/pure/header/index.tsx +++ b/packages/frontend/core/src/components/pure/header/index.tsx @@ -10,7 +10,6 @@ import type { ReactNode } from 'react'; import { forwardRef, useRef } from 'react'; import * as style from './style.css'; -import { TopTip } from './top-tip'; import { WindowsAppControls } from './windows-app-controls'; interface HeaderPros { @@ -49,61 +48,58 @@ export const Header = forwardRef(function Header( const open = useAtomValue(appSidebarOpenAtom); const appSidebarFloating = useAtomValue(appSidebarFloatingAtom); return ( - <> - +
-
-
- -
-
-
-
{left}
+
+
-
- {center} -
-
-
-
- {isWindowsDesktop ? : null} -
-
-
-
{right}
-
+
+
{left}
- +
+ {center} +
+
+
+
+ {isWindowsDesktop ? : null} +
+
+
+
{right}
+
+
+
); }); diff --git a/packages/frontend/core/src/components/pure/header/top-tip.tsx b/packages/frontend/core/src/components/pure/header/top-tip.tsx deleted file mode 100644 index 7490198670..0000000000 --- a/packages/frontend/core/src/components/pure/header/top-tip.tsx +++ /dev/null @@ -1,86 +0,0 @@ -import { BrowserWarning } from '@affine/component/affine-banner'; -import { DownloadTips } from '@affine/component/affine-banner'; -import { Trans } from '@affine/i18n'; -import { useAFFiNEI18N } from '@affine/i18n/hooks'; -import { useAtom } from 'jotai'; -import { useEffect, useState } from 'react'; - -import { guideDownloadClientTipAtom } from '../../../atoms/guide'; - -const minimumChromeVersion = 102; - -const shouldShowWarning = () => { - if (environment.isDesktop) { - // even though desktop has compatibility issues, - // we don't want to show the warning - return false; - } - if (!environment.isBrowser) { - // disable in SSR - return false; - } - if (environment.isChrome) { - return environment.chromeVersion < minimumChromeVersion; - } else { - return !environment.isMobile; - } -}; - -const OSWarningMessage = () => { - const t = useAFFiNEI18N(); - const [notChrome, setNotChrome] = useState(false); - const [notGoodVersion, setNotGoodVersion] = useState(false); - useEffect(() => { - setNotChrome(environment.isBrowser && !environment.isChrome); - setNotGoodVersion( - environment.isBrowser && - environment.isChrome && - environment.chromeVersion < minimumChromeVersion - ); - }, []); - - if (notChrome) { - return ( - - - We recommend the Chrome browser for an optimal - experience. - - - ); - } else if (notGoodVersion) { - return {t['upgradeBrowser']()}; - } - return null; -}; -export const TopTip = () => { - const [showWarning, setShowWarning] = useState(false); - const [showDownloadTip, setShowDownloadTip] = useAtom( - guideDownloadClientTipAtom - ); - - useEffect(() => { - setShowWarning(shouldShowWarning()); - }, []); - - if (showDownloadTip && environment.isDesktop) { - return ( - { - setShowDownloadTip(false); - localStorage.setItem('affine-is-dt-hide', '1'); - }} - /> - ); - } - - return ( - } - onClose={() => { - setShowWarning(false); - }} - /> - ); -}; diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx index 59981a169a..223a30b02c 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx @@ -5,6 +5,7 @@ import { } from '@affine/workspace/providers'; import { CloudWorkspaceIcon, + InformationFillDuotoneIcon, LocalWorkspaceIcon, NoNetworkIcon, UnsyncIcon, @@ -67,7 +68,11 @@ const UnSyncWorkspaceStatus = () => { const LocalWorkspaceStatus = () => { return ( <> - + {!environment.isDesktop ? ( + + ) : ( + + )} Local ); @@ -109,6 +114,9 @@ const WorkspaceStatus = ({ const content = useMemo(() => { // TODO: add i18n if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) { + if (!environment.isDesktop) { + return 'This is a local demo workspace.'; + } return 'Saved locally'; } if (!isOnline) { diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts index 00423908a0..45f2f0b924 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/styles.ts @@ -44,6 +44,9 @@ export const StyledWorkspaceStatus = styled('div')(() => { svg: { color: 'var(--affine-icon-color)', fontSize: 'var(--affine-font-base)', + '&[data-warning-color="true"]': { + color: 'var(--affine-error-color)', + }, }, }; }); diff --git a/packages/frontend/core/src/components/root-app-sidebar/index.tsx b/packages/frontend/core/src/components/root-app-sidebar/index.tsx index a6620301c5..c5367a1530 100644 --- a/packages/frontend/core/src/components/root-app-sidebar/index.tsx +++ b/packages/frontend/core/src/components/root-app-sidebar/index.tsx @@ -1,6 +1,7 @@ import { AnimatedDeleteIcon } from '@affine/component'; import { AddPageButton, + AppDownloadButton, AppSidebar, appSidebarOpenAtom, AppUpdaterButton, @@ -266,7 +267,7 @@ export const RootAppSidebar = ({ )} - {environment.isDesktop && } + {environment.isDesktop ? : }
diff --git a/packages/frontend/core/src/components/top-tip.tsx b/packages/frontend/core/src/components/top-tip.tsx new file mode 100644 index 0000000000..9a609380b7 --- /dev/null +++ b/packages/frontend/core/src/components/top-tip.tsx @@ -0,0 +1,122 @@ +import { BrowserWarning } from '@affine/component/affine-banner'; +import { LocalDemoTips } from '@affine/component/affine-banner'; +import { + type AffineOfficialWorkspace, + WorkspaceFlavour, +} from '@affine/env/workspace'; +import { Trans } from '@affine/i18n'; +import { useAFFiNEI18N } from '@affine/i18n/hooks'; +import { useSetAtom } from 'jotai'; +import { useCallback, useState } from 'react'; + +import { authAtom } from '../atoms'; +import { useCurrentLoginStatus } from '../hooks/affine/use-current-login-status'; +import { useOnTransformWorkspace } from '../hooks/root/use-on-transform-workspace'; +import { EnableAffineCloudModal } from './affine/enable-affine-cloud-modal'; + +const minimumChromeVersion = 106; + +const shouldShowWarning = (() => { + if (environment.isDesktop) { + // even though desktop has compatibility issues, + // we don't want to show the warning + return false; + } + if (!environment.isBrowser) { + // disable in SSR + return false; + } + if (environment.isChrome) { + return environment.chromeVersion < minimumChromeVersion; + } else { + return !environment.isMobile; + } +})(); + +const OSWarningMessage = () => { + const t = useAFFiNEI18N(); + const notChrome = environment.isBrowser && !environment.isChrome; + const notGoodVersion = + environment.isBrowser && + environment.isChrome && + environment.chromeVersion < minimumChromeVersion; + + if (notChrome) { + return ( + + + We recommend the Chrome browser for an optimal + experience. + + + ); + } else if (notGoodVersion) { + return {t['upgradeBrowser']()}; + } + return null; +}; + +export const TopTip = ({ + workspace, +}: { + workspace: AffineOfficialWorkspace; +}) => { + const loginStatus = useCurrentLoginStatus(); + const isLoggedIn = loginStatus === 'authenticated'; + + const [showWarning, setShowWarning] = useState(shouldShowWarning); + const [showLocalDemoTips, setShowLocalDemoTips] = useState(true); + const [open, setOpen] = useState(false); + + const setAuthModal = useSetAtom(authAtom); + const onLogin = useCallback(() => { + setAuthModal({ openModal: true, state: 'signIn' }); + }, [setAuthModal]); + + const onTransformWorkspace = useOnTransformWorkspace(); + const handleConfirm = useCallback(() => { + if (workspace.flavour !== WorkspaceFlavour.LOCAL) { + return; + } + onTransformWorkspace( + WorkspaceFlavour.LOCAL, + WorkspaceFlavour.AFFINE_CLOUD, + workspace + ); + setOpen(false); + }, [onTransformWorkspace, workspace]); + + if ( + showLocalDemoTips && + !environment.isDesktop && + workspace.flavour === WorkspaceFlavour.LOCAL + ) { + return ( + <> + setOpen(true)} + onClose={() => { + setShowLocalDemoTips(false); + }} + /> + + + ); + } + + return ( + } + onClose={() => { + setShowWarning(false); + }} + /> + ); +}; diff --git a/packages/frontend/core/src/components/workspace-header.tsx b/packages/frontend/core/src/components/workspace-header.tsx index 719a35b409..18d15026e1 100644 --- a/packages/frontend/core/src/components/workspace-header.tsx +++ b/packages/frontend/core/src/components/workspace-header.tsx @@ -29,6 +29,7 @@ import { filterContainerStyle } from './filter-container.css'; import { Header } from './pure/header'; import { PluginHeader } from './pure/plugin-header'; import { WorkspaceModeFilterTab } from './pure/workspace-mode-filter-tab'; +import { TopTip } from './top-tip'; import * as styles from './workspace-header.css'; const FilterContainer = ({ workspaceId }: { workspaceId: string }) => { @@ -161,23 +162,26 @@ export function WorkspaceHeader({ ) : null; return ( -
- } - right={ -
- {sharePageModal} - -
- } - bottomBorder - /> + <> +
+ } + right={ +
+ {sharePageModal} + +
+ } + bottomBorder + /> + + ); } diff --git a/tests/affine-local/e2e/open-affine.spec.ts b/tests/affine-local/e2e/open-affine.spec.ts index 9c80920342..2a9b639808 100644 --- a/tests/affine-local/e2e/open-affine.spec.ts +++ b/tests/affine-local/e2e/open-affine.spec.ts @@ -25,22 +25,20 @@ test('Open last workspace when back to affine', async ({ page }) => { expect(currentWorkspaceName).toEqual('New Workspace 2'); }); -test.skip('Download client tip', async ({ page }) => { +test('Download client tip', async ({ page }) => { await openHomePage(page); - const downloadClientTipItem = page.locator( - '[data-testid=download-client-tip]' - ); - await expect(downloadClientTipItem).toBeVisible(); + const localDemoTipsItem = page.locator('[data-testid=local-demo-tips]'); + await expect(localDemoTipsItem).toBeVisible(); const closeButton = page.locator( - '[data-testid=download-client-tip-close-button]' + '[data-testid=local-demo-tips-close-button]' ); await closeButton.click(); - await expect(downloadClientTipItem).not.toBeVisible(); - await page.goto('http://localhost:8080'); - const currentDownloadClientTipItem = page.locator( - '[data-testid=download-client-tip]' + await expect(localDemoTipsItem).not.toBeVisible(); + await page.reload(); + const currentLocalDemoTipsItemItem = page.locator( + '[data-testid=local-demo-tips]' ); - await expect(currentDownloadClientTipItem).toBeVisible(); + await expect(currentLocalDemoTipsItemItem).toBeVisible(); }); test('Check the class name for the scrollbar', async ({ page }) => { diff --git a/tests/storybook/src/stories/affine-banner.stories.tsx b/tests/storybook/src/stories/affine-banner.stories.tsx index 618a7752cc..27f95e66a2 100644 --- a/tests/storybook/src/stories/affine-banner.stories.tsx +++ b/tests/storybook/src/stories/affine-banner.stories.tsx @@ -1,4 +1,4 @@ -import { BrowserWarning, DownloadTips } from '@affine/component/affine-banner'; +import { BrowserWarning, LocalDemoTips } from '@affine/component/affine-banner'; import type { StoryFn } from '@storybook/react'; import { useState } from 'react'; @@ -24,9 +24,13 @@ export const Default: StoryFn = () => { export const Download: StoryFn = () => { const [, setIsClosed] = useState(true); + const [isLoggedIn, setIsLoggedIn] = useState(false); return (
- setIsLoggedIn(true)} + onEnableCloud={() => {}} onClose={() => { setIsClosed(false); }}