diff --git a/apps/web/src/atoms/first-load.ts b/apps/web/src/atoms/first-load.ts index 67282bd83c..e94138af5f 100644 --- a/apps/web/src/atoms/first-load.ts +++ b/apps/web/src/atoms/first-load.ts @@ -1,5 +1,28 @@ -import { atom } from 'jotai'; import { atomWithStorage } from 'jotai/utils'; -export const isFirstLoadAtom = atomWithStorage('isFirstLoad', true); -export const openTipsAtom = atom(false); +export type Visibility = Record; + +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('guideHidden', {}); + +export const guideHiddenUntilNextUpdateAtom = atomWithStorage( + 'guideHiddenUntilNextUpdate', + {} +); diff --git a/apps/web/src/components/affine/sidebar-switch/index.tsx b/apps/web/src/components/affine/sidebar-switch/index.tsx index 2f1ba99299..1073747fb8 100644 --- a/apps/web/src/components/affine/sidebar-switch/index.tsx +++ b/apps/web/src/components/affine/sidebar-switch/index.tsx @@ -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); }, [])} diff --git a/apps/web/src/components/blocksuite/header/index.tsx b/apps/web/src/components/blocksuite/header/index.tsx index e948223729..b6ef75c72e 100644 --- a/apps/web/src/components/blocksuite/header/index.tsx +++ b/apps/web/src/components/blocksuite/header/index.tsx @@ -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< setOpenTips(false)} + onClick={() => + setTipsHidden({ ...isTipsHidden, quickSearchTips: true }) + } > Got it @@ -130,7 +132,7 @@ export const BlockSuiteEditorHeader = forwardRef< content={TipsContent} placement="bottom" popperRef={popperRef} - open={openTips} + open={!isTipsHidden.quickSearchTips} offset={[0, -5]} > diff --git a/apps/web/src/components/pure/help-island/index.tsx b/apps/web/src/components/pure/help-island/index.tsx index 9d0138e893..f853a71842 100644 --- a/apps/web/src/components/pure/help-island/index.tsx +++ b/apps/web/src/components/pure/help-island/index.tsx @@ -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 = ({ + {showList.includes('whatNew') && ( + + { + window.open('https://affine.pro', '_blank'); + }} + > + + + + )} {showList.includes('contact') && ( { + 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 ( + <> + + + + {t("Discover what's new!")} + + { + onCloseWhatsNew(); + }} + data-testid="change-log-close-button" + > + + + + + ); +}; + +export default ChangeLog; diff --git a/apps/web/src/components/pure/workspace-slider-bar/index.tsx b/apps/web/src/components/pure/workspace-slider-bar/index.tsx index c842d5f45f..1921c0273c 100644 --- a/apps/web/src/components/pure/workspace-slider-bar/index.tsx +++ b/apps/web/src/components/pure/workspace-slider-bar/index.tsx @@ -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 = ({ currentWorkspace={currentWorkspace} onClick={onOpenWorkspaceListModal} /> - + { @@ -117,7 +118,6 @@ export const WorkSpaceSliderBar: React.FC = ({ {t('Workspace Settings')} - = ({ {t('All pages')} - = ({ allMetas={pageMeta} /> )} - { }); }); 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, + }, + }); }); }); diff --git a/apps/web/src/hooks/affine/use-is-first-load.ts b/apps/web/src/hooks/affine/use-is-first-load.ts index 3af0a9c198..dec11ff01e 100644 --- a/apps/web/src/hooks/affine/use-is-first-load.ts +++ b/apps/web/src/hooks/affine/use-is-first-load.ts @@ -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, + ]); } diff --git a/apps/web/src/layouts/index.tsx b/apps/web/src/layouts/index.tsx index 922871a0a4..80c90198fb 100644 --- a/apps/web/src/layouts/index.tsx +++ b/apps/web/src/layouts/index.tsx @@ -257,7 +257,9 @@ export const WorkspaceLayoutInner: React.FC = ({ {!isPublicWorkspace && ( )} diff --git a/packages/i18n/src/resources/en.json b/packages/i18n/src/resources/en.json index 534e33d59a..7fd6fc6dcb 100644 --- a/packages/i18n/src/resources/en.json +++ b/packages/i18n/src/resources/en.json @@ -200,5 +200,6 @@ "Move to": "Move to", "Move page to...": "Move page to...", "Remove from Pivots": "Remove from Pivots", - "RFP": "Pages can be freely added/removed from pivots, remaining accessible from \"All Pages\"." + "RFP": "Pages can be freely added/removed from pivots, remaining accessible from \"All Pages\".", + "Discover what's new!": "Discover what's new!" } diff --git a/tests/parallels/open-affine.spec.ts b/tests/parallels/open-affine.spec.ts index 0707c3ba88..99b4d76f59 100644 --- a/tests/parallels/open-affine.spec.ts +++ b/tests/parallels/open-affine.spec.ts @@ -29,3 +29,31 @@ test.describe('Open AFFiNE', () => { expect(currentWorkspaceName).toEqual('New Workspace 2'); }); }); + +test.describe('AFFiNE change log', () => { + test('Open affine in first time after updated', async ({ page }) => { + await openHomePage(page); + const changeLogItem = page.locator('[data-testid=change-log]'); + await expect(changeLogItem).toBeVisible(); + const closeButton = page.locator('[data-testid=change-log-close-button]'); + await closeButton.click(); + await expect(changeLogItem).not.toBeVisible(); + await page.goto('http://localhost:8080'); + const currentChangeLogItem = page.locator('[data-testid=change-log]'); + await expect(currentChangeLogItem).not.toBeVisible(); + }); + test('Click right-bottom corner change log icon', async ({ page }) => { + await openHomePage(page); + await waitMarkdownImported(page); + await page.locator('[data-testid=help-island]').click(); + const editorRightBottomChangeLog = page.locator( + '[data-testid=right-bottom-change-log-icon]' + ); + expect(await editorRightBottomChangeLog.isVisible()).toEqual(true); + await page.getByRole('link', { name: 'All pages' }).click(); + const normalRightBottomChangeLog = page.locator( + '[data-testid=right-bottom-change-log-icon]' + ); + expect(await normalRightBottomChangeLog.isVisible()).toEqual(true); + }); +}); diff --git a/tests/parallels/quick-search.spec.ts b/tests/parallels/quick-search.spec.ts index 91a7bc2cbe..1f639adbb5 100644 --- a/tests/parallels/quick-search.spec.ts +++ b/tests/parallels/quick-search.spec.ts @@ -186,4 +186,22 @@ test.describe('Novice guidance for quick search', () => { await page.locator('[data-testid=quick-search-got-it]').click(); await expect(quickSearchTips).not.toBeVisible(); }); + test('After appearing once, it will not appear a second time', async ({ + page, + }) => { + await openHomePage(page); + await waitMarkdownImported(page); + const quickSearchTips = page.locator('[data-testid=quick-search-tips]'); + await expect(quickSearchTips).not.toBeVisible(); + await page.getByTestId('sliderBar-arrowButton-collapse').click(); + const sliderBarArea = page.getByTestId('sliderBar'); + await expect(sliderBarArea).not.toBeVisible(); + await expect(quickSearchTips).toBeVisible(); + await page.locator('[data-testid=quick-search-got-it]').click(); + await expect(quickSearchTips).not.toBeVisible(); + await page.reload(); + await page.locator('[data-testid=sliderBar-arrowButton-expand]').click(); + await page.getByTestId('sliderBar-arrowButton-collapse').click(); + await expect(quickSearchTips).not.toBeVisible(); + }); });