@dxos/plugin-thread 0.6.11 → 0.6.12-main.5a87ad5

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 +68 -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,39 @@ 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
+ subjectId,
157
+ );
158
+ if (!object || !subjectId) {
207
159
  return;
208
160
  }
209
161
 
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;
162
+ const meta = metadataResolver(getTypename(object) ?? '');
163
+ const label = meta.label?.(object) ||
164
+ object.name ||
165
+ meta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
215
166
 
216
- const viewState = getViewState(docId);
167
+ const viewState = getViewState(subjectId);
217
168
 
218
169
  return {
219
170
  id,
220
171
  type: 'orphan-comments-for-subject',
221
- data: doc,
172
+ data: object,
222
173
  properties: {
223
- icon: meta.iconComponent,
174
+ icon: meta.icon,
224
175
  label,
225
176
  showResolvedThreads: viewState.showResolvedThreads,
226
177
  },
@@ -256,6 +207,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
256
207
  menuItemType: 'toggle',
257
208
  isChecked: viewState.showResolvedThreads,
258
209
  testId: 'threadPlugin.toggleShowResolved',
210
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
259
211
  },
260
212
  },
261
213
  ];
@@ -287,8 +239,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
287
239
  },
288
240
  properties: {
289
241
  label: ['create channel label', { ns: THREAD_PLUGIN }],
290
- icon: (props: IconProps) => <Chat {...props} />,
291
- iconSymbol: 'ph--chat--regular',
242
+ icon: 'ph--chat--regular',
292
243
  testId: 'threadPlugin.createObject',
293
244
  },
294
245
  },
@@ -314,8 +265,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
314
265
  case 'article':
315
266
  case 'complementary': {
316
267
  const location = navigationPlugin?.provides.location;
317
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
319
268
 
320
269
  if (data.object instanceof ChannelType && data.object.threads[0]) {
321
270
  const channel = data.object;
@@ -348,85 +297,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
348
297
  'threads' in data.subject &&
349
298
  Array.isArray(data.subject.threads)
350
299
  ) {
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);
300
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
370
301
 
371
302
  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>
303
+ <ThreadComplementary
304
+ role={role}
305
+ subject={data.subject}
306
+ stagedThreads={state.staging[fullyQualifiedId(data.subject)]}
307
+ current={state.current}
308
+ focus
309
+ showResolvedThreads={showResolvedThreads}
310
+ />
430
311
  );
431
312
  }
432
313
  }
@@ -442,6 +323,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
442
323
  if (intent.data && intent.data.cursor !== undefined) {
443
324
  const { cursor, name, subject } = intent.data;
444
325
 
326
+ // Seed the threads array if it does not exist.
327
+ if (subject?.threads === undefined) {
328
+ try {
329
+ // Static schema will throw an error if subject does not support threads array property.
330
+ subject.threads = [];
331
+ } catch (e) {
332
+ log.error('Subject does not support threads array', subject?.typename);
333
+ return;
334
+ }
335
+ }
336
+
445
337
  const subjectId = fullyQualifiedId(subject);
446
338
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
339
  const stagingArea = state.staging[subjectId];
@@ -455,17 +347,9 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
455
347
  data: thread,
456
348
  intents: [
457
349
  [
458
- {
459
- action: NavigationAction.OPEN,
460
- data: {
461
- activeParts: {
462
- complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
463
- },
464
- },
465
- },
466
350
  {
467
351
  action: ThreadAction.SELECT,
468
- data: { current: thread.id, focus: true },
352
+ data: { current: fullyQualifiedId(thread), focus: true },
469
353
  },
470
354
  {
471
355
  action: LayoutAction.SET_LAYOUT,
@@ -519,7 +403,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
519
403
 
520
404
  case ThreadAction.DELETE: {
521
405
  const { subject, thread } = intent.data ?? {};
522
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
406
+ if (
407
+ !(thread instanceof ThreadType) ||
408
+ !(subject && typeof subject === 'object' && 'threads' in subject)
409
+ ) {
523
410
  return;
524
411
  }
525
412
 
@@ -531,7 +418,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
531
418
  const index = stagingArea.findIndex((t) => t.id === thread.id);
532
419
  if (index !== -1) {
533
420
  stagingArea.splice(index, 1);
534
- return;
421
+ return { data: true };
535
422
  }
536
423
  }
537
424
 
@@ -541,7 +428,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
541
428
  }
542
429
 
543
430
  if (!intent.undo) {
544
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
431
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
545
432
  const cursor = subject.threads[index]?.anchor;
546
433
  if (index !== -1) {
547
434
  subject.threads?.splice(index, 1);
@@ -587,7 +474,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
587
474
  const { thread, subject } = intent.data ?? {};
588
475
  if (
589
476
  !(thread instanceof ThreadType) ||
590
- !(subject && typeof subject === 'object' && 'threads' in subject)
477
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
591
478
  ) {
592
479
  return;
593
480
  }
@@ -627,7 +514,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
627
514
  const { subject, thread, messageId } = intent.data ?? {};
628
515
  const space = getSpace(subject);
629
516
 
630
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
517
+ if (
518
+ !(thread instanceof ThreadType) ||
519
+ !(subject && typeof subject === 'object' && 'threads' in subject)
520
+ ) {
631
521
  return;
632
522
  }
633
523
 
@@ -658,7 +548,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
658
548
  action: ObservabilityAction.SEND_EVENT,
659
549
  data: {
660
550
  name: 'threads.message.delete',
661
- properties: { threadId: thread.id, spaceId: space.id },
551
+ properties: { threadId: thread.id, spaceId: space?.id },
662
552
  },
663
553
  },
664
554
  ],
@@ -682,7 +572,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
682
572
  action: ObservabilityAction.SEND_EVENT,
683
573
  data: {
684
574
  name: 'threads.message.undo-delete',
685
- properties: { threadId: thread.id, spaceId: space.id },
575
+ properties: { threadId: thread.id, spaceId: space?.id },
686
576
  },
687
577
  },
688
578
  ],
@@ -733,7 +623,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
733
623
  () =>
734
624
  threads.value
735
625
  .filter((thread) => thread?.anchor)
736
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! })),
626
+ .map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
737
627
  ),
738
628
  comments({
739
629
  id: doc.id,
@@ -754,7 +644,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
754
644
  // If the thread is in the staging area, remove it.
755
645
  const stagingArea = state.staging[fullyQualifiedId(doc)];
756
646
  if (stagingArea) {
757
- const index = stagingArea.findIndex((thread) => thread.id === id);
647
+ const index = stagingArea.findIndex((thread) => fullyQualifiedId(thread) === id);
758
648
  if (index !== -1) {
759
649
  stagingArea.splice(index, 1);
760
650
  }
@@ -767,7 +657,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
767
657
  },
768
658
  onUpdate: ({ id, cursor }) => {
769
659
  const thread =
770
- state.staging[fullyQualifiedId(doc)]?.find((thread) => thread.id === id) ??
660
+ state.staging[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
771
661
  doc.threads.find((thread) => thread?.id === id);
772
662
 
773
663
  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)} />