@dxos/plugin-thread 0.6.11 → 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 (64) 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 -153
  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 -196
  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/CommentContainer.d.ts.map +1 -1
  41. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  42. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  43. package/dist/types/src/components/ThreadComplementary.d.ts +11 -0
  44. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -0
  45. package/dist/types/src/types.d.ts +1 -1
  46. package/dist/types/src/types.d.ts.map +1 -1
  47. package/package.json +35 -32
  48. package/src/ThreadPlugin.tsx +46 -111
  49. package/src/components/ChatContainer.tsx +4 -4
  50. package/src/components/CommentContainer.tsx +10 -7
  51. package/src/components/Comments.stories.tsx +2 -2
  52. package/src/components/CommentsContainer.tsx +18 -14
  53. package/src/components/MessageContainer.tsx +2 -1
  54. package/src/components/ThreadComplementary.tsx +120 -0
  55. package/src/types.ts +1 -1
  56. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  57. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  58. package/dist/types/src/types.test.d.ts +0 -2
  59. package/dist/types/src/types.test.d.ts.map +0 -1
  60. package/src/types.test.ts +0 -13
  61. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-6L6BGE4Z.mjs.map} +0 -0
  62. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-DU6LBSN3.mjs.map} +0 -0
  63. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-7BWXVIJ4.cjs.map} +0 -0
  64. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-XSRB6NKJ.cjs.map} +0 -0
@@ -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. */
@@ -74,8 +63,6 @@ const initialViewState = { showResolvedThreads: false };
74
63
  type ViewStore = Record<SubjectId, typeof initialViewState>;
75
64
 
76
65
  // TODO(Zan): More robust runtime check.
77
- const providesThreadsConfig = (plugin: any): Plugin<ThreadProvides<any>> | undefined =>
78
- 'thread' in plugin.provides ? (plugin as Plugin<ThreadProvides<any>>) : undefined;
79
66
 
80
67
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
81
68
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
@@ -93,7 +80,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
93
80
  let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
94
81
  let navigationPlugin: Plugin<LocationProvides> | undefined;
95
82
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
96
- let dispatch: IntentDispatcher | undefined;
97
83
 
98
84
  const unsubscribeCallbacks = [] as UnsubscribeCallback[];
99
85
 
@@ -105,7 +91,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
105
91
  attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
106
92
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
107
93
  intentPlugin = resolvePlugin(plugins, parseIntentPlugin)!;
108
- dispatch = intentPlugin?.provides.intent.dispatch;
109
94
 
110
95
  const graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
111
96
  const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
@@ -125,7 +110,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
125
110
 
126
111
  const firstAttendedNodeWithComments = Array.from(attention.attended)
127
112
  .map((id) => graphPlugin?.provides.graph.findNode(id))
128
- .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
+ });
129
117
 
130
118
  if (firstAttendedNodeWithComments) {
131
119
  void intentPlugin?.provides.intent.dispatch({
@@ -211,7 +199,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
211
199
  const label = docMeta.label?.(doc) ||
212
200
  doc.name ||
213
201
  docMeta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
214
- // const label = docMeta.label?.(doc) || doc.name || docMeta.placeholder;
215
202
 
216
203
  const viewState = getViewState(docId);
217
204
 
@@ -256,6 +243,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
256
243
  menuItemType: 'toggle',
257
244
  isChecked: viewState.showResolvedThreads,
258
245
  testId: 'threadPlugin.toggleShowResolved',
246
+ iconSymbol: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
259
247
  },
260
248
  },
261
249
  ];
@@ -314,8 +302,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
314
302
  case 'article':
315
303
  case 'complementary': {
316
304
  const location = navigationPlugin?.provides.location;
317
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
319
305
 
320
306
  if (data.object instanceof ChannelType && data.object.threads[0]) {
321
307
  const channel = data.object;
@@ -348,85 +334,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
348
334
  'threads' in data.subject &&
349
335
  Array.isArray(data.subject.threads)
350
336
  ) {
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);
337
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
370
338
 
371
339
  return (
372
- <div role='none' className='contents group/attention' {...attendableAttrs}>
373
- {role === 'complementary' && <CommentsHeading attendableId={qualifiedSubjectId} />}
374
- <ScrollArea.Root classNames='row-span-2'>
375
- <ScrollArea.Viewport>
376
- <CommentsContainer
377
- threads={threads}
378
- detached={detached}
379
- currentId={attention.has(qualifiedSubjectId) ? state.current : undefined}
380
- autoFocusCurrentTextbox={state.focus}
381
- showResolvedThreads={showResolvedThreads}
382
- onThreadAttend={(thread) => {
383
- if (state.current !== thread.id) {
384
- state.current = thread.id;
385
- void dispatch?.({
386
- action: LayoutAction.SCROLL_INTO_VIEW,
387
- data: {
388
- id: fullyQualifiedId(data.subject!),
389
- thread: fullyQualifiedId(thread),
390
- cursor: thread.anchor,
391
- },
392
- });
393
- }
394
- }}
395
- onThreadDelete={(thread) =>
396
- dispatch?.({
397
- plugin: THREAD_PLUGIN,
398
- action: ThreadAction.DELETE,
399
- data: { subject: data.subject, thread },
400
- })
401
- }
402
- onMessageDelete={(thread, messageId) =>
403
- dispatch?.({
404
- plugin: THREAD_PLUGIN,
405
- action: ThreadAction.DELETE_MESSAGE,
406
- data: { subject: data.subject, thread, messageId },
407
- })
408
- }
409
- onThreadToggleResolved={(thread) =>
410
- dispatch?.({
411
- plugin: THREAD_PLUGIN,
412
- action: ThreadAction.TOGGLE_RESOLVED,
413
- data: { thread },
414
- })
415
- }
416
- onComment={(thread) => {
417
- void intentPlugin?.provides.intent.dispatch({
418
- action: ThreadAction.ON_MESSAGE_ADD,
419
- data: { thread, subject: data.subject },
420
- });
421
- }}
422
- />
423
- <div role='none' className='bs-10' />
424
- <ScrollArea.Scrollbar>
425
- <ScrollArea.Thumb />
426
- </ScrollArea.Scrollbar>
427
- </ScrollArea.Viewport>
428
- </ScrollArea.Root>
429
- </div>
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
+ />
430
348
  );
431
349
  }
432
350
  }
@@ -442,6 +360,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
442
360
  if (intent.data && intent.data.cursor !== undefined) {
443
361
  const { cursor, name, subject } = intent.data;
444
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
+
445
374
  const subjectId = fullyQualifiedId(subject);
446
375
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
376
  const stagingArea = state.staging[subjectId];
@@ -465,7 +394,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
465
394
  },
466
395
  {
467
396
  action: ThreadAction.SELECT,
468
- data: { current: thread.id, focus: true },
397
+ data: { current: fullyQualifiedId(thread), focus: true },
469
398
  },
470
399
  {
471
400
  action: LayoutAction.SET_LAYOUT,
@@ -519,7 +448,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
519
448
 
520
449
  case ThreadAction.DELETE: {
521
450
  const { subject, thread } = intent.data ?? {};
522
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
451
+ if (
452
+ !(thread instanceof ThreadType) ||
453
+ !(subject && typeof subject === 'object' && 'threads' in subject)
454
+ ) {
523
455
  return;
524
456
  }
525
457
 
@@ -531,7 +463,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
531
463
  const index = stagingArea.findIndex((t) => t.id === thread.id);
532
464
  if (index !== -1) {
533
465
  stagingArea.splice(index, 1);
534
- return;
466
+ return { data: true };
535
467
  }
536
468
  }
537
469
 
@@ -541,7 +473,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
541
473
  }
542
474
 
543
475
  if (!intent.undo) {
544
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
476
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
545
477
  const cursor = subject.threads[index]?.anchor;
546
478
  if (index !== -1) {
547
479
  subject.threads?.splice(index, 1);
@@ -587,7 +519,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
587
519
  const { thread, subject } = intent.data ?? {};
588
520
  if (
589
521
  !(thread instanceof ThreadType) ||
590
- !(subject && typeof subject === 'object' && 'threads' in subject)
522
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
591
523
  ) {
592
524
  return;
593
525
  }
@@ -627,7 +559,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
627
559
  const { subject, thread, messageId } = intent.data ?? {};
628
560
  const space = getSpace(subject);
629
561
 
630
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
562
+ if (
563
+ !(thread instanceof ThreadType) ||
564
+ !(subject && typeof subject === 'object' && 'threads' in subject)
565
+ ) {
631
566
  return;
632
567
  }
633
568
 
@@ -658,7 +593,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
658
593
  action: ObservabilityAction.SEND_EVENT,
659
594
  data: {
660
595
  name: 'threads.message.delete',
661
- properties: { threadId: thread.id, spaceId: space.id },
596
+ properties: { threadId: thread.id, spaceId: space?.id },
662
597
  },
663
598
  },
664
599
  ],
@@ -682,7 +617,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
682
617
  action: ObservabilityAction.SEND_EVENT,
683
618
  data: {
684
619
  name: 'threads.message.undo-delete',
685
- properties: { threadId: thread.id, spaceId: space.id },
620
+ properties: { threadId: thread.id, spaceId: space?.id },
686
621
  },
687
622
  },
688
623
  ],
@@ -733,7 +668,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
733
668
  () =>
734
669
  threads.value
735
670
  .filter((thread) => thread?.anchor)
736
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! })),
671
+ .map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
737
672
  ),
738
673
  comments({
739
674
  id: doc.id,
@@ -754,7 +689,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
754
689
  // If the thread is in the staging area, remove it.
755
690
  const stagingArea = state.staging[fullyQualifiedId(doc)];
756
691
  if (stagingArea) {
757
- const index = stagingArea.findIndex((thread) => thread.id === id);
692
+ const index = stagingArea.findIndex((thread) => fullyQualifiedId(thread) === id);
758
693
  if (index !== -1) {
759
694
  stagingArea.splice(index, 1);
760
695
  }
@@ -767,7 +702,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
767
702
  },
768
703
  onUpdate: ({ id, cursor }) => {
769
704
  const thread =
770
- state.staging[fullyQualifiedId(doc)]?.find((thread) => thread.id === id) ??
705
+ state.staging[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
771
706
  doc.threads.find((thread) => thread?.id === id);
772
707
 
773
708
  if (thread instanceof ThreadType && thread.anchor) {
@@ -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,7 +8,7 @@ import React, { useEffect, useState } from 'react';
8
8
 
9
9
  import { MessageType, ThreadType } from '@dxos/plugin-space/types';
10
10
  import { faker } from '@dxos/random';
11
- import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
11
+ import { Filter, fullyQualifiedId, useQuery, useSpace } from '@dxos/react-client/echo';
12
12
  import { useIdentity } from '@dxos/react-client/halo';
13
13
  import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
14
14
  import { withLayout, withTheme } from '@dxos/storybook-utils';
@@ -30,7 +30,7 @@ const Story = ({ spaceKey }: ClientRepeatedComponentProps) => {
30
30
  const t = setTimeout(async () => {
31
31
  space.db.add(createCommentThread(identity));
32
32
  const thread = space.db.add(createCommentThread(identity));
33
- setDetached([thread.id]);
33
+ setDetached([fullyQualifiedId(thread)]);
34
34
  });
35
35
 
36
36
  return () => clearTimeout(t);
@@ -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)} />
@@ -0,0 +1,120 @@
1
+ //
2
+ // Copyright 2024 DXOS.org
3
+ //
4
+
5
+ import React, { useEffect, useMemo } from 'react';
6
+
7
+ import { LayoutAction, type Plugin, useIntentDispatcher, useResolvePlugins } from '@dxos/app-framework';
8
+ import { type ThreadType } from '@dxos/plugin-space';
9
+ import { fullyQualifiedId } from '@dxos/react-client/echo';
10
+ import { ScrollArea } from '@dxos/react-ui';
11
+ import { createAttendableAttributes, useAttentionContext } from '@dxos/react-ui-attention';
12
+ import { nonNullable } from '@dxos/util';
13
+
14
+ import { CommentsContainer, CommentsHeading } from '../components';
15
+ import { THREAD_PLUGIN } from '../meta';
16
+ import { ThreadAction, type ThreadProvides } from '../types';
17
+
18
+ const providesThreadsConfig = (plugin: any): Plugin<ThreadProvides<any>> | undefined =>
19
+ 'thread' in plugin.provides ? (plugin as Plugin<ThreadProvides<any>>) : undefined;
20
+
21
+ export const ThreadComplementary = ({
22
+ role,
23
+ subject,
24
+ stagedThreads,
25
+ current,
26
+ focus,
27
+ showResolvedThreads,
28
+ }: {
29
+ role: string;
30
+ subject: any;
31
+ stagedThreads: ThreadType[] | undefined;
32
+ current?: string;
33
+ focus?: boolean;
34
+ showResolvedThreads?: boolean;
35
+ }) => {
36
+ const dispatch = useIntentDispatcher();
37
+
38
+ const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
39
+ const threadProvides = useMemo(() => threadsIntegrators.map((p) => p.provides.thread), [threadsIntegrators]);
40
+ const createSort = useMemo(
41
+ () => threadProvides.find((p) => p.predicate(subject))?.createSort,
42
+ [threadProvides, subject],
43
+ );
44
+ const sort = useMemo(() => createSort?.(subject), [createSort, subject]);
45
+
46
+ const threads = useMemo(() => {
47
+ return subject.threads.concat(stagedThreads ?? []).filter(nonNullable) as ThreadType[];
48
+ }, [JSON.stringify(subject.threads), JSON.stringify(stagedThreads)]);
49
+
50
+ const detachedIds = useMemo(() => {
51
+ return threads.filter(({ anchor }) => !anchor).map((thread) => fullyQualifiedId(thread));
52
+ }, [threads]);
53
+
54
+ useEffect(() => {
55
+ if (!sort) {
56
+ return;
57
+ }
58
+ threads.sort((a, b) => sort(a?.anchor, b?.anchor));
59
+ }, [sort, threads]);
60
+
61
+ // TODO(Zan): Maybe we should have a hook for this?
62
+ const { attended } = useAttentionContext('Thread Complementary');
63
+ const qualifiedSubjectId = fullyQualifiedId(subject);
64
+
65
+ const attendableAttrs = createAttendableAttributes(qualifiedSubjectId);
66
+
67
+ return (
68
+ <div role='none' className='contents group/attention' {...attendableAttrs}>
69
+ {role === 'complementary' && <CommentsHeading attendableId={qualifiedSubjectId} />}
70
+ <ScrollArea.Root classNames='row-span-2'>
71
+ <ScrollArea.Viewport>
72
+ <CommentsContainer
73
+ threads={threads}
74
+ detached={detachedIds}
75
+ currentId={attended.has(qualifiedSubjectId) ? current : undefined}
76
+ autoFocusCurrentTextbox={focus}
77
+ showResolvedThreads={showResolvedThreads}
78
+ onThreadAttend={(thread) => {
79
+ const threadId = fullyQualifiedId(thread);
80
+ if (current !== threadId) {
81
+ current = threadId;
82
+
83
+ void dispatch?.([
84
+ {
85
+ action: LayoutAction.SCROLL_INTO_VIEW,
86
+ data: {
87
+ id: fullyQualifiedId(subject),
88
+ thread: threadId,
89
+ cursor: thread.anchor,
90
+ },
91
+ },
92
+ ]);
93
+ }
94
+ }}
95
+ onThreadDelete={(thread) => {
96
+ return dispatch({ plugin: THREAD_PLUGIN, action: ThreadAction.DELETE, data: { subject, thread } });
97
+ }}
98
+ onMessageDelete={(thread, messageId) =>
99
+ dispatch({
100
+ plugin: THREAD_PLUGIN,
101
+ action: ThreadAction.DELETE_MESSAGE,
102
+ data: { subject, thread, messageId },
103
+ })
104
+ }
105
+ onThreadToggleResolved={(thread) =>
106
+ dispatch({ plugin: THREAD_PLUGIN, action: ThreadAction.TOGGLE_RESOLVED, data: { thread } })
107
+ }
108
+ onComment={(thread) => {
109
+ void dispatch({ action: ThreadAction.ON_MESSAGE_ADD, data: { thread, subject } });
110
+ }}
111
+ />
112
+ <div role='none' className='bs-10' />
113
+ <ScrollArea.Scrollbar>
114
+ <ScrollArea.Thumb />
115
+ </ScrollArea.Scrollbar>
116
+ </ScrollArea.Viewport>
117
+ </ScrollArea.Root>
118
+ </div>
119
+ );
120
+ };
package/src/types.ts CHANGED
@@ -31,7 +31,7 @@ export enum ThreadAction {
31
31
  export type ThreadProvides<T> = {
32
32
  thread: {
33
33
  predicate: (obj: any) => obj is T;
34
- createSort: (obj: T) => (anchorA: string, anchorB: string) => number;
34
+ createSort: (obj: T) => (anchorA: string | undefined, anchorB: string | undefined) => number;
35
35
  };
36
36
  };
37
37