@dxos/plugin-thread 0.6.13 → 0.6.14-main.69511f5

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 (101) hide show
  1. package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs → ThreadArticle-2Y7UWZLA.mjs} +3 -3
  2. package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs → ThreadMain-VK5UEBQ7.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-P7ZE5N5M.mjs → chunk-LHT42MTJ.mjs} +16 -16
  6. package/dist/lib/browser/chunk-LHT42MTJ.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-GKPZHUT3.mjs → chunk-WKCIKGGW.mjs} +2 -2
  8. package/dist/lib/browser/{chunk-GKPZHUT3.mjs.map → chunk-WKCIKGGW.mjs.map} +1 -1
  9. package/dist/lib/browser/index.mjs +431 -383
  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-CIYBYISR.cjs} +7 -7
  15. package/dist/lib/node/{ThreadMain-XJX75GHG.cjs → ThreadMain-V6KBUQLH.cjs} +7 -7
  16. package/dist/lib/node/{chunk-D5GOVXHS.cjs → chunk-A2IORKQK.cjs} +6 -6
  17. package/dist/lib/node/{chunk-D5GOVXHS.cjs.map → chunk-A2IORKQK.cjs.map} +1 -1
  18. package/dist/lib/node/{chunk-5PZNPPQF.cjs → chunk-HWN7Q5XK.cjs} +65 -65
  19. package/dist/lib/node/chunk-HWN7Q5XK.cjs.map +7 -0
  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 +480 -436
  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-PXO4ZJFU.mjs +19 -0
  30. package/dist/lib/node-esm/ThreadArticle-PXO4ZJFU.mjs.map +7 -0
  31. package/dist/lib/node-esm/ThreadMain-U7R24CN3.mjs +27 -0
  32. package/dist/lib/node-esm/ThreadMain-U7R24CN3.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-KWNXG6FG.mjs +21 -0
  36. package/dist/lib/node-esm/chunk-KWNXG6FG.mjs.map +7 -0
  37. package/dist/lib/node-esm/chunk-SXXOBRAX.mjs +411 -0
  38. package/dist/lib/node-esm/chunk-SXXOBRAX.mjs.map +7 -0
  39. package/dist/lib/node-esm/index.mjs +1151 -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/Chat.stories.d.ts +3 -42
  48. package/dist/types/src/components/Chat.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/ChatContainer.d.ts.map +1 -1
  50. package/dist/types/src/components/CommentContainer.d.ts +1 -1
  51. package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
  52. package/dist/types/src/components/Comments.stories.d.ts +3 -41
  53. package/dist/types/src/components/Comments.stories.d.ts.map +1 -1
  54. package/dist/types/src/components/CommentsContainer.d.ts +1 -5
  55. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  56. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  57. package/dist/types/src/components/ThreadComplementary.d.ts +10 -0
  58. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -0
  59. package/dist/types/src/components/index.d.ts +6 -6
  60. package/dist/types/src/components/index.d.ts.map +1 -1
  61. package/dist/types/src/components/testing.d.ts +2 -0
  62. package/dist/types/src/components/testing.d.ts.map +1 -1
  63. package/dist/types/src/components/types.d.ts +2 -2
  64. package/dist/types/src/components/types.d.ts.map +1 -1
  65. package/dist/types/src/extensions/index.d.ts +2 -0
  66. package/dist/types/src/extensions/index.d.ts.map +1 -0
  67. package/dist/types/src/extensions/threads.d.ts +9 -0
  68. package/dist/types/src/extensions/threads.d.ts.map +1 -0
  69. package/dist/types/src/meta.d.ts +3 -9
  70. package/dist/types/src/meta.d.ts.map +1 -1
  71. package/dist/types/src/types.d.ts +7 -2
  72. package/dist/types/src/types.d.ts.map +1 -1
  73. package/package.json +41 -46
  74. package/src/ThreadPlugin.tsx +146 -307
  75. package/src/components/Chat.stories.tsx +7 -4
  76. package/src/components/ChatContainer.tsx +7 -8
  77. package/src/components/CommentContainer.tsx +18 -19
  78. package/src/components/Comments.stories.tsx +9 -6
  79. package/src/components/CommentsContainer.tsx +18 -31
  80. package/src/components/MessageContainer.tsx +13 -23
  81. package/src/components/ThreadComplementary.tsx +113 -0
  82. package/src/components/ThreadSettings.tsx +3 -3
  83. package/src/components/index.ts +6 -6
  84. package/src/components/types.ts +2 -2
  85. package/src/extensions/index.ts +5 -0
  86. package/src/extensions/threads.ts +117 -0
  87. package/src/meta.ts +15 -0
  88. package/src/types.ts +9 -4
  89. package/dist/lib/browser/chunk-5IBJO53M.mjs +0 -21
  90. package/dist/lib/browser/chunk-5IBJO53M.mjs.map +0 -7
  91. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  92. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  93. package/dist/lib/node/chunk-BWBONZGU.cjs.map +0 -7
  94. package/dist/types/src/types.test.d.ts +0 -2
  95. package/dist/types/src/types.test.d.ts.map +0 -1
  96. package/src/meta.tsx +0 -20
  97. package/src/types.test.ts +0 -13
  98. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-2Y7UWZLA.mjs.map} +0 -0
  99. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-VK5UEBQ7.mjs.map} +0 -0
  100. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-CIYBYISR.cjs.map} +0 -0
  101. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-V6KBUQLH.cjs.map} +0 -0
@@ -2,85 +2,57 @@
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,
18
+ LayoutAction,
26
19
  } from '@dxos/app-framework';
27
20
  import { type UnsubscribeCallback } from '@dxos/async';
28
- import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
29
- import { create } 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 { create, type EchoReactiveObject, getTypename } from '@dxos/react-client/echo';
29
+ import { getSpace, fullyQualifiedId, loadObjectReferences, parseId } 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
32
  import {
53
- ThreadMain,
54
- ThreadSettings,
55
- CommentsContainer,
56
- CommentsHeading,
57
33
  ChatContainer,
58
34
  ChatHeading,
59
35
  ThreadArticle,
36
+ ThreadComplementary,
37
+ ThreadMain,
38
+ ThreadSettings,
60
39
  } from './components';
40
+ import { threads } from './extensions';
61
41
  import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
62
42
  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
- };
43
+ import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps, type ThreadState } from './types';
71
44
 
72
45
  type SubjectId = string;
46
+
73
47
  const initialViewState = { showResolvedThreads: false };
74
- type ViewStore = Record<SubjectId, typeof initialViewState>;
75
48
 
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;
49
+ type ViewStore = Record<SubjectId, typeof initialViewState>;
79
50
 
80
51
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
52
+ // NOTE(burdon): Review/discuss CursorConverter semantics.
81
53
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
82
54
  const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
83
- const state = create<ThreadState>({ staging: {} });
55
+ const state = create<ThreadState>({ drafts: {} });
84
56
 
85
57
  const viewStore = create<ViewStore>({});
86
58
  const getViewState = (subjectId: string) => {
@@ -90,10 +62,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
90
62
  return viewStore[subjectId];
91
63
  };
92
64
 
93
- let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
94
65
  let navigationPlugin: Plugin<LocationProvides> | undefined;
95
66
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
96
- let dispatch: IntentDispatcher | undefined;
97
67
 
98
68
  const unsubscribeCallbacks = [] as UnsubscribeCallback[];
99
69
 
@@ -102,45 +72,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
102
72
  ready: async (plugins) => {
103
73
  settings.prop({ key: 'standalone', type: LocalStorageStore.bool({ allowUndefined: true }) });
104
74
 
105
- attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
106
75
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
107
76
  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
77
  },
145
78
  unload: async () => {
146
79
  unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
@@ -151,8 +84,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
151
84
  records: {
152
85
  [ChannelType.typename]: {
153
86
  placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
154
- icon: (props: IconProps) => <Chat {...props} />,
155
- iconSymbol: 'ph--chat--regular',
87
+ icon: 'ph--chat--regular',
156
88
  // TODO(wittjosiah): Move out of metadata.
157
89
  loadReferences: (channel: ChannelType) => loadObjectReferences(channel, (channel) => channel.threads),
158
90
  },
@@ -185,44 +117,74 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
185
117
  const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
186
118
  const dispatch = resolvePlugin(plugins, parseIntentPlugin)?.provides.intent.dispatch;
187
119
  const metadataResolver = resolvePlugin(plugins, parseMetadataResolverPlugin)?.provides.metadata.resolver;
188
-
189
120
  if (!client || !dispatch || !metadataResolver) {
190
121
  return [];
191
122
  }
192
123
 
124
+ const type = 'orphan-comments-for-subject';
125
+ const icon = 'ph--chat-text--regular';
126
+
193
127
  return [
194
128
  createExtension({
195
129
  id: `${THREAD_PLUGIN}/comments-for-subject`,
196
130
  resolver: ({ id }) => {
131
+ // TODO(Zan): Find util (or make one).
197
132
  if (!id.endsWith('~comments')) {
198
133
  return;
199
134
  }
200
135
 
201
- // TODO(Zan): Find util (or make one)
202
- const docId = id.split('~').at(0);
203
- const [spaceId, objectId] = docId?.split(':') ?? [];
136
+ const [subjectId] = id.split('~');
137
+ const { spaceId, objectId } = parseId(subjectId);
204
138
  const space = client.spaces.get().find((space) => space.id === spaceId);
205
- const doc = space?.db.getObjectById(objectId);
206
- if (!doc || !docId) {
139
+ if (!objectId) {
140
+ // TODO(wittjosiah): Support comments for arbitrary subjects.
141
+ // This is to ensure that the comments panel is not stuck on an old object.
142
+ return {
143
+ id,
144
+ type,
145
+ data: null,
146
+ properties: {
147
+ icon,
148
+ label: ['unnamed object threads label', { ns: THREAD_PLUGIN }],
149
+ showResolvedThreads: false,
150
+ object: null,
151
+ space,
152
+ },
153
+ };
154
+ }
155
+
156
+ const object = toSignal(
157
+ (onChange) => {
158
+ const timeout = setTimeout(async () => {
159
+ await space?.db.loadObjectById(objectId);
160
+ onChange();
161
+ });
162
+
163
+ return () => clearTimeout(timeout);
164
+ },
165
+ () => space?.db.getObjectById(objectId),
166
+ subjectId,
167
+ );
168
+ if (!object || !subjectId) {
207
169
  return;
208
170
  }
209
171
 
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;
172
+ const meta = metadataResolver(getTypename(object) ?? '');
173
+ const label = meta.label?.(object) ||
174
+ object.name ||
175
+ meta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
215
176
 
216
- const viewState = getViewState(docId);
177
+ const viewState = getViewState(subjectId);
217
178
 
218
179
  return {
219
180
  id,
220
- type: 'orphan-comments-for-subject',
221
- data: doc,
181
+ type,
182
+ data: null,
222
183
  properties: {
223
- icon: meta.iconComponent,
184
+ icon,
224
185
  label,
225
186
  showResolvedThreads: viewState.showResolvedThreads,
187
+ object,
226
188
  },
227
189
  };
228
190
  },
@@ -256,6 +218,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
256
218
  menuItemType: 'toggle',
257
219
  isChecked: viewState.showResolvedThreads,
258
220
  testId: 'threadPlugin.toggleShowResolved',
221
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
259
222
  },
260
223
  },
261
224
  ];
@@ -287,8 +250,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
287
250
  },
288
251
  properties: {
289
252
  label: ['create channel label', { ns: THREAD_PLUGIN }],
290
- icon: (props: IconProps) => <Chat {...props} />,
291
- iconSymbol: 'ph--chat--regular',
253
+ icon: 'ph--chat--regular',
292
254
  testId: 'threadPlugin.createObject',
293
255
  },
294
256
  },
@@ -311,11 +273,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
311
273
  return data.plugin === meta.id ? <ThreadSettings settings={settings.values} /> : null;
312
274
  }
313
275
 
314
- case 'article':
315
- case 'complementary': {
276
+ case 'article': {
316
277
  const location = navigationPlugin?.provides.location;
317
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
319
278
 
320
279
  if (data.object instanceof ChannelType && data.object.threads[0]) {
321
280
  const channel = data.object;
@@ -342,93 +301,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
342
301
  );
343
302
  }
344
303
 
304
+ break;
305
+ }
306
+
307
+ case 'complementary--comments': {
345
308
  if (
346
309
  data.subject &&
347
310
  typeof data.subject === 'object' &&
348
311
  'threads' in data.subject &&
349
312
  Array.isArray(data.subject.threads)
350
313
  ) {
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);
314
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
370
315
 
371
316
  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>
317
+ <ThreadComplementary
318
+ role={role}
319
+ subject={data.subject}
320
+ drafts={state.drafts[fullyQualifiedId(data.subject)]}
321
+ current={state.current}
322
+ showResolvedThreads={showResolvedThreads}
323
+ />
430
324
  );
431
325
  }
326
+
327
+ break;
432
328
  }
433
329
  }
434
330
 
@@ -442,36 +338,48 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
442
338
  if (intent.data && intent.data.cursor !== undefined) {
443
339
  const { cursor, name, subject } = intent.data;
444
340
 
341
+ // Seed the threads array if it does not exist.
342
+ if (subject?.threads === undefined) {
343
+ try {
344
+ // Static schema will throw an error if subject does not support threads array property.
345
+ subject.threads = [];
346
+ } catch (err) {
347
+ log.error('Subject does not support threads array', subject?.typename);
348
+ return;
349
+ }
350
+ }
351
+
445
352
  const subjectId = fullyQualifiedId(subject);
446
353
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
- const stagingArea = state.staging[subjectId];
448
- if (stagingArea) {
449
- stagingArea.push(thread);
354
+ const draft = state.drafts[subjectId];
355
+ if (draft) {
356
+ draft.push(thread);
450
357
  } else {
451
- state.staging[subjectId] = [thread];
358
+ state.drafts[subjectId] = [thread];
452
359
  }
453
360
 
454
361
  return {
455
362
  data: thread,
456
363
  intents: [
364
+ [
365
+ {
366
+ action: ThreadAction.SELECT,
367
+ data: { current: fullyQualifiedId(thread) },
368
+ },
369
+ ],
457
370
  [
458
371
  {
459
372
  action: NavigationAction.OPEN,
460
373
  data: {
461
- activeParts: {
462
- complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
463
- },
374
+ activeParts: { complementary: 'comments' },
464
375
  },
465
376
  },
466
- {
467
- action: ThreadAction.SELECT,
468
- data: { current: thread.id, focus: true },
469
- },
377
+ ],
378
+ [
470
379
  {
471
380
  action: LayoutAction.SET_LAYOUT,
472
381
  data: {
473
382
  element: 'complementary',
474
- subject: subjectId,
475
383
  state: true,
476
384
  },
477
385
  },
@@ -480,14 +388,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
480
388
  };
481
389
  } else {
482
390
  // NOTE: This is the standalone thread creation case.
483
- return { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
391
+ return {
392
+ data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }),
393
+ };
484
394
  }
485
395
  }
486
396
 
487
397
  case ThreadAction.SELECT: {
488
- state.focus = intent.data?.current === state.current ? state.focus : intent.data?.focus;
489
398
  state.current = intent.data?.current;
490
- return { data: true };
399
+
400
+ return {
401
+ data: true,
402
+ intents: !intent.data?.skipOpen
403
+ ? [
404
+ [
405
+ {
406
+ action: NavigationAction.OPEN,
407
+ data: {
408
+ activeParts: { complementary: 'comments' },
409
+ },
410
+ },
411
+ ],
412
+ ]
413
+ : [],
414
+ };
491
415
  }
492
416
 
493
417
  case ThreadAction.TOGGLE_RESOLVED: {
@@ -506,6 +430,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
506
430
  const spaceId = space?.id;
507
431
 
508
432
  return {
433
+ data: true,
509
434
  intents: [
510
435
  [
511
436
  {
@@ -519,19 +444,21 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
519
444
 
520
445
  case ThreadAction.DELETE: {
521
446
  const { subject, thread } = intent.data ?? {};
522
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
447
+ if (
448
+ !(thread instanceof ThreadType) ||
449
+ !(subject && typeof subject === 'object' && 'threads' in subject)
450
+ ) {
523
451
  return;
524
452
  }
525
453
 
526
454
  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);
455
+ const draft = state.drafts[subjectId];
456
+ if (draft) {
457
+ // Check if we're deleting a draft; if so, remove it.
458
+ const index = draft.findIndex((t) => t.id === thread.id);
532
459
  if (index !== -1) {
533
- stagingArea.splice(index, 1);
534
- return;
460
+ draft.splice(index, 1);
461
+ return { data: true };
535
462
  }
536
463
  }
537
464
 
@@ -541,7 +468,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
541
468
  }
542
469
 
543
470
  if (!intent.undo) {
544
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
471
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
545
472
  const cursor = subject.threads[index]?.anchor;
546
473
  if (index !== -1) {
547
474
  subject.threads?.splice(index, 1);
@@ -587,7 +514,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
587
514
  const { thread, subject } = intent.data ?? {};
588
515
  if (
589
516
  !(thread instanceof ThreadType) ||
590
- !(subject && typeof subject === 'object' && 'threads' in subject)
517
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
591
518
  ) {
592
519
  return;
593
520
  }
@@ -597,12 +524,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
597
524
  const intents = [];
598
525
  const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
599
526
 
600
- if (state.staging[subjectId]?.find((t) => t === thread)) {
601
- // Move thread from staging to document.
527
+ if (state.drafts[subjectId]?.find((t) => t === thread)) {
528
+ // Move draft to document.
602
529
  thread.status = 'active';
603
530
  subject.threads ? subject.threads.push(thread) : (subject.threads = [thread]);
604
- state.staging[subjectId] = state.staging[subjectId]?.filter((t) => t.id !== thread.id);
605
-
531
+ state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
606
532
  intents.push({
607
533
  action: ObservabilityAction.SEND_EVENT,
608
534
  data: { name: 'threads.thread-created', properties: analyticsProperties },
@@ -627,7 +553,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
627
553
  const { subject, thread, messageId } = intent.data ?? {};
628
554
  const space = getSpace(subject);
629
555
 
630
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
556
+ if (
557
+ !(thread instanceof ThreadType) ||
558
+ !(subject && typeof subject === 'object' && 'threads' in subject)
559
+ ) {
631
560
  return;
632
561
  }
633
562
 
@@ -658,7 +587,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
658
587
  action: ObservabilityAction.SEND_EVENT,
659
588
  data: {
660
589
  name: 'threads.message.delete',
661
- properties: { threadId: thread.id, spaceId: space.id },
590
+ properties: { threadId: thread.id, spaceId: space?.id },
662
591
  },
663
592
  },
664
593
  ],
@@ -667,13 +596,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
667
596
  } else if (intent.undo) {
668
597
  const message = intent.data?.message;
669
598
  const messageIndex = intent.data?.messageIndex;
670
-
671
599
  if (!(message instanceof MessageType) || !(typeof messageIndex === 'number')) {
672
600
  return;
673
601
  }
674
602
 
675
603
  thread.messages.splice(messageIndex, 0, message);
676
-
677
604
  return {
678
605
  data: true,
679
606
  intents: [
@@ -682,7 +609,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
682
609
  action: ObservabilityAction.SEND_EVENT,
683
610
  data: {
684
611
  name: 'threads.message.undo-delete',
685
- properties: { threadId: thread.id, spaceId: space.id },
612
+ properties: { threadId: thread.id, spaceId: space?.id },
686
613
  },
687
614
  },
688
615
  ],
@@ -694,96 +621,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
694
621
  },
695
622
  },
696
623
  markdown: {
697
- // TODO(burdon): Factor out extension factory into separate file (for simplicity).
698
624
  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
- ];
625
+ return threads(state, doc, intentPlugin?.provides.intent.dispatch);
787
626
  },
788
627
  },
789
628
  },