@dxos/plugin-thread 0.6.11 → 0.6.12-main.2d19bf1

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 (89) 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 +360 -341
  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 +433 -418
  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 +1122 -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/testing.d.ts +2 -0
  56. package/dist/types/src/components/testing.d.ts.map +1 -1
  57. package/dist/types/src/extensions/index.d.ts +2 -0
  58. package/dist/types/src/extensions/index.d.ts.map +1 -0
  59. package/dist/types/src/extensions/threads.d.ts +9 -0
  60. package/dist/types/src/extensions/threads.d.ts.map +1 -0
  61. package/dist/types/src/meta.d.ts +3 -9
  62. package/dist/types/src/meta.d.ts.map +1 -1
  63. package/dist/types/src/types.d.ts +6 -1
  64. package/dist/types/src/types.d.ts.map +1 -1
  65. package/package.json +40 -37
  66. package/src/ThreadPlugin.tsx +115 -310
  67. package/src/components/ChatContainer.tsx +7 -8
  68. package/src/components/CommentContainer.tsx +18 -19
  69. package/src/components/Comments.stories.tsx +2 -2
  70. package/src/components/CommentsContainer.tsx +18 -31
  71. package/src/components/MessageContainer.tsx +2 -1
  72. package/src/components/ThreadComplementary.tsx +113 -0
  73. package/src/extensions/index.ts +5 -0
  74. package/src/extensions/threads.ts +117 -0
  75. package/src/meta.ts +15 -0
  76. package/src/types.ts +8 -3
  77. package/dist/lib/browser/chunk-5IBJO53M.mjs +0 -21
  78. package/dist/lib/browser/chunk-5IBJO53M.mjs.map +0 -7
  79. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  80. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  81. package/dist/lib/node/chunk-BWBONZGU.cjs.map +0 -7
  82. package/dist/types/src/types.test.d.ts +0 -2
  83. package/dist/types/src/types.test.d.ts.map +0 -1
  84. package/src/meta.tsx +0 -20
  85. package/src/types.test.ts +0 -13
  86. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-GBPIH47W.mjs.map} +0 -0
  87. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-PGF7W5EV.mjs.map} +0 -0
  88. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-GH5UBWFU.cjs.map} +0 -0
  89. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-CO6I3DNX.cjs.map} +0 -0
@@ -2,13 +2,10 @@
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
9
  NavigationAction,
13
10
  type LocationProvides,
14
11
  type Plugin,
@@ -16,71 +13,40 @@ import {
16
13
  parseIntentPlugin,
17
14
  parseNavigationPlugin,
18
15
  resolvePlugin,
19
- parseGraphPlugin,
20
- SLUG_PATH_SEPARATOR,
21
- SLUG_COLLECTION_INDICATOR,
22
16
  isLayoutParts,
23
17
  parseMetadataResolverPlugin,
24
- type IntentDispatcher,
25
- useResolvePlugins,
26
18
  } from '@dxos/app-framework';
27
19
  import { type UnsubscribeCallback } from '@dxos/async';
28
20
  import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
29
21
  import { create } from '@dxos/echo-schema';
30
22
  import { LocalStorageStore } from '@dxos/local-storage';
31
- import { type AttentionPluginProvides, parseAttentionPlugin } from '@dxos/plugin-attention';
23
+ import { log } from '@dxos/log';
32
24
  import { parseClientPlugin } from '@dxos/plugin-client';
33
- import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
34
- import { DocumentType } from '@dxos/plugin-markdown/types';
25
+ import { type ActionGroup, createExtension, isActionGroup, toSignal } from '@dxos/plugin-graph';
35
26
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
36
27
  import { SpaceAction } from '@dxos/plugin-space';
37
28
  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';
29
+ import { getSpace, fullyQualifiedId, loadObjectReferences } from '@dxos/react-client/echo';
49
30
  import { translations as threadTranslations } from '@dxos/react-ui-thread';
50
- import { nonNullable } from '@dxos/util';
51
31
 
52
- import {
53
- ThreadMain,
54
- ThreadSettings,
55
- CommentsContainer,
56
- CommentsHeading,
57
- ChatContainer,
58
- ChatHeading,
59
- ThreadArticle,
60
- } from './components';
32
+ import { ThreadMain, ThreadSettings, ChatContainer, ChatHeading, ThreadArticle } from './components';
33
+ import { ThreadComplementary } from './components/ThreadComplementary';
34
+ import { threads } from './extensions';
61
35
  import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
62
36
  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
- };
37
+ import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps, type ThreadState } from './types';
71
38
 
72
39
  type SubjectId = string;
40
+
73
41
  const initialViewState = { showResolvedThreads: false };
74
- type ViewStore = Record<SubjectId, typeof initialViewState>;
75
42
 
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;
43
+ type ViewStore = Record<SubjectId, typeof initialViewState>;
79
44
 
80
45
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
46
+ // NOTE(burdon): Review/discuss CursorConverter semantics.
81
47
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
82
48
  const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
83
- const state = create<ThreadState>({ staging: {} });
49
+ const state = create<ThreadState>({ drafts: {} });
84
50
 
85
51
  const viewStore = create<ViewStore>({});
86
52
  const getViewState = (subjectId: string) => {
@@ -90,10 +56,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
90
56
  return viewStore[subjectId];
91
57
  };
92
58
 
93
- let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
94
59
  let navigationPlugin: Plugin<LocationProvides> | undefined;
95
60
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
96
- let dispatch: IntentDispatcher | undefined;
97
61
 
98
62
  const unsubscribeCallbacks = [] as UnsubscribeCallback[];
99
63
 
@@ -102,45 +66,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
102
66
  ready: async (plugins) => {
103
67
  settings.prop({ key: 'standalone', type: LocalStorageStore.bool({ allowUndefined: true }) });
104
68
 
105
- attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
106
69
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
107
70
  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
71
  },
145
72
  unload: async () => {
146
73
  unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
@@ -151,8 +78,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
151
78
  records: {
152
79
  [ChannelType.typename]: {
153
80
  placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
154
- icon: (props: IconProps) => <Chat {...props} />,
155
- iconSymbol: 'ph--chat--regular',
81
+ icon: 'ph--chat--regular',
156
82
  // TODO(wittjosiah): Move out of metadata.
157
83
  loadReferences: (channel: ChannelType) => loadObjectReferences(channel, (channel) => channel.threads),
158
84
  },
@@ -198,31 +124,42 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
198
124
  return;
199
125
  }
200
126
 
201
- // TODO(Zan): Find util (or make one)
202
- const docId = id.split('~').at(0);
203
- const [spaceId, objectId] = docId?.split(':') ?? [];
127
+ // TODO(Zan): Find util (or make one).
128
+ const subjectId = id.split('~').at(0);
129
+ const [spaceId, objectId] = subjectId?.split(':') ?? [];
204
130
  const space = client.spaces.get().find((space) => space.id === spaceId);
205
- const doc = space?.db.getObjectById(objectId);
206
- if (!doc || !docId) {
131
+ const object = toSignal(
132
+ (onChange) => {
133
+ const timeout = setTimeout(async () => {
134
+ await space?.db.loadObjectById(objectId);
135
+ onChange();
136
+ });
137
+
138
+ return () => clearTimeout(timeout);
139
+ },
140
+ () => space?.db.getObjectById(objectId),
141
+ subjectId,
142
+ );
143
+ if (!object || !subjectId) {
207
144
  return;
208
145
  }
209
146
 
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;
147
+ const meta = metadataResolver(getTypename(object) ?? '');
148
+ const label = meta.label?.(object) ||
149
+ object.name ||
150
+ meta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
215
151
 
216
- const viewState = getViewState(docId);
152
+ const viewState = getViewState(subjectId);
217
153
 
218
154
  return {
219
155
  id,
220
156
  type: 'orphan-comments-for-subject',
221
- data: doc,
157
+ data: null,
222
158
  properties: {
223
- icon: meta.iconComponent,
159
+ icon: 'ph--chat-text--regular',
224
160
  label,
225
161
  showResolvedThreads: viewState.showResolvedThreads,
162
+ object,
226
163
  },
227
164
  };
228
165
  },
@@ -256,6 +193,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
256
193
  menuItemType: 'toggle',
257
194
  isChecked: viewState.showResolvedThreads,
258
195
  testId: 'threadPlugin.toggleShowResolved',
196
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
259
197
  },
260
198
  },
261
199
  ];
@@ -287,8 +225,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
287
225
  },
288
226
  properties: {
289
227
  label: ['create channel label', { ns: THREAD_PLUGIN }],
290
- icon: (props: IconProps) => <Chat {...props} />,
291
- iconSymbol: 'ph--chat--regular',
228
+ icon: 'ph--chat--regular',
292
229
  testId: 'threadPlugin.createObject',
293
230
  },
294
231
  },
@@ -311,11 +248,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
311
248
  return data.plugin === meta.id ? <ThreadSettings settings={settings.values} /> : null;
312
249
  }
313
250
 
314
- case 'article':
315
- case 'complementary': {
251
+ case 'article': {
316
252
  const location = navigationPlugin?.provides.location;
317
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
319
253
 
320
254
  if (data.object instanceof ChannelType && data.object.threads[0]) {
321
255
  const channel = data.object;
@@ -342,93 +276,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
342
276
  );
343
277
  }
344
278
 
279
+ break;
280
+ }
281
+
282
+ case 'complementary--comments': {
345
283
  if (
346
284
  data.subject &&
347
285
  typeof data.subject === 'object' &&
348
286
  'threads' in data.subject &&
349
287
  Array.isArray(data.subject.threads)
350
288
  ) {
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);
289
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
370
290
 
371
291
  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>
292
+ <ThreadComplementary
293
+ role={role}
294
+ subject={data.subject}
295
+ drafts={state.drafts[fullyQualifiedId(data.subject)]}
296
+ current={state.current}
297
+ showResolvedThreads={showResolvedThreads}
298
+ />
430
299
  );
431
300
  }
301
+
302
+ break;
432
303
  }
433
304
  }
434
305
 
@@ -442,37 +313,40 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
442
313
  if (intent.data && intent.data.cursor !== undefined) {
443
314
  const { cursor, name, subject } = intent.data;
444
315
 
316
+ // Seed the threads array if it does not exist.
317
+ if (subject?.threads === undefined) {
318
+ try {
319
+ // Static schema will throw an error if subject does not support threads array property.
320
+ subject.threads = [];
321
+ } catch (err) {
322
+ log.error('Subject does not support threads array', subject?.typename);
323
+ return;
324
+ }
325
+ }
326
+
445
327
  const subjectId = fullyQualifiedId(subject);
446
328
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
- const stagingArea = state.staging[subjectId];
448
- if (stagingArea) {
449
- stagingArea.push(thread);
329
+ const draft = state.drafts[subjectId];
330
+ if (draft) {
331
+ draft.push(thread);
450
332
  } else {
451
- state.staging[subjectId] = [thread];
333
+ state.drafts[subjectId] = [thread];
452
334
  }
453
335
 
454
336
  return {
455
337
  data: thread,
456
338
  intents: [
457
339
  [
458
- {
459
- action: NavigationAction.OPEN,
460
- data: {
461
- activeParts: {
462
- complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
463
- },
464
- },
465
- },
466
340
  {
467
341
  action: ThreadAction.SELECT,
468
- data: { current: thread.id, focus: true },
342
+ data: { current: fullyQualifiedId(thread) },
469
343
  },
344
+ ],
345
+ [
470
346
  {
471
- action: LayoutAction.SET_LAYOUT,
347
+ action: NavigationAction.OPEN,
472
348
  data: {
473
- element: 'complementary',
474
- subject: subjectId,
475
- state: true,
349
+ activeParts: { complementary: 'comments' },
476
350
  },
477
351
  },
478
352
  ],
@@ -480,14 +354,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
480
354
  };
481
355
  } else {
482
356
  // NOTE: This is the standalone thread creation case.
483
- return { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
357
+ return {
358
+ data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }),
359
+ };
484
360
  }
485
361
  }
486
362
 
487
363
  case ThreadAction.SELECT: {
488
- state.focus = intent.data?.current === state.current ? state.focus : intent.data?.focus;
489
364
  state.current = intent.data?.current;
490
- return { data: true };
365
+
366
+ return {
367
+ data: true,
368
+ intents: !intent.data?.skipOpen
369
+ ? [
370
+ [
371
+ {
372
+ action: NavigationAction.OPEN,
373
+ data: {
374
+ activeParts: { complementary: 'comments' },
375
+ },
376
+ },
377
+ ],
378
+ ]
379
+ : [],
380
+ };
491
381
  }
492
382
 
493
383
  case ThreadAction.TOGGLE_RESOLVED: {
@@ -506,6 +396,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
506
396
  const spaceId = space?.id;
507
397
 
508
398
  return {
399
+ data: true,
509
400
  intents: [
510
401
  [
511
402
  {
@@ -519,19 +410,21 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
519
410
 
520
411
  case ThreadAction.DELETE: {
521
412
  const { subject, thread } = intent.data ?? {};
522
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
413
+ if (
414
+ !(thread instanceof ThreadType) ||
415
+ !(subject && typeof subject === 'object' && 'threads' in subject)
416
+ ) {
523
417
  return;
524
418
  }
525
419
 
526
420
  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);
421
+ const draft = state.drafts[subjectId];
422
+ if (draft) {
423
+ // Check if we're deleting a draft; if so, remove it.
424
+ const index = draft.findIndex((t) => t.id === thread.id);
532
425
  if (index !== -1) {
533
- stagingArea.splice(index, 1);
534
- return;
426
+ draft.splice(index, 1);
427
+ return { data: true };
535
428
  }
536
429
  }
537
430
 
@@ -541,7 +434,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
541
434
  }
542
435
 
543
436
  if (!intent.undo) {
544
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
437
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
545
438
  const cursor = subject.threads[index]?.anchor;
546
439
  if (index !== -1) {
547
440
  subject.threads?.splice(index, 1);
@@ -587,7 +480,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
587
480
  const { thread, subject } = intent.data ?? {};
588
481
  if (
589
482
  !(thread instanceof ThreadType) ||
590
- !(subject && typeof subject === 'object' && 'threads' in subject)
483
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
591
484
  ) {
592
485
  return;
593
486
  }
@@ -597,12 +490,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
597
490
  const intents = [];
598
491
  const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
599
492
 
600
- if (state.staging[subjectId]?.find((t) => t === thread)) {
601
- // Move thread from staging to document.
493
+ if (state.drafts[subjectId]?.find((t) => t === thread)) {
494
+ // Move draft to document.
602
495
  thread.status = 'active';
603
496
  subject.threads ? subject.threads.push(thread) : (subject.threads = [thread]);
604
- state.staging[subjectId] = state.staging[subjectId]?.filter((t) => t.id !== thread.id);
605
-
497
+ state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
606
498
  intents.push({
607
499
  action: ObservabilityAction.SEND_EVENT,
608
500
  data: { name: 'threads.thread-created', properties: analyticsProperties },
@@ -627,7 +519,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
627
519
  const { subject, thread, messageId } = intent.data ?? {};
628
520
  const space = getSpace(subject);
629
521
 
630
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
522
+ if (
523
+ !(thread instanceof ThreadType) ||
524
+ !(subject && typeof subject === 'object' && 'threads' in subject)
525
+ ) {
631
526
  return;
632
527
  }
633
528
 
@@ -658,7 +553,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
658
553
  action: ObservabilityAction.SEND_EVENT,
659
554
  data: {
660
555
  name: 'threads.message.delete',
661
- properties: { threadId: thread.id, spaceId: space.id },
556
+ properties: { threadId: thread.id, spaceId: space?.id },
662
557
  },
663
558
  },
664
559
  ],
@@ -667,13 +562,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
667
562
  } else if (intent.undo) {
668
563
  const message = intent.data?.message;
669
564
  const messageIndex = intent.data?.messageIndex;
670
-
671
565
  if (!(message instanceof MessageType) || !(typeof messageIndex === 'number')) {
672
566
  return;
673
567
  }
674
568
 
675
569
  thread.messages.splice(messageIndex, 0, message);
676
-
677
570
  return {
678
571
  data: true,
679
572
  intents: [
@@ -682,7 +575,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
682
575
  action: ObservabilityAction.SEND_EVENT,
683
576
  data: {
684
577
  name: 'threads.message.undo-delete',
685
- properties: { threadId: thread.id, spaceId: space.id },
578
+ properties: { threadId: thread.id, spaceId: space?.id },
686
579
  },
687
580
  },
688
581
  ],
@@ -694,96 +587,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
694
587
  },
695
588
  },
696
589
  markdown: {
697
- // TODO(burdon): Factor out extension factory into separate file (for simplicity).
698
590
  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
- ];
591
+ return threads(state, doc, intentPlugin?.provides.intent.dispatch);
787
592
  },
788
593
  },
789
594
  },