@dxos/plugin-space 0.6.12 → 0.6.13-main.548ca8d

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (91) hide show
  1. package/dist/lib/browser/{chunk-LZEGRS7H.mjs → chunk-AVLRQF6L.mjs} +1 -1
  2. package/dist/lib/browser/chunk-AVLRQF6L.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-DTVUOG2C.mjs → chunk-QK5I2EPF.mjs} +23 -4
  4. package/dist/lib/browser/chunk-QK5I2EPF.mjs.map +7 -0
  5. package/dist/lib/browser/index.mjs +592 -246
  6. package/dist/lib/browser/index.mjs.map +4 -4
  7. package/dist/lib/browser/meta.json +1 -1
  8. package/dist/lib/browser/meta.mjs +1 -1
  9. package/dist/lib/browser/types/index.mjs +7 -3
  10. package/dist/lib/node/{chunk-CVZPI2P3.cjs → chunk-HE2GHO6Z.cjs} +29 -8
  11. package/dist/lib/node/chunk-HE2GHO6Z.cjs.map +7 -0
  12. package/dist/lib/node/{chunk-6CNYF6YU.cjs → chunk-P4XUXM7Y.cjs} +4 -4
  13. package/dist/lib/node/chunk-P4XUXM7Y.cjs.map +7 -0
  14. package/dist/lib/node/index.cjs +812 -469
  15. package/dist/lib/node/index.cjs.map +4 -4
  16. package/dist/lib/node/meta.cjs +5 -5
  17. package/dist/lib/node/meta.cjs.map +1 -1
  18. package/dist/lib/node/meta.json +1 -1
  19. package/dist/lib/node/types/index.cjs +14 -10
  20. package/dist/lib/node/types/index.cjs.map +2 -2
  21. package/dist/lib/node-esm/chunk-2TR4WD6U.mjs +116 -0
  22. package/dist/lib/node-esm/chunk-2TR4WD6U.mjs.map +7 -0
  23. package/dist/lib/node-esm/chunk-YPQGKWHJ.mjs +37 -0
  24. package/dist/lib/node-esm/chunk-YPQGKWHJ.mjs.map +7 -0
  25. package/dist/lib/node-esm/index.mjs +2987 -0
  26. package/dist/lib/node-esm/index.mjs.map +7 -0
  27. package/dist/lib/node-esm/meta.json +1 -0
  28. package/dist/lib/node-esm/meta.mjs +14 -0
  29. package/dist/lib/node-esm/meta.mjs.map +7 -0
  30. package/dist/lib/node-esm/types/index.mjs +26 -0
  31. package/dist/lib/node-esm/types/index.mjs.map +7 -0
  32. package/dist/types/src/SpacePlugin.d.ts.map +1 -1
  33. package/dist/types/src/components/FallbackSettings.d.ts +8 -0
  34. package/dist/types/src/components/FallbackSettings.d.ts.map +1 -0
  35. package/dist/types/src/components/MenuFooter.d.ts.map +1 -1
  36. package/dist/types/src/components/SaveStatus.d.ts +3 -0
  37. package/dist/types/src/components/SaveStatus.d.ts.map +1 -0
  38. package/dist/types/src/components/ShareSpaceButton.stories.d.ts +4 -0
  39. package/dist/types/src/components/ShareSpaceButton.stories.d.ts.map +1 -1
  40. package/dist/types/src/components/SpaceMain/SpaceMain.d.ts.map +1 -1
  41. package/dist/types/src/components/SpacePresence.stories.d.ts +4 -0
  42. package/dist/types/src/components/SpacePresence.stories.d.ts.map +1 -1
  43. package/dist/types/src/components/SpaceSettings.d.ts.map +1 -1
  44. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts +13 -0
  45. package/dist/types/src/components/SyncStatus/SyncStatus.d.ts.map +1 -0
  46. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts +117 -0
  47. package/dist/types/src/components/SyncStatus/SyncStatus.stories.d.ts.map +1 -0
  48. package/dist/types/src/components/SyncStatus/index.d.ts +2 -0
  49. package/dist/types/src/components/SyncStatus/index.d.ts.map +1 -0
  50. package/dist/types/src/components/SyncStatus/types.d.ts +14 -0
  51. package/dist/types/src/components/SyncStatus/types.d.ts.map +1 -0
  52. package/dist/types/src/components/index.d.ts +3 -2
  53. package/dist/types/src/components/index.d.ts.map +1 -1
  54. package/dist/types/src/meta.d.ts.map +1 -1
  55. package/dist/types/src/translations.d.ts +4 -0
  56. package/dist/types/src/translations.d.ts.map +1 -1
  57. package/dist/types/src/types/thread.d.ts +14 -0
  58. package/dist/types/src/types/thread.d.ts.map +1 -1
  59. package/dist/types/src/types/types.d.ts +18 -1
  60. package/dist/types/src/types/types.d.ts.map +1 -1
  61. package/dist/types/src/util.d.ts +1 -4
  62. package/dist/types/src/util.d.ts.map +1 -1
  63. package/package.json +44 -36
  64. package/src/SpacePlugin.tsx +145 -83
  65. package/src/components/FallbackSettings.tsx +35 -0
  66. package/src/components/MenuFooter.tsx +1 -0
  67. package/src/components/SaveStatus.tsx +95 -0
  68. package/src/components/SpaceMain/SpaceMain.tsx +1 -22
  69. package/src/components/SpacePresence.tsx +1 -1
  70. package/src/components/SpaceSettings.tsx +32 -3
  71. package/src/components/SyncStatus/SyncStatus.stories.tsx +62 -0
  72. package/src/components/SyncStatus/SyncStatus.tsx +188 -0
  73. package/src/components/SyncStatus/index.ts +5 -0
  74. package/src/components/SyncStatus/types.ts +77 -0
  75. package/src/components/index.ts +3 -2
  76. package/src/meta.ts +3 -1
  77. package/src/translations.ts +4 -0
  78. package/src/types/collection.ts +1 -1
  79. package/src/types/thread.ts +11 -1
  80. package/src/types/types.ts +25 -1
  81. package/src/util.tsx +15 -50
  82. package/dist/lib/browser/chunk-DTVUOG2C.mjs.map +0 -7
  83. package/dist/lib/browser/chunk-LZEGRS7H.mjs.map +0 -7
  84. package/dist/lib/node/chunk-6CNYF6YU.cjs.map +0 -7
  85. package/dist/lib/node/chunk-CVZPI2P3.cjs.map +0 -7
  86. package/dist/types/src/components/EmptySpace.d.ts +0 -3
  87. package/dist/types/src/components/EmptySpace.d.ts.map +0 -1
  88. package/dist/types/src/components/EmptyTree.d.ts +0 -3
  89. package/dist/types/src/components/EmptyTree.d.ts.map +0 -1
  90. package/src/components/EmptySpace.tsx +0 -25
  91. package/src/components/EmptyTree.tsx +0 -25
@@ -0,0 +1,95 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useState } from 'react';
6
+
7
+ import { type UnsubscribeCallback } from '@dxos/async';
8
+ import { type Client } from '@dxos/client';
9
+ import { type Space, type SpaceId } from '@dxos/client/echo';
10
+ import { Context } from '@dxos/context';
11
+ import { StatusBar } from '@dxos/plugin-status-bar';
12
+ import { useClient } from '@dxos/react-client';
13
+ import { Icon, useTranslation } from '@dxos/react-ui';
14
+
15
+ import { SPACE_PLUGIN } from '../meta';
16
+
17
+ export const SaveStatus = () => {
18
+ const { t } = useTranslation(SPACE_PLUGIN);
19
+ const client = useClient();
20
+ const [state, setState] = useState<'saved' | 'saving'>('saved');
21
+ useEffect(() => {
22
+ return createClientSaveTracker(client, (state) => {
23
+ setState(state);
24
+ });
25
+ }, []);
26
+
27
+ return (
28
+ <StatusBar.Item title={state === 'saving' ? t('saving label') : t('saved label')}>
29
+ <Icon icon={state === 'saving' ? 'ph--arrows-clockwise--regular' : 'ph--check-circle--regular'} size={4} />
30
+ </StatusBar.Item>
31
+ );
32
+ };
33
+
34
+ const createClientSaveTracker = (client: Client, cb: (state: 'saved' | 'saving') => void) => {
35
+ const unsubscribeCallbacks: Record<SpaceId, UnsubscribeCallback> = {};
36
+ const state: Record<SpaceId, 'saved' | 'saving'> = {};
37
+
38
+ const install = (spaces: Space[]) => {
39
+ for (const space of spaces) {
40
+ if (state[space.id]) {
41
+ continue;
42
+ }
43
+
44
+ state[space.id] = 'saved';
45
+ unsubscribeCallbacks[space.id] = createSpaceSaveTracker(space, (s) => {
46
+ state[space.id] = s;
47
+ cb(Object.values(state).some((s) => s === 'saving') ? 'saving' : 'saved');
48
+ });
49
+ }
50
+ };
51
+ client.spaces.subscribe((spaces) => {
52
+ install(spaces);
53
+ });
54
+ install(client.spaces.get());
55
+
56
+ return () => {
57
+ for (const unsubscribe of Object.values(unsubscribeCallbacks)) {
58
+ unsubscribe();
59
+ }
60
+ };
61
+ };
62
+
63
+ const createSpaceSaveTracker = (space: Space, cb: (state: 'saved' | 'saving') => void): UnsubscribeCallback => {
64
+ const ctx = new Context();
65
+
66
+ void space.waitUntilReady().then(() => {
67
+ if (ctx.disposed) {
68
+ return;
69
+ }
70
+
71
+ let hasUnsavedChanges = false;
72
+ let lastFlushPromise: Promise<void> | undefined;
73
+ space.crud.saveStateChanged.on(ctx, ({ unsavedDocuments }) => {
74
+ hasUnsavedChanges = unsavedDocuments.length > 0;
75
+ });
76
+ space.crud.saveStateChanged.debounce(500).on(ctx, () => {
77
+ if (hasUnsavedChanges) {
78
+ lastFlushPromise = undefined;
79
+ cb('saving');
80
+ } else {
81
+ const flushPromise = space.crud.flush();
82
+ lastFlushPromise = flushPromise;
83
+ void flushPromise.then(() => {
84
+ if (lastFlushPromise === flushPromise) {
85
+ cb('saved');
86
+ }
87
+ });
88
+ }
89
+ });
90
+ });
91
+
92
+ return () => {
93
+ void ctx.dispose();
94
+ };
95
+ };
@@ -6,35 +6,14 @@ import { Command } from '@phosphor-icons/react';
6
6
  import React from 'react';
7
7
 
8
8
  import { Surface } from '@dxos/app-framework';
9
- import { type Action } from '@dxos/plugin-graph';
10
9
  import { SpaceState, type Space } from '@dxos/react-client/echo';
11
- import { Button, Main, useTranslation, toLocalizedString } from '@dxos/react-ui';
10
+ import { Main, useTranslation } from '@dxos/react-ui';
12
11
  import { getSize, mx, topbarBlockPaddingStart } from '@dxos/react-ui-theme';
13
12
  import { ClipboardProvider } from '@dxos/shell/react';
14
13
 
15
14
  import { SpaceMembersSection } from './SpaceMembersSection';
16
15
  import { SPACE_PLUGIN } from '../../meta';
17
16
 
18
- const _InFlowSpaceActions = ({ actionsMap }: { actionsMap: Record<string, Action> }) => {
19
- const { t } = useTranslation(SPACE_PLUGIN);
20
- return (
21
- <section className='mbe-4 col-start-2 col-end-4 md:col-end-7 grid gap-2 auto-rows-min grid-cols-[repeat(auto-fill,minmax(8rem,1fr))]'>
22
- {Object.entries(actionsMap)
23
- .filter(([_, { properties }]) => properties?.mainAreaDisposition === 'in-flow')
24
- .map(([actionId, { data: invoke, properties }]) => {
25
- const Icon = properties?.icon;
26
- const label = properties?.label;
27
- return (
28
- <Button key={actionId} classNames='block text-center plb-2 font-normal'>
29
- {Icon && <Icon size={5} className='mli-auto' />}
30
- <p>{toLocalizedString(label, t)}</p>
31
- </Button>
32
- );
33
- })}
34
- </section>
35
- );
36
- };
37
-
38
17
  const KeyShortcuts = () => {
39
18
  const { t } = useTranslation(SPACE_PLUGIN);
40
19
  return (
@@ -110,7 +110,7 @@ export type MemberPresenceProps = ThemedClassName<{
110
110
 
111
111
  export const FullPresence = (props: MemberPresenceProps) => {
112
112
  const { size = 9, onMemberClick } = props;
113
- const members = useDefaultValue(props.members, []);
113
+ const members = useDefaultValue(props.members, () => []);
114
114
 
115
115
  if (members.length === 0) {
116
116
  return null;
@@ -4,16 +4,17 @@
4
4
 
5
5
  import React from 'react';
6
6
 
7
- import { useIntentDispatcher } from '@dxos/app-framework';
7
+ import { useIntentDispatcher, useResolvePlugins } from '@dxos/app-framework';
8
8
  import { SettingsValue } from '@dxos/plugin-settings';
9
- import { Input, useTranslation } from '@dxos/react-ui';
9
+ import { Input, Select, toLocalizedString, useTranslation } from '@dxos/react-ui';
10
10
 
11
11
  import { SpaceAction, SPACE_PLUGIN } from '../meta';
12
- import { type SpaceSettingsProps } from '../types';
12
+ import { parseSpaceInitPlugin, type SpaceSettingsProps } from '../types';
13
13
 
14
14
  export const SpaceSettings = ({ settings }: { settings: SpaceSettingsProps }) => {
15
15
  const { t } = useTranslation(SPACE_PLUGIN);
16
16
  const dispatch = useIntentDispatcher();
17
+ const plugins = useResolvePlugins(parseSpaceInitPlugin);
17
18
 
18
19
  return (
19
20
  <>
@@ -29,6 +30,34 @@ export const SpaceSettings = ({ settings }: { settings: SpaceSettingsProps }) =>
29
30
  }
30
31
  />
31
32
  </SettingsValue>
33
+
34
+ <SettingsValue label={t('default on space create label')}>
35
+ <Select.Root
36
+ value={settings.onSpaceCreate}
37
+ onValueChange={(value) => {
38
+ settings.onSpaceCreate = value;
39
+ }}
40
+ >
41
+ <Select.TriggerButton />
42
+ <Select.Portal>
43
+ <Select.Content>
44
+ <Select.Viewport>
45
+ {plugins.map(
46
+ ({
47
+ provides: {
48
+ space: { onSpaceCreate },
49
+ },
50
+ }) => (
51
+ <Select.Option key={onSpaceCreate.action} value={onSpaceCreate.action}>
52
+ {toLocalizedString(onSpaceCreate.label, t)}
53
+ </Select.Option>
54
+ ),
55
+ )}
56
+ </Select.Viewport>
57
+ </Select.Content>
58
+ </Select.Portal>
59
+ </Select.Root>
60
+ </SettingsValue>
32
61
  </>
33
62
  );
34
63
  };
@@ -0,0 +1,62 @@
1
+ //
2
+ // Copyright 2023 DXOS.org
3
+ //
4
+
5
+ import '@dxos-theme';
6
+
7
+ import React from 'react';
8
+
9
+ import { SpaceId } from '@dxos/keys';
10
+ import { withTheme, withLayout } from '@dxos/storybook-utils';
11
+
12
+ import { SyncStatusDetail, SyncStatusIndicator } from './SyncStatus';
13
+ import { getSyncSummary, type SpaceSyncStateMap } from './types';
14
+ import translations from '../../translations';
15
+
16
+ const Story = (props: any) => {
17
+ return (
18
+ <div className='flex flex-col-reverse p-4 '>
19
+ <SyncStatusIndicator {...props} />
20
+ </div>
21
+ );
22
+ };
23
+
24
+ export default {
25
+ title: 'plugin-space/SyncStatusIndicator',
26
+ decorators: [withTheme, withLayout({ fullscreen: true })],
27
+ component: SyncStatusIndicator,
28
+ parameters: { translations },
29
+ render: Story,
30
+ };
31
+
32
+ const random = ({ min, max }: { min: number; max: number }) => min + Math.floor(Math.random() * (max - min));
33
+
34
+ const state: SpaceSyncStateMap = Array.from({ length: 5 }).reduce<SpaceSyncStateMap>((map) => {
35
+ const total = random({ min: 10, max: 500 });
36
+ const haveLocal = random({ min: 0, max: total });
37
+ const haveRemote = random({ min: 0, max: total });
38
+ map[SpaceId.random()] = {
39
+ localDocumentCount: haveLocal,
40
+ remoteDocumentCount: haveRemote,
41
+ missingOnLocal: total - haveLocal,
42
+ missingOnRemote: total - haveRemote,
43
+ differentDocuments: 0,
44
+ };
45
+
46
+ return map;
47
+ }, {});
48
+
49
+ export const Default = {
50
+ args: {
51
+ state,
52
+ },
53
+ };
54
+
55
+ export const Detail = {
56
+ render: SyncStatusDetail,
57
+ args: {
58
+ state,
59
+ summary: getSyncSummary(state),
60
+ classNames: 'm-2 w-[200px] border border-separator rounded-md',
61
+ },
62
+ };
@@ -0,0 +1,188 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { type HTMLAttributes, useEffect, useState } from 'react';
6
+
7
+ import { StatusBar } from '@dxos/plugin-status-bar';
8
+ import { Icon, Popover, useTranslation } from '@dxos/react-ui';
9
+ import { type ThemedClassName } from '@dxos/react-ui';
10
+ import { SyntaxHighlighter } from '@dxos/react-ui-syntax-highlighter';
11
+ import { mx } from '@dxos/react-ui-theme';
12
+
13
+ import { type Progress, type PeerSyncState, type SpaceSyncStateMap, getSyncSummary, useSyncState } from './types';
14
+ import { SPACE_PLUGIN } from '../../meta';
15
+
16
+ const SYNC_STALLED_TIMEOUT = 5_000;
17
+
18
+ const styles = {
19
+ barBg: 'bg-neutral-50 dark:bg-green-900 text-black',
20
+ barFg: 'bg-neutral-100 bg-green-500',
21
+ barHover: 'dark:hover:bg-green-500',
22
+ };
23
+
24
+ export const SyncStatus = () => {
25
+ const state = useSyncState();
26
+ return <SyncStatusIndicator state={state} />;
27
+ };
28
+
29
+ export const SyncStatusIndicator = ({ state }: { state: SpaceSyncStateMap }) => {
30
+ const summary = getSyncSummary(state);
31
+ const offline = false;
32
+
33
+ const needsToUpload = summary.differentDocuments > 0 || summary.missingOnRemote > 0;
34
+ const needsToDownload = summary.differentDocuments > 0 || summary.missingOnLocal > 0;
35
+ const [classNames, setClassNames] = useState<string>();
36
+ useEffect(() => {
37
+ setClassNames(undefined);
38
+ if (!needsToUpload && !needsToDownload) {
39
+ return;
40
+ }
41
+
42
+ const t = setTimeout(() => {
43
+ setClassNames('text-orange-500');
44
+ }, SYNC_STALLED_TIMEOUT);
45
+ return () => clearTimeout(t);
46
+ }, [needsToUpload, needsToDownload]);
47
+
48
+ return (
49
+ <StatusBar.Item>
50
+ <Popover.Root>
51
+ <Popover.Trigger>
52
+ <Icon
53
+ icon={
54
+ offline
55
+ ? 'ph--cloud-x--regular'
56
+ : needsToUpload
57
+ ? 'ph--cloud-arrow-up--regular'
58
+ : needsToDownload
59
+ ? 'ph--cloud-arrow-down--regular'
60
+ : 'ph--cloud-check--regular'
61
+ }
62
+ size={4}
63
+ classNames={classNames}
64
+ />
65
+ </Popover.Trigger>
66
+ <Popover.Content>
67
+ <SyncStatusDetail state={state} summary={summary} debug={false} />
68
+ </Popover.Content>
69
+ </Popover.Root>
70
+ </StatusBar.Item>
71
+ );
72
+ };
73
+
74
+ export const SyncStatusDetail = ({
75
+ classNames,
76
+ state,
77
+ summary,
78
+ debug,
79
+ }: ThemedClassName<{
80
+ state: SpaceSyncStateMap;
81
+ summary: PeerSyncState;
82
+ debug?: boolean;
83
+ }>) => {
84
+ const { t } = useTranslation(SPACE_PLUGIN);
85
+ const entries = Object.entries(state).sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
86
+
87
+ // TODO(burdon): Normalize to max document count?
88
+ return (
89
+ <div className={mx('flex flex-col text-xs min-w-[16rem]', classNames)}>
90
+ <h1 className='p-2'>{t('sync status title')}</h1>
91
+ <div className='flex flex-col gap-[2px] my-[2px]'>
92
+ {entries.map(([spaceId, state]) => (
93
+ <SpaceRow key={spaceId} spaceId={spaceId} state={state} />
94
+ ))}
95
+ </div>
96
+ {debug && <SyntaxHighlighter language='json'>{JSON.stringify(summary, null, 2)}</SyntaxHighlighter>}
97
+ </div>
98
+ );
99
+ };
100
+
101
+ const useActive = (count: number) => {
102
+ const [current, setCurrent] = useState(count);
103
+ const [active, setActive] = useState(false);
104
+ useEffect(() => {
105
+ let t: NodeJS.Timeout | undefined;
106
+ if (count !== current) {
107
+ setActive(true);
108
+ setCurrent(count);
109
+ t && clearTimeout(t);
110
+ t = setTimeout(() => {
111
+ setActive(false);
112
+ }, SYNC_STALLED_TIMEOUT);
113
+ }
114
+
115
+ return () => {
116
+ setActive(false);
117
+ clearTimeout(t);
118
+ };
119
+ }, [count, current]);
120
+ return active;
121
+ };
122
+
123
+ const SpaceRow = ({
124
+ spaceId,
125
+ state: { localDocumentCount, remoteDocumentCount, missingOnLocal, missingOnRemote },
126
+ }: {
127
+ spaceId: string;
128
+ state: PeerSyncState;
129
+ }) => {
130
+ const downActive = useActive(localDocumentCount);
131
+ const upActive = useActive(remoteDocumentCount);
132
+
133
+ return (
134
+ <div
135
+ className={mx('flex items-center mx-[2px] gap-[2px] cursor-pointer', styles.barHover)}
136
+ title={spaceId}
137
+ onClick={() => {
138
+ void navigator.clipboard.writeText(spaceId);
139
+ }}
140
+ >
141
+ <Icon
142
+ icon='ph--arrow-fat-line-left--regular'
143
+ size={3}
144
+ classNames={mx(downActive && 'animate-[pulse_1s_infinite]')}
145
+ />
146
+ <Candle
147
+ up={{ count: remoteDocumentCount, total: remoteDocumentCount + missingOnRemote }}
148
+ down={{ count: localDocumentCount, total: localDocumentCount + missingOnLocal }}
149
+ title={spaceId}
150
+ />
151
+ <Icon
152
+ icon='ph--arrow-fat-line-right--regular'
153
+ size={3}
154
+ classNames={mx(upActive && 'animate-[pulse_1s_step-start_infinite]')}
155
+ />
156
+ </div>
157
+ );
158
+ };
159
+
160
+ type CandleProps = ThemedClassName<Pick<HTMLAttributes<HTMLDivElement>, 'title'>> & { up: Progress; down: Progress };
161
+
162
+ const Candle = ({ classNames, up, down }: CandleProps) => {
163
+ return (
164
+ <div className={mx('grid grid-cols-[1fr_2rem_1fr] w-full h-3', classNames)}>
165
+ <Bar classNames='justify-end' {...up} />
166
+ <div className='relative'>
167
+ <div className={mx('absolute inset-0 flex items-center justify-center text-xs', styles.barBg)}>{up.total}</div>
168
+ </div>
169
+ <Bar {...down} />
170
+ </div>
171
+ );
172
+ };
173
+
174
+ const Bar = ({ classNames, count, total }: ThemedClassName<Progress>) => {
175
+ let p = (count / total) * 100;
176
+ if (count < total) {
177
+ p = Math.min(p, 95);
178
+ }
179
+
180
+ return (
181
+ <div className={mx('relative flex w-full', styles.barBg, classNames)}>
182
+ <div className={mx('shrink-0', styles.barFg)} style={{ width: `${p}%` }}></div>
183
+ {count !== total && (
184
+ <div className='absolute top-0 bottom-0 flex items-center mx-0.5 text-black text-xs'>{count}</div>
185
+ )}
186
+ </div>
187
+ );
188
+ };
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ export * from './SyncStatus';
@@ -0,0 +1,77 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import { useEffect, useState } from 'react';
6
+
7
+ import { type Space, type SpaceId, type SpaceSyncState } from '@dxos/client/echo';
8
+ import { Context } from '@dxos/context';
9
+ import { EdgeService } from '@dxos/protocols';
10
+ import { useClient } from '@dxos/react-client';
11
+
12
+ export type Progress = { count: number; total: number };
13
+
14
+ export type PeerSyncState = Omit<SpaceSyncState.PeerState, 'peerId'>;
15
+
16
+ export type SpaceSyncStateMap = Record<SpaceId, PeerSyncState>;
17
+
18
+ export const createEmptyEdgeSyncState = (): PeerSyncState => ({
19
+ missingOnLocal: 0,
20
+ missingOnRemote: 0,
21
+ localDocumentCount: 0,
22
+ remoteDocumentCount: 0,
23
+ differentDocuments: 0,
24
+ });
25
+
26
+ export const getSyncSummary = (syncMap: SpaceSyncStateMap): PeerSyncState => {
27
+ return Object.entries(syncMap).reduce<PeerSyncState>((summary, [_spaceId, peerState]) => {
28
+ summary.missingOnLocal += peerState.missingOnLocal;
29
+ summary.missingOnRemote += peerState.missingOnRemote;
30
+ summary.localDocumentCount += peerState.localDocumentCount;
31
+ summary.remoteDocumentCount += peerState.remoteDocumentCount;
32
+ summary.differentDocuments += peerState.differentDocuments;
33
+ return summary;
34
+ }, createEmptyEdgeSyncState());
35
+ };
36
+
37
+ const isEdgePeerId = (peerId: string, spaceId: SpaceId) =>
38
+ peerId.startsWith(`${EdgeService.AUTOMERGE_REPLICATOR}:${spaceId}`);
39
+
40
+ /**
41
+ * Hook Subscribes to sync state for each space.
42
+ */
43
+ export const useSyncState = (): SpaceSyncStateMap => {
44
+ const client = useClient();
45
+ const [spaceState, setSpaceState] = useState<SpaceSyncStateMap>({});
46
+
47
+ useEffect(() => {
48
+ const ctx = new Context();
49
+ const createSubscriptions = (spaces: Space[]) => {
50
+ for (const space of spaces) {
51
+ if (spaceState[space.id]) {
52
+ continue;
53
+ }
54
+
55
+ ctx.onDispose(
56
+ space.crud.subscribeToSyncState(ctx, ({ peers = [] }) => {
57
+ const syncState = peers.find((state) => isEdgePeerId(state.peerId, space.id));
58
+ if (syncState) {
59
+ setSpaceState((spaceState) => ({ ...spaceState, [space.id]: syncState }));
60
+ }
61
+ }),
62
+ );
63
+ }
64
+ };
65
+
66
+ createSubscriptions(client.spaces.get());
67
+ client.spaces.subscribe((spaces) => {
68
+ createSubscriptions(spaces);
69
+ });
70
+
71
+ return () => {
72
+ void ctx.dispose();
73
+ };
74
+ }, [client]);
75
+
76
+ return spaceState;
77
+ };
@@ -5,8 +5,7 @@
5
5
  export * from './AwaitingObject';
6
6
  export * from './CollectionMain';
7
7
  export * from './CollectionSection';
8
- export * from './EmptySpace';
9
- export * from './EmptyTree';
8
+ export * from './FallbackSettings';
10
9
  export * from './MenuFooter';
11
10
  export * from './MissingObject';
12
11
  export * from './PersistenceStatus';
@@ -16,3 +15,5 @@ export * from './ShareSpaceButton';
16
15
  export * from './SpaceMain';
17
16
  export * from './SpacePresence';
18
17
  export * from './SpaceSettings';
18
+ export * from './SaveStatus';
19
+ export * from './SyncStatus';
package/src/meta.ts CHANGED
@@ -2,6 +2,8 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ import { type PluginMeta } from '@dxos/app-framework';
6
+
5
7
  export const SPACE_PLUGIN = 'dxos.org/plugin/space';
6
8
  export const SPACE_PLUGIN_SHORT_ID = 'space';
7
9
 
@@ -9,7 +11,7 @@ export default {
9
11
  id: SPACE_PLUGIN,
10
12
  shortId: SPACE_PLUGIN_SHORT_ID,
11
13
  name: 'Spaces',
12
- };
14
+ } satisfies PluginMeta;
13
15
 
14
16
  const SPACE_ACTION = `${SPACE_PLUGIN}/action`;
15
17
  export enum SpaceAction {
@@ -86,6 +86,10 @@ export default [
86
86
  'remove deleted objects label': 'Cleanup',
87
87
  'remove deleted objects alt': 'Permanently remove deleted objects to free up space.',
88
88
  'copy link label': 'Copy link',
89
+ 'default on space create label': 'On space create',
90
+ 'sync status title': 'Sync status',
91
+ 'name label': 'Name',
92
+ 'name placeholder': 'Name',
89
93
  },
90
94
  },
91
95
  },
@@ -12,5 +12,5 @@ export class CollectionType extends TypedObject({ typename: 'dxos.org/type/Colle
12
12
  // This also leaves open a future where this key could be changed to allow for multiple stack views per section.
13
13
  // TODO(wittjosiah): Any way to make this more type safe?
14
14
  // TODO(wittjosiah): Should the views be separate objects or just be schemas for view data in this record?
15
- views: S.mutable(S.Record(S.String, ref(Expando))),
15
+ views: S.mutable(S.Record({ key: S.String, value: ref(Expando) })),
16
16
  }) {}
@@ -34,9 +34,18 @@ export const ActorSchema = S.mutable(
34
34
 
35
35
  export type ActorType = S.Schema.Type<typeof ActorSchema>;
36
36
 
37
+ export enum MessageState {
38
+ NONE = 0,
39
+ ARCHIVED = 1,
40
+ DELETED = 2,
41
+ SPAM = 3,
42
+ }
43
+
37
44
  export class MessageType extends TypedObject({ typename: 'dxos.org/type/Message', version: '0.1.0' })({
38
45
  /** ISO date string when the message was sent. */
39
46
  timestamp: S.String,
47
+ /** Message state. */
48
+ state: S.optional(S.Enums(MessageState)),
40
49
  /** Identity of the message sender. */
41
50
  sender: ActorSchema,
42
51
  /** Text content of the message. */
@@ -44,7 +53,7 @@ export class MessageType extends TypedObject({ typename: 'dxos.org/type/Message'
44
53
  /** Non-text content sent with a message (e.g. files, polls, etc.) */
45
54
  parts: S.optional(S.mutable(S.Array(ref(Expando)))),
46
55
  /** Custom properties for specific message types (e.g. email subject or cc fields). */
47
- properties: S.optional(S.mutable(S.Record(S.String, S.Any))),
56
+ properties: S.optional(S.mutable(S.Record({ key: S.String, value: S.Any }))),
48
57
  // TODO(wittjosiah): Add read status:
49
58
  // - Read receipts need to be per space member.
50
59
  // - Read receipts don't need to be added to schema until they being implemented.
@@ -57,6 +66,7 @@ export const ThreadStatus = S.Union(S.Literal('staged'), S.Literal('active'), S.
57
66
 
58
67
  export class ThreadType extends TypedObject({ typename: 'dxos.org/type/Thread', version: '0.1.0' })({
59
68
  name: S.optional(S.String),
69
+ /** AM cursor-range: 'from:to'. */
60
70
  anchor: S.optional(S.String),
61
71
  status: S.optional(ThreadStatus),
62
72
  messages: S.mutable(S.Array(ref(MessageType))),
@@ -10,10 +10,12 @@ import type {
10
10
  SettingsProvides,
11
11
  SurfaceProvides,
12
12
  TranslationsProvides,
13
+ Plugin,
13
14
  } from '@dxos/app-framework';
14
15
  import { type Expando } from '@dxos/echo-schema';
15
16
  import { type SchemaProvides } from '@dxos/plugin-client';
16
17
  import { type PublicKey } from '@dxos/react-client';
18
+ import { type Label } from '@dxos/react-ui';
17
19
  import { type ComplexMap } from '@dxos/util';
18
20
 
19
21
  export const SPACE_DIRECTORY_HANDLE = 'dxos.org/plugin/space/directory';
@@ -53,7 +55,29 @@ export type PluginState = {
53
55
  sdkMigrationRunning: Record<string, boolean>;
54
56
  };
55
57
 
56
- export type SpaceSettingsProps = { showHidden?: boolean };
58
+ export type SpaceSettingsProps = {
59
+ /**
60
+ * Show closed spaces.
61
+ */
62
+ showHidden?: boolean;
63
+
64
+ /**
65
+ * Action to perform when a space is created.
66
+ */
67
+ onSpaceCreate?: string;
68
+ };
69
+
70
+ export type SpaceInitProvides = {
71
+ space: {
72
+ onSpaceCreate: {
73
+ label: Label;
74
+ action: string;
75
+ };
76
+ };
77
+ };
78
+
79
+ export const parseSpaceInitPlugin = (plugin: Plugin) =>
80
+ typeof (plugin.provides as any).space?.onSpaceCreate === 'object' ? (plugin as Plugin<SpaceInitProvides>) : undefined;
57
81
 
58
82
  export type SpacePluginProvides = SurfaceProvides &
59
83
  IntentResolverProvides &