feat(core): show sync state at doc info (#7244)

This commit is contained in:
EYHN
2024-06-18 08:35:22 +00:00
parent ea718d30e9
commit 98258b0211
9 changed files with 154 additions and 41 deletions

View File

@@ -345,7 +345,7 @@ export class LiveData<T = unknown>
duration: number, duration: number,
{ trailing = true, leading = true }: ThrottleConfig = {} { trailing = true, leading = true }: ThrottleConfig = {}
) { ) {
return LiveData.from( return LiveData.from<T>(
this.pipe(throttleTime(duration, undefined, { trailing, leading })), this.pipe(throttleTime(duration, undefined, { trailing, leading })),
null as any null as any
); );

View File

@@ -53,12 +53,25 @@ export class DocEngine {
const localState$ = this.localPart.docState$(docId); const localState$ = this.localPart.docState$(docId);
const remoteState$ = this.remotePart?.docState$(docId); const remoteState$ = this.remotePart?.docState$(docId);
return LiveData.computed(get => { return LiveData.computed(get => {
const local = get(localState$); const localState = get(localState$);
const remote = remoteState$ ? get(remoteState$) : null; const remoteState = remoteState$ ? get(remoteState$) : null;
if (remoteState) {
return { return {
ready: local.ready, syncing: remoteState.syncing,
saving: local.syncing, saving: localState.syncing,
syncing: local.syncing || remote?.syncing, retrying: remoteState.retrying,
ready: localState.ready,
errorMessage: remoteState.errorMessage,
serverClock: remoteState.serverClock,
};
}
return {
syncing: localState.syncing,
saving: localState.syncing,
ready: localState.ready,
retrying: false,
errorMessage: null,
serverClock: null,
}; };
}); });
} }

View File

@@ -60,6 +60,9 @@ export interface RemoteEngineState {
export interface RemoteDocState { export interface RemoteDocState {
syncing: boolean; syncing: boolean;
retrying: boolean;
serverClock: number | null;
errorMessage: string | null;
} }
export class DocEngineRemotePart { export class DocEngineRemotePart {
@@ -87,13 +90,14 @@ export class DocEngineRemotePart {
new Observable(subscribe => { new Observable(subscribe => {
const next = () => { const next = () => {
if (!this.status.syncing) { if (!this.status.syncing) {
// if syncing = false, jobMap is empty
subscribe.next({ subscribe.next({
total: this.status.docs.size, total: this.status.docs.size,
syncing: this.status.docs.size, syncing: this.status.docs.size,
retrying: this.status.retrying, retrying: this.status.retrying,
errorMessage: this.status.errorMessage, errorMessage: this.status.errorMessage,
}); });
} } else {
const syncing = this.status.jobMap.size; const syncing = this.status.jobMap.size;
subscribe.next({ subscribe.next({
total: this.status.docs.size, total: this.status.docs.size,
@@ -101,6 +105,7 @@ export class DocEngineRemotePart {
retrying: this.status.retrying, retrying: this.status.retrying,
errorMessage: this.status.errorMessage, errorMessage: this.status.errorMessage,
}); });
}
}; };
next(); next();
return this.statusUpdatedSubject$.subscribe(() => { return this.statusUpdatedSubject$.subscribe(() => {
@@ -123,6 +128,9 @@ export class DocEngineRemotePart {
syncing: syncing:
!this.status.connectedDocs.has(docId) || !this.status.connectedDocs.has(docId) ||
this.status.jobMap.has(docId), this.status.jobMap.has(docId),
serverClock: this.status.serverClocks.get(docId),
retrying: this.status.retrying,
errorMessage: this.status.errorMessage,
}); });
}; };
next(); next();
@@ -130,7 +138,7 @@ export class DocEngineRemotePart {
if (updatedId === true || updatedId === docId) next(); 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 = { readonly actions = {
updateServerClock: (docId: string, serverClock: number) => { updateServerClock: (docId: string, serverClock: number) => {
this.status.serverClocks.setIfBigger(docId, serverClock); this.status.serverClocks.setIfBigger(docId, serverClock);
this.statusUpdatedSubject$.next(docId);
}, },
addDoc: (docId: string) => { addDoc: (docId: string) => {
if (!this.status.docs.has(docId)) { if (!this.status.docs.has(docId)) {
@@ -359,7 +368,6 @@ export class DocEngineRemotePart {
// eslint-disable-next-line no-constant-condition // eslint-disable-next-line no-constant-condition
while (true) { while (true) {
try { try {
this.status.retrying = false;
await this.retryLoop(signal); await this.retryLoop(signal);
} catch (err) { } catch (err) {
if (signal?.aborted) { 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); throwIfAborted(signal);
disposes.push( disposes.push(
await this.server.subscribeAllDocs(({ docId, data, serverClock }) => { await this.server.subscribeAllDocs(({ docId, data, serverClock }) => {

View File

@@ -89,8 +89,8 @@ export const backlinksList = style({
export const tableHeaderTimestamp = style({ export const tableHeaderTimestamp = style({
display: 'flex', display: 'flex',
flexDirection: 'row', flexDirection: 'column',
alignItems: 'center', alignItems: 'start',
gap: '8px', gap: '8px',
cursor: 'default', cursor: 'default',
padding: '0 6px', padding: '0 6px',

View File

@@ -14,7 +14,11 @@ import type {
PageInfoCustomPropertyMeta, PageInfoCustomPropertyMeta,
PagePropertyType, PagePropertyType,
} from '@affine/core/modules/properties/services/schema'; } 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 { useAFFiNEI18N } from '@affine/i18n/hooks';
import { assertExists } from '@blocksuite/global/utils'; import { assertExists } from '@blocksuite/global/utils';
import { import {
@@ -41,6 +45,12 @@ import {
} from '@dnd-kit/modifiers'; } from '@dnd-kit/modifiers';
import { SortableContext, useSortable } from '@dnd-kit/sortable'; import { SortableContext, useSortable } from '@dnd-kit/sortable';
import * as Collapsible from '@radix-ui/react-collapsible'; import * as Collapsible from '@radix-ui/react-collapsible';
import {
DocService,
useLiveData,
useServices,
WorkspaceService,
} from '@toeverything/infra';
import clsx from 'clsx'; import clsx from 'clsx';
import { use } from 'foxact/use'; import { use } from 'foxact/use';
import { atom, useAtomValue, useSetAtom } from 'jotai'; import { atom, useAtomValue, useSetAtom } from 'jotai';
@@ -596,35 +606,69 @@ export const PagePropertiesTableHeader = ({
manager.pageId manager.pageId
); );
const timestampElement = useMemo(() => { const { docService, workspaceService } = useServices({
const localizedUpdateTime = manager.updatedDate DocService,
? timestampToLocalDate(manager.updatedDate) WorkspaceService,
: null; });
const { syncing, retrying, serverClock } = useLiveData(
workspaceService.workspace.engine.doc.docState$(docService.doc.id)
);
const timestampElement = useMemo(() => {
const localizedCreateTime = manager.createDate const localizedCreateTime = manager.createDate
? timestampToLocalDate(manager.createDate) ? timestampToLocalDate(manager.createDate)
: null; : null;
const updateTimeElement = (
<div className={styles.tableHeaderTimestamp}>
{t['Updated']()} {localizedUpdateTime}
</div>
);
const createTimeElement = ( const createTimeElement = (
<div className={styles.tableHeaderTimestamp}> <div className={styles.tableHeaderTimestamp}>
{t['Created']()} {localizedCreateTime} {t['Created']()} {localizedCreateTime}
</div> </div>
); );
return localizedUpdateTime ? ( return serverClock ? (
<Tooltip
side="right"
content={
<>
<div className={styles.tableHeaderTimestamp}>
{t['Updated']()} {timestampToLocalDateTime(serverClock)}
</div>
{manager.createDate && (
<div className={styles.tableHeaderTimestamp}>
{t['Created']()} {timestampToLocalDateTime(manager.createDate)}
</div>
)}
</>
}
>
<div className={styles.tableHeaderTimestamp}>
{!syncing && !retrying ? (
<>
{t['Updated']()} {timestampToHumanTime(serverClock)}
</>
) : (
<>{t['com.affine.syncing']()}</>
)}
</div>
</Tooltip>
) : manager.updatedDate ? (
<Tooltip side="right" content={createTimeElement}> <Tooltip side="right" content={createTimeElement}>
{updateTimeElement} <div className={styles.tableHeaderTimestamp}>
{t['Updated']()} {timestampToLocalDate(manager.updatedDate)}
</div>
</Tooltip> </Tooltip>
) : ( ) : (
createTimeElement createTimeElement
); );
}, [manager.createDate, manager.updatedDate, t]); }, [
manager.createDate,
manager.updatedDate,
retrying,
serverClock,
syncing,
t,
]);
const handleCollapse = useCallback(() => { const handleCollapse = useCallback(() => {
onOpenChange(!open); onOpenChange(!open);

View File

@@ -145,20 +145,22 @@ const useSyncEngineSyncProgress = () => {
if (!isOnline) { if (!isOnline) {
return 'Disconnected, please check your network connection'; return 'Disconnected, please check your network connection';
} }
if (syncing) { if (isOverCapacity) {
return ( return 'Sync failed due to insufficient cloud storage space.';
`Syncing with AFFiNE Cloud` + }
(progress ? ` (${Math.floor(progress * 100)}%)` : '') if (retrying && errorMessage) {
);
} else if (retrying && errorMessage) {
return `${errorMessage}, reconnecting.`; return `${errorMessage}, reconnecting.`;
} }
if (retrying) { if (retrying) {
return 'Sync disconnected due to unexpected issues, reconnecting.'; return 'Sync disconnected due to unexpected issues, reconnecting.';
} }
if (isOverCapacity) { if (syncing) {
return 'Sync failed due to insufficient cloud storage space.'; return (
`Syncing with AFFiNE Cloud` +
(progress ? ` (${Math.floor(progress * 100)}%)` : '')
);
} }
return 'Synced with AFFiNE Cloud'; return 'Synced with AFFiNE Cloud';
}, [ }, [
currentWorkspace.flavour, currentWorkspace.flavour,
@@ -196,7 +198,8 @@ const useSyncEngineSyncProgress = () => {
), ),
active: active:
currentWorkspace.flavour === WorkspaceFlavour.AFFINE_CLOUD && 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) => { const usePauseAnimation = (timeToResume = 5000) => {

View File

@@ -14,7 +14,7 @@ export function useDocEngineStatus() {
() => ({ () => ({
...engineState, ...engineState,
progress, progress,
syncing: engineState.syncing > 0, syncing: engineState.syncing > 0 || engineState.retrying,
}), }),
[engineState, progress] [engineState, progress]
); );

View File

@@ -7,6 +7,13 @@ function createTimeFormatter() {
}); });
} }
function createDateTimeFormatter() {
return new Intl.DateTimeFormat(getI18n()?.language, {
timeStyle: 'medium',
dateStyle: 'medium',
});
}
function createDateFormatter() { function createDateFormatter() {
return new Intl.DateTimeFormat(getI18n()?.language, { return new Intl.DateTimeFormat(getI18n()?.language, {
year: 'numeric', year: 'numeric',
@@ -31,6 +38,17 @@ export const timestampToLocalDate = (ts: string | number) => {
return formatter.format(dayjs(ts).toDate()); 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 { export interface CalendarTranslation {
yesterday: () => string; yesterday: () => string;
today: () => string; today: () => string;
@@ -64,3 +82,24 @@ export const timestampToCalendarDate = (
? `${translation.nextWeek()} ${week}` ? `${translation.nextWeek()} ${week}`
: sameElse; : 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);
}
};

View File

@@ -1303,6 +1303,8 @@
"com.affine.workspaceType.offline": "Available Offline", "com.affine.workspaceType.offline": "Available Offline",
"com.affine.write_with_a_blank_page": "Write with a blank page", "com.affine.write_with_a_blank_page": "Write with a blank page",
"com.affine.yesterday": "Yesterday", "com.affine.yesterday": "Yesterday",
"com.affine.just-now": "Just now",
"com.affine.syncing": "Syncing",
"core": "core", "core": "core",
"dark": "Dark", "dark": "Dark",
"emptyAllPages": "Click on the <1>$t(New Doc)</1> button to create your first doc.", "emptyAllPages": "Click on the <1>$t(New Doc)</1> button to create your first doc.",