@dxos/plugin-thread 0.6.11-staging.e6894a4 → 0.6.12-main.5cc132e

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 (68) hide show
  1. package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs → ThreadArticle-6L6BGE4Z.mjs} +2 -2
  2. package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs → ThreadMain-DU6LBSN3.mjs} +2 -2
  3. package/dist/lib/browser/{chunk-P7ZE5N5M.mjs → chunk-AVC4CGJM.mjs} +6 -6
  4. package/dist/lib/browser/chunk-AVC4CGJM.mjs.map +7 -0
  5. package/dist/lib/browser/{chunk-GKPZHUT3.mjs → chunk-U5UVX4ZP.mjs} +1 -1
  6. package/dist/lib/browser/{chunk-GKPZHUT3.mjs.map → chunk-U5UVX4ZP.mjs.map} +1 -1
  7. package/dist/lib/browser/index.mjs +217 -156
  8. package/dist/lib/browser/index.mjs.map +4 -4
  9. package/dist/lib/browser/meta.json +1 -1
  10. package/dist/lib/browser/types.mjs +1 -1
  11. package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs → ThreadArticle-7BWXVIJ4.cjs} +6 -6
  12. package/dist/lib/node/{ThreadMain-XJX75GHG.cjs → ThreadMain-XSRB6NKJ.cjs} +6 -6
  13. package/dist/lib/node/{chunk-D5GOVXHS.cjs → chunk-4B2ARV7L.cjs} +4 -4
  14. package/dist/lib/node/{chunk-D5GOVXHS.cjs.map → chunk-4B2ARV7L.cjs.map} +1 -1
  15. package/dist/lib/node/{chunk-5PZNPPQF.cjs → chunk-T7TXCNHL.cjs} +8 -8
  16. package/dist/lib/node/chunk-T7TXCNHL.cjs.map +7 -0
  17. package/dist/lib/node/index.cjs +258 -199
  18. package/dist/lib/node/index.cjs.map +4 -4
  19. package/dist/lib/node/meta.json +1 -1
  20. package/dist/lib/node/types.cjs +2 -2
  21. package/dist/lib/node/types.cjs.map +1 -1
  22. package/dist/lib/node-esm/ThreadArticle-6L6BGE4Z.mjs +18 -0
  23. package/dist/lib/node-esm/ThreadArticle-6L6BGE4Z.mjs.map +7 -0
  24. package/dist/lib/node-esm/ThreadMain-DU6LBSN3.mjs +26 -0
  25. package/dist/lib/node-esm/ThreadMain-DU6LBSN3.mjs.map +7 -0
  26. package/dist/lib/node-esm/chunk-5IBJO53M.mjs +21 -0
  27. package/dist/lib/node-esm/chunk-5IBJO53M.mjs.map +7 -0
  28. package/dist/lib/node-esm/chunk-AVC4CGJM.mjs +410 -0
  29. package/dist/lib/node-esm/chunk-AVC4CGJM.mjs.map +7 -0
  30. package/dist/lib/node-esm/chunk-U5UVX4ZP.mjs +20 -0
  31. package/dist/lib/node-esm/chunk-U5UVX4ZP.mjs.map +7 -0
  32. package/dist/lib/node-esm/index.mjs +1166 -0
  33. package/dist/lib/node-esm/index.mjs.map +7 -0
  34. package/dist/lib/node-esm/meta.json +1 -0
  35. package/dist/lib/node-esm/meta.mjs +11 -0
  36. package/dist/lib/node-esm/meta.mjs.map +7 -0
  37. package/dist/lib/node-esm/types.mjs +8 -0
  38. package/dist/lib/node-esm/types.mjs.map +7 -0
  39. package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
  40. package/dist/types/src/components/Chat.stories.d.ts +0 -1
  41. package/dist/types/src/components/Chat.stories.d.ts.map +1 -1
  42. package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
  43. package/dist/types/src/components/Comments.stories.d.ts.map +1 -1
  44. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  45. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  46. package/dist/types/src/components/ThreadComplementary.d.ts +11 -0
  47. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -0
  48. package/dist/types/src/types.d.ts +1 -1
  49. package/dist/types/src/types.d.ts.map +1 -1
  50. package/package.json +35 -32
  51. package/src/ThreadPlugin.tsx +49 -123
  52. package/src/components/Chat.stories.tsx +3 -3
  53. package/src/components/ChatContainer.tsx +4 -4
  54. package/src/components/CommentContainer.tsx +10 -7
  55. package/src/components/Comments.stories.tsx +10 -15
  56. package/src/components/CommentsContainer.tsx +18 -14
  57. package/src/components/MessageContainer.tsx +2 -1
  58. package/src/components/ThreadComplementary.tsx +120 -0
  59. package/src/types.ts +1 -1
  60. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  61. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  62. package/dist/types/src/types.test.d.ts +0 -2
  63. package/dist/types/src/types.test.d.ts.map +0 -1
  64. package/src/types.test.ts +0 -13
  65. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-6L6BGE4Z.mjs.map} +0 -0
  66. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-DU6LBSN3.mjs.map} +0 -0
  67. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-7BWXVIJ4.cjs.map} +0 -0
  68. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-XSRB6NKJ.cjs.map} +0 -0
@@ -4,7 +4,7 @@
4
4
 
5
5
  import { Chat, type IconProps } from '@phosphor-icons/react';
6
6
  import { computed, effect } from '@preact/signals-core';
7
- import React, { useCallback } from 'react';
7
+ import React from 'react';
8
8
 
9
9
  import {
10
10
  type IntentPluginProvides,
@@ -21,17 +21,15 @@ import {
21
21
  SLUG_COLLECTION_INDICATOR,
22
22
  isLayoutParts,
23
23
  parseMetadataResolverPlugin,
24
- type IntentDispatcher,
25
- useResolvePlugins,
26
24
  } from '@dxos/app-framework';
27
25
  import { type UnsubscribeCallback } from '@dxos/async';
28
26
  import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
29
27
  import { create } from '@dxos/echo-schema';
30
28
  import { LocalStorageStore } from '@dxos/local-storage';
29
+ import { log } from '@dxos/log';
31
30
  import { type AttentionPluginProvides, parseAttentionPlugin } from '@dxos/plugin-attention';
32
31
  import { parseClientPlugin } from '@dxos/plugin-client';
33
32
  import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
34
- import { DocumentType } from '@dxos/plugin-markdown/types';
35
33
  import { ObservabilityAction } from '@dxos/plugin-observability/meta';
36
34
  import { SpaceAction } from '@dxos/plugin-space';
37
35
  import { ThreadType, MessageType, ChannelType } from '@dxos/plugin-space/types';
@@ -43,24 +41,15 @@ import {
43
41
  fullyQualifiedId,
44
42
  loadObjectReferences,
45
43
  } from '@dxos/react-client/echo';
46
- import { ScrollArea } from '@dxos/react-ui';
47
- import { createAttendableAttributes } from '@dxos/react-ui-attention';
48
44
  import { comments, createExternalCommentSync, listener } from '@dxos/react-ui-editor';
49
45
  import { translations as threadTranslations } from '@dxos/react-ui-thread';
50
46
  import { nonNullable } from '@dxos/util';
51
47
 
52
- import {
53
- ThreadMain,
54
- ThreadSettings,
55
- CommentsContainer,
56
- CommentsHeading,
57
- ChatContainer,
58
- ChatHeading,
59
- ThreadArticle,
60
- } from './components';
48
+ import { ThreadMain, ThreadSettings, ChatContainer, ChatHeading, ThreadArticle } from './components';
49
+ import { ThreadComplementary } from './components/ThreadComplementary';
61
50
  import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
62
51
  import translations from './translations';
63
- import { ThreadAction, type ThreadProvides, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
52
+ import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
64
53
 
65
54
  type ThreadState = {
66
55
  /** An in-memory staging area for threads that are being drafted. */
@@ -73,6 +62,8 @@ type SubjectId = string;
73
62
  const initialViewState = { showResolvedThreads: false };
74
63
  type ViewStore = Record<SubjectId, typeof initialViewState>;
75
64
 
65
+ // TODO(Zan): More robust runtime check.
66
+
76
67
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
77
68
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
78
69
  const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
@@ -89,7 +80,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
89
80
  let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
90
81
  let navigationPlugin: Plugin<LocationProvides> | undefined;
91
82
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
92
- let dispatch: IntentDispatcher | undefined;
93
83
 
94
84
  const unsubscribeCallbacks = [] as UnsubscribeCallback[];
95
85
 
@@ -101,7 +91,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
101
91
  attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
102
92
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
103
93
  intentPlugin = resolvePlugin(plugins, parseIntentPlugin)!;
104
- dispatch = intentPlugin?.provides.intent.dispatch;
105
94
 
106
95
  const graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
107
96
  const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
@@ -121,7 +110,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
121
110
 
122
111
  const firstAttendedNodeWithComments = Array.from(attention.attended)
123
112
  .map((id) => graphPlugin?.provides.graph.findNode(id))
124
- .find((node) => node?.data instanceof DocumentType && (node.data.threads?.length ?? 0) > 0);
113
+ .find((node) => {
114
+ const data = node?.data as { threads?: unknown };
115
+ return Array.isArray(data?.threads) && data.threads.length > 0;
116
+ });
125
117
 
126
118
  if (firstAttendedNodeWithComments) {
127
119
  void intentPlugin?.provides.intent.dispatch({
@@ -199,7 +191,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
199
191
  const [spaceId, objectId] = docId?.split(':') ?? [];
200
192
  const space = client.spaces.get().find((space) => space.id === spaceId);
201
193
  const doc = space?.db.getObjectById(objectId);
202
-
203
194
  if (!doc || !docId) {
204
195
  return;
205
196
  }
@@ -208,7 +199,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
208
199
  const label = docMeta.label?.(doc) ||
209
200
  doc.name ||
210
201
  docMeta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
211
- // const label = docMeta.label?.(doc) || doc.name || docMeta.placeholder;
212
202
 
213
203
  const viewState = getViewState(docId);
214
204
 
@@ -225,7 +215,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
225
215
  },
226
216
  actions: ({ node }) => {
227
217
  const dataId = node.id.split('~').at(0);
228
-
229
218
  if (!node.id.endsWith('~comments') || !dataId) {
230
219
  return;
231
220
  }
@@ -254,6 +243,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
254
243
  menuItemType: 'toggle',
255
244
  isChecked: viewState.showResolvedThreads,
256
245
  testId: 'threadPlugin.toggleShowResolved',
246
+ iconSymbol: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
257
247
  },
258
248
  },
259
249
  ];
@@ -313,24 +303,12 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
313
303
  case 'complementary': {
314
304
  const location = navigationPlugin?.provides.location;
315
305
 
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
306
  if (data.object instanceof ChannelType && data.object.threads[0]) {
327
307
  const channel = data.object;
328
308
  // TODO(zan): Maybe we should have utility for positional main object ids.
329
309
  if (isLayoutParts(location?.active) && location.active.main) {
330
310
  const layoutEntries = location.active.main;
331
-
332
311
  const currentPosition = layoutEntries.findIndex((entry) => channel.id === entry.id);
333
-
334
312
  if (currentPosition > 0) {
335
313
  const objectToTheLeft = layoutEntries[currentPosition - 1];
336
314
  const context = getSpace(data.object)?.db.getObjectById(objectToTheLeft.id);
@@ -356,85 +334,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
356
334
  'threads' in data.subject &&
357
335
  Array.isArray(data.subject.threads)
358
336
  ) {
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);
337
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
378
338
 
379
339
  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>
340
+ <ThreadComplementary
341
+ role={role}
342
+ subject={data.subject}
343
+ stagedThreads={state.staging[fullyQualifiedId(data.subject)]}
344
+ current={state.current}
345
+ focus
346
+ showResolvedThreads={showResolvedThreads}
347
+ />
438
348
  );
439
349
  }
440
350
  }
@@ -450,9 +360,19 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
450
360
  if (intent.data && intent.data.cursor !== undefined) {
451
361
  const { cursor, name, subject } = intent.data;
452
362
 
363
+ // Seed the threads array if it does not exist.
364
+ if (subject?.threads === undefined) {
365
+ try {
366
+ // Static schema will throw an error if subject does not support threads array property.
367
+ subject.threads = [];
368
+ } catch (e) {
369
+ log.error('Subject does not support threads array', subject?.typename);
370
+ return;
371
+ }
372
+ }
373
+
453
374
  const subjectId = fullyQualifiedId(subject);
454
375
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
455
-
456
376
  const stagingArea = state.staging[subjectId];
457
377
  if (stagingArea) {
458
378
  stagingArea.push(thread);
@@ -474,7 +394,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
474
394
  },
475
395
  {
476
396
  action: ThreadAction.SELECT,
477
- data: { current: thread.id, focus: true },
397
+ data: { current: fullyQualifiedId(thread), focus: true },
478
398
  },
479
399
  {
480
400
  action: LayoutAction.SET_LAYOUT,
@@ -528,7 +448,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
528
448
 
529
449
  case ThreadAction.DELETE: {
530
450
  const { subject, thread } = intent.data ?? {};
531
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
451
+ if (
452
+ !(thread instanceof ThreadType) ||
453
+ !(subject && typeof subject === 'object' && 'threads' in subject)
454
+ ) {
532
455
  return;
533
456
  }
534
457
 
@@ -540,7 +463,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
540
463
  const index = stagingArea.findIndex((t) => t.id === thread.id);
541
464
  if (index !== -1) {
542
465
  stagingArea.splice(index, 1);
543
- return;
466
+ return { data: true };
544
467
  }
545
468
  }
546
469
 
@@ -550,7 +473,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
550
473
  }
551
474
 
552
475
  if (!intent.undo) {
553
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
476
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
554
477
  const cursor = subject.threads[index]?.anchor;
555
478
  if (index !== -1) {
556
479
  subject.threads?.splice(index, 1);
@@ -596,7 +519,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
596
519
  const { thread, subject } = intent.data ?? {};
597
520
  if (
598
521
  !(thread instanceof ThreadType) ||
599
- !(subject && typeof subject === 'object' && 'threads' in subject)
522
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
600
523
  ) {
601
524
  return;
602
525
  }
@@ -636,7 +559,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
636
559
  const { subject, thread, messageId } = intent.data ?? {};
637
560
  const space = getSpace(subject);
638
561
 
639
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
562
+ if (
563
+ !(thread instanceof ThreadType) ||
564
+ !(subject && typeof subject === 'object' && 'threads' in subject)
565
+ ) {
640
566
  return;
641
567
  }
642
568
 
@@ -667,7 +593,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
667
593
  action: ObservabilityAction.SEND_EVENT,
668
594
  data: {
669
595
  name: 'threads.message.delete',
670
- properties: { threadId: thread.id, spaceId: space.id },
596
+ properties: { threadId: thread.id, spaceId: space?.id },
671
597
  },
672
598
  },
673
599
  ],
@@ -691,7 +617,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
691
617
  action: ObservabilityAction.SEND_EVENT,
692
618
  data: {
693
619
  name: 'threads.message.undo-delete',
694
- properties: { threadId: thread.id, spaceId: space.id },
620
+ properties: { threadId: thread.id, spaceId: space?.id },
695
621
  },
696
622
  },
697
623
  ],
@@ -742,7 +668,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
742
668
  () =>
743
669
  threads.value
744
670
  .filter((thread) => thread?.anchor)
745
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! })),
671
+ .map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
746
672
  ),
747
673
  comments({
748
674
  id: doc.id,
@@ -763,7 +689,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
763
689
  // If the thread is in the staging area, remove it.
764
690
  const stagingArea = state.staging[fullyQualifiedId(doc)];
765
691
  if (stagingArea) {
766
- const index = stagingArea.findIndex((thread) => thread.id === id);
692
+ const index = stagingArea.findIndex((thread) => fullyQualifiedId(thread) === id);
767
693
  if (index !== -1) {
768
694
  stagingArea.splice(index, 1);
769
695
  }
@@ -776,7 +702,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
776
702
  },
777
703
  onUpdate: ({ id, cursor }) => {
778
704
  const thread =
779
- state.staging[fullyQualifiedId(doc)]?.find((thread) => thread.id === id) ??
705
+ state.staging[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
780
706
  doc.threads.find((thread) => thread?.id === id);
781
707
 
782
708
  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 = {};
@@ -7,7 +7,7 @@ import React, { useLayoutEffect, 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 { ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
13
13
  import { PlankHeading, plankHeadingIconProps } from '@dxos/react-ui-deck';
@@ -39,7 +39,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
39
39
  const identity = useIdentity()!;
40
40
  const space = getSpace(thread);
41
41
  const members = useMembers(space?.key);
42
- const activity = useStatus(space, thread.id);
42
+ const activity = useStatus(space, fullyQualifiedId(thread));
43
43
  const { t } = useTranslation(THREAD_PLUGIN);
44
44
  // TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
45
45
  const [_count, _setCount] = useState(0);
@@ -48,7 +48,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
48
48
  const threadScrollRef = useRef<HTMLDivElement | null>(null);
49
49
  const { themeMode } = useThemeContext();
50
50
 
51
- const textboxMetadata = getMessageMetadata(thread.id, identity);
51
+ const textboxMetadata = getMessageMetadata(fullyQualifiedId(thread), identity);
52
52
  const messageRef = useRef('');
53
53
  const extensions = useMemo(
54
54
  () => [
@@ -103,7 +103,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
103
103
  return (
104
104
  <Thread
105
105
  current={current}
106
- id={thread.id}
106
+ id={fullyQualifiedId(thread)}
107
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'
108
108
  >
109
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 { 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
 
@@ -6,6 +6,7 @@ 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';
9
10
  import { useTranslation, Trans } from '@dxos/react-ui';
10
11
  import { PlankHeading, plankHeadingIconProps } from '@dxos/react-ui-deck';
11
12
  import { descriptionText, mx } from '@dxos/react-ui-theme';
@@ -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)} />