@dxos/plugin-thread 0.6.11 → 0.6.12-main.568932b

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 (81) 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 +233 -234
  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 +313 -316
  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 +1102 -0
  40. package/dist/lib/node-esm/index.mjs.map +7 -0
  41. package/dist/lib/node-esm/meta.json +1 -0
  42. package/dist/lib/node-esm/meta.mjs +12 -0
  43. package/dist/lib/node-esm/meta.mjs.map +7 -0
  44. package/dist/lib/node-esm/types.mjs +9 -0
  45. package/dist/lib/node-esm/types.mjs.map +7 -0
  46. package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
  47. package/dist/types/src/components/ChatContainer.d.ts.map +1 -1
  48. package/dist/types/src/components/CommentContainer.d.ts +1 -1
  49. package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
  50. package/dist/types/src/components/CommentsContainer.d.ts +1 -5
  51. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  52. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  53. package/dist/types/src/components/ThreadComplementary.d.ts +10 -0
  54. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -0
  55. package/dist/types/src/components/testing.d.ts +2 -0
  56. package/dist/types/src/components/testing.d.ts.map +1 -1
  57. package/dist/types/src/meta.d.ts +1 -4
  58. package/dist/types/src/meta.d.ts.map +1 -1
  59. package/dist/types/src/types.d.ts +1 -1
  60. package/dist/types/src/types.d.ts.map +1 -1
  61. package/package.json +40 -37
  62. package/src/ThreadPlugin.tsx +70 -182
  63. package/src/components/ChatContainer.tsx +7 -8
  64. package/src/components/CommentContainer.tsx +18 -19
  65. package/src/components/Comments.stories.tsx +2 -2
  66. package/src/components/CommentsContainer.tsx +18 -31
  67. package/src/components/MessageContainer.tsx +2 -1
  68. package/src/components/ThreadComplementary.tsx +113 -0
  69. package/src/meta.tsx +1 -5
  70. package/src/types.ts +1 -1
  71. package/dist/lib/browser/chunk-5IBJO53M.mjs.map +0 -7
  72. package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
  73. package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
  74. package/dist/lib/node/chunk-BWBONZGU.cjs.map +0 -7
  75. package/dist/types/src/types.test.d.ts +0 -2
  76. package/dist/types/src/types.test.d.ts.map +0 -1
  77. package/src/types.test.ts +0 -13
  78. /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-4VU752F4.mjs.map} +0 -0
  79. /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-ML3P4XSD.mjs.map} +0 -0
  80. /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-ERUHPHVT.cjs.map} +0 -0
  81. /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-6DN7FDNB.cjs.map} +0 -0
@@ -2,7 +2,6 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Chat, type IconProps } from '@phosphor-icons/react';
6
5
  import { computed, effect } from '@preact/signals-core';
7
6
  import React from 'react';
8
7
 
@@ -16,57 +15,40 @@ 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. */
67
50
  staging: Record<string, ThreadType[]>;
68
51
  current?: string | undefined;
69
- focus?: boolean;
70
52
  };
71
53
 
72
54
  type SubjectId = string;
@@ -74,8 +56,6 @@ const initialViewState = { showResolvedThreads: false };
74
56
  type ViewStore = Record<SubjectId, typeof initialViewState>;
75
57
 
76
58
  // 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
59
 
80
60
  // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
81
61
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
@@ -90,10 +70,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
90
70
  return viewStore[subjectId];
91
71
  };
92
72
 
93
- let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
94
73
  let navigationPlugin: Plugin<LocationProvides> | undefined;
95
74
  let intentPlugin: Plugin<IntentPluginProvides> | undefined;
96
- let dispatch: IntentDispatcher | undefined;
97
75
 
98
76
  const unsubscribeCallbacks = [] as UnsubscribeCallback[];
99
77
 
@@ -102,45 +80,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
102
80
  ready: async (plugins) => {
103
81
  settings.prop({ key: 'standalone', type: LocalStorageStore.bool({ allowUndefined: true }) });
104
82
 
105
- attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
106
83
  navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
107
84
  intentPlugin = resolvePlugin(plugins, parseIntentPlugin)!;
108
- dispatch = intentPlugin?.provides.intent.dispatch;
109
-
110
- const graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
111
- const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
112
- if (!client) {
113
- return;
114
- }
115
-
116
- // TODO(wittjosiah): This is a hack to make standalone threads work in the c11y sidebar.
117
- // This should have a better solution when deck is introduced.
118
- const channelsQuery = client.spaces.query(Filter.schema(ChannelType));
119
- const queryUnsubscribe = channelsQuery.subscribe();
120
- const unsubscribe = effect(() => {
121
- const attention = attentionPlugin?.provides.attention;
122
- if (!attention?.attended) {
123
- return;
124
- }
125
-
126
- const firstAttendedNodeWithComments = Array.from(attention.attended)
127
- .map((id) => graphPlugin?.provides.graph.findNode(id))
128
- .find((node) => node?.data instanceof DocumentType && (node.data.threads?.length ?? 0) > 0);
129
-
130
- if (firstAttendedNodeWithComments) {
131
- void intentPlugin?.provides.intent.dispatch({
132
- action: NavigationAction.OPEN,
133
- data: {
134
- activeParts: {
135
- complementary: `${firstAttendedNodeWithComments.id}${SLUG_PATH_SEPARATOR}comments`,
136
- },
137
- },
138
- });
139
- }
140
- });
141
-
142
- unsubscribeCallbacks.push(queryUnsubscribe);
143
- unsubscribeCallbacks.push(unsubscribe);
144
85
  },
145
86
  unload: async () => {
146
87
  unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
@@ -151,8 +92,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
151
92
  records: {
152
93
  [ChannelType.typename]: {
153
94
  placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
154
- icon: (props: IconProps) => <Chat {...props} />,
155
- iconSymbol: 'ph--chat--regular',
95
+ icon: 'ph--chat--regular',
156
96
  // TODO(wittjosiah): Move out of metadata.
157
97
  loadReferences: (channel: ChannelType) => loadObjectReferences(channel, (channel) => channel.threads),
158
98
  },
@@ -198,31 +138,42 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
198
138
  return;
199
139
  }
200
140
 
201
- // TODO(Zan): Find util (or make one)
202
- const docId = id.split('~').at(0);
203
- const [spaceId, objectId] = docId?.split(':') ?? [];
141
+ // TODO(Zan): Find util (or make one).
142
+ const subjectId = id.split('~').at(0);
143
+ const [spaceId, objectId] = subjectId?.split(':') ?? [];
204
144
  const space = client.spaces.get().find((space) => space.id === spaceId);
205
- const doc = space?.db.getObjectById(objectId);
206
- if (!doc || !docId) {
145
+ const object = toSignal(
146
+ (onChange) => {
147
+ const timeout = setTimeout(async () => {
148
+ await space?.db.loadObjectById(objectId);
149
+ onChange();
150
+ });
151
+
152
+ return () => clearTimeout(timeout);
153
+ },
154
+ () => space?.db.getObjectById(objectId),
155
+ subjectId,
156
+ );
157
+ if (!object || !subjectId) {
207
158
  return;
208
159
  }
209
160
 
210
- const docMeta = metadataResolver(getTypename(doc) ?? '');
211
- const label = docMeta.label?.(doc) ||
212
- doc.name ||
213
- docMeta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
214
- // const label = docMeta.label?.(doc) || doc.name || docMeta.placeholder;
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 }];
215
165
 
216
- const viewState = getViewState(docId);
166
+ const viewState = getViewState(subjectId);
217
167
 
218
168
  return {
219
169
  id,
220
170
  type: 'orphan-comments-for-subject',
221
- data: doc,
171
+ data: null,
222
172
  properties: {
223
- icon: meta.iconComponent,
173
+ icon: 'ph--chat-text--regular',
224
174
  label,
225
175
  showResolvedThreads: viewState.showResolvedThreads,
176
+ object,
226
177
  },
227
178
  };
228
179
  },
@@ -256,6 +207,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
256
207
  menuItemType: 'toggle',
257
208
  isChecked: viewState.showResolvedThreads,
258
209
  testId: 'threadPlugin.toggleShowResolved',
210
+ icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
259
211
  },
260
212
  },
261
213
  ];
@@ -287,8 +239,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
287
239
  },
288
240
  properties: {
289
241
  label: ['create channel label', { ns: THREAD_PLUGIN }],
290
- icon: (props: IconProps) => <Chat {...props} />,
291
- iconSymbol: 'ph--chat--regular',
242
+ icon: 'ph--chat--regular',
292
243
  testId: 'threadPlugin.createObject',
293
244
  },
294
245
  },
@@ -314,8 +265,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
314
265
  case 'article':
315
266
  case 'complementary': {
316
267
  const location = navigationPlugin?.provides.location;
317
- const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
- const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
319
268
 
320
269
  if (data.object instanceof ChannelType && data.object.threads[0]) {
321
270
  const channel = data.object;
@@ -348,85 +297,16 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
348
297
  'threads' in data.subject &&
349
298
  Array.isArray(data.subject.threads)
350
299
  ) {
351
- const threads = data.subject.threads
352
- .concat(state.staging[fullyQualifiedId(data.subject)])
353
- .filter(nonNullable);
354
-
355
- const createSort = threadProvides.find((p) => p.predicate(data.subject))?.createSort;
356
- if (createSort) {
357
- const sort = createSort(data.subject);
358
- threads.sort((a, b) => sort(a.anchor, b.anchor));
359
- }
360
-
361
- const detached = data.subject.threads
362
- .filter(nonNullable)
363
- .filter(({ anchor }) => !anchor)
364
- .map((thread) => thread.id);
365
-
366
- const qualifiedSubjectId = fullyQualifiedId(data.subject);
367
- const attention = attentionPlugin?.provides.attention?.attended ?? new Set([qualifiedSubjectId]);
368
- const attendableAttrs = createAttendableAttributes(qualifiedSubjectId);
369
- const { showResolvedThreads } = getViewState(qualifiedSubjectId);
300
+ const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
370
301
 
371
302
  return (
372
- <div role='none' className='contents group/attention' {...attendableAttrs}>
373
- {role === 'complementary' && <CommentsHeading attendableId={qualifiedSubjectId} />}
374
- <ScrollArea.Root classNames='row-span-2'>
375
- <ScrollArea.Viewport>
376
- <CommentsContainer
377
- threads={threads}
378
- detached={detached}
379
- currentId={attention.has(qualifiedSubjectId) ? state.current : undefined}
380
- autoFocusCurrentTextbox={state.focus}
381
- showResolvedThreads={showResolvedThreads}
382
- onThreadAttend={(thread) => {
383
- if (state.current !== thread.id) {
384
- state.current = thread.id;
385
- void dispatch?.({
386
- action: LayoutAction.SCROLL_INTO_VIEW,
387
- data: {
388
- id: fullyQualifiedId(data.subject!),
389
- thread: fullyQualifiedId(thread),
390
- cursor: thread.anchor,
391
- },
392
- });
393
- }
394
- }}
395
- onThreadDelete={(thread) =>
396
- dispatch?.({
397
- plugin: THREAD_PLUGIN,
398
- action: ThreadAction.DELETE,
399
- data: { subject: data.subject, thread },
400
- })
401
- }
402
- onMessageDelete={(thread, messageId) =>
403
- dispatch?.({
404
- plugin: THREAD_PLUGIN,
405
- action: ThreadAction.DELETE_MESSAGE,
406
- data: { subject: data.subject, thread, messageId },
407
- })
408
- }
409
- onThreadToggleResolved={(thread) =>
410
- dispatch?.({
411
- plugin: THREAD_PLUGIN,
412
- action: ThreadAction.TOGGLE_RESOLVED,
413
- data: { thread },
414
- })
415
- }
416
- onComment={(thread) => {
417
- void intentPlugin?.provides.intent.dispatch({
418
- action: ThreadAction.ON_MESSAGE_ADD,
419
- data: { thread, subject: data.subject },
420
- });
421
- }}
422
- />
423
- <div role='none' className='bs-10' />
424
- <ScrollArea.Scrollbar>
425
- <ScrollArea.Thumb />
426
- </ScrollArea.Scrollbar>
427
- </ScrollArea.Viewport>
428
- </ScrollArea.Root>
429
- </div>
303
+ <ThreadComplementary
304
+ role={role}
305
+ subject={data.subject}
306
+ stagedThreads={state.staging[fullyQualifiedId(data.subject)]}
307
+ current={state.current}
308
+ showResolvedThreads={showResolvedThreads}
309
+ />
430
310
  );
431
311
  }
432
312
  }
@@ -442,6 +322,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
442
322
  if (intent.data && intent.data.cursor !== undefined) {
443
323
  const { cursor, name, subject } = intent.data;
444
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
+
445
336
  const subjectId = fullyQualifiedId(subject);
446
337
  const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
338
  const stagingArea = state.staging[subjectId];
@@ -455,17 +346,9 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
455
346
  data: thread,
456
347
  intents: [
457
348
  [
458
- {
459
- action: NavigationAction.OPEN,
460
- data: {
461
- activeParts: {
462
- complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
463
- },
464
- },
465
- },
466
349
  {
467
350
  action: ThreadAction.SELECT,
468
- data: { current: thread.id, focus: true },
351
+ data: { current: fullyQualifiedId(thread) },
469
352
  },
470
353
  {
471
354
  action: LayoutAction.SET_LAYOUT,
@@ -485,7 +368,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
485
368
  }
486
369
 
487
370
  case ThreadAction.SELECT: {
488
- state.focus = intent.data?.current === state.current ? state.focus : intent.data?.focus;
489
371
  state.current = intent.data?.current;
490
372
  return { data: true };
491
373
  }
@@ -519,7 +401,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
519
401
 
520
402
  case ThreadAction.DELETE: {
521
403
  const { subject, thread } = intent.data ?? {};
522
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
404
+ if (
405
+ !(thread instanceof ThreadType) ||
406
+ !(subject && typeof subject === 'object' && 'threads' in subject)
407
+ ) {
523
408
  return;
524
409
  }
525
410
 
@@ -531,7 +416,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
531
416
  const index = stagingArea.findIndex((t) => t.id === thread.id);
532
417
  if (index !== -1) {
533
418
  stagingArea.splice(index, 1);
534
- return;
419
+ return { data: true };
535
420
  }
536
421
  }
537
422
 
@@ -541,7 +426,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
541
426
  }
542
427
 
543
428
  if (!intent.undo) {
544
- const index = subject.threads.findIndex((t) => t?.id === thread.id);
429
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
545
430
  const cursor = subject.threads[index]?.anchor;
546
431
  if (index !== -1) {
547
432
  subject.threads?.splice(index, 1);
@@ -587,7 +472,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
587
472
  const { thread, subject } = intent.data ?? {};
588
473
  if (
589
474
  !(thread instanceof ThreadType) ||
590
- !(subject && typeof subject === 'object' && 'threads' in subject)
475
+ !(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
591
476
  ) {
592
477
  return;
593
478
  }
@@ -627,7 +512,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
627
512
  const { subject, thread, messageId } = intent.data ?? {};
628
513
  const space = getSpace(subject);
629
514
 
630
- if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
515
+ if (
516
+ !(thread instanceof ThreadType) ||
517
+ !(subject && typeof subject === 'object' && 'threads' in subject)
518
+ ) {
631
519
  return;
632
520
  }
633
521
 
@@ -658,7 +546,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
658
546
  action: ObservabilityAction.SEND_EVENT,
659
547
  data: {
660
548
  name: 'threads.message.delete',
661
- properties: { threadId: thread.id, spaceId: space.id },
549
+ properties: { threadId: thread.id, spaceId: space?.id },
662
550
  },
663
551
  },
664
552
  ],
@@ -682,7 +570,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
682
570
  action: ObservabilityAction.SEND_EVENT,
683
571
  data: {
684
572
  name: 'threads.message.undo-delete',
685
- properties: { threadId: thread.id, spaceId: space.id },
573
+ properties: { threadId: thread.id, spaceId: space?.id },
686
574
  },
687
575
  },
688
576
  ],
@@ -728,15 +616,15 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
728
616
  },
729
617
  }),
730
618
  createExternalCommentSync(
731
- doc.id,
619
+ fullyQualifiedId(doc),
732
620
  (sink) => effect(() => sink()),
733
621
  () =>
734
622
  threads.value
735
623
  .filter((thread) => thread?.anchor)
736
- .map((thread) => ({ id: thread.id, cursor: thread.anchor! })),
624
+ .map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
737
625
  ),
738
626
  comments({
739
- id: doc.id,
627
+ id: fullyQualifiedId(doc),
740
628
  onCreate: ({ cursor }) => {
741
629
  const [start, end] = cursor.split(':');
742
630
  const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
@@ -754,7 +642,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
754
642
  // If the thread is in the staging area, remove it.
755
643
  const stagingArea = state.staging[fullyQualifiedId(doc)];
756
644
  if (stagingArea) {
757
- const index = stagingArea.findIndex((thread) => thread.id === id);
645
+ const index = stagingArea.findIndex((thread) => fullyQualifiedId(thread) === id);
758
646
  if (index !== -1) {
759
647
  stagingArea.splice(index, 1);
760
648
  }
@@ -767,7 +655,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
767
655
  },
768
656
  onUpdate: ({ id, cursor }) => {
769
657
  const thread =
770
- state.staging[fullyQualifiedId(doc)]?.find((thread) => thread.id === id) ??
658
+ state.staging[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
771
659
  doc.threads.find((thread) => thread?.id === id);
772
660
 
773
661
  if (thread instanceof ThreadType && thread.anchor) {
@@ -2,15 +2,14 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
- import { Chat } from '@phosphor-icons/react';
6
5
  import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
7
6
 
8
7
  import { create } from '@dxos/echo-schema';
9
8
  import { MessageType } from '@dxos/plugin-space/types';
10
- import { getSpace, useMembers } from '@dxos/react-client/echo';
9
+ import { fullyQualifiedId, getSpace, useMembers } from '@dxos/react-client/echo';
11
10
  import { useIdentity } from '@dxos/react-client/halo';
12
- import { ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
13
- import { PlankHeading, plankHeadingIconProps } from '@dxos/react-ui-deck';
11
+ import { Icon, ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
12
+ import { PlankHeading } from '@dxos/react-ui-deck';
14
13
  import { createBasicExtensions, createThemeExtensions, listener } from '@dxos/react-ui-editor';
15
14
  import { mx } from '@dxos/react-ui-theme';
16
15
  import { MessageTextbox, type MessageTextboxProps, Thread, ThreadFooter, threadLayout } from '@dxos/react-ui-thread';
@@ -28,7 +27,7 @@ export const ChatHeading = ({ attendableId }: { attendableId?: string }) => {
28
27
  return (
29
28
  <div role='none' className='flex items-center'>
30
29
  <PlankHeading.Button attendableId={attendableId}>
31
- <Chat {...plankHeadingIconProps} />
30
+ <Icon icon='ph--chat--regular' size={5} />
32
31
  </PlankHeading.Button>
33
32
  <PlankHeading.Label attendableId={attendableId}>{t('chat heading')}</PlankHeading.Label>
34
33
  </div>
@@ -39,7 +38,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
39
38
  const identity = useIdentity()!;
40
39
  const space = getSpace(thread);
41
40
  const members = useMembers(space?.key);
42
- const activity = useStatus(space, thread.id);
41
+ const activity = useStatus(space, fullyQualifiedId(thread));
43
42
  const { t } = useTranslation(THREAD_PLUGIN);
44
43
  // TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
45
44
  const [_count, _setCount] = useState(0);
@@ -48,7 +47,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
48
47
  const threadScrollRef = useRef<HTMLDivElement | null>(null);
49
48
  const { themeMode } = useThemeContext();
50
49
 
51
- const textboxMetadata = getMessageMetadata(thread.id, identity);
50
+ const textboxMetadata = getMessageMetadata(fullyQualifiedId(thread), identity);
52
51
  const messageRef = useRef('');
53
52
  const extensions = useMemo(
54
53
  () => [
@@ -103,7 +102,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
103
102
  return (
104
103
  <Thread
105
104
  current={current}
106
- id={thread.id}
105
+ id={fullyQualifiedId(thread)}
107
106
  classNames='bs-full grid-rows-[1fr_min-content_min-content] overflow-hidden transition-[padding-block-end] [[data-sidebar-inline-start-state=open]_&]:lg:pbe-0'
108
107
  >
109
108
  <ScrollArea.Root classNames='col-span-2'>
@@ -3,11 +3,11 @@
3
3
  //
4
4
 
5
5
  import { CheckCircle, X } from '@phosphor-icons/react';
6
- import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
6
+ import React, { useCallback, 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';
@@ -93,7 +93,6 @@ export const CommentContainer = ({
93
93
  detached,
94
94
  context,
95
95
  current,
96
- autoFocusTextbox,
97
96
  onAttend,
98
97
  onThreadDelete,
99
98
  onMessageDelete,
@@ -103,13 +102,12 @@ export const CommentContainer = ({
103
102
  const identity = useIdentity()!;
104
103
  const space = getSpace(thread);
105
104
  const members = useMembers(space?.key);
106
- const activity = useStatus(space, thread.id);
105
+ const activity = useStatus(space, fullyQualifiedId(thread));
107
106
  const { t } = useTranslation(THREAD_PLUGIN);
108
107
  const threadScrollRef = useRef<HTMLDivElement | null>(null);
109
- const [autoFocus, setAutoFocus] = useState(!!autoFocusTextbox);
110
108
  const { themeMode } = useThemeContext();
111
109
 
112
- const textboxMetadata = getMessageMetadata(thread.id, identity);
110
+ const textboxMetadata = getMessageMetadata(fullyQualifiedId(thread), identity);
113
111
  // TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
114
112
  const [_count, _setCount] = useState(0);
115
113
  const rerenderEditor = () => _setCount((count) => count + 1);
@@ -124,13 +122,6 @@ export const CommentContainer = ({
124
122
  [_count],
125
123
  );
126
124
 
127
- // TODO(thure): Because of the way the `autoFocus` property is handled by TextEditor,
128
- // this is the least-bad way of moving focus at the right time, though it is an anti-pattern.
129
- // Refactor to behave more like <input/>’s `autoFocus` or `autofocus` (yes, they’re different).
130
- useEffect(() => {
131
- setAutoFocus(!!autoFocusTextbox);
132
- }, [autoFocusTextbox]);
133
-
134
125
  // TODO(thure): Factor out.
135
126
  const scrollToEnd = (behavior: ScrollBehavior) =>
136
127
  setTimeout(() => threadScrollRef.current?.scrollIntoView({ behavior, block: 'end' }), 10);
@@ -151,7 +142,6 @@ export const CommentContainer = ({
151
142
  onComment?.(thread);
152
143
 
153
144
  messageRef.current = '';
154
- setAutoFocus(true);
155
145
  scrollToEnd('instant');
156
146
  rerenderEditor();
157
147
 
@@ -159,10 +149,8 @@ export const CommentContainer = ({
159
149
  return true;
160
150
  }, [thread, identity]);
161
151
 
162
- const handleDeleteMessage = (id: string) => onMessageDelete?.(id);
163
-
164
152
  return (
165
- <Thread onClickCapture={onAttend} onFocusCapture={onAttend} current={current} id={thread.id}>
153
+ <Thread onClickCapture={onAttend} onFocusCapture={onAttend} current={current} id={fullyQualifiedId(thread)}>
166
154
  <div
167
155
  role='none'
168
156
  className={mx(
@@ -195,9 +183,20 @@ export const CommentContainer = ({
195
183
  </div>
196
184
  </div>
197
185
  {thread.messages.filter(nonNullable).map((message) => (
198
- <MessageContainer key={message.id} message={message} members={members} onDelete={handleDeleteMessage} />
186
+ <MessageContainer
187
+ key={message.id}
188
+ message={message}
189
+ members={members}
190
+ onDelete={(id: string) => onMessageDelete?.(id)}
191
+ />
199
192
  ))}
200
- <MessageTextbox extensions={extensions} autoFocus={autoFocus} onSend={handleCreate} {...textboxMetadata} />
193
+ {/*
194
+ TODO(wittjosiah): Can't autofocus this generally.
195
+ There can be multiple threads with inputs and they can't all be focused.
196
+ Also, it steals focus from documents when first rendered.
197
+ Need to find a way to autofocus in one scenario only: when a new thread is created.
198
+ */}
199
+ <MessageTextbox extensions={extensions} onSend={handleCreate} {...textboxMetadata} />
201
200
  <ThreadFooter activity={activity}>{t('activity message')}</ThreadFooter>
202
201
  {/* NOTE(thure): This can’t also be the `overflow-anchor` because `ScrollArea` injects an interceding node that contains this necessary ref’d element. */}
203
202
  <div role='none' className='bs-px -mbs-px' ref={threadScrollRef} />
@@ -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);