refactor(mobile): determine the currently active tab through a persistent state (#9018)

close AF-1868

Only tap on specific tab can change active tab
This commit is contained in:
CatsJuice 2024-12-09 04:07:05 +00:00
parent f073df3ee5
commit 8fe188e773
No known key found for this signature in database
GPG Key ID: 1C1E76924FAFDDE4
5 changed files with 123 additions and 111 deletions

View File

@ -1,51 +1,29 @@
import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils'; import { usePageHelper } from '@affine/core/components/blocksuite/block-suite-page-list/utils';
import { JournalService } from '@affine/core/modules/journal';
import { WorkbenchService } from '@affine/core/modules/workbench';
import track from '@affine/track'; import track from '@affine/track';
import { EditIcon } from '@blocksuite/icons/rc'; import { EditIcon } from '@blocksuite/icons/rc';
import { import { useService, WorkspaceService } from '@toeverything/infra';
DocsService,
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { tabItem } from './styles.css'; import type { AppTabCustomFCProps } from './data';
import { TabItem } from './tab-item';
export const AppTabCreate = () => { export const AppTabCreate = ({ tab }: AppTabCustomFCProps) => {
const { docsService, workbenchService, workspaceService, journalService } = const workspaceService = useService(WorkspaceService);
useServices({
DocsService,
WorkbenchService,
WorkspaceService,
JournalService,
});
const workbench = workbenchService.workbench;
const currentWorkspace = workspaceService.workspace; const currentWorkspace = workspaceService.workspace;
const location = useLiveData(workbench.location$);
const pageHelper = usePageHelper(currentWorkspace.docCollection); const pageHelper = usePageHelper(currentWorkspace.docCollection);
const maybeDocId = location.pathname.split('/')[1].split('?')[0]; const createPage = useCallback(
const doc = useLiveData(docsService.list.doc$(maybeDocId)); (isActive: boolean) => {
const journalDate = useLiveData(journalService.journalDate$(maybeDocId)); if (isActive) return;
const isActive = !!doc && !journalDate; pageHelper.createPage(undefined, true);
track.$.navigationPanel.$.createDoc();
const createPage = useCallback(() => { },
if (isActive) return; [pageHelper]
pageHelper.createPage(undefined, true); );
track.$.navigationPanel.$.createDoc();
}, [isActive, pageHelper]);
return ( return (
<div <TabItem id={tab.key} onClick={createPage} label="New Page">
className={tabItem}
data-active={isActive}
role="tab"
aria-label="New Page"
onClick={createPage}
>
<EditIcon /> <EditIcon />
</div> </TabItem>
); );
}; };

View File

@ -0,0 +1,46 @@
import { AllDocsIcon, MobileHomeIcon } from '@blocksuite/icons/rc';
import type { Framework } from '@toeverything/infra';
import { AppTabCreate } from './create';
import { AppTabJournal } from './journal';
interface AppTabBase {
key: string;
onClick?: (framework: Framework, isActive: boolean) => void;
}
export interface AppTabLink extends AppTabBase {
Icon: React.FC;
to: string;
LinkComponent?: React.FC;
}
export interface AppTabCustom extends AppTabBase {
custom: (props: AppTabCustomFCProps) => React.ReactNode;
}
export type Tab = AppTabLink | AppTabCustom;
export interface AppTabCustomFCProps {
tab: Tab;
}
export const tabs: Tab[] = [
{
key: 'home',
to: '/home',
Icon: MobileHomeIcon,
},
{
key: 'all',
to: '/all',
Icon: AllDocsIcon,
},
{
key: 'journal',
custom: AppTabJournal,
},
{
key: 'new',
custom: AppTabCreate,
},
];

View File

@ -1,59 +1,14 @@
import { SafeArea } from '@affine/component'; import { SafeArea } from '@affine/component';
import { import { WorkbenchLink } from '@affine/core/modules/workbench';
WorkbenchLink,
WorkbenchService,
} from '@affine/core/modules/workbench';
import { AllDocsIcon, MobileHomeIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { assignInlineVars } from '@vanilla-extract/dynamic'; import { assignInlineVars } from '@vanilla-extract/dynamic';
import React from 'react'; import React from 'react';
import { createPortal } from 'react-dom'; import { createPortal } from 'react-dom';
import type { Location } from 'react-router-dom';
import { VirtualKeyboardService } from '../../modules/virtual-keyboard/services/virtual-keyboard'; import { VirtualKeyboardService } from '../../modules/virtual-keyboard/services/virtual-keyboard';
import { AppTabCreate } from './create'; import { type AppTabLink, tabs } from './data';
import { AppTabJournal } from './journal';
import * as styles from './styles.css'; import * as styles from './styles.css';
import { TabItem } from './tab-item';
interface AppTabBaseProps {
key: string;
}
interface AppTabLinkProps extends AppTabBaseProps {
Icon: React.FC;
to: string;
LinkComponent?: React.FC;
isActive?: (location: Location) => boolean;
}
interface AppTabCustomProps extends AppTabBaseProps {
node: React.ReactNode;
}
type Route = AppTabLinkProps | AppTabCustomProps;
const routes: Route[] = [
{
key: 'home',
to: '/home',
Icon: MobileHomeIcon,
},
{
key: 'all',
to: '/all',
Icon: AllDocsIcon,
isActive: location =>
location.pathname === '/all' ||
location.pathname.startsWith('/collection') ||
location.pathname.startsWith('/tag'),
},
{
key: 'journal',
node: <AppTabJournal />,
},
{
key: 'new',
node: <AppTabCreate />,
},
];
export const AppTabs = ({ background }: { background?: string }) => { export const AppTabs = ({ background }: { background?: string }) => {
const virtualKeyboardService = useService(VirtualKeyboardService); const virtualKeyboardService = useService(VirtualKeyboardService);
@ -72,12 +27,14 @@ export const AppTabs = ({ background }: { background?: string }) => {
}} }}
> >
<ul className={styles.appTabsInner} id="app-tabs" role="tablist"> <ul className={styles.appTabsInner} id="app-tabs" role="tablist">
{routes.map(route => { {tabs.map(tab => {
if ('to' in route) { if ('to' in tab) {
return <AppTabLink route={route} key={route.key} />; return <AppTabLink route={tab} key={tab.key} />;
} else { } else {
return ( return (
<React.Fragment key={route.key}>{route.node}</React.Fragment> <React.Fragment key={tab.key}>
{<tab.custom tab={tab} />}
</React.Fragment>
); );
} }
})} })}
@ -87,27 +44,19 @@ export const AppTabs = ({ background }: { background?: string }) => {
); );
}; };
const AppTabLink = ({ route }: { route: AppTabLinkProps }) => { const AppTabLink = ({ route }: { route: AppTabLink }) => {
const workbench = useService(WorkbenchService).workbench;
const location = useLiveData(workbench.location$);
const Link = route.LinkComponent || WorkbenchLink; const Link = route.LinkComponent || WorkbenchLink;
const isActive = route.isActive
? route.isActive(location)
: location.pathname === route.to;
return ( return (
<Link <Link
data-active={isActive} className={styles.tabItem}
to={route.to} to={route.to}
key={route.to} key={route.to}
className={styles.tabItem}
role="tab"
aria-label={route.to.slice(1)}
replaceHistory replaceHistory
> >
<li style={{ lineHeight: 0 }}> <TabItem id={route.key} label={route.to.slice(1)}>
<route.Icon /> <route.Icon />
</li> </TabItem>
</Link> </Link>
); );
}; };

View File

@ -6,9 +6,10 @@ import { TodayIcon } from '@blocksuite/icons/rc';
import { useLiveData, useService } from '@toeverything/infra'; import { useLiveData, useService } from '@toeverything/infra';
import { useCallback } from 'react'; import { useCallback } from 'react';
import { tabItem } from './styles.css'; import type { AppTabCustomFCProps } from './data';
import { TabItem } from './tab-item';
export const AppTabJournal = () => { export const AppTabJournal = ({ tab }: AppTabCustomFCProps) => {
const workbench = useService(WorkbenchService).workbench; const workbench = useService(WorkbenchService).workbench;
const location = useLiveData(workbench.location$); const location = useLiveData(workbench.location$);
const journalService = useService(JournalService); const journalService = useService(JournalService);
@ -26,14 +27,8 @@ export const AppTabJournal = () => {
const Icon = journalDate ? JournalIcon : TodayIcon; const Icon = journalDate ? JournalIcon : TodayIcon;
return ( return (
<div <TabItem onClick={handleOpenToday} id={tab.key} label="Journal">
className={tabItem}
onClick={handleOpenToday}
data-active={!!journalDate}
role="tab"
aria-label="Journal"
>
<Icon /> <Icon />
</div> </TabItem>
); );
}; };

View File

@ -0,0 +1,44 @@
import {
GlobalCacheService,
LiveData,
useLiveData,
useService,
} from '@toeverything/infra';
import { type PropsWithChildren, useCallback, useMemo } from 'react';
import { tabItem } from './styles.css';
export interface TabItemProps extends PropsWithChildren {
id: string;
label: string;
onClick?: (isActive: boolean) => void;
}
const cacheKey = 'activeAppTabId';
export const TabItem = ({ id, label, children, onClick }: TabItemProps) => {
const globalCache = useService(GlobalCacheService).globalCache;
const activeTabId$ = useMemo(
() => LiveData.from(globalCache.watch(cacheKey), 'home'),
[globalCache]
);
const activeTabId = useLiveData(activeTabId$) ?? 'home';
const isActive = id === activeTabId;
const handleClick = useCallback(() => {
globalCache.set(cacheKey, id);
onClick?.(isActive);
}, [globalCache, id, isActive, onClick]);
return (
<li
className={tabItem}
role="tab"
aria-label={label}
data-active={isActive}
onClick={handleClick}
>
{children}
</li>
);
};