From 98258b0211b13eed105b5814e31288f1f722bbf2 Mon Sep 17 00:00:00 2001 From: EYHN Date: Tue, 18 Jun 2024 08:35:22 +0000 Subject: [PATCH] feat(core): show sync state at doc info (#7244) --- .../common/infra/src/livedata/livedata.ts | 2 +- packages/common/infra/src/sync/doc/index.ts | 23 ++++-- packages/common/infra/src/sync/doc/remote.ts | 30 +++++--- .../affine/page-properties/styles.css.ts | 4 +- .../affine/page-properties/table.tsx | 72 +++++++++++++++---- .../workspace-card/index.tsx | 21 +++--- .../hooks/affine/use-doc-engine-status.tsx | 2 +- .../frontend/core/src/utils/intl-formatter.ts | 39 ++++++++++ packages/frontend/i18n/src/resources/en.json | 2 + 9 files changed, 154 insertions(+), 41 deletions(-) diff --git a/packages/common/infra/src/livedata/livedata.ts b/packages/common/infra/src/livedata/livedata.ts index 3432956f61..afcc36916d 100644 --- a/packages/common/infra/src/livedata/livedata.ts +++ b/packages/common/infra/src/livedata/livedata.ts @@ -345,7 +345,7 @@ export class LiveData duration: number, { trailing = true, leading = true }: ThrottleConfig = {} ) { - return LiveData.from( + return LiveData.from( this.pipe(throttleTime(duration, undefined, { trailing, leading })), null as any ); diff --git a/packages/common/infra/src/sync/doc/index.ts b/packages/common/infra/src/sync/doc/index.ts index 1b52d75a54..c75106c6a6 100644 --- a/packages/common/infra/src/sync/doc/index.ts +++ b/packages/common/infra/src/sync/doc/index.ts @@ -53,12 +53,25 @@ export class DocEngine { const localState$ = this.localPart.docState$(docId); const remoteState$ = this.remotePart?.docState$(docId); return LiveData.computed(get => { - const local = get(localState$); - const remote = remoteState$ ? get(remoteState$) : null; + const localState = get(localState$); + const remoteState = remoteState$ ? get(remoteState$) : null; + if (remoteState) { + return { + syncing: remoteState.syncing, + saving: localState.syncing, + retrying: remoteState.retrying, + ready: localState.ready, + errorMessage: remoteState.errorMessage, + serverClock: remoteState.serverClock, + }; + } return { - ready: local.ready, - saving: local.syncing, - syncing: local.syncing || remote?.syncing, + syncing: localState.syncing, + saving: localState.syncing, + ready: localState.ready, + retrying: false, + errorMessage: null, + serverClock: null, }; }); } diff --git a/packages/common/infra/src/sync/doc/remote.ts b/packages/common/infra/src/sync/doc/remote.ts index e75bdc4709..92afc8489f 100644 --- a/packages/common/infra/src/sync/doc/remote.ts +++ b/packages/common/infra/src/sync/doc/remote.ts @@ -60,6 +60,9 @@ export interface RemoteEngineState { export interface RemoteDocState { syncing: boolean; + retrying: boolean; + serverClock: number | null; + errorMessage: string | null; } export class DocEngineRemotePart { @@ -87,20 +90,22 @@ export class DocEngineRemotePart { new Observable(subscribe => { const next = () => { if (!this.status.syncing) { + // if syncing = false, jobMap is empty subscribe.next({ total: this.status.docs.size, syncing: this.status.docs.size, retrying: this.status.retrying, errorMessage: this.status.errorMessage, }); + } else { + const syncing = this.status.jobMap.size; + subscribe.next({ + total: this.status.docs.size, + syncing: syncing, + retrying: this.status.retrying, + errorMessage: this.status.errorMessage, + }); } - const syncing = this.status.jobMap.size; - subscribe.next({ - total: this.status.docs.size, - syncing: syncing, - retrying: this.status.retrying, - errorMessage: this.status.errorMessage, - }); }; next(); return this.statusUpdatedSubject$.subscribe(() => { @@ -123,6 +128,9 @@ export class DocEngineRemotePart { syncing: !this.status.connectedDocs.has(docId) || this.status.jobMap.has(docId), + serverClock: this.status.serverClocks.get(docId), + retrying: this.status.retrying, + errorMessage: this.status.errorMessage, }); }; next(); @@ -130,7 +138,7 @@ export class DocEngineRemotePart { if (updatedId === true || updatedId === docId) next(); }); }), - { syncing: false } + { syncing: false, retrying: false, errorMessage: null, serverClock: null } ); } @@ -326,6 +334,7 @@ export class DocEngineRemotePart { readonly actions = { updateServerClock: (docId: string, serverClock: number) => { this.status.serverClocks.setIfBigger(docId, serverClock); + this.statusUpdatedSubject$.next(docId); }, addDoc: (docId: string) => { if (!this.status.docs.has(docId)) { @@ -359,7 +368,6 @@ export class DocEngineRemotePart { // eslint-disable-next-line no-constant-condition while (true) { try { - this.status.retrying = false; await this.retryLoop(signal); } catch (err) { if (signal?.aborted) { @@ -448,6 +456,10 @@ export class DocEngineRemotePart { }), ]); + // reset retrying flag after connected with server + this.status.retrying = false; + this.statusUpdatedSubject$.next(true); + throwIfAborted(signal); disposes.push( await this.server.subscribeAllDocs(({ docId, data, serverClock }) => { diff --git a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts index 36df1a65ca..8c473eafcc 100644 --- a/packages/frontend/core/src/components/affine/page-properties/styles.css.ts +++ b/packages/frontend/core/src/components/affine/page-properties/styles.css.ts @@ -89,8 +89,8 @@ export const backlinksList = style({ export const tableHeaderTimestamp = style({ display: 'flex', - flexDirection: 'row', - alignItems: 'center', + flexDirection: 'column', + alignItems: 'start', gap: '8px', cursor: 'default', padding: '0 6px', diff --git a/packages/frontend/core/src/components/affine/page-properties/table.tsx b/packages/frontend/core/src/components/affine/page-properties/table.tsx index 6436375cc2..096e24fde3 100644 --- a/packages/frontend/core/src/components/affine/page-properties/table.tsx +++ b/packages/frontend/core/src/components/affine/page-properties/table.tsx @@ -14,7 +14,11 @@ import type { PageInfoCustomPropertyMeta, PagePropertyType, } from '@affine/core/modules/properties/services/schema'; -import { timestampToLocalDate } from '@affine/core/utils'; +import { + timestampToHumanTime, + timestampToLocalDate, + timestampToLocalDateTime, +} from '@affine/core/utils'; import { useAFFiNEI18N } from '@affine/i18n/hooks'; import { assertExists } from '@blocksuite/global/utils'; import { @@ -41,6 +45,12 @@ import { } from '@dnd-kit/modifiers'; import { SortableContext, useSortable } from '@dnd-kit/sortable'; import * as Collapsible from '@radix-ui/react-collapsible'; +import { + DocService, + useLiveData, + useServices, + WorkspaceService, +} from '@toeverything/infra'; import clsx from 'clsx'; import { use } from 'foxact/use'; import { atom, useAtomValue, useSetAtom } from 'jotai'; @@ -596,35 +606,69 @@ export const PagePropertiesTableHeader = ({ manager.pageId ); - const timestampElement = useMemo(() => { - const localizedUpdateTime = manager.updatedDate - ? timestampToLocalDate(manager.updatedDate) - : null; + const { docService, workspaceService } = useServices({ + DocService, + WorkspaceService, + }); + const { syncing, retrying, serverClock } = useLiveData( + workspaceService.workspace.engine.doc.docState$(docService.doc.id) + ); + + const timestampElement = useMemo(() => { const localizedCreateTime = manager.createDate ? timestampToLocalDate(manager.createDate) : null; - const updateTimeElement = ( -
- {t['Updated']()} {localizedUpdateTime} -
- ); - const createTimeElement = (
{t['Created']()} {localizedCreateTime}
); - return localizedUpdateTime ? ( + return serverClock ? ( + +
+ {t['Updated']()} {timestampToLocalDateTime(serverClock)} +
+ {manager.createDate && ( +
+ {t['Created']()} {timestampToLocalDateTime(manager.createDate)} +
+ )} + + } + > +
+ {!syncing && !retrying ? ( + <> + {t['Updated']()} {timestampToHumanTime(serverClock)} + + ) : ( + <>{t['com.affine.syncing']()} + )} +
+
+ ) : manager.updatedDate ? ( - {updateTimeElement} +
+ {t['Updated']()} {timestampToLocalDate(manager.updatedDate)} +
) : ( createTimeElement ); - }, [manager.createDate, manager.updatedDate, t]); + }, [ + manager.createDate, + manager.updatedDate, + retrying, + serverClock, + syncing, + t, + ]); const handleCollapse = useCallback(() => { onOpenChange(!open); diff --git a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx index 736119e50d..afc1e14c0a 100644 --- a/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx +++ b/packages/frontend/core/src/components/pure/workspace-slider-bar/workspace-card/index.tsx @@ -145,20 +145,22 @@ const useSyncEngineSyncProgress = () => { if (!isOnline) { return 'Disconnected, please check your network connection'; } - if (syncing) { - return ( - `Syncing with AFFiNE Cloud` + - (progress ? ` (${Math.floor(progress * 100)}%)` : '') - ); - } else if (retrying && errorMessage) { + if (isOverCapacity) { + return 'Sync failed due to insufficient cloud storage space.'; + } + if (retrying && errorMessage) { return `${errorMessage}, reconnecting.`; } if (retrying) { return 'Sync disconnected due to unexpected issues, reconnecting.'; } - if (isOverCapacity) { - return 'Sync failed due to insufficient cloud storage space.'; + if (syncing) { + return ( + `Syncing with AFFiNE Cloud` + + (progress ? ` (${Math.floor(progress * 100)}%)` : '') + ); } + return 'Synced with AFFiNE Cloud'; }, [ currentWorkspace.flavour, @@ -196,7 +198,8 @@ const useSyncEngineSyncProgress = () => { ), active: currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD && - ((syncing && progress !== undefined) || isOverCapacity || !isOnline), + ((syncing && progress !== undefined) || retrying) && // active if syncing or retrying + !isOverCapacity, // not active if isOffline or OverCapacity }; }; const usePauseAnimation = (timeToResume = 5000) => { diff --git a/packages/frontend/core/src/hooks/affine/use-doc-engine-status.tsx b/packages/frontend/core/src/hooks/affine/use-doc-engine-status.tsx index 9b90c532ca..3b972c7b92 100644 --- a/packages/frontend/core/src/hooks/affine/use-doc-engine-status.tsx +++ b/packages/frontend/core/src/hooks/affine/use-doc-engine-status.tsx @@ -14,7 +14,7 @@ export function useDocEngineStatus() { () => ({ ...engineState, progress, - syncing: engineState.syncing > 0, + syncing: engineState.syncing > 0 || engineState.retrying, }), [engineState, progress] ); diff --git a/packages/frontend/core/src/utils/intl-formatter.ts b/packages/frontend/core/src/utils/intl-formatter.ts index ef88b8d399..d83dbe8bcf 100644 --- a/packages/frontend/core/src/utils/intl-formatter.ts +++ b/packages/frontend/core/src/utils/intl-formatter.ts @@ -7,6 +7,13 @@ function createTimeFormatter() { }); } +function createDateTimeFormatter() { + return new Intl.DateTimeFormat(getI18n()?.language, { + timeStyle: 'medium', + dateStyle: 'medium', + }); +} + function createDateFormatter() { return new Intl.DateTimeFormat(getI18n()?.language, { year: 'numeric', @@ -31,6 +38,17 @@ export const timestampToLocalDate = (ts: string | number) => { return formatter.format(dayjs(ts).toDate()); }; +export const timestampToLocalDateTime = (ts: string | number) => { + const formatter = createDateTimeFormatter(); + return formatter.format(dayjs(ts).toDate()); +}; + +export const createRelativeTimeFormatter = () => { + return new Intl.RelativeTimeFormat(getI18n()?.language, { + style: 'narrow', + }); +}; + export interface CalendarTranslation { yesterday: () => string; today: () => string; @@ -64,3 +82,24 @@ export const timestampToCalendarDate = ( ? `${translation.nextWeek()} ${week}` : sameElse; }; + +// TODO: refactor this to @affine/i18n +export const timestampToHumanTime = (ts: number) => { + const diff = Math.abs(dayjs(ts).diff(dayjs())); + + if (diff < 1000 * 60) { + return getI18n().t('com.affine.just-now'); + } else if (diff < 1000 * 60 * 60) { + return createRelativeTimeFormatter().format( + -Math.floor(diff / 1000 / 60), + 'minutes' + ); + } else if (diff < 1000 * 60 * 60 * 24) { + return createRelativeTimeFormatter().format( + -Math.floor(diff / 1000 / 60 / 60), + 'hours' + ); + } else { + return timestampToLocalDate(ts); + } +}; diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index c2a7383ee7..7c2b5ceedc 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1303,6 +1303,8 @@ "com.affine.workspaceType.offline": "Available Offline", "com.affine.write_with_a_blank_page": "Write with a blank page", "com.affine.yesterday": "Yesterday", + "com.affine.just-now": "Just now", + "com.affine.syncing": "Syncing", "core": "core", "dark": "Dark", "emptyAllPages": "Click on the <1>$t(New Doc) button to create your first doc.",