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%',
|
width: '100%',
|
||||||
height: '100%',
|
height: '100%',
|
||||||
borderTop: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
|
borderTop: `0.5px solid ${cssVarV2.layer.insideBorder.border}`,
|
||||||
|
selectors: {
|
||||||
|
'&[data-mobile]': {
|
||||||
|
borderTop: 'none',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const content = style({
|
export const content = style({
|
||||||
maxWidth: 944,
|
maxWidth: 944,
|
||||||
padding: '0px 50px',
|
padding: '0px 50px',
|
||||||
margin: '0 auto',
|
margin: '0 auto',
|
||||||
|
selectors: {
|
||||||
|
'[data-mobile] &': {
|
||||||
|
padding: '0 24px',
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const docTitleContainer = style({
|
export const docTitleContainer = style({
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
import { AllDocSidebarTabs } from '../layouts/all-doc-sidebar-tabs';
|
||||||
import * as styles from './index.css';
|
import * as styles from './index.css';
|
||||||
|
|
||||||
function getDateFromUrl(location: Location) {
|
export function getDateFromUrl(location: Location) {
|
||||||
const searchParams = new URLSearchParams(location.search);
|
const searchParams = new URLSearchParams(location.search);
|
||||||
const date = searchParams.get('date')
|
const date = searchParams.get('date')
|
||||||
? dayjs(searchParams.get('date'))
|
? dayjs(searchParams.get('date'))
|
||||||
@@ -41,6 +41,49 @@ function getDateFromUrl(location: Location) {
|
|||||||
return date.format(JOURNAL_DATE_FORMAT);
|
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%' };
|
const weekStyle = { maxWidth: 800, width: '100%' };
|
||||||
// this route page acts as a redirector to today's journal
|
// this route page acts as a redirector to today's journal
|
||||||
export const JournalsPageWithConfirmation = () => {
|
export const JournalsPageWithConfirmation = () => {
|
||||||
@@ -55,19 +98,8 @@ export const JournalsPageWithConfirmation = () => {
|
|||||||
const todayString = dayjs().format(JOURNAL_DATE_FORMAT);
|
const todayString = dayjs().format(JOURNAL_DATE_FORMAT);
|
||||||
const isToday = dateString === todayString;
|
const isToday = dateString === todayString;
|
||||||
|
|
||||||
const [redirecting, setRedirecting] = useState(false);
|
|
||||||
const [ready, setReady] = 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(
|
const openJournal = useCallback(
|
||||||
(date: string) => {
|
(date: string) => {
|
||||||
workbench.open(`/journals?date=${date}`, { at: 'active' });
|
workbench.open(`/journals?date=${date}`, { at: 'active' });
|
||||||
@@ -118,29 +150,7 @@ export const JournalsPageWithConfirmation = () => {
|
|||||||
</div>
|
</div>
|
||||||
</ViewHeader>
|
</ViewHeader>
|
||||||
<ViewBody>
|
<ViewBody>
|
||||||
<div className={styles.body}>
|
<JournalPlaceholder dateString={dateString} />
|
||||||
<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>
|
|
||||||
</ViewBody>
|
</ViewBody>
|
||||||
<AllDocSidebarTabs />
|
<AllDocSidebarTabs />
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
|
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 { JournalService } from '@affine/core/modules/journal';
|
||||||
import { WorkbenchService } from '@affine/core/modules/workbench';
|
import { WorkbenchService } from '@affine/core/modules/workbench';
|
||||||
import { TodayIcon } from '@blocksuite/icons/rc';
|
import { TodayIcon } from '@blocksuite/icons/rc';
|
||||||
@@ -13,15 +14,23 @@ export const AppTabJournal = ({ tab }: AppTabCustomFCProps) => {
|
|||||||
const location = useLiveData(workbench.location$);
|
const location = useLiveData(workbench.location$);
|
||||||
const journalService = useService(JournalService);
|
const journalService = useService(JournalService);
|
||||||
const docDisplayMetaService = useService(DocDisplayMetaService);
|
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 maybeDocId = location.pathname.split('/')[1];
|
||||||
const journalDate = useLiveData(journalService.journalDate$(maybeDocId));
|
const journalDate = useLiveData(journalService.journalDate$(maybeDocId));
|
||||||
const JournalIcon = useLiveData(docDisplayMetaService.icon$(maybeDocId));
|
const JournalIcon = useLiveData(docDisplayMetaService.icon$(maybeDocId));
|
||||||
|
|
||||||
const handleOpenToday = useCallback(() => {
|
const handleOpenToday = useCallback(() => {
|
||||||
|
if (isTwoStepJournalConfirmationEnabled) {
|
||||||
|
workbench.open('/journals', { at: 'active' });
|
||||||
|
} else {
|
||||||
const docId = journalService.ensureJournalByDate(new Date()).id;
|
const docId = journalService.ensureJournalByDate(new Date()).id;
|
||||||
workbench.openDoc({ docId, fromTab: 'true' }, { at: 'active' });
|
workbench.openDoc({ docId, fromTab: 'true' }, { at: 'active' });
|
||||||
}, [journalService, workbench]);
|
}
|
||||||
|
}, [workbench, journalService, isTwoStepJournalConfirmationEnabled]);
|
||||||
|
|
||||||
const Icon = journalDate ? JournalIcon : TodayIcon;
|
const Icon = journalDate ? JournalIcon : TodayIcon;
|
||||||
|
|
||||||
|
|||||||
@@ -242,6 +242,10 @@ const MobileDetailPage = ({
|
|||||||
const workbench = useService(WorkbenchService).workbench;
|
const workbench = useService(WorkbenchService).workbench;
|
||||||
const [showTitle, setShowTitle] = useState(checkShowTitle);
|
const [showTitle, setShowTitle] = useState(checkShowTitle);
|
||||||
const title = useLiveData(docDisplayMetaService.title$(pageId));
|
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);
|
const canAccess = useGuard('Doc_Read', pageId);
|
||||||
|
|
||||||
@@ -252,13 +256,25 @@ const MobileDetailPage = ({
|
|||||||
|
|
||||||
const handleDateChange = useCallback(
|
const handleDateChange = useCallback(
|
||||||
(date: string) => {
|
(date: string) => {
|
||||||
|
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;
|
const docId = journalService.ensureJournalByDate(date).id;
|
||||||
workbench.openDoc(
|
workbench.openDoc(
|
||||||
{ docId, fromTab: fromTab ? 'true' : undefined },
|
{ docId, fromTab: fromTab ? 'true' : undefined },
|
||||||
{ replaceHistory: true }
|
{ replaceHistory: true }
|
||||||
);
|
);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
[fromTab, journalService, workbench]
|
[fromTab, isTwoStepJournalConfirmationEnabled, journalService, workbench]
|
||||||
);
|
);
|
||||||
|
|
||||||
useGlobalEvent(
|
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'),
|
||||||
|
});
|
||||||
107
packages/frontend/core/src/mobile/pages/workspace/journals.tsx
Normal file
107
packages/frontend/core/src/mobile/pages/workspace/journals.tsx
Normal file
@@ -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 Collection } from './pages/workspace/collection';
|
||||||
import { Component as CollectionDetail } from './pages/workspace/collection/detail';
|
import { Component as CollectionDetail } from './pages/workspace/collection/detail';
|
||||||
import { Component as Home } from './pages/workspace/home';
|
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 Search } from './pages/workspace/search';
|
||||||
import { Component as Tag } from './pages/workspace/tag';
|
import { Component as Tag } from './pages/workspace/tag';
|
||||||
import { Component as TagDetail } from './pages/workspace/tag/detail';
|
import { Component as TagDetail } from './pages/workspace/tag/detail';
|
||||||
@@ -41,6 +42,11 @@ export const workbenchRoutes = [
|
|||||||
// lazy: () => import('./pages/workspace/tag/detail'),
|
// lazy: () => import('./pages/workspace/tag/detail'),
|
||||||
Component: TagDetail,
|
Component: TagDetail,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/journals',
|
||||||
|
// lazy: () => import('./pages/workspace/journals'),
|
||||||
|
Component: Journals,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/trash',
|
path: '/trash',
|
||||||
lazy: () => import('./pages/workspace/trash'),
|
lazy: () => import('./pages/workspace/trash'),
|
||||||
|
|||||||
Reference in New Issue
Block a user