@dxos/plugin-thread 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 (92) hide show
  1. package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs → ThreadArticle-GBPIH47W.mjs} +3 -3
  2. package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs → ThreadMain-PGF7W5EV.mjs} +3 -3
  3. package/dist/lib/browser/chunk-5G3KL6TC.mjs +16 -0
  4. package/dist/lib/browser/chunk-5G3KL6TC.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-GKPZHUT3.mjs → chunk-JFQZ5W2I.mjs} +2 -2
  6. package/dist/lib/browser/{chunk-GKPZHUT3.mjs.map → chunk-JFQZ5W2I.mjs.map} +1 -1
  7. package/dist/lib/browser/{chunk-P7ZE5N5M.mjs → chunk-O4YRHIZC.mjs} +13 -11
  8. package/dist/lib/browser/chunk-O4YRHIZC.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +411 -393
  10. package/dist/lib/browser/index.mjs.map +4 -4
  11. package/dist/lib/browser/meta.json +1 -1
  12. package/dist/lib/browser/meta.mjs +1 -1
  13. package/dist/lib/browser/types.mjs +2 -2
  14. package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs → ThreadArticle-GH5UBWFU.cjs} +7 -7
  15. package/dist/lib/node/{ThreadMain-XJX75GHG.cjs → ThreadMain-CO6I3DNX.cjs} +7 -7
  16. package/dist/lib/node/{chunk-5PZNPPQF.cjs → chunk-FDYRG7HE.cjs} +65 -63
  17. package/dist/lib/node/chunk-FDYRG7HE.cjs.map +7 -0
  18. package/dist/lib/node/{chunk-D5GOVXHS.cjs → chunk-FOZUQZF4.cjs} +6 -6
  19. package/dist/lib/node/{chunk-D5GOVXHS.cjs.map → chunk-FOZUQZF4.cjs.map} +1 -1
  20. package/dist/lib/node/{chunk-BWBONZGU.cjs → chunk-JIJ7Z6NO.cjs} +7 -22
  21. package/dist/lib/node/chunk-JIJ7Z6NO.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +442 -428
  23. package/dist/lib/node/index.cjs.map +4 -4
  24. package/dist/lib/node/meta.cjs +4 -4
  25. package/dist/lib/node/meta.cjs.map +1 -1
  26. package/dist/lib/node/meta.json +1 -1
  27. package/dist/lib/node/types.cjs +3 -3
  28. package/dist/lib/node/types.cjs.map +1 -1
  29. package/dist/lib/node-esm/ThreadArticle-ZTYD52TR.mjs +19 -0
  30. package/dist/lib/node-esm/ThreadArticle-ZTYD52TR.mjs.map +7 -0
  31. package/dist/lib/node-esm/ThreadMain-LEHGOF7R.mjs +27 -0
  32. package/dist/lib/node-esm/ThreadMain-LEHGOF7R.mjs.map +7 -0
  33. package/dist/lib/node-esm/chunk-DC5ZA6H5.mjs +18 -0
  34. package/dist/lib/node-esm/chunk-DC5ZA6H5.mjs.map +7 -0
  35. package/dist/lib/node-esm/chunk-XOVJ65DG.mjs +413 -0
  36. package/dist/lib/node-esm/chunk-XOVJ65DG.mjs.map +7 -0
  37. package/dist/lib/node-esm/chunk-ZXIPFXSA.mjs +21 -0
  38. package/dist/lib/node-esm/chunk-ZXIPFXSA.mjs.map +7 -0
  39. package/dist/lib/node-esm/index.mjs +1121 -0
  40. package/dist/lib/node-esm/index.mjs.map +7 -0
  41. package/dist/lib/node-esm/meta.json +1 -0
  42. package/dist/lib/node-esm/meta.mjs +12 -0
  43. package/dist/lib/node-esm/meta.mjs.map +7 -0
  44. package/dist/lib/node-esm/types.mjs +9 -0
  45. package/dist/lib/node-esm/types.mjs.map +7 -0
  46. package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
  47. package/dist/types/src/components/ChatContainer.d.ts.map +1 -1
  48. package/dist/types/src/components/CommentContainer.d.ts +1 -1
  49. package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
  50. package/dist/types/src/components/CommentsContainer.d.ts +1 -5
  51. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  52. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  53. package/dist/types/src/components/ThreadComplementary.d.ts +10 -0
  54. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -0
  55. package/dist/types/src/components/index.d.ts +3 -2
  56. package/dist/types/src/components/index.d.ts.map +1 -1
  57. package/dist/types/src/components/testing.d.ts +2 -0
  58. package/dist/types/src/components/testing.d.ts.map +1 -1
  59. package/dist/types/src/extensions/index.d.ts +2 -0
  60. package/dist/types/src/extensions/index.d.ts.map +1 -0
  61. package/dist/types/src/extensions/threads.d.ts +9 -0
  62. package/dist/types/src/extensions/threads.d.ts.map +1 -0
  63. package/dist/types/src/meta.d.ts +3 -9
  64. package/dist/types/src/meta.d.ts.map +1 -1
  65. package/dist/types/src/types.d.ts +6 -1
  66. package/dist/types/src/types.d.ts.map +1 -1
  67. package/package.json +40 -37
  68. package/src/ThreadPlugin.tsx +120 -310
  69. package/src/components/ChatContainer.tsx +7 -8
  70. package/src/components/CommentContainer.tsx +18 -19
  71. package/src/components/Comments.stories.tsx +2 -2
  72. package/src/components/CommentsContainer.tsx +18 -31
  73. package/src/components/MessageContainer.tsx +2 -1
  74. package/src/components/ThreadComplementary.tsx +113 -0
  75. package/src/components/index.ts +3 -2
  76. package/src/extensions/index.ts +5 -0
  77. package/src/extensions/threads.ts +117 -0
  78. package/src/meta.ts +15 -0
  79. package/src/types.ts +8 -3
  80. package/dist/lib/browser/chunk-5IBJO53M.mjs +0 -21
  81. package/dist/lib/browser/chunk-5IBJO53M.mjs.map +0 -7
  82. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  83. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  84. package/dist/lib/node/chunk-BWBONZGU.cjs.map +0 -7
  85. package/dist/types/src/types.test.d.ts +0 -2
  86. package/dist/types/src/types.test.d.ts.map +0 -1
  87. package/src/meta.tsx +0 -20
  88. package/src/types.test.ts +0 -13
  89. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-GBPIH47W.mjs.map} +0 -0
  90. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-PGF7W5EV.mjs.map} +0 -0
  91. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-GH5UBWFU.cjs.map} +0 -0
  92. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-CO6I3DNX.cjs.map} +0 -0
@@ -2,85 +2,56 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Chat, type IconProps } from '@phosphor-icons/react';
6
- import { computed, effect } from '@preact/signals-core';
7
5
  import React from 'react';
8
6
 
9
7
  import {
10
8
  type IntentPluginProvides,
11
- LayoutAction,
12
- NavigationAction,
13
9
  type LocationProvides,
10
+ NavigationAction,
14
11
  type Plugin,
15
12
  type PluginDefinition,
13
+ isLayoutParts,
16
14
  parseIntentPlugin,
17
15
  parseNavigationPlugin,
18
- resolvePlugin,
19
- parseGraphPlugin,
20
- SLUG_PATH_SEPARATOR,
21
- SLUG_COLLECTION_INDICATOR,
22
- isLayoutParts,
23
16
  parseMetadataResolverPlugin,
24
- type IntentDispatcher,
25
- useResolvePlugins,
17
+ resolvePlugin,
26
18
  } from '@dxos/app-framework';
27
19
  import { type UnsubscribeCallback } from '@dxos/async';
28
- import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
29
- import { create } from '@dxos/echo-schema';
20
+ import { create, type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
30
21
  import { LocalStorageStore } from '@dxos/local-storage';
31
- import { type AttentionPluginProvides, parseAttentionPlugin } from '@dxos/plugin-attention';
22
+ import { log } from '@dxos/log';
32
23
  import { parseClientPlugin } from '@dxos/plugin-client';
33
- import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
34
- import { DocumentType } from '@dxos/plugin-markdown/types';
24
+ import { type ActionGroup, createExtension, isActionGroup, toSignal } from '@dxos/plugin-graph';
35
25
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
36
26
  import { SpaceAction } from '@dxos/plugin-space';
37
27
  import { ThreadType, MessageType, ChannelType } from '@dxos/plugin-space/types';
38
- import {
39
- getSpace,
40
- getTextInRange,
41
- Filter,
42
- createDocAccessor,
43
- fullyQualifiedId,
44
- loadObjectReferences,
45
- } from '@dxos/react-client/echo';
46
- import { ScrollArea } from '@dxos/react-ui';
47
- import { createAttendableAttributes } from '@dxos/react-ui-attention';
48
- import { comments, createExternalCommentSync, listener } from '@dxos/react-ui-editor';
28
+ import { getSpace, fullyQualifiedId, loadObjectReferences, parseFullyQualifiedId } from '@dxos/react-client/echo';
49
29
  import { translations as threadTranslations } from '@dxos/react-ui-thread';
50
- import { nonNullable } from '@dxos/util';
51
30
 
52
31
  import {
53
- ThreadMain,
54
- ThreadSettings,
55
- CommentsContainer,
56
- CommentsHeading,
57
32
  ChatContainer,
58
33
  ChatHeading,
59
34
  ThreadArticle,
35
+ ThreadComplementary,
36
+ ThreadMain,
37
+ ThreadSettings,
60
38
  } from './components';
39
+ import { threads } from './extensions';
61
40
  import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
62
41
  import translations from './translations';
63
- import { ThreadAction, type ThreadProvides, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
64
-
65
- type ThreadState = {
66
- /** An in-memory staging area for threads that are being drafted. */
67
- staging: Record<string, ThreadType[]>;
68
- current?: string | undefined;
69
- focus?: boolean;
70
- };
42
+ import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps, type ThreadState } from './types';
71
43
 
72
44
  type SubjectId = string;
45
+
73
46
  const initialViewState = { showResolvedThreads: false };
74
- type ViewStore = Record<SubjectId, typeof initialViewState>;
75
47
 
76
- // TODO(Zan): More robust runtime check.
77
- const providesThreadsConfig = (plugin: any): Plugin<ThreadProvides<any>> | undefined =>
78
- 'thread' in plugin.provides ? (plugin as Plugin<ThreadProvides<any>>) : undefined;
48
+ type ViewStore = Record<SubjectId, typeof initialViewState>;
79
49
 
80
50
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
51
+ // NOTE(burdon): Review/discuss CursorConverter semantics.
81
52
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
82
53
  const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
83
- const state = create<ThreadState>({ staging: {} });
54
+ const state = create<ThreadState>({ drafts: {} });
84
55
 
85
56
  const viewStore = create<ViewStore>({});
86
57
  const getViewState = (subjectId: string) => {
@@ -90,10 +61,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
90
61
  return viewStore[subjectId];
91
62
  };
92
63
 
93
- let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
94
64
  let navigationPlugin: Plugin<LocationProvides> | undefined;
95
65
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
96
- let dispatch: IntentDispatcher | undefined;
97
66
 
98
67
  const unsubscribeCallbacks = [] as UnsubscribeCallback[];
99
68
 
@@ -102,45 +71,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
102
71
  ready: async (plugins) => {
103
72
  settings.prop({ key: 'standalone', type: LocalStorageStore.bool({ allowUndefined: true }) });
104
73
 
105
- attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
106
74
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
107
75
  intentPlugin = resolvePlugin(plugins, parseIntentPlugin)!;
108
- dispatch = intentPlugin?.provides.intent.dispatch;
109
-
110
- const graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
111
- const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
112
- if (!client) {
113
- return;
114
- }
115
-
116
- // TODO(wittjosiah): This is a hack to make standalone threads work in the c11y sidebar.
117
- // This should have a better solution when deck is introduced.
118
- const channelsQuery = client.spaces.query(Filter.schema(ChannelType));
119
- const queryUnsubscribe = channelsQuery.subscribe();
120
- const unsubscribe = effect(() => {
121
- const attention = attentionPlugin?.provides.attention;
122
- if (!attention?.attended) {
123
- return;
124
- }
125
-
126
- const firstAttendedNodeWithComments = Array.from(attention.attended)
127
- .map((id) => graphPlugin?.provides.graph.findNode(id))
128
- .find((node) => node?.data instanceof DocumentType && (node.data.threads?.length ?? 0) > 0);
129
-
130
- if (firstAttendedNodeWithComments) {
131
- void intentPlugin?.provides.intent.dispatch({
132
- action: NavigationAction.OPEN,
133
- data: {
134
- activeParts: {
135
- complementary: `${firstAttendedNodeWithComments.id}${SLUG_PATH_SEPARATOR}comments`,
136
- },
137
- },
138
- });
139
- }
140
- });
141
-
142
- unsubscribeCallbacks.push(queryUnsubscribe);
143
- unsubscribeCallbacks.push(unsubscribe);
144
76
  },
145
77
  unload: async () => {
146
78
  unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
@@ -151,8 +83,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
151
83
  records: {
152
84
  [ChannelType.typename]: {
153
85
  placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
154
- icon: (props: IconProps) => <Chat {...props} />,
155
- iconSymbol: 'ph--chat--regular',
86
+ icon: 'ph--chat--regular',
156
87
  // TODO(wittjosiah): Move out of metadata.
157
88
  loadReferences: (channel: ChannelType) => loadObjectReferences(channel, (channel) => channel.threads),
158
89
  },
@@ -194,35 +125,46 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
194
125
  createExtension({
195
126
  id: `${THREAD_PLUGIN}/comments-for-subject`,
196
127
  resolver: ({ id }) => {
128
+ // TODO(Zan): Find util (or make one).
197
129
  if (!id.endsWith('~comments')) {
198
130
  return;
199
131
  }
200
132
 
201
- // TODO(Zan): Find util (or make one)
202
- const docId = id.split('~').at(0);
203
- const [spaceId, objectId] = docId?.split(':') ?? [];
133
+ const [subjectId] = id.split('~');
134
+ const [spaceId, objectId] = parseFullyQualifiedId(subjectId);
204
135
  const space = client.spaces.get().find((space) => space.id === spaceId);
205
- const doc = space?.db.getObjectById(objectId);
206
- if (!doc || !docId) {
136
+ const object = toSignal(
137
+ (onChange) => {
138
+ const timeout = setTimeout(async () => {
139
+ await space?.db.loadObjectById(objectId);
140
+ onChange();
141
+ });
142
+
143
+ return () => clearTimeout(timeout);
144
+ },
145
+ () => space?.db.getObjectById(objectId),
146
+ subjectId,
147
+ );
148
+ if (!object || !subjectId) {
207
149
  return;
208
150
  }
209
151
 
210
- const docMeta = metadataResolver(getTypename(doc) ?? '');
211
- const label = docMeta.label?.(doc) ||
212
- doc.name ||
213
- docMeta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
214
- // const label = docMeta.label?.(doc) || doc.name || docMeta.placeholder;
152
+ const meta = metadataResolver(getTypename(object) ?? '');
153
+ const label = meta.label?.(object) ||
154
+ object.name ||
155
+ meta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
215
156
 
216
- const viewState = getViewState(docId);
157
+ const viewState = getViewState(subjectId);
217
158
 
218
159
  return {
219
160
  id,
220
161
  type: 'orphan-comments-for-subject',
221
- data: doc,
162
+ data: null,
222
163
  properties: {
223
- icon: meta.iconComponent,
164
+ icon: 'ph--chat-text--regular',
224
165
  label,
225
166
  showResolvedThreads: viewState.showResolvedThreads,
167
+ object,
226
168
  },
227
169
  };
228
170
  },
@@ -256,6 +198,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
256
198
  menuItemType: 'toggle',
257
199
  isChecked: viewState.showResolvedThreads,
258
200
  testId: 'threadPlugin.toggleShowResolved',
201
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
259
202
  },
260
203
  },
261
204
  ];
@@ -287,8 +230,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
287
230
  },
288
231
  properties: {
289
232
  label: ['create channel label', { ns: THREAD_PLUGIN }],
290
- icon: (props: IconProps) => <Chat {...props} />,
291
- iconSymbol: 'ph--chat--regular',
233
+ icon: 'ph--chat--regular',
292
234
  testId: 'threadPlugin.createObject',
293
235
  },
294
236
  },
@@ -311,11 +253,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
311
253
  return data.plugin === meta.id ? <ThreadSettings settings={settings.values} /> : null;
312
254
  }
313
255
 
314
- case 'article':
315
- case 'complementary': {
256
+ case 'article': {
316
257
  const location = navigationPlugin?.provides.location;
317
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
319
258
 
320
259
  if (data.object instanceof ChannelType && data.object.threads[0]) {
321
260
  const channel = data.object;
@@ -342,93 +281,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
342
281
  );
343
282
  }
344
283
 
284
+ break;
285
+ }
286
+
287
+ case 'complementary--comments': {
345
288
  if (
346
289
  data.subject &&
347
290
  typeof data.subject === 'object' &&
348
291
  'threads' in data.subject &&
349
292
  Array.isArray(data.subject.threads)
350
293
  ) {
351
- const threads = data.subject.threads
352
- .concat(state.staging[fullyQualifiedId(data.subject)])
353
- .filter(nonNullable);
354
-
355
- const createSort = threadProvides.find((p) => p.predicate(data.subject))?.createSort;
356
- if (createSort) {
357
- const sort = createSort(data.subject);
358
- threads.sort((a, b) => sort(a.anchor, b.anchor));
359
- }
360
-
361
- const detached = data.subject.threads
362
- .filter(nonNullable)
363
- .filter(({ anchor }) => !anchor)
364
- .map((thread) => thread.id);
365
-
366
- const qualifiedSubjectId = fullyQualifiedId(data.subject);
367
- const attention = attentionPlugin?.provides.attention?.attended ?? new Set([qualifiedSubjectId]);
368
- const attendableAttrs = createAttendableAttributes(qualifiedSubjectId);
369
- const { showResolvedThreads } = getViewState(qualifiedSubjectId);
294
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
370
295
 
371
296
  return (
372
- <div role='none' className='contents group/attention' {...attendableAttrs}>
373
- {role === 'complementary' && <CommentsHeading attendableId={qualifiedSubjectId} />}
374
- <ScrollArea.Root classNames='row-span-2'>
375
- <ScrollArea.Viewport>
376
- <CommentsContainer
377
- threads={threads}
378
- detached={detached}
379
- currentId={attention.has(qualifiedSubjectId) ? state.current : undefined}
380
- autoFocusCurrentTextbox={state.focus}
381
- showResolvedThreads={showResolvedThreads}
382
- onThreadAttend={(thread) => {
383
- if (state.current !== thread.id) {
384
- state.current = thread.id;
385
- void dispatch?.({
386
- action: LayoutAction.SCROLL_INTO_VIEW,
387
- data: {
388
- id: fullyQualifiedId(data.subject!),
389
- thread: fullyQualifiedId(thread),
390
- cursor: thread.anchor,
391
- },
392
- });
393
- }
394
- }}
395
- onThreadDelete={(thread) =>
396
- dispatch?.({
397
- plugin: THREAD_PLUGIN,
398
- action: ThreadAction.DELETE,
399
- data: { subject: data.subject, thread },
400
- })
401
- }
402
- onMessageDelete={(thread, messageId) =>
403
- dispatch?.({
404
- plugin: THREAD_PLUGIN,
405
- action: ThreadAction.DELETE_MESSAGE,
406
- data: { subject: data.subject, thread, messageId },
407
- })
408
- }
409
- onThreadToggleResolved={(thread) =>
410
- dispatch?.({
411
- plugin: THREAD_PLUGIN,
412
- action: ThreadAction.TOGGLE_RESOLVED,
413
- data: { thread },
414
- })
415
- }
416
- onComment={(thread) => {
417
- void intentPlugin?.provides.intent.dispatch({
418
- action: ThreadAction.ON_MESSAGE_ADD,
419
- data: { thread, subject: data.subject },
420
- });
421
- }}
422
- />
423
- <div role='none' className='bs-10' />
424
- <ScrollArea.Scrollbar>
425
- <ScrollArea.Thumb />
426
- </ScrollArea.Scrollbar>
427
- </ScrollArea.Viewport>
428
- </ScrollArea.Root>
429
- </div>
297
+ <ThreadComplementary
298
+ role={role}
299
+ subject={data.subject}
300
+ drafts={state.drafts[fullyQualifiedId(data.subject)]}
301
+ current={state.current}
302
+ showResolvedThreads={showResolvedThreads}
303
+ />
430
304
  );
431
305
  }
306
+
307
+ break;
432
308
  }
433
309
  }
434
310
 
@@ -442,37 +318,40 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
442
318
  if (intent.data && intent.data.cursor !== undefined) {
443
319
  const { cursor, name, subject } = intent.data;
444
320
 
321
+ // Seed the threads array if it does not exist.
322
+ if (subject?.threads === undefined) {
323
+ try {
324
+ // Static schema will throw an error if subject does not support threads array property.
325
+ subject.threads = [];
326
+ } catch (err) {
327
+ log.error('Subject does not support threads array', subject?.typename);
328
+ return;
329
+ }
330
+ }
331
+
445
332
  const subjectId = fullyQualifiedId(subject);
446
333
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
- const stagingArea = state.staging[subjectId];
448
- if (stagingArea) {
449
- stagingArea.push(thread);
334
+ const draft = state.drafts[subjectId];
335
+ if (draft) {
336
+ draft.push(thread);
450
337
  } else {
451
- state.staging[subjectId] = [thread];
338
+ state.drafts[subjectId] = [thread];
452
339
  }
453
340
 
454
341
  return {
455
342
  data: thread,
456
343
  intents: [
457
344
  [
458
- {
459
- action: NavigationAction.OPEN,
460
- data: {
461
- activeParts: {
462
- complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
463
- },
464
- },
465
- },
466
345
  {
467
346
  action: ThreadAction.SELECT,
468
- data: { current: thread.id, focus: true },
347
+ data: { current: fullyQualifiedId(thread) },
469
348
  },
349
+ ],
350
+ [
470
351
  {
471
- action: LayoutAction.SET_LAYOUT,
352
+ action: NavigationAction.OPEN,
472
353
  data: {
473
- element: 'complementary',
474
- subject: subjectId,
475
- state: true,
354
+ activeParts: { complementary: 'comments' },
476
355
  },
477
356
  },
478
357
  ],
@@ -480,14 +359,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
480
359
  };
481
360
  } else {
482
361
  // NOTE: This is the standalone thread creation case.
483
- return { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
362
+ return {
363
+ data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }),
364
+ };
484
365
  }
485
366
  }
486
367
 
487
368
  case ThreadAction.SELECT: {
488
- state.focus = intent.data?.current === state.current ? state.focus : intent.data?.focus;
489
369
  state.current = intent.data?.current;
490
- return { data: true };
370
+
371
+ return {
372
+ data: true,
373
+ intents: !intent.data?.skipOpen
374
+ ? [
375
+ [
376
+ {
377
+ action: NavigationAction.OPEN,
378
+ data: {
379
+ activeParts: { complementary: 'comments' },
380
+ },
381
+ },
382
+ ],
383
+ ]
384
+ : [],
385
+ };
491
386
  }
492
387
 
493
388
  case ThreadAction.TOGGLE_RESOLVED: {
@@ -506,6 +401,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
506
401
  const spaceId = space?.id;
507
402
 
508
403
  return {
404
+ data: true,
509
405
  intents: [
510
406
  [
511
407
  {
@@ -519,19 +415,21 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
519
415
 
520
416
  case ThreadAction.DELETE: {
521
417
  const { subject, thread } = intent.data ?? {};
522
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
418
+ if (
419
+ !(thread instanceof ThreadType) ||
420
+ !(subject && typeof subject === 'object' && 'threads' in subject)
421
+ ) {
523
422
  return;
524
423
  }
525
424
 
526
425
  const subjectId = fullyQualifiedId(subject);
527
- const stagingArea = state.staging[subjectId];
528
- if (stagingArea) {
529
- // Check if we're deleting a thread that's in the staging area.
530
- // If so, remove it from the staging area without ceremony.
531
- const index = stagingArea.findIndex((t) => t.id === thread.id);
426
+ const draft = state.drafts[subjectId];
427
+ if (draft) {
428
+ // Check if we're deleting a draft; if so, remove it.
429
+ const index = draft.findIndex((t) => t.id === thread.id);
532
430
  if (index !== -1) {
533
- stagingArea.splice(index, 1);
534
- return;
431
+ draft.splice(index, 1);
432
+ return { data: true };
535
433
  }
536
434
  }
537
435
 
@@ -541,7 +439,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
541
439
  }
542
440
 
543
441
  if (!intent.undo) {
544
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
442
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
545
443
  const cursor = subject.threads[index]?.anchor;
546
444
  if (index !== -1) {
547
445
  subject.threads?.splice(index, 1);
@@ -587,7 +485,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
587
485
  const { thread, subject } = intent.data ?? {};
588
486
  if (
589
487
  !(thread instanceof ThreadType) ||
590
- !(subject && typeof subject === 'object' && 'threads' in subject)
488
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
591
489
  ) {
592
490
  return;
593
491
  }
@@ -597,12 +495,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
597
495
  const intents = [];
598
496
  const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
599
497
 
600
- if (state.staging[subjectId]?.find((t) => t === thread)) {
601
- // Move thread from staging to document.
498
+ if (state.drafts[subjectId]?.find((t) => t === thread)) {
499
+ // Move draft to document.
602
500
  thread.status = 'active';
603
501
  subject.threads ? subject.threads.push(thread) : (subject.threads = [thread]);
604
- state.staging[subjectId] = state.staging[subjectId]?.filter((t) => t.id !== thread.id);
605
-
502
+ state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
606
503
  intents.push({
607
504
  action: ObservabilityAction.SEND_EVENT,
608
505
  data: { name: 'threads.thread-created', properties: analyticsProperties },
@@ -627,7 +524,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
627
524
  const { subject, thread, messageId } = intent.data ?? {};
628
525
  const space = getSpace(subject);
629
526
 
630
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
527
+ if (
528
+ !(thread instanceof ThreadType) ||
529
+ !(subject && typeof subject === 'object' && 'threads' in subject)
530
+ ) {
631
531
  return;
632
532
  }
633
533
 
@@ -658,7 +558,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
658
558
  action: ObservabilityAction.SEND_EVENT,
659
559
  data: {
660
560
  name: 'threads.message.delete',
661
- properties: { threadId: thread.id, spaceId: space.id },
561
+ properties: { threadId: thread.id, spaceId: space?.id },
662
562
  },
663
563
  },
664
564
  ],
@@ -667,13 +567,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
667
567
  } else if (intent.undo) {
668
568
  const message = intent.data?.message;
669
569
  const messageIndex = intent.data?.messageIndex;
670
-
671
570
  if (!(message instanceof MessageType) || !(typeof messageIndex === 'number')) {
672
571
  return;
673
572
  }
674
573
 
675
574
  thread.messages.splice(messageIndex, 0, message);
676
-
677
575
  return {
678
576
  data: true,
679
577
  intents: [
@@ -682,7 +580,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
682
580
  action: ObservabilityAction.SEND_EVENT,
683
581
  data: {
684
582
  name: 'threads.message.undo-delete',
685
- properties: { threadId: thread.id, spaceId: space.id },
583
+ properties: { threadId: thread.id, spaceId: space?.id },
686
584
  },
687
585
  },
688
586
  ],
@@ -694,96 +592,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
694
592
  },
695
593
  },
696
594
  markdown: {
697
- // TODO(burdon): Factor out extension factory into separate file (for simplicity).
698
595
  extensions: ({ document: doc }) => {
699
- const space = doc && getSpace(doc);
700
- if (!doc || !space) {
701
- // Include no-op comments extension here to ensure that the facets are always present when they are expected.
702
- // TODO(wittjosiah): The Editor should only look for these facets when comments are available.
703
- return [comments()];
704
- }
705
-
706
- // TODO(Zan): When we have the deepsignal specific equivalent of this we should use that instead.
707
- const threads = computed(() =>
708
- [...doc.threads.filter(nonNullable), ...(state.staging[fullyQualifiedId(doc)] ?? [])].filter(
709
- (thread) => !(thread?.status === 'resolved'),
710
- ),
711
- );
712
-
713
- return [
714
- listener({
715
- onChange: () => {
716
- doc.threads.forEach((thread) => {
717
- if (thread?.anchor) {
718
- const [start, end] = thread.anchor.split(':');
719
- const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
720
- // TODO(burdon): This seems unsafe; review.
721
- // Only update if the name has changed, otherwise this will cause an infinite loop.
722
- // Skip if the name is empty - this means comment text was deleted, but thread name should remain.
723
- if (name && name !== thread.name) {
724
- thread.name = name;
725
- }
726
- }
727
- });
728
- },
729
- }),
730
- createExternalCommentSync(
731
- doc.id,
732
- (sink) => effect(() => sink()),
733
- () =>
734
- threads.value
735
- .filter((thread) => thread?.anchor)
736
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! })),
737
- ),
738
- comments({
739
- id: doc.id,
740
- onCreate: ({ cursor }) => {
741
- const [start, end] = cursor.split(':');
742
- const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
743
-
744
- void intentPlugin?.provides.intent.dispatch({
745
- action: ThreadAction.CREATE,
746
- data: {
747
- cursor,
748
- name,
749
- subject: doc,
750
- },
751
- });
752
- },
753
- onDelete: ({ id }) => {
754
- // If the thread is in the staging area, remove it.
755
- const stagingArea = state.staging[fullyQualifiedId(doc)];
756
- if (stagingArea) {
757
- const index = stagingArea.findIndex((thread) => thread.id === id);
758
- if (index !== -1) {
759
- stagingArea.splice(index, 1);
760
- }
761
- }
762
-
763
- const thread = doc.threads.find((thread) => thread?.id === id);
764
- if (thread) {
765
- thread.anchor = undefined;
766
- }
767
- },
768
- onUpdate: ({ id, cursor }) => {
769
- const thread =
770
- state.staging[fullyQualifiedId(doc)]?.find((thread) => thread.id === id) ??
771
- doc.threads.find((thread) => thread?.id === id);
772
-
773
- if (thread instanceof ThreadType && thread.anchor) {
774
- const [start, end] = thread.anchor.split(':');
775
- thread.name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
776
- thread.anchor = cursor;
777
- }
778
- },
779
- onSelect: ({ selection: { current, closest } }) => {
780
- const dispatch = intentPlugin?.provides.intent.dispatch;
781
- if (dispatch) {
782
- void dispatch([{ action: ThreadAction.SELECT, data: { current: current ?? closest } }]);
783
- }
784
- },
785
- }),
786
- ];
596
+ return threads(state, doc, intentPlugin?.provides.intent.dispatch);
787
597
  },
788
598
  },
789
599
  },