mirror of
https://github.com/toeverything/AFFiNE.git
synced 2026-07-02 02:00:49 +08:00
feat(mobile): add two step confirmation for mobile journal (#13266)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **New Features** * Introduced a new mobile journals page with a two-step confirmation flow, allowing users to select a date and confirm before creating or opening a journal. * Added a dedicated route for journals on mobile devices. * Implemented a placeholder view when no journal exists for a selected date on both desktop and mobile. * **Enhancements** * Improved mobile and desktop styling for journals pages, including responsive adjustments for mobile layouts. * Updated journal navigation behavior based on a feature flag, enabling or disabling the two-step confirmation flow. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -20,12 +20,22 @@ export const body = style({
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
borderTop: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||
selectors: {
|
||||
'&[data-mobile]': {
|
||||
borderTop: 'none',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const content = style({
|
||||
maxWidth: 944,
|
||||
padding: '0px 50px',
|
||||
margin: '0 auto',
|
||||
selectors: {
|
||||
'[data-mobile] &': {
|
||||
padding: '0 24px',
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
export const docTitleContainer = style({
|
||||
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||
import * as styles from './index.css';
|
||||
|
||||
function getDateFromUrl(location: Location) {
|
||||
export function getDateFromUrl(location: Location) {
|
||||
const searchParams = new URLSearchParams(location.search);
|
||||
const date = searchParams.get('date')
|
||||
? dayjs(searchParams.get('date'))
|
||||
@@ -41,6 +41,49 @@ function getDateFromUrl(location: Location) {
|
||||
return date.format(JOURNAL_DATE_FORMAT);
|
||||
}
|
||||
|
||||
export const JournalPlaceholder = ({ dateString }: { dateString: string }) => {
|
||||
const t = useI18n();
|
||||
const [redirecting, setRedirecting] = useState(false);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const journalService = useService(JournalService);
|
||||
|
||||
const createJournal = useCallback(() => {
|
||||
if (redirecting) return;
|
||||
setRedirecting(true);
|
||||
const doc = journalService.ensureJournalByDate(dateString);
|
||||
workbench.openDoc(doc.id, {
|
||||
replaceHistory: true,
|
||||
at: 'active',
|
||||
});
|
||||
}, [dateString, journalService, redirecting, workbench]);
|
||||
|
||||
return (
|
||||
<div className={styles.body} data-mobile={BUILD_CONFIG.isMobileEdition}>
|
||||
<div className={styles.content}>
|
||||
<BlocksuiteEditorJournalDocTitleUI
|
||||
date={dateString}
|
||||
overrideClassName={styles.docTitleContainer}
|
||||
/>
|
||||
<div className={styles.placeholder}>
|
||||
<div className={styles.placeholderIcon}>
|
||||
<TodayIcon />
|
||||
</div>
|
||||
<div className={styles.placeholderText}>
|
||||
{t['com.affine.journal.placeholder.title']()}
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={createJournal}
|
||||
data-testid="confirm-create-journal-button"
|
||||
>
|
||||
{t['com.affine.journal.placeholder.create']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const weekStyle = { maxWidth: 800, width: '100%' };
|
||||
// this route page acts as a redirector to today's journal
|
||||
export const JournalsPageWithConfirmation = () => {
|
||||
@@ -55,19 +98,8 @@ export const JournalsPageWithConfirmation = () => {
|
||||
const todayString = dayjs().format(JOURNAL_DATE_FORMAT);
|
||||
const isToday = dateString === todayString;
|
||||
|
||||
const [redirecting, setRedirecting] = useState(false);
|
||||
const [ready, setReady] = useState(false);
|
||||
|
||||
const createJournal = useCallback(() => {
|
||||
if (redirecting) return;
|
||||
setRedirecting(true);
|
||||
const doc = journalService.ensureJournalByDate(dateString);
|
||||
workbench.openDoc(doc.id, {
|
||||
replaceHistory: true,
|
||||
at: 'active',
|
||||
});
|
||||
}, [dateString, journalService, redirecting, workbench]);
|
||||
|
||||
const openJournal = useCallback(
|
||||
(date: string) => {
|
||||
workbench.open(`/journals?date=${date}`, { at: 'active' });
|
||||
@@ -118,29 +150,7 @@ export const JournalsPageWithConfirmation = () => {
|
||||
</div>
|
||||
</ViewHeader>
|
||||
<ViewBody>
|
||||
<div className={styles.body}>
|
||||
<div className={styles.content}>
|
||||
<BlocksuiteEditorJournalDocTitleUI
|
||||
date={dateString}
|
||||
overrideClassName={styles.docTitleContainer}
|
||||
/>
|
||||
<div className={styles.placeholder}>
|
||||
<div className={styles.placeholderIcon}>
|
||||
<TodayIcon />
|
||||
</div>
|
||||
<div className={styles.placeholderText}>
|
||||
{t['com.affine.journal.placeholder.title']()}
|
||||
</div>
|
||||
<Button
|
||||
variant="primary"
|
||||
onClick={createJournal}
|
||||
data-testid="confirm-create-journal-button"
|
||||
>
|
||||
{t['com.affine.journal.placeholder.create']()}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<JournalPlaceholder dateString={dateString} />
|
||||
</ViewBody>
|
||||
<AllDocSidebarTabs />
|
||||
</>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import { JournalService } from '@affine/core/modules/journal';
|
||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { TodayIcon } from '@blocksuite/icons/rc';
|
||||
@@ -13,15 +14,23 @@ export const AppTabJournal = ({ tab }: AppTabCustomFCProps) => {
|
||||
const location = useLiveData(workbench.location$);
|
||||
const journalService = useService(JournalService);
|
||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const isTwoStepJournalConfirmationEnabled = useLiveData(
|
||||
featureFlagService.flags.enable_two_step_journal_confirmation.$
|
||||
);
|
||||
|
||||
const maybeDocId = location.pathname.split('/')[1];
|
||||
const journalDate = useLiveData(journalService.journalDate$(maybeDocId));
|
||||
const JournalIcon = useLiveData(docDisplayMetaService.icon$(maybeDocId));
|
||||
|
||||
const handleOpenToday = useCallback(() => {
|
||||
const docId = journalService.ensureJournalByDate(new Date()).id;
|
||||
workbench.openDoc({ docId, fromTab: 'true' }, { at: 'active' });
|
||||
}, [journalService, workbench]);
|
||||
if (isTwoStepJournalConfirmationEnabled) {
|
||||
workbench.open('/journals', { at: 'active' });
|
||||
} else {
|
||||
const docId = journalService.ensureJournalByDate(new Date()).id;
|
||||
workbench.openDoc({ docId, fromTab: 'true' }, { at: 'active' });
|
||||
}
|
||||
}, [workbench, journalService, isTwoStepJournalConfirmationEnabled]);
|
||||
|
||||
const Icon = journalDate ? JournalIcon : TodayIcon;
|
||||
|
||||
|
||||
@@ -242,6 +242,10 @@ const MobileDetailPage = ({
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const [showTitle, setShowTitle] = useState(checkShowTitle);
|
||||
const title = useLiveData(docDisplayMetaService.title$(pageId));
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const isTwoStepJournalConfirmationEnabled = useLiveData(
|
||||
featureFlagService.flags.enable_two_step_journal_confirmation.$
|
||||
);
|
||||
|
||||
const canAccess = useGuard('Doc_Read', pageId);
|
||||
|
||||
@@ -252,13 +256,25 @@ const MobileDetailPage = ({
|
||||
|
||||
const handleDateChange = useCallback(
|
||||
(date: string) => {
|
||||
const docId = journalService.ensureJournalByDate(date).id;
|
||||
workbench.openDoc(
|
||||
{ docId, fromTab: fromTab ? 'true' : undefined },
|
||||
{ replaceHistory: true }
|
||||
);
|
||||
if (isTwoStepJournalConfirmationEnabled) {
|
||||
const docs = journalService.journalsByDate$(date).value;
|
||||
if (docs.length > 0) {
|
||||
workbench.openDoc(
|
||||
{ docId: docs[0].id, fromTab: fromTab ? 'true' : undefined },
|
||||
{ replaceHistory: true }
|
||||
);
|
||||
} else {
|
||||
workbench.open(`/journals?date=${date}`);
|
||||
}
|
||||
} else {
|
||||
const docId = journalService.ensureJournalByDate(date).id;
|
||||
workbench.openDoc(
|
||||
{ docId, fromTab: fromTab ? 'true' : undefined },
|
||||
{ replaceHistory: true }
|
||||
);
|
||||
}
|
||||
},
|
||||
[fromTab, journalService, workbench]
|
||||
[fromTab, isTwoStepJournalConfirmationEnabled, journalService, workbench]
|
||||
);
|
||||
|
||||
useGlobalEvent(
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import { style } from '@vanilla-extract/css';
|
||||
|
||||
export const container = style({
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
height: '100dvh',
|
||||
overflow: 'hidden',
|
||||
backgroundColor: cssVarV2('layer/background/primary'),
|
||||
});
|
||||
|
||||
export const header = style({
|
||||
backgroundColor: cssVarV2('layer/background/primary'),
|
||||
});
|
||||
|
||||
export const headerTitle = style({
|
||||
color: cssVarV2('text/primary'),
|
||||
fontSize: 17,
|
||||
lineHeight: '22px',
|
||||
fontWeight: 600,
|
||||
letterSpacing: -0.43,
|
||||
});
|
||||
|
||||
export const journalDatePicker = style({
|
||||
backgroundColor: cssVarV2('layer/background/primary'),
|
||||
});
|
||||
@@ -0,0 +1,107 @@
|
||||
import {
|
||||
getDateFromUrl,
|
||||
JournalPlaceholder,
|
||||
} from '@affine/core/desktop/pages/workspace/journals';
|
||||
import { FeatureFlagService } from '@affine/core/modules/feature-flag';
|
||||
import {
|
||||
JOURNAL_DATE_FORMAT,
|
||||
JournalService,
|
||||
} from '@affine/core/modules/journal';
|
||||
import { ViewService, WorkbenchService } from '@affine/core/modules/workbench';
|
||||
import { i18nTime } from '@affine/i18n';
|
||||
import { useLiveData, useService } from '@toeverything/infra';
|
||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||
import dayjs from 'dayjs';
|
||||
import { useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
|
||||
import { AppTabs, PageHeader } from '../../components';
|
||||
import { JournalDatePicker } from './detail/journal-date-picker';
|
||||
import * as styles from './journals.css';
|
||||
|
||||
export const JournalsPageWithConfirmation = () => {
|
||||
const journalService = useService(JournalService);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
const view = useService(ViewService).view;
|
||||
const location = useLiveData(view.location$);
|
||||
const dateString = getDateFromUrl(location);
|
||||
const [ready, setReady] = useState(false);
|
||||
const allJournalDates = useLiveData(journalService.allJournalDates$);
|
||||
|
||||
const handleDateChange = useCallback(
|
||||
(date: string) => {
|
||||
workbench.open(`/journals?date=${date}`, { at: 'active' });
|
||||
},
|
||||
[workbench]
|
||||
);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
// only handle current route
|
||||
if (!location.pathname.startsWith('/journals')) return;
|
||||
|
||||
// check if the journal is created
|
||||
const docs = journalService.journalsByDate$(dateString).value;
|
||||
if (docs.length === 0) {
|
||||
setReady(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// if created, redirect to the journal
|
||||
const journal = docs[0];
|
||||
workbench.openDoc(journal.id, { replaceHistory: true, at: 'active' });
|
||||
}, [dateString, journalService, location.pathname, view, workbench]);
|
||||
|
||||
if (!ready) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className={styles.container}>
|
||||
<PageHeader
|
||||
className={styles.header}
|
||||
bottom={
|
||||
<JournalDatePicker
|
||||
date={dateString}
|
||||
onChange={handleDateChange}
|
||||
withDotDates={allJournalDates}
|
||||
className={styles.journalDatePicker}
|
||||
/>
|
||||
}
|
||||
contentClassName={styles.headerTitle}
|
||||
bottomSpacer={94}
|
||||
>
|
||||
{i18nTime(dayjs(dateString), { absolute: { accuracy: 'month' } })}
|
||||
</PageHeader>
|
||||
<JournalPlaceholder dateString={dateString} />
|
||||
</div>
|
||||
<AppTabs background={cssVarV2('layer/background/primary')} />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const JournalsPageWithoutConfirmation = () => {
|
||||
const journalService = useService(JournalService);
|
||||
const workbench = useService(WorkbenchService).workbench;
|
||||
|
||||
useEffect(() => {
|
||||
const today = dayjs().format(JOURNAL_DATE_FORMAT);
|
||||
const doc = journalService.ensureJournalByDate(today);
|
||||
workbench.openDoc(doc.id, {
|
||||
replaceHistory: true,
|
||||
at: 'active',
|
||||
});
|
||||
}, [journalService, workbench]);
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
export const Component = () => {
|
||||
const featureFlagService = useService(FeatureFlagService);
|
||||
const isTwoStepJournalConfirmationEnabled = useLiveData(
|
||||
featureFlagService.flags.enable_two_step_journal_confirmation.$
|
||||
);
|
||||
|
||||
if (isTwoStepJournalConfirmationEnabled) {
|
||||
return <JournalsPageWithConfirmation />;
|
||||
}
|
||||
|
||||
return <JournalsPageWithoutConfirmation />;
|
||||
};
|
||||
@@ -4,6 +4,7 @@ import { Component as All } from './pages/workspace/all';
|
||||
import { Component as Collection } from './pages/workspace/collection';
|
||||
import { Component as CollectionDetail } from './pages/workspace/collection/detail';
|
||||
import { Component as Home } from './pages/workspace/home';
|
||||
import { Component as Journals } from './pages/workspace/journals';
|
||||
import { Component as Search } from './pages/workspace/search';
|
||||
import { Component as Tag } from './pages/workspace/tag';
|
||||
import { Component as TagDetail } from './pages/workspace/tag/detail';
|
||||
@@ -41,6 +42,11 @@ export const workbenchRoutes = [
|
||||
// lazy: () => import('./pages/workspace/tag/detail'),
|
||||
Component: TagDetail,
|
||||
},
|
||||
{
|
||||
path: '/journals',
|
||||
// lazy: () => import('./pages/workspace/journals'),
|
||||
Component: Journals,
|
||||
},
|
||||
{
|
||||
path: '/trash',
|
||||
lazy: () => import('./pages/workspace/trash'),
|
||||
|
||||
Reference in New Issue
Block a user