Skip to content

Commit

Permalink
feat(mobile): jouranl daily activity and conflict operations
Browse files Browse the repository at this point in the history
  • Loading branch information
CatsJuice committed Nov 11, 2024
1 parent 8cc98be commit 32c0be4
Show file tree
Hide file tree
Showing 10 changed files with 353 additions and 6 deletions.
9 changes: 7 additions & 2 deletions packages/frontend/component/src/ui/menu/mobile/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { useCallback, useContext, useEffect, useState } from 'react';
import { observeResize } from '../../../utils';
import { Button } from '../../button';
import { Modal } from '../../modal';
import { Scrollable } from '../../scrollbar';
import type { MenuProps } from '../menu.types';
import type { SubMenuContent } from './context';
import { MobileMenuContext } from './context';
Expand Down Expand Up @@ -138,8 +139,12 @@ export const MobileMenu = ({
>
{sub.title || t['com.affine.backButton']()}
</Button>

{sub.items}
<Scrollable.Root>
<Scrollable.Viewport className={styles.scrollArea}>
{sub.items}
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
</div>
))}
</div>
Expand Down
12 changes: 11 additions & 1 deletion packages/frontend/component/src/ui/menu/mobile/styles.css.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';

import { modalContent } from '../../modal/styles.css';
import { bgColor } from '../styles.css';
import { bgColor, iconColor, labelColor } from '../styles.css';

// To override desktop menu style defined in '../styles.css.ts'

Expand Down Expand Up @@ -55,6 +55,12 @@ export const mobileMenuItem = style({
},
},
selectors: {
'&.danger': {
vars: {
[labelColor]: cssVarV2('button/error'),
[iconColor]: cssVarV2('button/error'),
},
},
'&.danger:hover': {
vars: { [bgColor]: 'transparent' },
},
Expand Down Expand Up @@ -97,3 +103,7 @@ export const backButton = style({
paddingLeft: 0,
marginLeft: 20,
});

export const scrollArea = style({
maxHeight: '80dvh',
});
Original file line number Diff line number Diff line change
Expand Up @@ -168,7 +168,7 @@ export const EditorJournalPanel = () => {
);
};

const sortPagesByDate = (
export const sortPagesByDate = (
docs: DocRecord[],
field: 'updatedDate' | 'createDate',
order: 'asc' | 'desc' = 'desc'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { cssVar } from '@toeverything/theme';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';

export const docItem = style({
display: 'flex',
alignItems: 'center',
gap: 8,
});

export const duplicateTag = style({
borderRadius: 4,
padding: '0 8px',
fontSize: cssVar('fontXs'),
lineHeight: '20px',
color: cssVarV2('toast/iconState/error'),
background: cssVarV2('layer/background/error'),
border: `1px solid ${cssVarV2('database/border')}`,
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,154 @@
import {
IconButton,
MobileMenu,
MobileMenuItem,
MobileMenuSub,
useConfirmModal,
} from '@affine/component';
import { MoveToTrash } from '@affine/core/components/page-list';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { JournalService } from '@affine/core/modules/journal';
import { WorkbenchLink } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import { CalendarXmarkIcon, EditIcon, TodayIcon } from '@blocksuite/icons/rc';
import type { DocRecord } from '@toeverything/infra';
import {
DocService,
DocsService,
useLiveData,
useService,
} from '@toeverything/infra';
import { type MouseEvent, useCallback, useMemo } from 'react';

import * as styles from './journal-conflicts.css';

const ResolveConflictOperations = ({ docRecord }: { docRecord: DocRecord }) => {
const t = useI18n();
const journalService = useService(JournalService);
const { openConfirmModal } = useConfirmModal();

const handleOpenTrashModal = useCallback(
(docRecord: DocRecord) => {
openConfirmModal({
title: t['com.affine.moveToTrash.confirmModal.title'](),
description: t['com.affine.moveToTrash.confirmModal.description']({
title: docRecord.title$.value || t['Untitled'](),
}),
cancelText: t['com.affine.confirmModal.button.cancel'](),
confirmText: t.Delete(),
onConfirm: () => {
docRecord.moveToTrash();
},
});
},
[openConfirmModal, t]
);
const handleRemoveJournalMark = useCallback(
(docId: string) => {
journalService.removeJournalDate(docId);
},
[journalService]
);

return (
<>
<MobileMenuItem
prefixIcon={<CalendarXmarkIcon />}
onClick={() => {
handleRemoveJournalMark(docRecord.id);
}}
data-testid="journal-conflict-remove-mark"
>
{t['com.affine.page-properties.property.journal-remove']()}
</MobileMenuItem>
<MoveToTrash onSelect={() => handleOpenTrashModal(docRecord)} />
</>
);
};

const preventNav = (e: MouseEvent<HTMLButtonElement>) => {
e.stopPropagation();
e.preventDefault();
};

const DocItem = ({ docRecord }: { docRecord: DocRecord }) => {
const docId = docRecord.id;
const i18n = useI18n();
const docDisplayMetaService = useService(DocDisplayMetaService);
const Icon = useLiveData(
docDisplayMetaService.icon$(docId, { compareDate: new Date() })
);
const titleMeta = useLiveData(docDisplayMetaService.title$(docId));
const title = i18n.t(titleMeta);
return (
<WorkbenchLink aria-label={title} to={`/${docId}`}>
<MobileMenuItem
prefixIcon={<Icon />}
suffix={
<MobileMenu
items={<ResolveConflictOperations docRecord={docRecord} />}
>
<IconButton onClick={preventNav} icon={<EditIcon />} />
</MobileMenu>
}
>
<div className={styles.docItem}>
{title}
<div className={styles.duplicateTag}>
{i18n['com.affine.page-properties.property.journal-duplicated']()}
</div>
</div>
</MobileMenuItem>
</WorkbenchLink>
);
};

const ConflictList = ({ docRecords }: { docRecords: DocRecord[] }) => {
return docRecords.map(docRecord => (
<DocItem key={docRecord.id} docRecord={docRecord} />
));
};

const ConflictListMenuItem = ({ docRecords }: { docRecords: DocRecord[] }) => {
const t = useI18n();
return (
<MobileMenuSub
triggerOptions={{
prefixIcon: <TodayIcon />,
type: 'danger',
}}
items={<ConflictList docRecords={docRecords} />}
>
{t['com.affine.m.selector.journal-menu.conflicts']()}
</MobileMenuSub>
);
};

const JournalConflictsChecker = ({ date }: { date: string }) => {
const docRecordList = useService(DocsService).list;
const journalService = useService(JournalService);
const docs = useLiveData(
useMemo(() => journalService.journalsByDate$(date), [journalService, date])
);
const docRecords = useLiveData(
docRecordList.docs$.map(records =>
records.filter(v => {
return docs.some(doc => doc.id === v.id);
})
)
);

if (docRecords.length <= 1) return null;

return <ConflictListMenuItem docRecords={docRecords} />;
};

export const JournalConflictsMenuItem = () => {
const journalService = useService(JournalService);
const docId = useService(DocService).doc.id;
const journalDate = useLiveData(journalService.journalDate$(docId));

if (!journalDate) return null;

return <JournalConflictsChecker date={journalDate} />;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { bodyEmphasized, bodyRegular } from '@toeverything/theme/typography';
import { cssVarV2 } from '@toeverything/theme/v2';
import { style } from '@vanilla-extract/css';

export const title = style([
bodyEmphasized,
{
padding: '11px 20px',
},
]);

export const empty = style([
bodyRegular,
{
padding: '11px 20px',
color: cssVarV2('text/placeholder'),
},
]);
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import { MenuItem, MenuSeparator, MobileMenuSub } from '@affine/component';
import { sortPagesByDate } from '@affine/core/desktop/pages/workspace/detail-page/tabs/journal';
import { DocDisplayMetaService } from '@affine/core/modules/doc-display-meta';
import { JournalService } from '@affine/core/modules/journal';
import { WorkbenchLink } from '@affine/core/modules/workbench';
import { useI18n } from '@affine/i18n';
import { HistoryIcon } from '@blocksuite/icons/rc';
import type { DocRecord } from '@toeverything/infra';
import {
DocService,
DocsService,
useLiveData,
useService,
} from '@toeverything/infra';
import dayjs from 'dayjs';
import { type ReactNode, useCallback, useMemo } from 'react';

import * as styles from './journal-today-activity.css';

interface JournalTodayActivityMenuItemProps {
prefix?: ReactNode;
suffix?: ReactNode;
}
type Category = 'created' | 'updated';

const DocItem = ({ docId }: { docId: string }) => {
const i18n = useI18n();
const docDisplayMetaService = useService(DocDisplayMetaService);
const Icon = useLiveData(
docDisplayMetaService.icon$(docId, { compareDate: new Date() })
);
const titleMeta = useLiveData(docDisplayMetaService.title$(docId));
const title = i18n.t(titleMeta);
return (
<WorkbenchLink aria-label={title} to={`/${docId}`}>
<MenuItem prefixIcon={<Icon />}>{title}</MenuItem>
</WorkbenchLink>
);
};

const ActivityBlock = ({
name,
list,
}: {
name: Category;
list: DocRecord[];
}) => {
const t = useI18n();

const title =
name === 'created'
? t['com.affine.journal.created-today']()
: t['com.affine.journal.updated-today']();
return (
<>
<div className={styles.title}>{title}</div>
{list.length > 0 ? (
list.map(doc => {
return <DocItem docId={doc.id} key={doc.id} />;
})
) : (
<div className={styles.empty}>
{name === 'created'
? t['com.affine.journal.daily-count-created-empty-tips']()
: t['com.affine.journal.daily-count-updated-empty-tips']()}
</div>
)}
</>
);
};

const TodaysActivity = ({ date }: { date: string }) => {
const docRecords = useLiveData(useService(DocsService).list.docs$);
const getTodaysPages = useCallback(
(field: 'createDate' | 'updatedDate') => {
return sortPagesByDate(
docRecords.filter(docRecord => {
const meta = docRecord.meta$.value;
if (meta.trash) return false;
return meta[field] && dayjs(meta[field]).isSame(date, 'day');
}),
field
);
},
[date, docRecords]
);

const createdToday = useMemo(
() => getTodaysPages('createDate'),
[getTodaysPages]
);
const updatedToday = useMemo(
() => getTodaysPages('updatedDate'),
[getTodaysPages]
);

return (
<>
<ActivityBlock name="created" list={createdToday} />
<MenuSeparator />
<ActivityBlock name="updated" list={updatedToday} />
</>
);
};

export const JournalTodayActivityMenuItem = ({
prefix,
suffix,
}: JournalTodayActivityMenuItemProps) => {
const docService = useService(DocService);
const journalService = useService(JournalService);

const docId = docService.doc.id;
const journalDate = useLiveData(journalService.journalDate$(docId));

const t = useI18n();

if (!journalDate) return null;

return (
<>
{prefix}
<MobileMenuSub
triggerOptions={{
prefixIcon: <HistoryIcon />,
}}
items={<TodaysActivity date={journalDate} />}
title={t['com.affine.m.selector.journal-menu.today-activity']()}
>
{t['com.affine.m.selector.journal-menu.today-activity']()}
</MobileMenuSub>
{suffix}
</>
);
};
Loading

0 comments on commit 32c0be4

Please sign in to comment.