@dxos/plugin-thread 0.6.13 → 0.6.14-main.1366248

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 (106) hide show
  1. package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs → ThreadArticle-6RFOFTU2.mjs} +3 -3
  2. package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs → ThreadMain-45OFO3G6.mjs} +3 -3
  3. package/dist/lib/browser/{chunk-P7ZE5N5M.mjs → chunk-OTCR7JTS.mjs} +17 -17
  4. package/dist/lib/browser/chunk-OTCR7JTS.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-YLYCGZ62.mjs +17 -0
  6. package/dist/lib/browser/chunk-YLYCGZ62.mjs.map +7 -0
  7. package/dist/lib/browser/{chunk-GKPZHUT3.mjs → chunk-YQUONHZJ.mjs} +2 -2
  8. package/dist/lib/browser/chunk-YQUONHZJ.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +452 -388
  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-IUK2EECC.cjs} +7 -7
  15. package/dist/lib/node/{ThreadMain-XJX75GHG.cjs → ThreadMain-CXIQPH4H.cjs} +7 -7
  16. package/dist/lib/node/{chunk-D5GOVXHS.cjs → chunk-LGA276VA.cjs} +6 -6
  17. package/dist/lib/node/chunk-LGA276VA.cjs.map +7 -0
  18. package/dist/lib/node/{chunk-5PZNPPQF.cjs → chunk-XOW5SL5M.cjs} +66 -66
  19. package/dist/lib/node/chunk-XOW5SL5M.cjs.map +7 -0
  20. package/dist/lib/node/{chunk-BWBONZGU.cjs → chunk-YWFYN3OY.cjs} +8 -22
  21. package/dist/lib/node/chunk-YWFYN3OY.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +501 -441
  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-OA3T4XFJ.mjs +19 -0
  30. package/dist/lib/node-esm/ThreadArticle-OA3T4XFJ.mjs.map +7 -0
  31. package/dist/lib/node-esm/ThreadMain-VXFO6JL6.mjs +27 -0
  32. package/dist/lib/node-esm/ThreadMain-VXFO6JL6.mjs.map +7 -0
  33. package/dist/lib/node-esm/chunk-3CZBPOEH.mjs +21 -0
  34. package/dist/lib/node-esm/chunk-3CZBPOEH.mjs.map +7 -0
  35. package/dist/lib/node-esm/chunk-5R5QKQY4.mjs +411 -0
  36. package/dist/lib/node-esm/chunk-5R5QKQY4.mjs.map +7 -0
  37. package/dist/lib/node-esm/chunk-SSEJYCYQ.mjs +19 -0
  38. package/dist/lib/node-esm/chunk-SSEJYCYQ.mjs.map +7 -0
  39. package/dist/lib/node-esm/index.mjs +1167 -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 +4 -9
  70. package/dist/types/src/meta.d.ts.map +1 -1
  71. package/dist/types/src/translations.d.ts +1 -0
  72. package/dist/types/src/translations.d.ts.map +1 -1
  73. package/dist/types/src/types.d.ts +8 -2
  74. package/dist/types/src/types.d.ts.map +1 -1
  75. package/package.json +42 -46
  76. package/src/ThreadPlugin.tsx +158 -309
  77. package/src/components/Chat.stories.tsx +7 -4
  78. package/src/components/ChatContainer.tsx +7 -8
  79. package/src/components/CommentContainer.tsx +25 -23
  80. package/src/components/Comments.stories.tsx +9 -6
  81. package/src/components/CommentsContainer.tsx +18 -31
  82. package/src/components/MessageContainer.tsx +15 -24
  83. package/src/components/ThreadComplementary.tsx +113 -0
  84. package/src/components/ThreadSettings.tsx +3 -3
  85. package/src/components/index.ts +6 -6
  86. package/src/components/types.ts +2 -2
  87. package/src/extensions/index.ts +5 -0
  88. package/src/extensions/threads.ts +117 -0
  89. package/src/meta.ts +16 -0
  90. package/src/translations.ts +1 -0
  91. package/src/types.ts +11 -4
  92. package/dist/lib/browser/chunk-5IBJO53M.mjs +0 -21
  93. package/dist/lib/browser/chunk-5IBJO53M.mjs.map +0 -7
  94. package/dist/lib/browser/chunk-GKPZHUT3.mjs.map +0 -7
  95. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  96. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  97. package/dist/lib/node/chunk-BWBONZGU.cjs.map +0 -7
  98. package/dist/lib/node/chunk-D5GOVXHS.cjs.map +0 -7
  99. package/dist/types/src/types.test.d.ts +0 -2
  100. package/dist/types/src/types.test.d.ts.map +0 -1
  101. package/src/meta.tsx +0 -20
  102. package/src/types.test.ts +0 -13
  103. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-6RFOFTU2.mjs.map} +0 -0
  104. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-45OFO3G6.mjs.map} +0 -0
  105. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-IUK2EECC.cjs.map} +0 -0
  106. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-CXIQPH4H.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());
@@ -150,9 +83,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
150
83
  metadata: {
151
84
  records: {
152
85
  [ChannelType.typename]: {
153
- placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
154
- icon: (props: IconProps) => <Chat {...props} />,
155
- iconSymbol: 'ph--chat--regular',
86
+ placeholder: ['channel name placeholder', { ns: THREAD_PLUGIN }],
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
  },
@@ -180,49 +112,88 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
180
112
  },
181
113
  translations: [...translations, ...threadTranslations],
182
114
  echo: { schema: [ChannelType, ThreadType, MessageType] },
115
+ complementary: {
116
+ panels: [
117
+ {
118
+ id: 'comments',
119
+ label: ['open comments panel label', { ns: THREAD_PLUGIN }],
120
+ icon: 'ph--chat-text--regular',
121
+ },
122
+ ],
123
+ },
183
124
  graph: {
184
125
  builder: (plugins) => {
185
126
  const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
186
127
  const dispatch = resolvePlugin(plugins, parseIntentPlugin)?.provides.intent.dispatch;
187
128
  const metadataResolver = resolvePlugin(plugins, parseMetadataResolverPlugin)?.provides.metadata.resolver;
188
-
189
129
  if (!client || !dispatch || !metadataResolver) {
190
130
  return [];
191
131
  }
192
132
 
133
+ const type = 'orphan-comments-for-subject';
134
+ const icon = 'ph--chat-text--regular';
135
+
193
136
  return [
194
137
  createExtension({
195
138
  id: `${THREAD_PLUGIN}/comments-for-subject`,
196
139
  resolver: ({ id }) => {
140
+ // TODO(Zan): Find util (or make one).
197
141
  if (!id.endsWith('~comments')) {
198
142
  return;
199
143
  }
200
144
 
201
- // TODO(Zan): Find util (or make one)
202
- const docId = id.split('~').at(0);
203
- const [spaceId, objectId] = docId?.split(':') ?? [];
145
+ const [subjectId] = id.split('~');
146
+ const { spaceId, objectId } = parseId(subjectId);
204
147
  const space = client.spaces.get().find((space) => space.id === spaceId);
205
- const doc = space?.db.getObjectById(objectId);
206
- if (!doc || !docId) {
148
+ if (!objectId) {
149
+ // TODO(wittjosiah): Support comments for arbitrary subjects.
150
+ // This is to ensure that the comments panel is not stuck on an old object.
151
+ return {
152
+ id,
153
+ type,
154
+ data: null,
155
+ properties: {
156
+ icon,
157
+ label: ['unnamed object threads label', { ns: THREAD_PLUGIN }],
158
+ showResolvedThreads: false,
159
+ object: null,
160
+ space,
161
+ },
162
+ };
163
+ }
164
+
165
+ const object = toSignal(
166
+ (onChange) => {
167
+ const timeout = setTimeout(async () => {
168
+ await space?.db.loadObjectById(objectId);
169
+ onChange();
170
+ });
171
+
172
+ return () => clearTimeout(timeout);
173
+ },
174
+ () => space?.db.getObjectById(objectId),
175
+ subjectId,
176
+ );
177
+ if (!object || !subjectId) {
207
178
  return;
208
179
  }
209
180
 
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;
181
+ const meta = metadataResolver(getTypename(object) ?? '');
182
+ const label = meta.label?.(object) ||
183
+ object.name ||
184
+ meta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
215
185
 
216
- const viewState = getViewState(docId);
186
+ const viewState = getViewState(subjectId);
217
187
 
218
188
  return {
219
189
  id,
220
- type: 'orphan-comments-for-subject',
221
- data: doc,
190
+ type,
191
+ data: null,
222
192
  properties: {
223
- icon: meta.iconComponent,
193
+ icon,
224
194
  label,
225
195
  showResolvedThreads: viewState.showResolvedThreads,
196
+ object,
226
197
  },
227
198
  };
228
199
  },
@@ -256,6 +227,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
256
227
  menuItemType: 'toggle',
257
228
  isChecked: viewState.showResolvedThreads,
258
229
  testId: 'threadPlugin.toggleShowResolved',
230
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
259
231
  },
260
232
  },
261
233
  ];
@@ -287,8 +259,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
287
259
  },
288
260
  properties: {
289
261
  label: ['create channel label', { ns: THREAD_PLUGIN }],
290
- icon: (props: IconProps) => <Chat {...props} />,
291
- iconSymbol: 'ph--chat--regular',
262
+ icon: 'ph--chat--regular',
292
263
  testId: 'threadPlugin.createObject',
293
264
  },
294
265
  },
@@ -311,11 +282,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
311
282
  return data.plugin === meta.id ? <ThreadSettings settings={settings.values} /> : null;
312
283
  }
313
284
 
314
- case 'article':
315
- case 'complementary': {
285
+ case 'article': {
316
286
  const location = navigationPlugin?.provides.location;
317
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
319
287
 
320
288
  if (data.object instanceof ChannelType && data.object.threads[0]) {
321
289
  const channel = data.object;
@@ -342,93 +310,31 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
342
310
  );
343
311
  }
344
312
 
313
+ break;
314
+ }
315
+
316
+ case 'complementary--comments': {
345
317
  if (
346
318
  data.subject &&
347
319
  typeof data.subject === 'object' &&
348
320
  'threads' in data.subject &&
349
- Array.isArray(data.subject.threads)
321
+ Array.isArray(data.subject.threads) &&
322
+ !(data.subject instanceof ChannelType)
350
323
  ) {
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);
324
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
370
325
 
371
326
  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>
327
+ <ThreadComplementary
328
+ role={role}
329
+ subject={data.subject}
330
+ drafts={state.drafts[fullyQualifiedId(data.subject)]}
331
+ current={state.current}
332
+ showResolvedThreads={showResolvedThreads}
333
+ />
430
334
  );
431
335
  }
336
+
337
+ break;
432
338
  }
433
339
  }
434
340
 
@@ -442,36 +348,48 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
442
348
  if (intent.data && intent.data.cursor !== undefined) {
443
349
  const { cursor, name, subject } = intent.data;
444
350
 
351
+ // Seed the threads array if it does not exist.
352
+ if (subject?.threads === undefined) {
353
+ try {
354
+ // Static schema will throw an error if subject does not support threads array property.
355
+ subject.threads = [];
356
+ } catch (err) {
357
+ log.error('Subject does not support threads array', subject?.typename);
358
+ return;
359
+ }
360
+ }
361
+
445
362
  const subjectId = fullyQualifiedId(subject);
446
363
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
- const stagingArea = state.staging[subjectId];
448
- if (stagingArea) {
449
- stagingArea.push(thread);
364
+ const draft = state.drafts[subjectId];
365
+ if (draft) {
366
+ draft.push(thread);
450
367
  } else {
451
- state.staging[subjectId] = [thread];
368
+ state.drafts[subjectId] = [thread];
452
369
  }
453
370
 
454
371
  return {
455
372
  data: thread,
456
373
  intents: [
374
+ [
375
+ {
376
+ action: ThreadAction.SELECT,
377
+ data: { current: fullyQualifiedId(thread) },
378
+ },
379
+ ],
457
380
  [
458
381
  {
459
382
  action: NavigationAction.OPEN,
460
383
  data: {
461
- activeParts: {
462
- complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
463
- },
384
+ activeParts: { complementary: 'comments' },
464
385
  },
465
386
  },
466
- {
467
- action: ThreadAction.SELECT,
468
- data: { current: thread.id, focus: true },
469
- },
387
+ ],
388
+ [
470
389
  {
471
390
  action: LayoutAction.SET_LAYOUT,
472
391
  data: {
473
392
  element: 'complementary',
474
- subject: subjectId,
475
393
  state: true,
476
394
  },
477
395
  },
@@ -480,14 +398,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
480
398
  };
481
399
  } else {
482
400
  // NOTE: This is the standalone thread creation case.
483
- return { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
401
+ return {
402
+ data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }),
403
+ };
484
404
  }
485
405
  }
486
406
 
487
407
  case ThreadAction.SELECT: {
488
- state.focus = intent.data?.current === state.current ? state.focus : intent.data?.focus;
489
408
  state.current = intent.data?.current;
490
- return { data: true };
409
+
410
+ return {
411
+ data: true,
412
+ intents: !intent.data?.skipOpen
413
+ ? [
414
+ [
415
+ {
416
+ action: NavigationAction.OPEN,
417
+ data: {
418
+ activeParts: { complementary: 'comments' },
419
+ },
420
+ },
421
+ ],
422
+ ]
423
+ : [],
424
+ };
491
425
  }
492
426
 
493
427
  case ThreadAction.TOGGLE_RESOLVED: {
@@ -506,6 +440,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
506
440
  const spaceId = space?.id;
507
441
 
508
442
  return {
443
+ data: true,
509
444
  intents: [
510
445
  [
511
446
  {
@@ -519,19 +454,21 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
519
454
 
520
455
  case ThreadAction.DELETE: {
521
456
  const { subject, thread } = intent.data ?? {};
522
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
457
+ if (
458
+ !(thread instanceof ThreadType) ||
459
+ !(subject && typeof subject === 'object' && 'threads' in subject)
460
+ ) {
523
461
  return;
524
462
  }
525
463
 
526
464
  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);
465
+ const draft = state.drafts[subjectId];
466
+ if (draft) {
467
+ // Check if we're deleting a draft; if so, remove it.
468
+ const index = draft.findIndex((t) => t.id === thread.id);
532
469
  if (index !== -1) {
533
- stagingArea.splice(index, 1);
534
- return;
470
+ draft.splice(index, 1);
471
+ return { data: true };
535
472
  }
536
473
  }
537
474
 
@@ -541,7 +478,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
541
478
  }
542
479
 
543
480
  if (!intent.undo) {
544
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
481
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
545
482
  const cursor = subject.threads[index]?.anchor;
546
483
  if (index !== -1) {
547
484
  subject.threads?.splice(index, 1);
@@ -587,7 +524,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
587
524
  const { thread, subject } = intent.data ?? {};
588
525
  if (
589
526
  !(thread instanceof ThreadType) ||
590
- !(subject && typeof subject === 'object' && 'threads' in subject)
527
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
591
528
  ) {
592
529
  return;
593
530
  }
@@ -597,12 +534,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
597
534
  const intents = [];
598
535
  const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
599
536
 
600
- if (state.staging[subjectId]?.find((t) => t === thread)) {
601
- // Move thread from staging to document.
537
+ if (state.drafts[subjectId]?.find((t) => t === thread)) {
538
+ // Move draft to document.
602
539
  thread.status = 'active';
603
540
  subject.threads ? subject.threads.push(thread) : (subject.threads = [thread]);
604
- state.staging[subjectId] = state.staging[subjectId]?.filter((t) => t.id !== thread.id);
605
-
541
+ state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
606
542
  intents.push({
607
543
  action: ObservabilityAction.SEND_EVENT,
608
544
  data: { name: 'threads.thread-created', properties: analyticsProperties },
@@ -627,7 +563,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
627
563
  const { subject, thread, messageId } = intent.data ?? {};
628
564
  const space = getSpace(subject);
629
565
 
630
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
566
+ if (
567
+ !(thread instanceof ThreadType) ||
568
+ !(subject && typeof subject === 'object' && 'threads' in subject)
569
+ ) {
631
570
  return;
632
571
  }
633
572
 
@@ -658,7 +597,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
658
597
  action: ObservabilityAction.SEND_EVENT,
659
598
  data: {
660
599
  name: 'threads.message.delete',
661
- properties: { threadId: thread.id, spaceId: space.id },
600
+ properties: { threadId: thread.id, spaceId: space?.id },
662
601
  },
663
602
  },
664
603
  ],
@@ -667,13 +606,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
667
606
  } else if (intent.undo) {
668
607
  const message = intent.data?.message;
669
608
  const messageIndex = intent.data?.messageIndex;
670
-
671
609
  if (!(message instanceof MessageType) || !(typeof messageIndex === 'number')) {
672
610
  return;
673
611
  }
674
612
 
675
613
  thread.messages.splice(messageIndex, 0, message);
676
-
677
614
  return {
678
615
  data: true,
679
616
  intents: [
@@ -682,7 +619,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
682
619
  action: ObservabilityAction.SEND_EVENT,
683
620
  data: {
684
621
  name: 'threads.message.undo-delete',
685
- properties: { threadId: thread.id, spaceId: space.id },
622
+ properties: { threadId: thread.id, spaceId: space?.id },
686
623
  },
687
624
  },
688
625
  ],
@@ -694,96 +631,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
694
631
  },
695
632
  },
696
633
  markdown: {
697
- // TODO(burdon): Factor out extension factory into separate file (for simplicity).
698
634
  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
- ];
635
+ return threads(state, doc, intentPlugin?.provides.intent.dispatch);
787
636
  },
788
637
  },
789
638
  },