Skip to content

Commit

Permalink
feat: history timeline shows relative time, such as today and yesterday
Browse files Browse the repository at this point in the history
  • Loading branch information
akumatus committed May 10, 2024
1 parent 931e996 commit 37f09de
Show file tree
Hide file tree
Showing 5 changed files with 189 additions and 17 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { useDocMetaHelper } from '@affine/core/hooks/use-block-suite-page-meta';
import { useDocCollectionPage } from '@affine/core/hooks/use-block-suite-workspace-page';
import { timestampToLocalDate } from '@affine/core/utils';
import {
type CalendarTranslation,
timestampToCalendarDate,
} from '@affine/core/utils';
import { DebugLogger } from '@affine/debug';
import type { ListHistoryQuery } from '@affine/graphql';
import { listHistoryQuery, recoverDocMutation } from '@affine/graphql';
Expand Down Expand Up @@ -174,10 +177,13 @@ export const useSnapshotPage = (
return page;
};

export const historyListGroupByDay = (histories: DocHistory[]) => {
export const historyListGroupByDay = (
histories: DocHistory[],
translation: CalendarTranslation
) => {
const map = new Map<string, DocHistory[]>();
for (const history of histories) {
const day = timestampToLocalDate(history.timestamp);
const day = timestampToCalendarDate(history.timestamp, translation);
const list = map.get(day) ?? [];
list.push(history);
map.set(day, list);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -308,14 +308,13 @@ const PageHistoryList = ({
onLoadMore: (() => void) | false;
loadingMore: boolean;
}) => {
const t = useAFFiNEI18N();
const historyListByDay = useMemo(() => {
return historyListGroupByDay(historyList);
}, [historyList]);
return historyListGroupByDay(historyList, t);
}, [historyList, t]);

const [collapsedMap, setCollapsedMap] = useState<Record<number, boolean>>({});

const t = useAFFiNEI18N();

useLayoutEffect(() => {
if (historyList.length > 0 && !activeVersion) {
onVersionChange(historyList[0].timestamp);
Expand Down
119 changes: 119 additions & 0 deletions packages/frontend/core/src/utils/__tests__/intl-formatter.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
import { describe, expect, test, vi } from 'vitest';

import type { CalendarTranslation } from '../intl-formatter';
import { timestampToCalendarDate } from '../intl-formatter';

vi.stubGlobal('window', {
document: {
documentElement: {
lang: 'en',
},
},
});

const translation: CalendarTranslation = {
['com.affine.yesterday']: () => 'Yesterday',
['com.affine.today']: () => 'Today',
['com.affine.tomorrow']: () => 'Tomorrow',
['com.affine.nextWeek']: () => 'Next Week',
};

const ONE_DAY = 24 * 60 * 60 * 1000;

describe('intl calendar date formatter', () => {
const week = new Intl.DateTimeFormat(undefined, {
weekday: 'long',
});

test('someday before last week', async () => {
const timestamp = '2000-01-01 10:00';
expect(timestampToCalendarDate(timestamp, translation)).toBe('Jan 1, 2000');
});

test('someday in last week', async () => {
const timestamp = Date.now() - 6 * ONE_DAY;
expect(timestampToCalendarDate(timestamp, translation)).toBe(
week.format(timestamp)
);
});

test('someday is yesterday', async () => {
const timestamp = Date.now() - ONE_DAY;
expect(timestampToCalendarDate(timestamp, translation)).toBe('Yesterday');
});

test('someday is today', async () => {
const timestamp = Date.now();
expect(timestampToCalendarDate(timestamp, translation)).toBe('Today');
});

test('someday is tomorrow', async () => {
const timestamp = Date.now() + ONE_DAY;
expect(timestampToCalendarDate(timestamp, translation)).toBe('Tomorrow');
});

test('someday in next week', async () => {
const timestamp = Date.now() + 6 * ONE_DAY;
expect(timestampToCalendarDate(timestamp, translation)).toBe(
`Next Week ${week.format(timestamp)}`
);
});

test('someday after next week', async () => {
const timestamp = '3000-01-01 10:00';
expect(timestampToCalendarDate(timestamp, translation)).toBe('Jan 1, 3000');
});
});

describe('intl calendar date formatter with specific reference time', () => {
const referenceTime = '2024-05-10 14:00';

test('someday before last week', async () => {
const timestamp = '2024-04-27 10:00';
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
'Apr 27, 2024'
);
});

test('someday in last week', async () => {
const timestamp = '2024-05-6 10:00';
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
'Monday'
);
});

test('someday is yesterday', async () => {
const timestamp = '2024-05-9 10:00';
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
'Yesterday'
);
});

test('someday is today', async () => {
const timestamp = '2024-05-10 10:00';
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
'Today'
);
});

test('someday is tomorrow', async () => {
const timestamp = '2024-05-11 10:00';
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
'Tomorrow'
);
});

test('someday in next week', async () => {
const timestamp = '2024-05-15 10:00';
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
'Next Week Wednesday'
);
});

test('someday after next week', async () => {
const timestamp = '2024-05-30 10:00';
expect(timestampToCalendarDate(timestamp, translation, referenceTime)).toBe(
'May 30, 2024'
);
});
});
66 changes: 56 additions & 10 deletions packages/frontend/core/src/utils/intl-formatter.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,65 @@
import dayjs from 'dayjs';

const timeFormatter = new Intl.DateTimeFormat(undefined, {
timeStyle: 'short',
});
function createTimeFormatter() {
return new Intl.DateTimeFormat(window.document?.documentElement?.lang, {
timeStyle: 'short',
});
}

const dateFormatter = new Intl.DateTimeFormat(undefined, {
year: 'numeric',
month: 'short',
day: 'numeric',
});
function createDateFormatter() {
return new Intl.DateTimeFormat(window.document?.documentElement?.lang, {
year: 'numeric',
month: 'short',

Check failure on line 12 in packages/frontend/core/src/utils/intl-formatter.ts

View workflow job for this annotation

GitHub Actions / Unit Test

packages/frontend/core/src/hooks/__tests__/use-block-suite-workspace-page-title.spec.tsx > useDocCollectionPageTitle > journal

RangeError: Incorrect locale information provided ❯ dayjs packages/frontend/core/src/utils/intl-formatter.ts:12:5 ❯ Module.timestampToLocalDate packages/frontend/core/src/utils/intl-formatter.ts:46:16 ❯ packages/frontend/core/src/hooks/use-journal.ts:142:5 ❯ packages/frontend/core/src/hooks/use-journal.ts:2170:67 ❯ mountMemo node_modules/react-dom/cjs/react-dom.development.js:16406:19 ❯ Object.useMemo node_modules/react-dom/cjs/react-dom.development.js:16851:16 ❯ Proxy.useMemo node_modules/react/cjs/react.development.js:1650:21 ❯ Module.docCollection packages/frontend/core/src/hooks/use-journal.ts:2164:32 ❯ Module.useDocCollectionPageTitle packages/frontend/core/src/hooks/use-block-suite-workspace-page-title.ts:56:6
day: 'numeric',
});
}

function createWeekFormatter() {
return new Intl.DateTimeFormat(window.document?.documentElement?.lang, {
weekday: 'long',
});
}

export const timestampToLocalTime = (ts: string | number) => {
return timeFormatter.format(dayjs(ts).toDate());
const formatter = createTimeFormatter();
return formatter.format(dayjs(ts).toDate());
};

export const timestampToLocalDate = (ts: string | number) => {
return dateFormatter.format(dayjs(ts).toDate());
const formatter = createDateFormatter();
return formatter.format(dayjs(ts).toDate());
};

export interface CalendarTranslation {
['com.affine.yesterday'](): string;
['com.affine.today'](): string;
['com.affine.tomorrow'](): string;
['com.affine.nextWeek'](): string;
}

export const timestampToCalendarDate = (
ts: string | number,
translation: CalendarTranslation,
referenceTime?: string | number
) => {
const startOfDay = dayjs(referenceTime).startOf('d');
const diff = dayjs(ts).diff(startOfDay, 'd', true);
const sameElse = timestampToLocalDate(ts);

const formatter = createWeekFormatter();
const week = formatter.format(dayjs(ts).toDate());

return diff < -6
? sameElse
: diff < -1
? week
: diff < 0
? translation['com.affine.yesterday']()
: diff < 1
? translation['com.affine.today']()
: diff < 2
? translation['com.affine.tomorrow']()
: diff < 7
? `${translation['com.affine.nextWeek']()} ${week}`
: sameElse;
};
2 changes: 2 additions & 0 deletions packages/frontend/i18n/src/resources/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -783,6 +783,7 @@
"com.affine.last7Days": "Last 7 Days",
"com.affine.lastMonth": "Last month",
"com.affine.lastWeek": "Last week",
"com.affine.nextWeek": "Next week",
"com.affine.lastYear": "Last year",
"com.affine.loading": "Loading...",
"com.affine.moreThan30Days": "Older than a month",
Expand Down Expand Up @@ -1238,6 +1239,7 @@
"com.affine.toastMessage.restored": "{{title}} restored",
"com.affine.toastMessage.successfullyDeleted": "Successfully deleted",
"com.affine.today": "Today",
"com.affine.tomorrow": "Tomorrow",
"com.affine.trashOperation.delete": "Delete",
"com.affine.trashOperation.delete.description": "Once deleted, you can't undo this action. Do you confirm?",
"com.affine.trashOperation.delete.title": "Permanently delete",
Expand Down

0 comments on commit 37f09de

Please sign in to comment.