mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-17 06:16:59 +08:00
feat(core): add download app button to web (#5023)
This commit is contained in:
@@ -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;
|
|
||||||
@@ -1,13 +1,4 @@
|
|||||||
import { keyframes, style } from '@vanilla-extract/css';
|
import { style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
const slideDown = keyframes({
|
|
||||||
'0%': {
|
|
||||||
height: '0px',
|
|
||||||
},
|
|
||||||
'100%': {
|
|
||||||
height: '44px',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const browserWarningStyle = style({
|
export const browserWarningStyle = style({
|
||||||
backgroundColor: 'var(--affine-background-warning-color)',
|
backgroundColor: 'var(--affine-background-warning-color)',
|
||||||
@@ -36,52 +27,31 @@ export const closeIconStyle = style({
|
|||||||
position: 'relative',
|
position: 'relative',
|
||||||
zIndex: 1,
|
zIndex: 1,
|
||||||
});
|
});
|
||||||
export const downloadTipContainerStyle = style({
|
export const tipsContainer = style({
|
||||||
backgroundColor: 'var(--affine-primary-color)',
|
backgroundColor: 'var(--affine-background-error-color)',
|
||||||
color: 'var(--affine-white)',
|
color: 'var(--affine-error-color)',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
height: '44px',
|
fontSize: 'var(--affine-font-sm)',
|
||||||
fontSize: 'var(--affine-font-base)',
|
fontWeight: '700',
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
position: 'relative',
|
padding: '12px 16px',
|
||||||
animation: `${slideDown} .3s ease-in-out forwards`,
|
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',
|
display: 'flex',
|
||||||
justifyContent: 'center',
|
flexShrink: 0,
|
||||||
|
justifyContent: 'space-between',
|
||||||
alignItems: 'center',
|
alignItems: 'center',
|
||||||
});
|
gap: '16px',
|
||||||
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',
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
export * from './browser-warning';
|
export * from './browser-warning';
|
||||||
export * from './download-client';
|
export * from './local-demo-tips';
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -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',
|
||||||
|
});
|
||||||
@@ -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>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -159,6 +159,7 @@ export const AppSidebarFallback = (): ReactElement | null => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export * from './add-page-button';
|
export * from './add-page-button';
|
||||||
|
export * from './app-download-button';
|
||||||
export * from './app-updater-button';
|
export * from './app-updater-button';
|
||||||
export * from './category-divider';
|
export * from './category-divider';
|
||||||
export * from './index.css';
|
export * from './index.css';
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import type { ReactNode } from 'react';
|
|||||||
import { forwardRef, useRef } from 'react';
|
import { forwardRef, useRef } from 'react';
|
||||||
|
|
||||||
import * as style from './style.css';
|
import * as style from './style.css';
|
||||||
import { TopTip } from './top-tip';
|
|
||||||
import { WindowsAppControls } from './windows-app-controls';
|
import { WindowsAppControls } from './windows-app-controls';
|
||||||
|
|
||||||
interface HeaderPros {
|
interface HeaderPros {
|
||||||
@@ -49,61 +48,58 @@ export const Header = forwardRef<HTMLDivElement, HeaderPros>(function Header(
|
|||||||
const open = useAtomValue(appSidebarOpenAtom);
|
const open = useAtomValue(appSidebarOpenAtom);
|
||||||
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
const appSidebarFloating = useAtomValue(appSidebarFloatingAtom);
|
||||||
return (
|
return (
|
||||||
<>
|
<div
|
||||||
<TopTip />
|
className={clsx(style.header, bottomBorder && style.bottomBorder)}
|
||||||
|
// data-has-warning={showWarning}
|
||||||
|
data-open={open}
|
||||||
|
data-sidebar-floating={appSidebarFloating}
|
||||||
|
data-testid="header"
|
||||||
|
ref={ref}
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(style.header, bottomBorder && style.bottomBorder)}
|
className={clsx(style.headerSideContainer, {
|
||||||
// data-has-warning={showWarning}
|
block: isTinyScreen,
|
||||||
data-open={open}
|
})}
|
||||||
data-sidebar-floating={appSidebarFloating}
|
|
||||||
data-testid="header"
|
|
||||||
ref={ref}
|
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(style.headerSideContainer, {
|
className={clsx(
|
||||||
block: isTinyScreen,
|
style.headerItem,
|
||||||
})}
|
'top-item',
|
||||||
|
!open ? 'top-item-visible' : ''
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<div
|
<div ref={sidebarSwitchRef}>
|
||||||
className={clsx(
|
<SidebarSwitch show={!open} />
|
||||||
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div className={clsx(style.headerItem, 'left')}>
|
||||||
className={clsx({
|
<div ref={leftSlotRef}>{left}</div>
|
||||||
[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>
|
||||||
</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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
} from '@affine/workspace/providers';
|
} from '@affine/workspace/providers';
|
||||||
import {
|
import {
|
||||||
CloudWorkspaceIcon,
|
CloudWorkspaceIcon,
|
||||||
|
InformationFillDuotoneIcon,
|
||||||
LocalWorkspaceIcon,
|
LocalWorkspaceIcon,
|
||||||
NoNetworkIcon,
|
NoNetworkIcon,
|
||||||
UnsyncIcon,
|
UnsyncIcon,
|
||||||
@@ -67,7 +68,11 @@ const UnSyncWorkspaceStatus = () => {
|
|||||||
const LocalWorkspaceStatus = () => {
|
const LocalWorkspaceStatus = () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<LocalWorkspaceIcon />
|
{!environment.isDesktop ? (
|
||||||
|
<InformationFillDuotoneIcon data-warning-color="true" />
|
||||||
|
) : (
|
||||||
|
<LocalWorkspaceIcon />
|
||||||
|
)}
|
||||||
Local
|
Local
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -109,6 +114,9 @@ const WorkspaceStatus = ({
|
|||||||
const content = useMemo(() => {
|
const content = useMemo(() => {
|
||||||
// TODO: add i18n
|
// TODO: add i18n
|
||||||
if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
|
if (currentWorkspace.flavour === WorkspaceFlavour.LOCAL) {
|
||||||
|
if (!environment.isDesktop) {
|
||||||
|
return 'This is a local demo workspace.';
|
||||||
|
}
|
||||||
return 'Saved locally';
|
return 'Saved locally';
|
||||||
}
|
}
|
||||||
if (!isOnline) {
|
if (!isOnline) {
|
||||||
|
|||||||
@@ -44,6 +44,9 @@ export const StyledWorkspaceStatus = styled('div')(() => {
|
|||||||
svg: {
|
svg: {
|
||||||
color: 'var(--affine-icon-color)',
|
color: 'var(--affine-icon-color)',
|
||||||
fontSize: 'var(--affine-font-base)',
|
fontSize: 'var(--affine-font-base)',
|
||||||
|
'&[data-warning-color="true"]': {
|
||||||
|
color: 'var(--affine-error-color)',
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { AnimatedDeleteIcon } from '@affine/component';
|
import { AnimatedDeleteIcon } from '@affine/component';
|
||||||
import {
|
import {
|
||||||
AddPageButton,
|
AddPageButton,
|
||||||
|
AppDownloadButton,
|
||||||
AppSidebar,
|
AppSidebar,
|
||||||
appSidebarOpenAtom,
|
appSidebarOpenAtom,
|
||||||
AppUpdaterButton,
|
AppUpdaterButton,
|
||||||
@@ -266,7 +267,7 @@ export const RootAppSidebar = ({
|
|||||||
)}
|
)}
|
||||||
</SidebarScrollableContainer>
|
</SidebarScrollableContainer>
|
||||||
<SidebarContainer>
|
<SidebarContainer>
|
||||||
{environment.isDesktop && <AppUpdaterButton />}
|
{environment.isDesktop ? <AppUpdaterButton /> : <AppDownloadButton />}
|
||||||
<div style={{ height: '4px' }} />
|
<div style={{ height: '4px' }} />
|
||||||
<AddPageButton onClick={onClickNewPage} />
|
<AddPageButton onClick={onClickNewPage} />
|
||||||
</SidebarContainer>
|
</SidebarContainer>
|
||||||
|
|||||||
122
packages/frontend/core/src/components/top-tip.tsx
Normal file
122
packages/frontend/core/src/components/top-tip.tsx
Normal 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);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -29,6 +29,7 @@ import { filterContainerStyle } from './filter-container.css';
|
|||||||
import { Header } from './pure/header';
|
import { Header } from './pure/header';
|
||||||
import { PluginHeader } from './pure/plugin-header';
|
import { PluginHeader } from './pure/plugin-header';
|
||||||
import { WorkspaceModeFilterTab } from './pure/workspace-mode-filter-tab';
|
import { WorkspaceModeFilterTab } from './pure/workspace-mode-filter-tab';
|
||||||
|
import { TopTip } from './top-tip';
|
||||||
import * as styles from './workspace-header.css';
|
import * as styles from './workspace-header.css';
|
||||||
|
|
||||||
const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
|
const FilterContainer = ({ workspaceId }: { workspaceId: string }) => {
|
||||||
@@ -161,23 +162,26 @@ export function WorkspaceHeader({
|
|||||||
<SharePageModal workspace={currentWorkspace} page={currentPage} />
|
<SharePageModal workspace={currentWorkspace} page={currentPage} />
|
||||||
) : null;
|
) : null;
|
||||||
return (
|
return (
|
||||||
<Header
|
<>
|
||||||
mainContainerAtom={mainContainerAtom}
|
<Header
|
||||||
ref={setAppHeader}
|
mainContainerAtom={mainContainerAtom}
|
||||||
center={
|
ref={setAppHeader}
|
||||||
<BlockSuiteHeaderTitle
|
center={
|
||||||
workspace={currentWorkspace}
|
<BlockSuiteHeaderTitle
|
||||||
pageId={currentEntry.pageId}
|
workspace={currentWorkspace}
|
||||||
/>
|
pageId={currentEntry.pageId}
|
||||||
}
|
/>
|
||||||
right={
|
}
|
||||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
right={
|
||||||
{sharePageModal}
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px' }}>
|
||||||
<PluginHeader />
|
{sharePageModal}
|
||||||
</div>
|
<PluginHeader />
|
||||||
}
|
</div>
|
||||||
bottomBorder
|
}
|
||||||
/>
|
bottomBorder
|
||||||
|
/>
|
||||||
|
<TopTip workspace={currentWorkspace} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -25,22 +25,20 @@ test('Open last workspace when back to affine', async ({ page }) => {
|
|||||||
expect(currentWorkspaceName).toEqual('New Workspace 2');
|
expect(currentWorkspaceName).toEqual('New Workspace 2');
|
||||||
});
|
});
|
||||||
|
|
||||||
test.skip('Download client tip', async ({ page }) => {
|
test('Download client tip', async ({ page }) => {
|
||||||
await openHomePage(page);
|
await openHomePage(page);
|
||||||
const downloadClientTipItem = page.locator(
|
const localDemoTipsItem = page.locator('[data-testid=local-demo-tips]');
|
||||||
'[data-testid=download-client-tip]'
|
await expect(localDemoTipsItem).toBeVisible();
|
||||||
);
|
|
||||||
await expect(downloadClientTipItem).toBeVisible();
|
|
||||||
const closeButton = page.locator(
|
const closeButton = page.locator(
|
||||||
'[data-testid=download-client-tip-close-button]'
|
'[data-testid=local-demo-tips-close-button]'
|
||||||
);
|
);
|
||||||
await closeButton.click();
|
await closeButton.click();
|
||||||
await expect(downloadClientTipItem).not.toBeVisible();
|
await expect(localDemoTipsItem).not.toBeVisible();
|
||||||
await page.goto('http://localhost:8080');
|
await page.reload();
|
||||||
const currentDownloadClientTipItem = page.locator(
|
const currentLocalDemoTipsItemItem = page.locator(
|
||||||
'[data-testid=download-client-tip]'
|
'[data-testid=local-demo-tips]'
|
||||||
);
|
);
|
||||||
await expect(currentDownloadClientTipItem).toBeVisible();
|
await expect(currentLocalDemoTipsItemItem).toBeVisible();
|
||||||
});
|
});
|
||||||
|
|
||||||
test('Check the class name for the scrollbar', async ({ page }) => {
|
test('Check the class name for the scrollbar', async ({ page }) => {
|
||||||
|
|||||||
@@ -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 type { StoryFn } from '@storybook/react';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
|
||||||
@@ -24,9 +24,13 @@ export const Default: StoryFn = () => {
|
|||||||
|
|
||||||
export const Download: StoryFn = () => {
|
export const Download: StoryFn = () => {
|
||||||
const [, setIsClosed] = useState(true);
|
const [, setIsClosed] = useState(true);
|
||||||
|
const [isLoggedIn, setIsLoggedIn] = useState(false);
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<DownloadTips
|
<LocalDemoTips
|
||||||
|
isLoggedIn={isLoggedIn}
|
||||||
|
onLogin={() => setIsLoggedIn(true)}
|
||||||
|
onEnableCloud={() => {}}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
setIsClosed(false);
|
setIsClosed(false);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user