@dxos/plugin-thread 0.6.12-main.ed7cda7 → 0.6.12-staging.0b4bb48

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