@dxos/plugin-thread 0.6.11-staging.e6894a4 → 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 (83) 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 -210
  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 -299
  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/Chat.stories.d.ts +0 -1
  48. package/dist/types/src/components/Chat.stories.d.ts.map +1 -1
  49. package/dist/types/src/components/ChatContainer.d.ts.map +1 -1
  50. package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
  51. package/dist/types/src/components/Comments.stories.d.ts.map +1 -1
  52. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  53. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  54. package/dist/types/src/components/ThreadComplementary.d.ts +11 -0
  55. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -0
  56. package/dist/types/src/components/testing.d.ts +2 -0
  57. package/dist/types/src/components/testing.d.ts.map +1 -1
  58. package/dist/types/src/meta.d.ts +1 -4
  59. package/dist/types/src/meta.d.ts.map +1 -1
  60. package/dist/types/src/types.d.ts +1 -1
  61. package/dist/types/src/types.d.ts.map +1 -1
  62. package/package.json +37 -34
  63. package/src/ThreadPlugin.tsx +70 -190
  64. package/src/components/Chat.stories.tsx +3 -3
  65. package/src/components/ChatContainer.tsx +7 -8
  66. package/src/components/CommentContainer.tsx +10 -7
  67. package/src/components/Comments.stories.tsx +10 -15
  68. package/src/components/CommentsContainer.tsx +22 -18
  69. package/src/components/MessageContainer.tsx +2 -1
  70. package/src/components/ThreadComplementary.tsx +120 -0
  71. package/src/meta.tsx +1 -5
  72. package/src/types.ts +1 -1
  73. package/dist/lib/browser/chunk-5IBJO53M.mjs.map +0 -7
  74. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  75. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  76. package/dist/lib/node/chunk-BWBONZGU.cjs.map +0 -7
  77. package/dist/types/src/types.test.d.ts +0 -2
  78. package/dist/types/src/types.test.d.ts.map +0 -1
  79. package/src/types.test.ts +0 -13
  80. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-4VU752F4.mjs.map} +0 -0
  81. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-ML3P4XSD.mjs.map} +0 -0
  82. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-ERUHPHVT.cjs.map} +0 -0
  83. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-6DN7FDNB.cjs.map} +0 -0
@@ -2,9 +2,8 @@
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
- import React, { useCallback } from 'react';
6
+ import React from 'react';
8
7
 
9
8
  import {
10
9
  type IntentPluginProvides,
@@ -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. */
@@ -73,6 +56,8 @@ type SubjectId = string;
73
56
  const initialViewState = { showResolvedThreads: false };
74
57
  type ViewStore = Record<SubjectId, typeof initialViewState>;
75
58
 
59
+ // TODO(Zan): More robust runtime check.
60
+
76
61
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
77
62
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
78
63
  const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
@@ -86,10 +71,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
86
71
  return viewStore[subjectId];
87
72
  };
88
73
 
89
- let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
90
74
  let navigationPlugin: Plugin<LocationProvides> | undefined;
91
75
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
92
- let dispatch: IntentDispatcher | undefined;
93
76
 
94
77
  const unsubscribeCallbacks = [] as UnsubscribeCallback[];
95
78
 
@@ -98,45 +81,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
98
81
  ready: async (plugins) => {
99
82
  settings.prop({ key: 'standalone', type: LocalStorageStore.bool({ allowUndefined: true }) });
100
83
 
101
- attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
102
84
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
103
85
  intentPlugin = resolvePlugin(plugins, parseIntentPlugin)!;
104
- dispatch = intentPlugin?.provides.intent.dispatch;
105
-
106
- const graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
107
- const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
108
- if (!client) {
109
- return;
110
- }
111
-
112
- // TODO(wittjosiah): This is a hack to make standalone threads work in the c11y sidebar.
113
- // This should have a better solution when deck is introduced.
114
- const channelsQuery = client.spaces.query(Filter.schema(ChannelType));
115
- const queryUnsubscribe = channelsQuery.subscribe();
116
- const unsubscribe = effect(() => {
117
- const attention = attentionPlugin?.provides.attention;
118
- if (!attention?.attended) {
119
- return;
120
- }
121
-
122
- const firstAttendedNodeWithComments = Array.from(attention.attended)
123
- .map((id) => graphPlugin?.provides.graph.findNode(id))
124
- .find((node) => node?.data instanceof DocumentType && (node.data.threads?.length ?? 0) > 0);
125
-
126
- if (firstAttendedNodeWithComments) {
127
- void intentPlugin?.provides.intent.dispatch({
128
- action: NavigationAction.OPEN,
129
- data: {
130
- activeParts: {
131
- complementary: `${firstAttendedNodeWithComments.id}${SLUG_PATH_SEPARATOR}comments`,
132
- },
133
- },
134
- });
135
- }
136
- });
137
-
138
- unsubscribeCallbacks.push(queryUnsubscribe);
139
- unsubscribeCallbacks.push(unsubscribe);
140
86
  },
141
87
  unload: async () => {
142
88
  unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
@@ -147,8 +93,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
147
93
  records: {
148
94
  [ChannelType.typename]: {
149
95
  placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
150
- icon: (props: IconProps) => <Chat {...props} />,
151
- iconSymbol: 'ph--chat--regular',
96
+ icon: 'ph--chat--regular',
152
97
  // TODO(wittjosiah): Move out of metadata.
153
98
  loadReferences: (channel: ChannelType) => loadObjectReferences(channel, (channel) => channel.threads),
154
99
  },
@@ -194,30 +139,38 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
194
139
  return;
195
140
  }
196
141
 
197
- // TODO(Zan): Find util (or make one)
198
- const docId = id.split('~').at(0);
199
- 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(':') ?? [];
200
145
  const space = client.spaces.get().find((space) => space.id === spaceId);
201
- const doc = space?.db.getObjectById(objectId);
202
-
203
- 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) {
204
158
  return;
205
159
  }
206
160
 
207
- const docMeta = metadataResolver(getTypename(doc) ?? '');
208
- const label = docMeta.label?.(doc) ||
209
- doc.name ||
210
- docMeta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
211
- // 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 }];
212
165
 
213
- const viewState = getViewState(docId);
166
+ const viewState = getViewState(subjectId);
214
167
 
215
168
  return {
216
169
  id,
217
170
  type: 'orphan-comments-for-subject',
218
- data: doc,
171
+ data: object,
219
172
  properties: {
220
- icon: meta.iconComponent,
173
+ icon: meta.icon,
221
174
  label,
222
175
  showResolvedThreads: viewState.showResolvedThreads,
223
176
  },
@@ -225,7 +178,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
225
178
  },
226
179
  actions: ({ node }) => {
227
180
  const dataId = node.id.split('~').at(0);
228
-
229
181
  if (!node.id.endsWith('~comments') || !dataId) {
230
182
  return;
231
183
  }
@@ -254,6 +206,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
254
206
  menuItemType: 'toggle',
255
207
  isChecked: viewState.showResolvedThreads,
256
208
  testId: 'threadPlugin.toggleShowResolved',
209
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
257
210
  },
258
211
  },
259
212
  ];
@@ -285,8 +238,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
285
238
  },
286
239
  properties: {
287
240
  label: ['create channel label', { ns: THREAD_PLUGIN }],
288
- icon: (props: IconProps) => <Chat {...props} />,
289
- iconSymbol: 'ph--chat--regular',
241
+ icon: 'ph--chat--regular',
290
242
  testId: 'threadPlugin.createObject',
291
243
  },
292
244
  },
@@ -313,24 +265,12 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
313
265
  case 'complementary': {
314
266
  const location = navigationPlugin?.provides.location;
315
267
 
316
- const providesThreadsConfig = useCallback(
317
- (plugin: any): Plugin<ThreadProvides<any>> | undefined =>
318
- // TODO(Zan): More robust runtime check.
319
- 'thread' in plugin.provides ? (plugin as Plugin<ThreadProvides<any>>) : undefined,
320
- [],
321
- );
322
-
323
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
324
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
325
-
326
268
  if (data.object instanceof ChannelType && data.object.threads[0]) {
327
269
  const channel = data.object;
328
270
  // TODO(zan): Maybe we should have utility for positional main object ids.
329
271
  if (isLayoutParts(location?.active) && location.active.main) {
330
272
  const layoutEntries = location.active.main;
331
-
332
273
  const currentPosition = layoutEntries.findIndex((entry) => channel.id === entry.id);
333
-
334
274
  if (currentPosition > 0) {
335
275
  const objectToTheLeft = layoutEntries[currentPosition - 1];
336
276
  const context = getSpace(data.object)?.db.getObjectById(objectToTheLeft.id);
@@ -356,85 +296,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
356
296
  'threads' in data.subject &&
357
297
  Array.isArray(data.subject.threads)
358
298
  ) {
359
- const threads = data.subject.threads
360
- .concat(state.staging[fullyQualifiedId(data.subject)])
361
- .filter(nonNullable);
362
-
363
- const createSort = threadProvides.find((p) => p.predicate(data.subject))?.createSort;
364
- if (createSort) {
365
- const sort = createSort(data.subject);
366
- threads.sort((a, b) => sort(a.anchor, b.anchor));
367
- }
368
-
369
- const detached = data.subject.threads
370
- .filter(nonNullable)
371
- .filter(({ anchor }) => !anchor)
372
- .map((thread) => thread.id);
373
-
374
- const qualifiedSubjectId = fullyQualifiedId(data.subject);
375
- const attention = attentionPlugin?.provides.attention?.attended ?? new Set([qualifiedSubjectId]);
376
- const attendableAttrs = createAttendableAttributes(qualifiedSubjectId);
377
- const { showResolvedThreads } = getViewState(qualifiedSubjectId);
299
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
378
300
 
379
301
  return (
380
- <div role='none' className='contents group/attention' {...attendableAttrs}>
381
- {role === 'complementary' && <CommentsHeading attendableId={qualifiedSubjectId} />}
382
- <ScrollArea.Root classNames='row-span-2'>
383
- <ScrollArea.Viewport>
384
- <CommentsContainer
385
- threads={threads}
386
- detached={detached}
387
- currentId={attention.has(qualifiedSubjectId) ? state.current : undefined}
388
- autoFocusCurrentTextbox={state.focus}
389
- showResolvedThreads={showResolvedThreads}
390
- onThreadAttend={(thread) => {
391
- if (state.current !== thread.id) {
392
- state.current = thread.id;
393
- void dispatch?.({
394
- action: LayoutAction.SCROLL_INTO_VIEW,
395
- data: {
396
- id: fullyQualifiedId(data.subject!),
397
- thread: fullyQualifiedId(thread),
398
- cursor: thread.anchor,
399
- },
400
- });
401
- }
402
- }}
403
- onThreadDelete={(thread) =>
404
- dispatch?.({
405
- plugin: THREAD_PLUGIN,
406
- action: ThreadAction.DELETE,
407
- data: { subject: data.subject, thread },
408
- })
409
- }
410
- onMessageDelete={(thread, messageId) =>
411
- dispatch?.({
412
- plugin: THREAD_PLUGIN,
413
- action: ThreadAction.DELETE_MESSAGE,
414
- data: { subject: data.subject, thread, messageId },
415
- })
416
- }
417
- onThreadToggleResolved={(thread) =>
418
- dispatch?.({
419
- plugin: THREAD_PLUGIN,
420
- action: ThreadAction.TOGGLE_RESOLVED,
421
- data: { thread },
422
- })
423
- }
424
- onComment={(thread) => {
425
- void intentPlugin?.provides.intent.dispatch({
426
- action: ThreadAction.ON_MESSAGE_ADD,
427
- data: { thread, subject: data.subject },
428
- });
429
- }}
430
- />
431
- <div role='none' className='bs-10' />
432
- <ScrollArea.Scrollbar>
433
- <ScrollArea.Thumb />
434
- </ScrollArea.Scrollbar>
435
- </ScrollArea.Viewport>
436
- </ScrollArea.Root>
437
- </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
+ />
438
310
  );
439
311
  }
440
312
  }
@@ -450,9 +322,19 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
450
322
  if (intent.data && intent.data.cursor !== undefined) {
451
323
  const { cursor, name, subject } = intent.data;
452
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
+
453
336
  const subjectId = fullyQualifiedId(subject);
454
337
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
455
-
456
338
  const stagingArea = state.staging[subjectId];
457
339
  if (stagingArea) {
458
340
  stagingArea.push(thread);
@@ -464,17 +346,9 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
464
346
  data: thread,
465
347
  intents: [
466
348
  [
467
- {
468
- action: NavigationAction.OPEN,
469
- data: {
470
- activeParts: {
471
- complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
472
- },
473
- },
474
- },
475
349
  {
476
350
  action: ThreadAction.SELECT,
477
- data: { current: thread.id, focus: true },
351
+ data: { current: fullyQualifiedId(thread), focus: true },
478
352
  },
479
353
  {
480
354
  action: LayoutAction.SET_LAYOUT,
@@ -528,7 +402,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
528
402
 
529
403
  case ThreadAction.DELETE: {
530
404
  const { subject, thread } = intent.data ?? {};
531
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
405
+ if (
406
+ !(thread instanceof ThreadType) ||
407
+ !(subject && typeof subject === 'object' && 'threads' in subject)
408
+ ) {
532
409
  return;
533
410
  }
534
411
 
@@ -540,7 +417,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
540
417
  const index = stagingArea.findIndex((t) => t.id === thread.id);
541
418
  if (index !== -1) {
542
419
  stagingArea.splice(index, 1);
543
- return;
420
+ return { data: true };
544
421
  }
545
422
  }
546
423
 
@@ -550,7 +427,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
550
427
  }
551
428
 
552
429
  if (!intent.undo) {
553
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
430
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
554
431
  const cursor = subject.threads[index]?.anchor;
555
432
  if (index !== -1) {
556
433
  subject.threads?.splice(index, 1);
@@ -596,7 +473,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
596
473
  const { thread, subject } = intent.data ?? {};
597
474
  if (
598
475
  !(thread instanceof ThreadType) ||
599
- !(subject && typeof subject === 'object' && 'threads' in subject)
476
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
600
477
  ) {
601
478
  return;
602
479
  }
@@ -636,7 +513,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
636
513
  const { subject, thread, messageId } = intent.data ?? {};
637
514
  const space = getSpace(subject);
638
515
 
639
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
516
+ if (
517
+ !(thread instanceof ThreadType) ||
518
+ !(subject && typeof subject === 'object' && 'threads' in subject)
519
+ ) {
640
520
  return;
641
521
  }
642
522
 
@@ -667,7 +547,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
667
547
  action: ObservabilityAction.SEND_EVENT,
668
548
  data: {
669
549
  name: 'threads.message.delete',
670
- properties: { threadId: thread.id, spaceId: space.id },
550
+ properties: { threadId: thread.id, spaceId: space?.id },
671
551
  },
672
552
  },
673
553
  ],
@@ -691,7 +571,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
691
571
  action: ObservabilityAction.SEND_EVENT,
692
572
  data: {
693
573
  name: 'threads.message.undo-delete',
694
- properties: { threadId: thread.id, spaceId: space.id },
574
+ properties: { threadId: thread.id, spaceId: space?.id },
695
575
  },
696
576
  },
697
577
  ],
@@ -742,7 +622,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
742
622
  () =>
743
623
  threads.value
744
624
  .filter((thread) => thread?.anchor)
745
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! })),
625
+ .map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
746
626
  ),
747
627
  comments({
748
628
  id: doc.id,
@@ -763,7 +643,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
763
643
  // If the thread is in the staging area, remove it.
764
644
  const stagingArea = state.staging[fullyQualifiedId(doc)];
765
645
  if (stagingArea) {
766
- const index = stagingArea.findIndex((thread) => thread.id === id);
646
+ const index = stagingArea.findIndex((thread) => fullyQualifiedId(thread) === id);
767
647
  if (index !== -1) {
768
648
  stagingArea.splice(index, 1);
769
649
  }
@@ -776,7 +656,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
776
656
  },
777
657
  onUpdate: ({ id, cursor }) => {
778
658
  const thread =
779
- state.staging[fullyQualifiedId(doc)]?.find((thread) => thread.id === id) ??
659
+ state.staging[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
780
660
  doc.threads.find((thread) => thread?.id === id);
781
661
 
782
662
  if (thread instanceof ThreadType && thread.anchor) {
@@ -15,7 +15,7 @@ import { useIdentity } from '@dxos/react-client/halo';
15
15
  import { ClientRepeater } from '@dxos/react-client/testing';
16
16
  import { Mosaic } from '@dxos/react-ui-mosaic';
17
17
  import { Thread } from '@dxos/react-ui-thread';
18
- import { withTheme } from '@dxos/storybook-utils';
18
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
19
19
 
20
20
  import { ChatContainer } from './ChatContainer';
21
21
  import { createChatThread } from './testing';
@@ -69,8 +69,8 @@ export default {
69
69
  component: Thread,
70
70
  // TODO(wittjosiah): Register schemas.
71
71
  render: () => <ClientRepeater component={Story} createIdentity createSpace types={[ThreadType]} />,
72
- decorators: [withTheme],
73
- parameters: { translations, layout: 'fullscreen' },
72
+ decorators: [withTheme, withLayout({ fullscreen: true })],
73
+ parameters: { translations },
74
74
  };
75
75
 
76
76
  export const Default = {};
@@ -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,12 +8,10 @@ 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 { type PublicKey } from '@dxos/react-client';
12
- import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
11
+ import { Filter, fullyQualifiedId, useQuery, useSpace } from '@dxos/react-client/echo';
13
12
  import { useIdentity } from '@dxos/react-client/halo';
14
- import { ClientRepeater } from '@dxos/react-client/testing';
15
- import { Tooltip } from '@dxos/react-ui';
16
- import { withFullscreen, withTheme } from '@dxos/storybook-utils';
13
+ import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
14
+ import { withLayout, withTheme } from '@dxos/storybook-utils';
17
15
 
18
16
  import { CommentsContainer } from './CommentsContainer';
19
17
  import { createCommentThread } from './testing';
@@ -21,7 +19,7 @@ import translations from '../translations';
21
19
 
22
20
  faker.seed(1);
23
21
 
24
- const Story = ({ spaceKey }: { spaceKey: PublicKey }) => {
22
+ const Story = ({ spaceKey }: ClientRepeatedComponentProps) => {
25
23
  const identity = useIdentity();
26
24
  const space = useSpace(spaceKey);
27
25
  const threads = useQuery(space, Filter.schema(ThreadType));
@@ -32,7 +30,7 @@ const Story = ({ spaceKey }: { spaceKey: PublicKey }) => {
32
30
  const t = setTimeout(async () => {
33
31
  space.db.add(createCommentThread(identity));
34
32
  const thread = space.db.add(createCommentThread(identity));
35
- setDetached([thread.id]);
33
+ setDetached([fullyQualifiedId(thread)]);
36
34
  });
37
35
 
38
36
  return () => clearTimeout(t);
@@ -43,15 +41,12 @@ const Story = ({ spaceKey }: { spaceKey: PublicKey }) => {
43
41
  return null;
44
42
  }
45
43
 
46
- // TODO(wittjosiah): Include Tooltip.Provider in `withTheme` decorator?
47
44
  return (
48
- <Tooltip.Provider>
49
- <div className='flex justify-center overflow-y-auto bg-white dark:bg-black'>
50
- <div className='flex flex-col w-[400px]'>
51
- <CommentsContainer threads={threads} detached={detached} onThreadDelete={console.log} />
52
- </div>
45
+ <div className='flex justify-center overflow-y-auto bg-white dark:bg-black'>
46
+ <div className='flex flex-col w-[400px]'>
47
+ <CommentsContainer threads={threads} detached={detached} onThreadDelete={console.log} />
53
48
  </div>
54
- </Tooltip.Provider>
49
+ </div>
55
50
  );
56
51
  };
57
52
 
@@ -59,7 +54,7 @@ export default {
59
54
  title: 'plugin-thread/Comments',
60
55
  // TODO(wittjosiah): Register schemas.
61
56
  render: () => <ClientRepeater component={Story} createIdentity createSpace types={[ThreadType, MessageType]} />,
62
- decorators: [withTheme, withFullscreen()],
57
+ decorators: [withTheme, withLayout({ fullscreen: true, tooltips: true })],
63
58
  parameters: { translations },
64
59
  };
65
60