mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-13 12:55:00 +00:00
feat: add change log (#1734)
Co-authored-by: Himself65 <himself65@outlook.com>
This commit is contained in:
@@ -1,5 +1,28 @@
|
||||
import { atom } from 'jotai';
|
||||
import { atomWithStorage } from 'jotai/utils';
|
||||
|
||||
export const isFirstLoadAtom = atomWithStorage<boolean>('isFirstLoad', true);
|
||||
export const openTipsAtom = atom<boolean>(false);
|
||||
export type Visibility = Record<string, boolean>;
|
||||
|
||||
const DEFAULT_VALUE = '0.0.0';
|
||||
//atomWithStorage always uses initial value when first render
|
||||
//https://github.com/pmndrs/jotai/discussions/1737
|
||||
|
||||
function getInitialValue() {
|
||||
if (typeof window !== 'undefined') {
|
||||
const storedValue = window.localStorage.getItem('lastVersion');
|
||||
if (storedValue) {
|
||||
return JSON.parse(storedValue);
|
||||
}
|
||||
}
|
||||
return DEFAULT_VALUE;
|
||||
}
|
||||
|
||||
export const lastVersionAtom = atomWithStorage(
|
||||
'lastVersion',
|
||||
getInitialValue()
|
||||
);
|
||||
export const guideHiddenAtom = atomWithStorage<Visibility>('guideHidden', {});
|
||||
|
||||
export const guideHiddenUntilNextUpdateAtom = atomWithStorage<Visibility>(
|
||||
'guideHiddenUntilNextUpdate',
|
||||
{}
|
||||
);
|
||||
|
||||
@@ -3,8 +3,9 @@ import { useTranslation } from '@affine/i18n';
|
||||
import { useCallback, useState } from 'react';
|
||||
|
||||
import {
|
||||
useIsFirstLoad,
|
||||
useOpenTips,
|
||||
useGuideHidden,
|
||||
useGuideHiddenUntilNextUpdate,
|
||||
useUpdateTipsOnVersionChange,
|
||||
} from '../../../hooks/affine/use-is-first-load';
|
||||
import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
|
||||
import { SidebarSwitchIcon } from './icons';
|
||||
@@ -19,10 +20,12 @@ export const SidebarSwitch = ({
|
||||
tooltipContent,
|
||||
testid = '',
|
||||
}: SidebarSwitchProps) => {
|
||||
useUpdateTipsOnVersionChange();
|
||||
const [open, setOpen] = useSidebarStatus();
|
||||
const [tooltipVisible, setTooltipVisible] = useState(false);
|
||||
const [isFirstLoad, setIsFirstLoad] = useIsFirstLoad();
|
||||
const [, setOpenTips] = useOpenTips();
|
||||
const [guideHidden, setGuideHidden] = useGuideHidden();
|
||||
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
|
||||
useGuideHiddenUntilNextUpdate();
|
||||
const { t } = useTranslation();
|
||||
tooltipContent =
|
||||
tooltipContent || (open ? t('Collapse sidebar') : t('Expand sidebar'));
|
||||
@@ -41,13 +44,23 @@ export const SidebarSwitch = ({
|
||||
onClick={useCallback(() => {
|
||||
setOpen(!open);
|
||||
setTooltipVisible(false);
|
||||
if (isFirstLoad) {
|
||||
setIsFirstLoad(false);
|
||||
if (guideHiddenUntilNextUpdate['quickSearchTips'] === false) {
|
||||
setGuideHiddenUntilNextUpdate({
|
||||
...guideHiddenUntilNextUpdate,
|
||||
quickSearchTips: true,
|
||||
});
|
||||
setTimeout(() => {
|
||||
setOpenTips(true);
|
||||
setGuideHidden({ ...guideHidden, quickSearchTips: false });
|
||||
}, 200);
|
||||
}
|
||||
}, [isFirstLoad, open, setIsFirstLoad, setOpen, setOpenTips])}
|
||||
}, [
|
||||
guideHidden,
|
||||
guideHiddenUntilNextUpdate,
|
||||
open,
|
||||
setGuideHidden,
|
||||
setGuideHiddenUntilNextUpdate,
|
||||
setOpen,
|
||||
])}
|
||||
onMouseEnter={useCallback(() => {
|
||||
setTooltipVisible(true);
|
||||
}, [])}
|
||||
|
||||
@@ -8,7 +8,7 @@ import type { HTMLAttributes } from 'react';
|
||||
import { forwardRef, useCallback, useRef } from 'react';
|
||||
|
||||
import { currentEditorAtom, openQuickSearchModalAtom } from '../../../atoms';
|
||||
import { useOpenTips } from '../../../hooks/affine/use-is-first-load';
|
||||
import { useGuideHidden } from '../../../hooks/affine/use-is-first-load';
|
||||
import { usePageMeta } from '../../../hooks/use-page-meta';
|
||||
import { useElementResizeEffect } from '../../../hooks/use-workspaces';
|
||||
import type { BlockSuiteWorkspace } from '../../../shared';
|
||||
@@ -53,7 +53,7 @@ export const BlockSuiteEditorHeader = forwardRef<
|
||||
assertExists(pageMeta);
|
||||
const title = pageMeta.title;
|
||||
const { trash: isTrash } = pageMeta;
|
||||
const [openTips, setOpenTips] = useOpenTips();
|
||||
const [isTipsHidden, setTipsHidden] = useGuideHidden();
|
||||
const isMac = () => {
|
||||
const env = getEnvironment();
|
||||
return env.isBrowser && env.isMacOs;
|
||||
@@ -64,11 +64,11 @@ export const BlockSuiteEditorHeader = forwardRef<
|
||||
useElementResizeEffect(
|
||||
useAtomValue(currentEditorAtom),
|
||||
useCallback(() => {
|
||||
if (!openTips || !popperRef.current) {
|
||||
if (isTipsHidden.quickSearchTips || !popperRef.current) {
|
||||
return;
|
||||
}
|
||||
popperRef.current.update();
|
||||
}, [openTips])
|
||||
}, [isTipsHidden.quickSearchTips])
|
||||
);
|
||||
|
||||
const TipsContent = (
|
||||
@@ -91,7 +91,9 @@ export const BlockSuiteEditorHeader = forwardRef<
|
||||
</div>
|
||||
<StyledQuickSearchTipButton
|
||||
data-testid="quick-search-got-it"
|
||||
onClick={() => setOpenTips(false)}
|
||||
onClick={() =>
|
||||
setTipsHidden({ ...isTipsHidden, quickSearchTips: true })
|
||||
}
|
||||
>
|
||||
Got it
|
||||
</StyledQuickSearchTipButton>
|
||||
@@ -130,7 +132,7 @@ export const BlockSuiteEditorHeader = forwardRef<
|
||||
content={TipsContent}
|
||||
placement="bottom"
|
||||
popperRef={popperRef}
|
||||
open={openTips}
|
||||
open={!isTipsHidden.quickSearchTips}
|
||||
offset={[0, -5]}
|
||||
>
|
||||
<StyledSearchArrowWrapper>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { MuiFade, Tooltip } from '@affine/component';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { CloseIcon } from '@blocksuite/icons';
|
||||
import { CloseIcon, DoneIcon } from '@blocksuite/icons';
|
||||
import dynamic from 'next/dynamic';
|
||||
import { useState } from 'react';
|
||||
|
||||
@@ -23,9 +23,9 @@ const ContactModal = dynamic(
|
||||
}
|
||||
);
|
||||
|
||||
export type IslandItemNames = 'contact' | 'shortcuts';
|
||||
export type IslandItemNames = 'whatNew' | 'contact' | 'shortcuts';
|
||||
export const HelpIsland = ({
|
||||
showList = ['contact', 'shortcuts'],
|
||||
showList = ['whatNew', 'contact', 'shortcuts'],
|
||||
}: {
|
||||
showList?: IslandItemNames[];
|
||||
}) => {
|
||||
@@ -62,6 +62,18 @@ export const HelpIsland = ({
|
||||
<StyledAnimateWrapper
|
||||
style={{ height: spread ? `${showList.length * 44}px` : 0 }}
|
||||
>
|
||||
{showList.includes('whatNew') && (
|
||||
<Tooltip content={t("Discover what's new")} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
data-testid="right-bottom-change-log-icon"
|
||||
onClick={() => {
|
||||
window.open('https://affine.pro', '_blank');
|
||||
}}
|
||||
>
|
||||
<DoneIcon />
|
||||
</StyledIconWrapper>
|
||||
</Tooltip>
|
||||
)}
|
||||
{showList.includes('contact') && (
|
||||
<Tooltip content={t('Contact Us')} placement="left-end">
|
||||
<StyledIconWrapper
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { IconButton } from '@affine/component';
|
||||
import { useTranslation } from '@affine/i18n';
|
||||
import { CloseIcon, DoneIcon } from '@blocksuite/icons';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import {
|
||||
useGuideHidden,
|
||||
useGuideHiddenUntilNextUpdate,
|
||||
} from '../../../../hooks/affine/use-is-first-load';
|
||||
import { StyledListItem } from '../shared-styles';
|
||||
import { StyledLink } from '../style';
|
||||
export const ChangeLog = () => {
|
||||
const [guideHidden, setGuideHidden] = useGuideHidden();
|
||||
const [guideHiddenUntilNextUpdate, setGuideHiddenUntilNextUpdate] =
|
||||
useGuideHiddenUntilNextUpdate();
|
||||
const { t } = useTranslation();
|
||||
const onCloseWhatsNew = useCallback(() => {
|
||||
setGuideHiddenUntilNextUpdate({
|
||||
...guideHiddenUntilNextUpdate,
|
||||
changeLog: true,
|
||||
});
|
||||
setGuideHidden({ ...guideHidden, changeLog: true });
|
||||
}, [
|
||||
guideHidden,
|
||||
guideHiddenUntilNextUpdate,
|
||||
setGuideHidden,
|
||||
setGuideHiddenUntilNextUpdate,
|
||||
]);
|
||||
if (guideHiddenUntilNextUpdate.changeLog) {
|
||||
return <></>;
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<StyledListItem data-testid="change-log">
|
||||
<StyledLink href={'https://affine.pro'} target="_blank">
|
||||
<DoneIcon />
|
||||
{t("Discover what's new!")}
|
||||
</StyledLink>
|
||||
<IconButton
|
||||
onClick={() => {
|
||||
onCloseWhatsNew();
|
||||
}}
|
||||
data-testid="change-log-close-button"
|
||||
>
|
||||
<CloseIcon />
|
||||
</IconButton>
|
||||
</StyledListItem>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChangeLog;
|
||||
@@ -15,6 +15,7 @@ import { useSidebarStatus } from '../../../hooks/affine/use-sidebar-status';
|
||||
import { usePageMeta } from '../../../hooks/use-page-meta';
|
||||
import type { RemWorkspace } from '../../../shared';
|
||||
import { SidebarSwitch } from '../../affine/sidebar-switch';
|
||||
import { ChangeLog } from './changeLog';
|
||||
import Favorite from './favorite';
|
||||
import { Pivots } from './Pivots';
|
||||
import { StyledListItem } from './shared-styles';
|
||||
@@ -86,7 +87,7 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
||||
currentWorkspace={currentWorkspace}
|
||||
onClick={onOpenWorkspaceListModal}
|
||||
/>
|
||||
|
||||
<ChangeLog />
|
||||
<StyledListItem
|
||||
data-testid="slider-bar-quick-search-button"
|
||||
onClick={useCallback(() => {
|
||||
@@ -117,7 +118,6 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
||||
{t('Workspace Settings')}
|
||||
</StyledLink>
|
||||
</StyledListItem>
|
||||
|
||||
<StyledListItem
|
||||
active={
|
||||
currentPath ===
|
||||
@@ -133,7 +133,6 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
||||
<span data-testid="all-pages">{t('All pages')}</span>
|
||||
</StyledLink>
|
||||
</StyledListItem>
|
||||
|
||||
<Favorite
|
||||
currentPath={currentPath}
|
||||
paths={paths}
|
||||
@@ -148,7 +147,6 @@ export const WorkSpaceSliderBar: React.FC<WorkSpaceSliderBarProps> = ({
|
||||
allMetas={pageMeta}
|
||||
/>
|
||||
)}
|
||||
|
||||
<StyledListItem
|
||||
active={
|
||||
currentPath ===
|
||||
|
||||
@@ -25,7 +25,12 @@ import {
|
||||
import { LocalPlugin } from '../../plugins/local';
|
||||
import type { LocalWorkspace } from '../../shared';
|
||||
import { BlockSuiteWorkspace, WorkspaceSubPath } from '../../shared';
|
||||
import { useIsFirstLoad, useOpenTips } from '../affine/use-is-first-load';
|
||||
import {
|
||||
useGuideHidden,
|
||||
useGuideHiddenUntilNextUpdate,
|
||||
useLastVersion,
|
||||
useTipsDisplayStatus,
|
||||
} from '../affine/use-is-first-load';
|
||||
import {
|
||||
useRecentlyViewed,
|
||||
useSyncRecentViewsWithRouter,
|
||||
@@ -348,20 +353,47 @@ describe('useRecentlyViewed', () => {
|
||||
});
|
||||
});
|
||||
describe('useIsFirstLoad', () => {
|
||||
test('basic', async () => {
|
||||
const firstLoad = renderHook(() => useIsFirstLoad());
|
||||
const setFirstLoad = firstLoad.result.current[1];
|
||||
expect(firstLoad.result.current[0]).toEqual(true);
|
||||
setFirstLoad(false);
|
||||
firstLoad.rerender();
|
||||
expect(firstLoad.result.current[0]).toEqual(false);
|
||||
test('useLastVersion', async () => {
|
||||
const lastVersion = renderHook(() => useLastVersion());
|
||||
const setLastVersion = lastVersion.result.current[1];
|
||||
expect(lastVersion.result.current[0]).toEqual('0.0.0');
|
||||
setLastVersion('testVersion');
|
||||
lastVersion.rerender();
|
||||
expect(lastVersion.result.current[0]).toEqual('testVersion');
|
||||
});
|
||||
test('useOpenTips', async () => {
|
||||
const openTips = renderHook(() => useOpenTips());
|
||||
const setOpenTips = openTips.result.current[1];
|
||||
expect(openTips.result.current[0]).toEqual(false);
|
||||
setOpenTips(true);
|
||||
openTips.rerender();
|
||||
expect(openTips.result.current[0]).toEqual(true);
|
||||
test('useGuideHidden', async () => {
|
||||
const guideHidden = renderHook(() => useGuideHidden());
|
||||
const setGuideHidden = guideHidden.result.current[1];
|
||||
expect(guideHidden.result.current[0]).toEqual({});
|
||||
setGuideHidden({ test: true });
|
||||
guideHidden.rerender();
|
||||
expect(guideHidden.result.current[0]).toEqual({ test: true });
|
||||
});
|
||||
test('useGuideHiddenUntilNextUpdate', async () => {
|
||||
const guideHiddenUntilNextUpdate = renderHook(() =>
|
||||
useGuideHiddenUntilNextUpdate()
|
||||
);
|
||||
const setGuideHiddenUntilNextUpdate =
|
||||
guideHiddenUntilNextUpdate.result.current[1];
|
||||
expect(guideHiddenUntilNextUpdate.result.current[0]).toEqual({});
|
||||
setGuideHiddenUntilNextUpdate({ test: true });
|
||||
guideHiddenUntilNextUpdate.rerender();
|
||||
expect(guideHiddenUntilNextUpdate.result.current[0]).toEqual({
|
||||
test: true,
|
||||
});
|
||||
});
|
||||
test('useTipsDisplayStatus', async () => {
|
||||
const tipsDisplayStatus = renderHook(() => useTipsDisplayStatus());
|
||||
const setTipsDisplayStatus = tipsDisplayStatus.result.current;
|
||||
expect(tipsDisplayStatus.result.current).toEqual({
|
||||
quickSearchTips: {
|
||||
permanentlyHidden: true,
|
||||
hiddenUntilNextUpdate: true,
|
||||
},
|
||||
changeLog: {
|
||||
permanentlyHidden: true,
|
||||
hiddenUntilNextUpdate: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,12 +1,74 @@
|
||||
import { useAtom } from 'jotai';
|
||||
import { config } from '@affine/env';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { isFirstLoadAtom, openTipsAtom } from '../../atoms/first-load';
|
||||
import {
|
||||
guideHiddenAtom,
|
||||
guideHiddenUntilNextUpdateAtom,
|
||||
lastVersionAtom,
|
||||
} from '../../atoms/first-load';
|
||||
|
||||
export function useIsFirstLoad() {
|
||||
const [isFirstLoad, setIsFirstLoad] = useAtom(isFirstLoadAtom);
|
||||
return [isFirstLoad, setIsFirstLoad] as const;
|
||||
export function useLastVersion() {
|
||||
return useAtom(lastVersionAtom);
|
||||
}
|
||||
export function useOpenTips() {
|
||||
const [openTips, setOpenTips] = useAtom(openTipsAtom);
|
||||
return [openTips, setOpenTips] as const;
|
||||
|
||||
export function useGuideHidden() {
|
||||
return useAtom(guideHiddenAtom);
|
||||
}
|
||||
|
||||
export function useGuideHiddenUntilNextUpdate() {
|
||||
return useAtom(guideHiddenUntilNextUpdateAtom);
|
||||
}
|
||||
|
||||
const TIPS = {
|
||||
quickSearchTips: true,
|
||||
changeLog: true,
|
||||
};
|
||||
|
||||
export function useTipsDisplayStatus() {
|
||||
const permanentlyHiddenTips = useAtomValue(guideHiddenAtom);
|
||||
const hiddenUntilNextUpdateTips = useAtomValue(
|
||||
guideHiddenUntilNextUpdateAtom
|
||||
);
|
||||
|
||||
return {
|
||||
quickSearchTips: {
|
||||
permanentlyHidden: permanentlyHiddenTips.quickSearchTips || true,
|
||||
hiddenUntilNextUpdate: hiddenUntilNextUpdateTips.quickSearchTips || true,
|
||||
},
|
||||
changeLog: {
|
||||
permanentlyHidden: permanentlyHiddenTips.changeLog || true,
|
||||
hiddenUntilNextUpdate: hiddenUntilNextUpdateTips.changeLog || true,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export function useUpdateTipsOnVersionChange() {
|
||||
const [lastVersion, setLastVersion] = useLastVersion();
|
||||
const currentVersion = config.gitVersion;
|
||||
const tipsDisplayStatus = useTipsDisplayStatus();
|
||||
const setPermanentlyHiddenTips = useSetAtom(guideHiddenAtom);
|
||||
const setHiddenUntilNextUpdateTips = useSetAtom(
|
||||
guideHiddenUntilNextUpdateAtom
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (lastVersion !== currentVersion) {
|
||||
setLastVersion(currentVersion);
|
||||
const newHiddenUntilNextUpdateTips = { ...TIPS };
|
||||
const newPermanentlyHiddenTips = { ...TIPS, changeLog: false };
|
||||
Object.keys(tipsDisplayStatus).forEach(tipKey => {
|
||||
newHiddenUntilNextUpdateTips[tipKey as keyof typeof TIPS] = false;
|
||||
});
|
||||
setHiddenUntilNextUpdateTips(newHiddenUntilNextUpdateTips);
|
||||
setPermanentlyHiddenTips(newPermanentlyHiddenTips);
|
||||
}
|
||||
}, [
|
||||
currentVersion,
|
||||
lastVersion,
|
||||
setLastVersion,
|
||||
setPermanentlyHiddenTips,
|
||||
setHiddenUntilNextUpdateTips,
|
||||
tipsDisplayStatus,
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -257,7 +257,9 @@ export const WorkspaceLayoutInner: React.FC<React.PropsWithChildren> = ({
|
||||
</div>
|
||||
{!isPublicWorkspace && (
|
||||
<HelpIsland
|
||||
showList={router.query.pageId ? undefined : ['contact']}
|
||||
showList={
|
||||
router.query.pageId ? undefined : ['whatNew', 'contact']
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</StyledToolWrapper>
|
||||
|
||||
Reference in New Issue
Block a user