@dxos/plugin-thread 0.6.11 → 0.6.12-main.15a606f

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 (79) hide show
  1. package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs → ThreadArticle-4VU752F4.mjs} +3 -3
  2. package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs → ThreadMain-ML3P4XSD.mjs} +3 -3
  3. package/dist/lib/browser/{chunk-5IBJO53M.mjs → chunk-7NUYW5IW.mjs} +2 -5
  4. package/dist/lib/browser/chunk-7NUYW5IW.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-GKPZHUT3.mjs → chunk-GJRBBPXI.mjs} +2 -2
  6. package/dist/lib/browser/{chunk-GKPZHUT3.mjs.map → chunk-GJRBBPXI.mjs.map} +1 -1
  7. package/dist/lib/browser/{chunk-P7ZE5N5M.mjs → chunk-XF5QKI75.mjs} +13 -11
  8. package/dist/lib/browser/chunk-XF5QKI75.mjs.map +7 -0
  9. package/dist/lib/browser/index.mjs +237 -207
  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-ERUHPHVT.cjs} +7 -7
  15. package/dist/lib/node/{ThreadMain-XJX75GHG.cjs → ThreadMain-6DN7FDNB.cjs} +7 -7
  16. package/dist/lib/node/{chunk-BWBONZGU.cjs → chunk-6EVXCSDS.cjs} +5 -18
  17. package/dist/lib/node/chunk-6EVXCSDS.cjs.map +7 -0
  18. package/dist/lib/node/{chunk-D5GOVXHS.cjs → chunk-FXD3DYMO.cjs} +6 -6
  19. package/dist/lib/node/{chunk-D5GOVXHS.cjs.map → chunk-FXD3DYMO.cjs.map} +1 -1
  20. package/dist/lib/node/{chunk-5PZNPPQF.cjs → chunk-PZVUW4W2.cjs} +65 -63
  21. package/dist/lib/node/chunk-PZVUW4W2.cjs.map +7 -0
  22. package/dist/lib/node/index.cjs +324 -296
  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-NKI5F4W5.mjs +19 -0
  30. package/dist/lib/node-esm/ThreadArticle-NKI5F4W5.mjs.map +7 -0
  31. package/dist/lib/node-esm/ThreadMain-2A3VQLCW.mjs +27 -0
  32. package/dist/lib/node-esm/ThreadMain-2A3VQLCW.mjs.map +7 -0
  33. package/dist/lib/node-esm/chunk-A44C4MTQ.mjs +413 -0
  34. package/dist/lib/node-esm/chunk-A44C4MTQ.mjs.map +7 -0
  35. package/dist/lib/node-esm/chunk-Y475KZTX.mjs +20 -0
  36. package/dist/lib/node-esm/chunk-Y475KZTX.mjs.map +7 -0
  37. package/dist/lib/node-esm/chunk-YO46JGAE.mjs +21 -0
  38. package/dist/lib/node-esm/chunk-YO46JGAE.mjs.map +7 -0
  39. package/dist/lib/node-esm/index.mjs +1133 -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.map +1 -1
  49. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  50. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  51. package/dist/types/src/components/ThreadComplementary.d.ts +11 -0
  52. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -0
  53. package/dist/types/src/components/testing.d.ts +2 -0
  54. package/dist/types/src/components/testing.d.ts.map +1 -1
  55. package/dist/types/src/meta.d.ts +1 -4
  56. package/dist/types/src/meta.d.ts.map +1 -1
  57. package/dist/types/src/types.d.ts +1 -1
  58. package/dist/types/src/types.d.ts.map +1 -1
  59. package/package.json +37 -34
  60. package/src/ThreadPlugin.tsx +67 -178
  61. package/src/components/ChatContainer.tsx +7 -8
  62. package/src/components/CommentContainer.tsx +10 -7
  63. package/src/components/Comments.stories.tsx +2 -2
  64. package/src/components/CommentsContainer.tsx +22 -18
  65. package/src/components/MessageContainer.tsx +2 -1
  66. package/src/components/ThreadComplementary.tsx +120 -0
  67. package/src/meta.tsx +1 -5
  68. package/src/types.ts +1 -1
  69. package/dist/lib/browser/chunk-5IBJO53M.mjs.map +0 -7
  70. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  71. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  72. package/dist/lib/node/chunk-BWBONZGU.cjs.map +0 -7
  73. package/dist/types/src/types.test.d.ts +0 -2
  74. package/dist/types/src/types.test.d.ts.map +0 -1
  75. package/src/types.test.ts +0 -13
  76. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-4VU752F4.mjs.map} +0 -0
  77. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-ML3P4XSD.mjs.map} +0 -0
  78. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-ERUHPHVT.cjs.map} +0 -0
  79. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-6DN7FDNB.cjs.map} +0 -0
@@ -2,7 +2,6 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Chat, type IconProps } from '@phosphor-icons/react';
6
5
  import { computed, effect } from '@preact/signals-core';
7
6
  import React from 'react';
8
7
 
@@ -16,51 +15,35 @@ import {
16
15
  parseIntentPlugin,
17
16
  parseNavigationPlugin,
18
17
  resolvePlugin,
19
- parseGraphPlugin,
20
- SLUG_PATH_SEPARATOR,
21
- SLUG_COLLECTION_INDICATOR,
22
18
  isLayoutParts,
23
19
  parseMetadataResolverPlugin,
24
- type IntentDispatcher,
25
- useResolvePlugins,
26
20
  } from '@dxos/app-framework';
27
21
  import { type UnsubscribeCallback } from '@dxos/async';
28
22
  import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
29
23
  import { create } from '@dxos/echo-schema';
30
24
  import { LocalStorageStore } from '@dxos/local-storage';
31
- import { type AttentionPluginProvides, parseAttentionPlugin } from '@dxos/plugin-attention';
25
+ import { log } from '@dxos/log';
32
26
  import { parseClientPlugin } from '@dxos/plugin-client';
33
- import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
34
- import { DocumentType } from '@dxos/plugin-markdown/types';
27
+ import { type ActionGroup, createExtension, isActionGroup, toSignal } from '@dxos/plugin-graph';
35
28
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
36
29
  import { SpaceAction } from '@dxos/plugin-space';
37
30
  import { ThreadType, MessageType, ChannelType } from '@dxos/plugin-space/types';
38
31
  import {
39
32
  getSpace,
40
33
  getTextInRange,
41
- Filter,
42
34
  createDocAccessor,
43
35
  fullyQualifiedId,
44
36
  loadObjectReferences,
45
37
  } from '@dxos/react-client/echo';
46
- import { ScrollArea } from '@dxos/react-ui';
47
- import { createAttendableAttributes } from '@dxos/react-ui-attention';
48
38
  import { comments, createExternalCommentSync, listener } from '@dxos/react-ui-editor';
49
39
  import { translations as threadTranslations } from '@dxos/react-ui-thread';
50
40
  import { nonNullable } from '@dxos/util';
51
41
 
52
- import {
53
- ThreadMain,
54
- ThreadSettings,
55
- CommentsContainer,
56
- CommentsHeading,
57
- ChatContainer,
58
- ChatHeading,
59
- ThreadArticle,
60
- } from './components';
42
+ import { ThreadMain, ThreadSettings, ChatContainer, ChatHeading, ThreadArticle } from './components';
43
+ import { ThreadComplementary } from './components/ThreadComplementary';
61
44
  import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
62
45
  import translations from './translations';
63
- import { ThreadAction, type ThreadProvides, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
46
+ import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
64
47
 
65
48
  type ThreadState = {
66
49
  /** An in-memory staging area for threads that are being drafted. */
@@ -74,8 +57,6 @@ const initialViewState = { showResolvedThreads: false };
74
57
  type ViewStore = Record<SubjectId, typeof initialViewState>;
75
58
 
76
59
  // 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;
79
60
 
80
61
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
81
62
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
@@ -90,10 +71,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
90
71
  return viewStore[subjectId];
91
72
  };
92
73
 
93
- let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
94
74
  let navigationPlugin: Plugin<LocationProvides> | undefined;
95
75
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
96
- let dispatch: IntentDispatcher | undefined;
97
76
 
98
77
  const unsubscribeCallbacks = [] as UnsubscribeCallback[];
99
78
 
@@ -102,45 +81,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
102
81
  ready: async (plugins) => {
103
82
  settings.prop({ key: 'standalone', type: LocalStorageStore.bool({ allowUndefined: true }) });
104
83
 
105
- attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
106
84
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
107
85
  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
86
  },
145
87
  unload: async () => {
146
88
  unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
@@ -151,8 +93,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
151
93
  records: {
152
94
  [ChannelType.typename]: {
153
95
  placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
154
- icon: (props: IconProps) => <Chat {...props} />,
155
- iconSymbol: 'ph--chat--regular',
96
+ icon: 'ph--chat--regular',
156
97
  // TODO(wittjosiah): Move out of metadata.
157
98
  loadReferences: (channel: ChannelType) => loadObjectReferences(channel, (channel) => channel.threads),
158
99
  },
@@ -198,29 +139,38 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
198
139
  return;
199
140
  }
200
141
 
201
- // TODO(Zan): Find util (or make one)
202
- const docId = id.split('~').at(0);
203
- const [spaceId, objectId] = docId?.split(':') ?? [];
142
+ // TODO(Zan): Find util (or make one).
143
+ const subjectId = id.split('~').at(0);
144
+ const [spaceId, objectId] = subjectId?.split(':') ?? [];
204
145
  const space = client.spaces.get().find((space) => space.id === spaceId);
205
- const doc = space?.db.getObjectById(objectId);
206
- if (!doc || !docId) {
146
+ const object = toSignal(
147
+ (onChange) => {
148
+ const timeout = setTimeout(async () => {
149
+ await space?.db.loadObjectById(objectId);
150
+ onChange();
151
+ });
152
+
153
+ return () => clearTimeout(timeout);
154
+ },
155
+ () => space?.db.getObjectById(objectId),
156
+ );
157
+ if (!object || !subjectId) {
207
158
  return;
208
159
  }
209
160
 
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;
161
+ const meta = metadataResolver(getTypename(object) ?? '');
162
+ const label = meta.label?.(object) ||
163
+ object.name ||
164
+ meta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
215
165
 
216
- const viewState = getViewState(docId);
166
+ const viewState = getViewState(subjectId);
217
167
 
218
168
  return {
219
169
  id,
220
170
  type: 'orphan-comments-for-subject',
221
- data: doc,
171
+ data: object,
222
172
  properties: {
223
- icon: meta.iconComponent,
173
+ icon: meta.icon,
224
174
  label,
225
175
  showResolvedThreads: viewState.showResolvedThreads,
226
176
  },
@@ -256,6 +206,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
256
206
  menuItemType: 'toggle',
257
207
  isChecked: viewState.showResolvedThreads,
258
208
  testId: 'threadPlugin.toggleShowResolved',
209
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
259
210
  },
260
211
  },
261
212
  ];
@@ -287,8 +238,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
287
238
  },
288
239
  properties: {
289
240
  label: ['create channel label', { ns: THREAD_PLUGIN }],
290
- icon: (props: IconProps) => <Chat {...props} />,
291
- iconSymbol: 'ph--chat--regular',
241
+ icon: 'ph--chat--regular',
292
242
  testId: 'threadPlugin.createObject',
293
243
  },
294
244
  },
@@ -314,8 +264,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
314
264
  case 'article':
315
265
  case 'complementary': {
316
266
  const location = navigationPlugin?.provides.location;
317
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
319
267
 
320
268
  if (data.object instanceof ChannelType && data.object.threads[0]) {
321
269
  const channel = data.object;
@@ -348,85 +296,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
348
296
  'threads' in data.subject &&
349
297
  Array.isArray(data.subject.threads)
350
298
  ) {
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);
299
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
370
300
 
371
301
  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>
302
+ <ThreadComplementary
303
+ role={role}
304
+ subject={data.subject}
305
+ stagedThreads={state.staging[fullyQualifiedId(data.subject)]}
306
+ current={state.current}
307
+ focus
308
+ showResolvedThreads={showResolvedThreads}
309
+ />
430
310
  );
431
311
  }
432
312
  }
@@ -442,6 +322,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
442
322
  if (intent.data && intent.data.cursor !== undefined) {
443
323
  const { cursor, name, subject } = intent.data;
444
324
 
325
+ // Seed the threads array if it does not exist.
326
+ if (subject?.threads === undefined) {
327
+ try {
328
+ // Static schema will throw an error if subject does not support threads array property.
329
+ subject.threads = [];
330
+ } catch (e) {
331
+ log.error('Subject does not support threads array', subject?.typename);
332
+ return;
333
+ }
334
+ }
335
+
445
336
  const subjectId = fullyQualifiedId(subject);
446
337
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
338
  const stagingArea = state.staging[subjectId];
@@ -455,17 +346,9 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
455
346
  data: thread,
456
347
  intents: [
457
348
  [
458
- {
459
- action: NavigationAction.OPEN,
460
- data: {
461
- activeParts: {
462
- complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
463
- },
464
- },
465
- },
466
349
  {
467
350
  action: ThreadAction.SELECT,
468
- data: { current: thread.id, focus: true },
351
+ data: { current: fullyQualifiedId(thread), focus: true },
469
352
  },
470
353
  {
471
354
  action: LayoutAction.SET_LAYOUT,
@@ -519,7 +402,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
519
402
 
520
403
  case ThreadAction.DELETE: {
521
404
  const { subject, thread } = intent.data ?? {};
522
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
405
+ if (
406
+ !(thread instanceof ThreadType) ||
407
+ !(subject && typeof subject === 'object' && 'threads' in subject)
408
+ ) {
523
409
  return;
524
410
  }
525
411
 
@@ -531,7 +417,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
531
417
  const index = stagingArea.findIndex((t) => t.id === thread.id);
532
418
  if (index !== -1) {
533
419
  stagingArea.splice(index, 1);
534
- return;
420
+ return { data: true };
535
421
  }
536
422
  }
537
423
 
@@ -541,7 +427,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
541
427
  }
542
428
 
543
429
  if (!intent.undo) {
544
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
430
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
545
431
  const cursor = subject.threads[index]?.anchor;
546
432
  if (index !== -1) {
547
433
  subject.threads?.splice(index, 1);
@@ -587,7 +473,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
587
473
  const { thread, subject } = intent.data ?? {};
588
474
  if (
589
475
  !(thread instanceof ThreadType) ||
590
- !(subject && typeof subject === 'object' && 'threads' in subject)
476
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
591
477
  ) {
592
478
  return;
593
479
  }
@@ -627,7 +513,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
627
513
  const { subject, thread, messageId } = intent.data ?? {};
628
514
  const space = getSpace(subject);
629
515
 
630
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
516
+ if (
517
+ !(thread instanceof ThreadType) ||
518
+ !(subject && typeof subject === 'object' && 'threads' in subject)
519
+ ) {
631
520
  return;
632
521
  }
633
522
 
@@ -658,7 +547,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
658
547
  action: ObservabilityAction.SEND_EVENT,
659
548
  data: {
660
549
  name: 'threads.message.delete',
661
- properties: { threadId: thread.id, spaceId: space.id },
550
+ properties: { threadId: thread.id, spaceId: space?.id },
662
551
  },
663
552
  },
664
553
  ],
@@ -682,7 +571,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
682
571
  action: ObservabilityAction.SEND_EVENT,
683
572
  data: {
684
573
  name: 'threads.message.undo-delete',
685
- properties: { threadId: thread.id, spaceId: space.id },
574
+ properties: { threadId: thread.id, spaceId: space?.id },
686
575
  },
687
576
  },
688
577
  ],
@@ -733,7 +622,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
733
622
  () =>
734
623
  threads.value
735
624
  .filter((thread) => thread?.anchor)
736
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! })),
625
+ .map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
737
626
  ),
738
627
  comments({
739
628
  id: doc.id,
@@ -754,7 +643,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
754
643
  // If the thread is in the staging area, remove it.
755
644
  const stagingArea = state.staging[fullyQualifiedId(doc)];
756
645
  if (stagingArea) {
757
- const index = stagingArea.findIndex((thread) => thread.id === id);
646
+ const index = stagingArea.findIndex((thread) => fullyQualifiedId(thread) === id);
758
647
  if (index !== -1) {
759
648
  stagingArea.splice(index, 1);
760
649
  }
@@ -767,7 +656,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
767
656
  },
768
657
  onUpdate: ({ id, cursor }) => {
769
658
  const thread =
770
- state.staging[fullyQualifiedId(doc)]?.find((thread) => thread.id === id) ??
659
+ state.staging[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
771
660
  doc.threads.find((thread) => thread?.id === id);
772
661
 
773
662
  if (thread instanceof ThreadType && thread.anchor) {
@@ -2,15 +2,14 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Chat } from '@phosphor-icons/react';
6
5
  import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
7
6
 
8
7
  import { create } from '@dxos/echo-schema';
9
8
  import { MessageType } from '@dxos/plugin-space/types';
10
- import { getSpace, useMembers } from '@dxos/react-client/echo';
9
+ import { fullyQualifiedId, getSpace, useMembers } from '@dxos/react-client/echo';
11
10
  import { useIdentity } from '@dxos/react-client/halo';
12
- import { ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
13
- import { PlankHeading, plankHeadingIconProps } from '@dxos/react-ui-deck';
11
+ import { Icon, ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
12
+ import { PlankHeading } from '@dxos/react-ui-deck';
14
13
  import { createBasicExtensions, createThemeExtensions, listener } from '@dxos/react-ui-editor';
15
14
  import { mx } from '@dxos/react-ui-theme';
16
15
  import { MessageTextbox, type MessageTextboxProps, Thread, ThreadFooter, threadLayout } from '@dxos/react-ui-thread';
@@ -28,7 +27,7 @@ export const ChatHeading = ({ attendableId }: { attendableId?: string }) => {
28
27
  return (
29
28
  <div role='none' className='flex items-center'>
30
29
  <PlankHeading.Button attendableId={attendableId}>
31
- <Chat {...plankHeadingIconProps} />
30
+ <Icon icon='ph--chat--regular' size={5} />
32
31
  </PlankHeading.Button>
33
32
  <PlankHeading.Label attendableId={attendableId}>{t('chat heading')}</PlankHeading.Label>
34
33
  </div>
@@ -39,7 +38,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
39
38
  const identity = useIdentity()!;
40
39
  const space = getSpace(thread);
41
40
  const members = useMembers(space?.key);
42
- const activity = useStatus(space, thread.id);
41
+ const activity = useStatus(space, fullyQualifiedId(thread));
43
42
  const { t } = useTranslation(THREAD_PLUGIN);
44
43
  // TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
45
44
  const [_count, _setCount] = useState(0);
@@ -48,7 +47,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
48
47
  const threadScrollRef = useRef<HTMLDivElement | null>(null);
49
48
  const { themeMode } = useThemeContext();
50
49
 
51
- const textboxMetadata = getMessageMetadata(thread.id, identity);
50
+ const textboxMetadata = getMessageMetadata(fullyQualifiedId(thread), identity);
52
51
  const messageRef = useRef('');
53
52
  const extensions = useMemo(
54
53
  () => [
@@ -103,7 +102,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
103
102
  return (
104
103
  <Thread
105
104
  current={current}
106
- id={thread.id}
105
+ id={fullyQualifiedId(thread)}
107
106
  classNames='bs-full grid-rows-[1fr_min-content_min-content] overflow-hidden transition-[padding-block-end] [[data-sidebar-inline-start-state=open]_&]:lg:pbe-0'
108
107
  >
109
108
  <ScrollArea.Root classNames='col-span-2'>
@@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
7
7
 
8
8
  import { create } from '@dxos/echo-schema';
9
9
  import { MessageType } from '@dxos/plugin-space/types';
10
- import { getSpace, useMembers } from '@dxos/react-client/echo';
10
+ import { fullyQualifiedId, getSpace, useMembers } from '@dxos/react-client/echo';
11
11
  import { useIdentity } from '@dxos/react-client/halo';
12
12
  import { Button, Tag, Tooltip, useThemeContext, useTranslation } from '@dxos/react-ui';
13
13
  import { createBasicExtensions, createThemeExtensions, listener } from '@dxos/react-ui-editor';
@@ -103,13 +103,13 @@ export const CommentContainer = ({
103
103
  const identity = useIdentity()!;
104
104
  const space = getSpace(thread);
105
105
  const members = useMembers(space?.key);
106
- const activity = useStatus(space, thread.id);
106
+ const activity = useStatus(space, fullyQualifiedId(thread));
107
107
  const { t } = useTranslation(THREAD_PLUGIN);
108
108
  const threadScrollRef = useRef<HTMLDivElement | null>(null);
109
109
  const [autoFocus, setAutoFocus] = useState(!!autoFocusTextbox);
110
110
  const { themeMode } = useThemeContext();
111
111
 
112
- const textboxMetadata = getMessageMetadata(thread.id, identity);
112
+ const textboxMetadata = getMessageMetadata(fullyQualifiedId(thread), identity);
113
113
  // TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
114
114
  const [_count, _setCount] = useState(0);
115
115
  const rerenderEditor = () => _setCount((count) => count + 1);
@@ -159,10 +159,8 @@ export const CommentContainer = ({
159
159
  return true;
160
160
  }, [thread, identity]);
161
161
 
162
- const handleDeleteMessage = (id: string) => onMessageDelete?.(id);
163
-
164
162
  return (
165
- <Thread onClickCapture={onAttend} onFocusCapture={onAttend} current={current} id={thread.id}>
163
+ <Thread onClickCapture={onAttend} onFocusCapture={onAttend} current={current} id={fullyQualifiedId(thread)}>
166
164
  <div
167
165
  role='none'
168
166
  className={mx(
@@ -195,7 +193,12 @@ export const CommentContainer = ({
195
193
  </div>
196
194
  </div>
197
195
  {thread.messages.filter(nonNullable).map((message) => (
198
- <MessageContainer key={message.id} message={message} members={members} onDelete={handleDeleteMessage} />
196
+ <MessageContainer
197
+ key={message.id}
198
+ message={message}
199
+ members={members}
200
+ onDelete={(id: string) => onMessageDelete?.(id)}
201
+ />
199
202
  ))}
200
203
  <MessageTextbox extensions={extensions} autoFocus={autoFocus} onSend={handleCreate} {...textboxMetadata} />
201
204
  <ThreadFooter activity={activity}>{t('activity message')}</ThreadFooter>
@@ -8,7 +8,7 @@ import React, { useEffect, useState } from 'react';
8
8
 
9
9
  import { MessageType, ThreadType } from '@dxos/plugin-space/types';
10
10
  import { faker } from '@dxos/random';
11
- import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
11
+ import { Filter, fullyQualifiedId, useQuery, useSpace } from '@dxos/react-client/echo';
12
12
  import { useIdentity } from '@dxos/react-client/halo';
13
13
  import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
14
14
  import { withLayout, withTheme } from '@dxos/storybook-utils';
@@ -30,7 +30,7 @@ const Story = ({ spaceKey }: ClientRepeatedComponentProps) => {
30
30
  const t = setTimeout(async () => {
31
31
  space.db.add(createCommentThread(identity));
32
32
  const thread = space.db.add(createCommentThread(identity));
33
- setDetached([thread.id]);
33
+ setDetached([fullyQualifiedId(thread)]);
34
34
  });
35
35
 
36
36
  return () => clearTimeout(t);
@@ -2,12 +2,13 @@
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
4
 
5
- import { ChatText, Quotes } from '@phosphor-icons/react';
5
+ import { ChatText } from '@phosphor-icons/react';
6
6
  import React, { useEffect } from 'react';
7
7
 
8
8
  import { type ThreadType } from '@dxos/plugin-space/types';
9
- import { useTranslation, Trans } from '@dxos/react-ui';
10
- import { PlankHeading, plankHeadingIconProps } from '@dxos/react-ui-deck';
9
+ import { fullyQualifiedId } from '@dxos/react-client/echo';
10
+ import { useTranslation, Icon, Trans } from '@dxos/react-ui';
11
+ import { PlankHeading } from '@dxos/react-ui-deck';
11
12
  import { descriptionText, mx } from '@dxos/react-ui-theme';
12
13
 
13
14
  import { CommentContainer } from './CommentContainer';
@@ -39,7 +40,7 @@ export const CommentsHeading = ({ attendableId }: { attendableId?: string }) =>
39
40
  return (
40
41
  <div role='none' className='flex items-center'>
41
42
  <PlankHeading.Button attendableId={attendableId}>
42
- <Quotes {...plankHeadingIconProps} />
43
+ <Icon icon='ph--quotes--regular' size={5} />
43
44
  </PlankHeading.Button>
44
45
  <PlankHeading.Label attendableId={attendableId}>{t('comments heading')}</PlankHeading.Label>
45
46
  </div>
@@ -73,20 +74,23 @@ export const CommentsContainer = ({
73
74
  return (
74
75
  <>
75
76
  {filteredThreads.length > 0 ? (
76
- filteredThreads.map((thread) => (
77
- <CommentContainer
78
- key={thread.id}
79
- thread={thread}
80
- current={currentId === thread.id}
81
- detached={detached.includes(thread.id)}
82
- autoFocusTextbox={autoFocusCurrentTextbox && currentId === thread.id}
83
- {...(onThreadAttend && { onAttend: () => onThreadAttend(thread) })}
84
- {...(onThreadDelete && { onThreadDelete: () => onThreadDelete(thread) })}
85
- {...(onMessageDelete && { onMessageDelete: (messageId: string) => onMessageDelete(thread, messageId) })}
86
- {...(onThreadToggleResolved && { onResolve: () => onThreadToggleResolved(thread) })}
87
- {...props}
88
- />
89
- ))
77
+ filteredThreads.map((thread) => {
78
+ const threadId = fullyQualifiedId(thread);
79
+ return (
80
+ <CommentContainer
81
+ key={threadId}
82
+ thread={thread}
83
+ current={currentId === threadId}
84
+ detached={detached.includes(threadId)}
85
+ autoFocusTextbox={autoFocusCurrentTextbox && currentId === threadId}
86
+ {...(onThreadAttend && { onAttend: () => onThreadAttend(thread) })}
87
+ {...(onThreadDelete && { onThreadDelete: () => onThreadDelete(thread) })}
88
+ {...(onMessageDelete && { onMessageDelete: (messageId: string) => onMessageDelete(thread, messageId) })}
89
+ {...(onThreadToggleResolved && { onResolve: () => onThreadToggleResolved(thread) })}
90
+ {...props}
91
+ />
92
+ );
93
+ })
90
94
  ) : (
91
95
  <div
92
96
  role='alert'
@@ -1,6 +1,7 @@
1
1
  //
2
2
  // Copyright 2024 DXOS.org
3
3
  //
4
+
4
5
  import { EditorView } from '@codemirror/view';
5
6
  import { Check, PencilSimple, X } from '@phosphor-icons/react';
6
7
  import React, { forwardRef, useCallback, useEffect, useRef, useState } from 'react';
@@ -91,7 +92,7 @@ export const MessageContainer = ({
91
92
  variant='ghost'
92
93
  data-testid='thread.message.delete'
93
94
  classNames={messageControlClassNames}
94
- onClick={handleDelete}
95
+ onClick={() => handleDelete()}
95
96
  >
96
97
  <span className='sr-only'>{deleteLabel}</span>
97
98
  <X className={getSize(4)} />