mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-02-22 00:37:05 +08:00
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';
|
||||
import { AllDocsIcon, MobileHomeIcon, SearchIcon } from '@blocksuite/icons/rc';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { assignInlineVars } from '@vanilla-extract/dynamic';
|
||||
import React from 'react';
|
||||
import type { Location } from 'react-router-dom';
|
||||
|
||||
import { AppTabJournal } from './journal';
|
||||
import * as styles from './styles.css';
|
||||
|
||||
interface Route {
|
||||
to: string;
|
||||
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 =>
|
||||
@@ -30,41 +43,62 @@ const routes: Route[] = [
|
||||
location.pathname.startsWith('/tag'),
|
||||
},
|
||||
{
|
||||
key: 'journal',
|
||||
node: <AppTabJournal />,
|
||||
},
|
||||
{
|
||||
key: 'search',
|
||||
to: '/search',
|
||||
Icon: SearchIcon,
|
||||
},
|
||||
];
|
||||
|
||||
export const AppTabs = () => {
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const location = useLiveData(workbench.location$);
|
||||
|
||||
export const AppTabs = ({ background }: { background?: string }) => {
|
||||
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">
|
||||
{routes.map(route => {
|
||||
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>
|
||||
);
|
||||
if ('to' in route) {
|
||||
return <AppTabLink route={route} key={route.key} />;
|
||||
} else {
|
||||
return (
|
||||
<React.Fragment key={route.key}>{route.node}</React.Fragment>
|
||||
);
|
||||
}
|
||||
})}
|
||||
</ul>
|
||||
</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 { style } from '@vanilla-extract/css';
|
||||
import { createVar, style } from '@vanilla-extract/css';
|
||||
|
||||
import { globalVars } from '../../styles/mobile.css';
|
||||
|
||||
export const appTabsBackground = createVar('appTabsBackground');
|
||||
|
||||
export const appTabs = style({
|
||||
backgroundColor: cssVarV2('layer/background/secondary'),
|
||||
vars: {
|
||||
[appTabsBackground]: cssVarV2('layer/background/secondary'),
|
||||
},
|
||||
backgroundColor: appTabsBackground,
|
||||
borderTop: `1px solid ${cssVarV2('layer/insideBorder/border')}`,
|
||||
|
||||
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 { DetailPageWrapper } from '@affine/core/desktop/pages/workspace/detail-page/detail-page-wrapper';
|
||||
import { EditorService } from '@affine/core/modules/editor';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { ViewService } from '@affine/core/modules/workbench/services/view';
|
||||
import { i18nTime } from '@affine/i18n';
|
||||
import {
|
||||
BookmarkBlockService,
|
||||
customImageProxyMiddleware,
|
||||
@@ -28,14 +30,18 @@ import {
|
||||
FrameworkScope,
|
||||
GlobalContextService,
|
||||
useLiveData,
|
||||
useService,
|
||||
useServices,
|
||||
WorkspaceService,
|
||||
} from '@toeverything/infra';
|
||||
import { bodyEmphasized } from '@toeverything/theme/typography';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import clsx from 'clsx';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
import { useParams } from 'react-router-dom';
|
||||
|
||||
import { PageHeader } from '../../../components';
|
||||
import { AppTabs, PageHeader } from '../../../components';
|
||||
import { JournalIconButton } from './journal-icon-button';
|
||||
import * as styles from './mobile-detail-page.css';
|
||||
import { PageHeaderMenuButton } from './page-header-more-button';
|
||||
@@ -214,15 +220,42 @@ const notFound = (
|
||||
</>
|
||||
);
|
||||
|
||||
export const Component = () => {
|
||||
useThemeColorV2('layer/background/primary');
|
||||
const params = useParams();
|
||||
const pageId = params.pageId;
|
||||
|
||||
if (!pageId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const JournalDetailPage = ({
|
||||
pageId,
|
||||
date,
|
||||
}: {
|
||||
pageId: string;
|
||||
date: string;
|
||||
}) => {
|
||||
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 (
|
||||
<div className={styles.root}>
|
||||
<DetailPageWrapper
|
||||
@@ -245,3 +278,21 @@ export const Component = () => {
|
||||
</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