feat(core): add download app button to web (#5023)

This commit is contained in:
JimmFly 2023-11-23 14:33:25 +08:00 committed by GitHub
parent 3499dbbb7f
commit ad2d3b9167
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 369 additions and 266 deletions

View File

@ -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 (
<div
className={downloadTipContainerStyle}
data-testid="download-client-tip"
>
<div className={downloadTipStyle}>
<Logo1Icon className={downloadTipIconStyle} />
<div className={downloadMessageStyle}>
<Trans i18nKey="com.affine.banner.content">
This demo is limited.
<a
className={linkStyle}
href="https://affine.pro/download"
target="_blank"
rel="noreferrer"
>
Download the AFFiNE Client
</a>
for the latest features and Performance.
</Trans>
</div>
</div>
<div
className={downloadCloseButtonStyle}
onClick={onClose}
data-testid="download-client-tip-close-button"
>
<CloseIcon className={downloadTipIconStyle} />
</div>
</div>
);
};
export default DownloadTips;

View File

@ -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',
});

View File

@ -1,2 +1,2 @@
export * from './browser-warning';
export * from './download-client';
export * from './local-demo-tips';

View File

@ -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 (
<div className={styles.tipsContainer} data-testid="local-demo-tips">
<div className={styles.tipsMessage}>{content}</div>
<div className={styles.tipsRightItem}>
<div>
<Button onClick={handleClick}>{buttonLabel}</Button>
</div>
<IconButton
onClick={onClose}
data-testid="local-demo-tips-close-button"
>
<CloseIcon />
</IconButton>
</div>
</div>
);
};
export default LocalDemoTips;

View File

@ -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',
});

View File

@ -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 (
<button
style={style}
className={clsx([styles.root, styles.rootPadding, className])}
onClick={handleClick}
>
<div className={clsx([styles.label])}>
<DownloadIcon className={styles.icon} />
<span className={styles.ellipsisTextOverflow}>Download App</span>
</div>
<div
className={styles.closeIcon}
onClick={e => {
e.stopPropagation();
handleClose();
}}
>
<CloseIcon />
</div>
<div className={styles.particles} aria-hidden="true"></div>
<span className={styles.halo} aria-hidden="true"></span>
</button>
);
}

View File

@ -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';

View File

@ -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<HTMLDivElement, HeaderPros>(function Header(
const open = useAtomValue(appSidebarOpenAtom);
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
return (
<>
<TopTip />
<div
className={clsx(style.header, bottomBorder && style.bottomBorder)}
// data-has-warning={showWarning}
data-open={open}
data-sidebar-floating={appSidebarFloating}
data-testid="header"
ref={ref}
>
<div
className={clsx(style.header, bottomBorder && style.bottomBorder)}
// data-has-warning={showWarning}
data-open={open}
data-sidebar-floating={appSidebarFloating}
data-testid="header"
ref={ref}
className={clsx(style.headerSideContainer, {
block: isTinyScreen,
})}
>
<div
className={clsx(style.headerSideContainer, {
block: isTinyScreen,
})}
className={clsx(
style.headerItem,
'top-item',
!open ? 'top-item-visible' : ''
)}
>
<div
className={clsx(
style.headerItem,
'top-item',
!open ? 'top-item-visible' : ''
)}
>
<div ref={sidebarSwitchRef}>
<SidebarSwitch show={!open} />
</div>
</div>
<div className={clsx(style.headerItem, 'left')}>
<div ref={leftSlotRef}>{left}</div>
<div ref={sidebarSwitchRef}>
<SidebarSwitch show={!open} />
</div>
</div>
<div
className={clsx({
[style.headerCenter]: center,
'is-window': isWindowsDesktop,
})}
ref={centerSlotRef}
>
{center}
</div>
<div
className={clsx(style.headerSideContainer, 'right', {
block: isTinyScreen,
})}
>
<div className={clsx(style.headerItem, 'top-item')}>
<div ref={windowControlsRef}>
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
</div>
<div className={clsx(style.headerItem, 'right')}>
<div ref={rightSlotRef}>{right}</div>
</div>
<div className={clsx(style.headerItem, 'left')}>
<div ref={leftSlotRef}>{left}</div>
</div>
</div>
</>
<div
className={clsx({
[style.headerCenter]: center,
'is-window': isWindowsDesktop,
})}
ref={centerSlotRef}
>
{center}
</div>
<div
className={clsx(style.headerSideContainer, 'right', {
block: isTinyScreen,
})}
>
<div className={clsx(style.headerItem, 'top-item')}>
<div ref={windowControlsRef}>
{isWindowsDesktop ? <WindowsAppControls /> : null}
</div>
</div>
<div className={clsx(style.headerItem, 'right')}>
<div ref={rightSlotRef}>{right}</div>
</div>
</div>
</div>
);
});

View File

@ -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 (
<span>
<Trans i18nKey="recommendBrowser">
We recommend the <strong>Chrome</strong> browser for an optimal
experience.
</Trans>
</span>
);
} else if (notGoodVersion) {
return <span>{t['upgradeBrowser']()}</span>;
}
return null;
};
export const TopTip = () => {
const [showWarning, setShowWarning] = useState(false);
const [showDownloadTip, setShowDownloadTip] = useAtom(
guideDownloadClientTipAtom
);
useEffect(() => {
setShowWarning(shouldShowWarning());
}, []);
if (showDownloadTip && environment.isDesktop) {
return (
<DownloadTips
onClose={() => {
setShowDownloadTip(false);
localStorage.setItem('affine-is-dt-hide', '1');
}}
/>
);
}
return (
<BrowserWarning
show={showWarning}
message={<OSWarningMessage />}
onClose={() => {
setShowWarning(false);
}}
/>
);
};

View File

@ -5,6 +5,7 @@ import {
} from '@affine/workspace/providers';
import {
CloudWorkspaceIcon,
InformationFillDuotoneIcon,
LocalWorkspaceIcon,
NoNetworkIcon,
UnsyncIcon,
@ -67,7 +68,11 @@ const UnSyncWorkspaceStatus = () => {
const LocalWorkspaceStatus = () => {
return (
<>
<LocalWorkspaceIcon />
{!environment.isDesktop ? (
<InformationFillDuotoneIcon data-warning-color="true" />
) : (
<LocalWorkspaceIcon />
)}
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) {

View File

@ -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)',
},
},
};
});

View File

@ -1,6 +1,7 @@
import { AnimatedDeleteIcon } from '@affine/component';
import {
AddPageButton,
AppDownloadButton,
AppSidebar,
appSidebarOpenAtom,
AppUpdaterButton,
@ -266,7 +267,7 @@ export const RootAppSidebar = ({
)}
</SidebarScrollableContainer>
<SidebarContainer>
{environment.isDesktop && <AppUpdaterButton />}
{environment.isDesktop ? <AppUpdaterButton /> : <AppDownloadButton />}
<div style={{ height: '4px' }} />
<AddPageButton onClick={onClickNewPage} />
</SidebarContainer>

View File

@ -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 (
<span>
<Trans i18nKey="recommendBrowser">
We recommend the <strong>Chrome</strong> browser for an optimal
experience.
</Trans>
</span>
);
} else if (notGoodVersion) {
return <span>{t['upgradeBrowser']()}</span>;
}
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 (
<>
<LocalDemoTips
isLoggedIn={isLoggedIn}
onLogin={onLogin}
onEnableCloud={() => setOpen(true)}
onClose={() => {
setShowLocalDemoTips(false);
}}
/>
<EnableAffineCloudModal
open={open}
onOpenChange={setOpen}
onConfirm={handleConfirm}
/>
</>
);
}
return (
<BrowserWarning
show={showWarning}
message={<OSWarningMessage />}
onClose={() => {
setShowWarning(false);
}}
/>
);
};

View File

@ -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({
<SharePageModal workspace={currentWorkspace} page={currentPage} />
) : null;
return (
<Header
mainContainerAtom={mainContainerAtom}
ref={setAppHeader}
center={
<BlockSuiteHeaderTitle
workspace={currentWorkspace}
pageId={currentEntry.pageId}
/>
}
right={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{sharePageModal}
<PluginHeader />
</div>
}
bottomBorder
/>
<>
<Header
mainContainerAtom={mainContainerAtom}
ref={setAppHeader}
center={
<BlockSuiteHeaderTitle
workspace={currentWorkspace}
pageId={currentEntry.pageId}
/>
}
right={
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
{sharePageModal}
<PluginHeader />
</div>
}
bottomBorder
/>
<TopTip workspace={currentWorkspace} />
</>
);
}

View File

@ -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 }) => {

View File

@ -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 (
<div>
<DownloadTips
<LocalDemoTips
isLoggedIn={isLoggedIn}
onLogin={() => setIsLoggedIn(true)}
onEnableCloud={() => {}}
onClose={() => {
setIsClosed(false);
}}