feat(mobile): new journal tab, show App tab for journal page (#8738)
close AF-1648
This commit is contained in:
@@ -5,23 +5,36 @@ import {
|
|||||||
} from '@affine/core/modules/workbench';
|
} from '@affine/core/modules/workbench';
|
||||||
import { AllDocsIcon, MobileHomeIcon, SearchIcon } from '@blocksuite/icons/rc';
|
import { AllDocsIcon, MobileHomeIcon, SearchIcon } from '@blocksuite/icons/rc';
|
||||||
import { useLiveData, useService } from '@toeverything/infra';
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||||
|
import React from 'react';
|
||||||
import type { Location } from 'react-router-dom';
|
import type { Location } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { AppTabJournal } from './journal';
|
||||||
import * as styles from './styles.css';
|
import * as styles from './styles.css';
|
||||||
|
|
||||||
interface Route {
|
interface AppTabBaseProps {
|
||||||
to: string;
|
key: string;
|
||||||
|
}
|
||||||
|
interface AppTabLinkProps extends AppTabBaseProps {
|
||||||
Icon: React.FC;
|
Icon: React.FC;
|
||||||
|
to: string;
|
||||||
LinkComponent?: React.FC;
|
LinkComponent?: React.FC;
|
||||||
isActive?: (location: Location) => boolean;
|
isActive?: (location: Location) => boolean;
|
||||||
}
|
}
|
||||||
|
interface AppTabCustomProps extends AppTabBaseProps {
|
||||||
|
node: React.ReactNode;
|
||||||
|
}
|
||||||
|
|
||||||
|
type Route = AppTabLinkProps | AppTabCustomProps;
|
||||||
|
|
||||||
const routes: Route[] = [
|
const routes: Route[] = [
|
||||||
{
|
{
|
||||||
|
key: 'home',
|
||||||
to: '/home',
|
to: '/home',
|
||||||
Icon: MobileHomeIcon,
|
Icon: MobileHomeIcon,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'all',
|
||||||
to: '/all',
|
to: '/all',
|
||||||
Icon: AllDocsIcon,
|
Icon: AllDocsIcon,
|
||||||
isActive: location =>
|
isActive: location =>
|
||||||
@@ -30,41 +43,62 @@ const routes: Route[] = [
|
|||||||
location.pathname.startsWith('/tag'),
|
location.pathname.startsWith('/tag'),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
key: 'journal',
|
||||||
|
node: <AppTabJournal />,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'search',
|
||||||
to: '/search',
|
to: '/search',
|
||||||
Icon: SearchIcon,
|
Icon: SearchIcon,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export const AppTabs = () => {
|
export const AppTabs = ({ background }: { background?: string }) => {
|
||||||
const workbench = useService(WorkbenchService).workbench;
|
|
||||||
const location = useLiveData(workbench.location$);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SafeArea bottom className={styles.appTabs} bottomOffset={2}>
|
<SafeArea
|
||||||
|
bottom
|
||||||
|
className={styles.appTabs}
|
||||||
|
bottomOffset={2}
|
||||||
|
style={assignInlineVars({
|
||||||
|
[styles.appTabsBackground]: background,
|
||||||
|
})}
|
||||||
|
>
|
||||||
<ul className={styles.appTabsInner} id="app-tabs" role="tablist">
|
<ul className={styles.appTabsInner} id="app-tabs" role="tablist">
|
||||||
{routes.map(route => {
|
{routes.map(route => {
|
||||||
const Link = route.LinkComponent || WorkbenchLink;
|
if ('to' in route) {
|
||||||
|
return <AppTabLink route={route} key={route.key} />;
|
||||||
const isActive = route.isActive
|
} else {
|
||||||
? route.isActive(location)
|
return (
|
||||||
: location.pathname === route.to;
|
<React.Fragment key={route.key}>{route.node}</React.Fragment>
|
||||||
return (
|
);
|
||||||
<Link
|
}
|
||||||
data-active={isActive}
|
|
||||||
to={route.to}
|
|
||||||
key={route.to}
|
|
||||||
className={styles.tabItem}
|
|
||||||
role="tab"
|
|
||||||
aria-label={route.to.slice(1)}
|
|
||||||
replaceHistory
|
|
||||||
>
|
|
||||||
<li style={{ lineHeight: 0 }}>
|
|
||||||
<route.Icon />
|
|
||||||
</li>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
</SafeArea>
|
</SafeArea>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AppTabLink = ({ route }: { route: AppTabLinkProps }) => {
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
const location = useLiveData(workbench.location$);
|
||||||
|
const Link = route.LinkComponent || WorkbenchLink;
|
||||||
|
|
||||||
|
const isActive = route.isActive
|
||||||
|
? route.isActive(location)
|
||||||
|
: location.pathname === route.to;
|
||||||
|
return (
|
||||||
|
<Link
|
||||||
|
data-active={isActive}
|
||||||
|
to={route.to}
|
||||||
|
key={route.to}
|
||||||
|
className={styles.tabItem}
|
||||||
|
role="tab"
|
||||||
|
aria-label={route.to.slice(1)}
|
||||||
|
replaceHistory
|
||||||
|
>
|
||||||
|
<li style={{ lineHeight: 0 }}>
|
||||||
|
<route.Icon />
|
||||||
|
</li>
|
||||||
|
</Link>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { useJournalRouteHelper } from '@affine/core/components/hooks/use-journal';
|
||||||
|
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||||
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
|
import { TodayIcon } from '@blocksuite/icons/rc';
|
||||||
|
import { useLiveData, useService } from '@toeverything/infra';
|
||||||
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
import { tabItem } from './styles.css';
|
||||||
|
|
||||||
|
export const AppTabJournal = () => {
|
||||||
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
|
const location = useLiveData(workbench.location$);
|
||||||
|
const journalService = useService(JournalService);
|
||||||
|
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||||
|
|
||||||
|
const maybeDocId = location.pathname.split('/')[1];
|
||||||
|
const journalDate = useLiveData(journalService.journalDate$(maybeDocId));
|
||||||
|
const JournalIcon = useLiveData(
|
||||||
|
docDisplayMetaService.icon$(maybeDocId, { compareDate: new Date() })
|
||||||
|
);
|
||||||
|
|
||||||
|
const { openToday } = useJournalRouteHelper();
|
||||||
|
const handleOpenToday = useCallback(() => openToday(false), [openToday]);
|
||||||
|
|
||||||
|
const Icon = journalDate ? JournalIcon : TodayIcon;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={tabItem}
|
||||||
|
onClick={handleOpenToday}
|
||||||
|
data-active={!!journalDate}
|
||||||
|
role="tab"
|
||||||
|
aria-label="Journal"
|
||||||
|
>
|
||||||
|
<Icon />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { style } from '@vanilla-extract/css';
|
import { createVar, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
import { globalVars } from '../../styles/mobile.css';
|
import { globalVars } from '../../styles/mobile.css';
|
||||||
|
|
||||||
|
export const appTabsBackground = createVar('appTabsBackground');
|
||||||
|
|
||||||
export const appTabs = style({
|
export const appTabs = style({
|
||||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
vars: {
|
||||||
|
[appTabsBackground]: cssVarV2('layer/background/secondary'),
|
||||||
|
},
|
||||||
|
backgroundColor: appTabsBackground,
|
||||||
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||||
|
|
||||||
width: '100dvw',
|
width: '100dvw',
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ import { useNavigateHelper } from '@affine/core/components/hooks/use-navigate-he
|
|||||||
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
import { PageDetailEditor } from '@affine/core/components/page-detail-editor';
|
||||||
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
|
import { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
|
||||||
import { EditorService } from '@affine/core/modules/editor';
|
import { EditorService } from '@affine/core/modules/editor';
|
||||||
|
import { JournalService } from '@affine/core/modules/journal';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||||
|
import { i18nTime } from '@affine/i18n';
|
||||||
import {
|
import {
|
||||||
BookmarkBlockService,
|
BookmarkBlockService,
|
||||||
customImageProxyMiddleware,
|
customImageProxyMiddleware,
|
||||||
@@ -28,14 +30,18 @@ import {
|
|||||||
FrameworkScope,
|
FrameworkScope,
|
||||||
GlobalContextService,
|
GlobalContextService,
|
||||||
useLiveData,
|
useLiveData,
|
||||||
|
useService,
|
||||||
useServices,
|
useServices,
|
||||||
WorkspaceService,
|
WorkspaceService,
|
||||||
} from '@toeverything/infra';
|
} from '@toeverything/infra';
|
||||||
|
import { bodyEmphasized } from '@toeverything/theme/typography';
|
||||||
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
|
import dayjs from 'dayjs';
|
||||||
import { useCallback, useEffect, useRef } from 'react';
|
import { useCallback, useEffect, useRef } from 'react';
|
||||||
import { useParams } from 'react-router-dom';
|
import { useParams } from 'react-router-dom';
|
||||||
|
|
||||||
import { PageHeader } from '../../../components';
|
import { AppTabs, PageHeader } from '../../../components';
|
||||||
import { JournalIconButton } from './journal-icon-button';
|
import { JournalIconButton } from './journal-icon-button';
|
||||||
import * as styles from './mobile-detail-page.css';
|
import * as styles from './mobile-detail-page.css';
|
||||||
import { PageHeaderMenuButton } from './page-header-more-button';
|
import { PageHeaderMenuButton } from './page-header-more-button';
|
||||||
@@ -214,15 +220,42 @@ const notFound = (
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
export const Component = () => {
|
const JournalDetailPage = ({
|
||||||
useThemeColorV2('layer/background/primary');
|
pageId,
|
||||||
const params = useParams();
|
date,
|
||||||
const pageId = params.pageId;
|
}: {
|
||||||
|
pageId: string;
|
||||||
if (!pageId) {
|
date: string;
|
||||||
return null;
|
}) => {
|
||||||
}
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
<DetailPageWrapper
|
||||||
|
skeleton={skeleton}
|
||||||
|
notFound={notFound}
|
||||||
|
pageId={pageId}
|
||||||
|
>
|
||||||
|
<PageHeader
|
||||||
|
back
|
||||||
|
className={styles.header}
|
||||||
|
suffix={
|
||||||
|
<>
|
||||||
|
<PageHeaderShareButton />
|
||||||
|
<PageHeaderMenuButton />
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className={bodyEmphasized}>
|
||||||
|
{i18nTime(dayjs(date), { absolute: { accuracy: 'month' } })}
|
||||||
|
</span>
|
||||||
|
</PageHeader>
|
||||||
|
{/* TODO(@CatsJuice): <JournalDatePicker /> */}
|
||||||
|
<DetailPageImpl />
|
||||||
|
<AppTabs background={cssVarV2('layer/background/primary')} />
|
||||||
|
</DetailPageWrapper>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
const NormalDetailPage = ({ pageId }: { pageId: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className={styles.root}>
|
<div className={styles.root}>
|
||||||
<DetailPageWrapper
|
<DetailPageWrapper
|
||||||
@@ -245,3 +278,21 @@ export const Component = () => {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const Component = () => {
|
||||||
|
useThemeColorV2('layer/background/primary');
|
||||||
|
const journalService = useService(JournalService);
|
||||||
|
const params = useParams();
|
||||||
|
const pageId = params.pageId;
|
||||||
|
const journalDate = useLiveData(journalService.journalDate$(pageId ?? ''));
|
||||||
|
|
||||||
|
if (!pageId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return journalDate ? (
|
||||||
|
<JournalDetailPage pageId={pageId} date={journalDate} />
|
||||||
|
) : (
|
||||||
|
<NormalDetailPage pageId={pageId} />
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user