@dxos/plugin-thread 0.6.10 → 0.6.11-staging.9c4dc38

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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["types.cjs"],
4
- "sourcesContent": ["import {\n ThreadAction\n} from \"./chunk-WPPLQNMJ.cjs\";\nimport \"./chunk-BWBONZGU.cjs\";\nexport {\n ThreadAction\n};\n//# sourceMappingURL=types.cjs.map\n"],
4
+ "sourcesContent": ["import {\n ThreadAction\n} from \"./chunk-D5GOVXHS.cjs\";\nimport \"./chunk-BWBONZGU.cjs\";\nexport {\n ThreadAction\n};\n//# sourceMappingURL=types.cjs.map\n"],
5
5
  "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,4BAEO;AACP,4BAAO;",
6
6
  "names": []
7
7
  }
@@ -1 +1 @@
1
- {"version":3,"file":"ThreadPlugin.d.ts","sourceRoot":"","sources":["../../../src/ThreadPlugin.tsx"],"names":[],"mappings":"AAQA,OAAO,EAML,KAAK,gBAAgB,EAUtB,MAAM,qBAAqB,CAAC;AAsC7B,OAAO,EAAgB,KAAK,oBAAoB,EAA4B,MAAM,SAAS,CAAC;AAY5F,eAAO,MAAM,YAAY,QAAO,gBAAgB,CAAC,oBAAoB,CAiqBpE,CAAC"}
1
+ {"version":3,"file":"ThreadPlugin.d.ts","sourceRoot":"","sources":["../../../src/ThreadPlugin.tsx"],"names":[],"mappings":"AAQA,OAAO,EAML,KAAK,gBAAgB,EAWtB,MAAM,qBAAqB,CAAC;AAqC7B,OAAO,EAAqC,KAAK,oBAAoB,EAA4B,MAAM,SAAS,CAAC;AAkBjH,eAAO,MAAM,YAAY,QAAO,gBAAgB,CAAC,oBAAoB,CAssBpE,CAAC"}
@@ -38,7 +38,6 @@ declare const _default: {
38
38
  };
39
39
  };
40
40
  }[];
41
- layout: string;
42
41
  };
43
42
  };
44
43
  export default _default;
@@ -1 +1 @@
1
- {"version":3,"file":"Chat.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/Chat.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,aAAa,CAAC;AAErB,OAAO,KAA8B,MAAM,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DnD,wBAOE;AAEF,eAAO,MAAM,OAAO,IAAK,CAAC"}
1
+ {"version":3,"file":"Chat.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/Chat.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,aAAa,CAAC;AAErB,OAAO,KAA8B,MAAM,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4DnD,wBAOE;AAEF,eAAO,MAAM,OAAO,IAAK,CAAC"}
@@ -1 +1 @@
1
- {"version":3,"file":"Comments.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/Comments.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,aAAa,CAAC;AAErB,OAAO,KAA8B,MAAM,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDnD,wBAME;AAEF,eAAO,MAAM,OAAO,IAAK,CAAC"}
1
+ {"version":3,"file":"Comments.stories.d.ts","sourceRoot":"","sources":["../../../../src/components/Comments.stories.tsx"],"names":[],"mappings":"AAIA,OAAO,aAAa,CAAC;AAErB,OAAO,KAA8B,MAAM,OAAO,CAAC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8CnD,wBAME;AAEF,eAAO,MAAM,OAAO,IAAK,CAAC"}
@@ -6,9 +6,16 @@ export declare enum ThreadAction {
6
6
  CREATE = "dxos.org/plugin/thread/action/create",
7
7
  SELECT = "dxos.org/plugin/thread/action/select",
8
8
  DELETE = "dxos.org/plugin/thread/action/delete",
9
+ ON_MESSAGE_ADD = "dxos.org/plugin/thread/action/on-message-add",
9
10
  DELETE_MESSAGE = "dxos.org/plugin/thread/action/delete-message",
10
11
  TOGGLE_RESOLVED = "dxos.org/plugin/thread/action/toggle-resolved"
11
12
  }
13
+ export type ThreadProvides<T> = {
14
+ thread: {
15
+ predicate: (obj: any) => obj is T;
16
+ createSort: (obj: T) => (anchorA: string, anchorB: string) => number;
17
+ };
18
+ };
12
19
  export type ThreadPluginProvides = SurfaceProvides & IntentResolverProvides & GraphBuilderProvides & MetadataRecordsProvides & SettingsProvides & TranslationsProvides & SchemaProvides & MarkdownExtensionProvides;
13
20
  export type ThreadSettingsProps = {
14
21
  standalone?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,KAAK,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAK3D,oBAAY,YAAY;IACtB,MAAM,yCAA4B;IAClC,MAAM,yCAA4B;IAClC,MAAM,yCAA4B;IAClC,cAAc,iDAAoC;IAClD,eAAe,kDAAqC;CACrD;AAED,MAAM,MAAM,oBAAoB,GAAG,eAAe,GAChD,sBAAsB,GACtB,oBAAoB,GACpB,uBAAuB,GACvB,gBAAgB,GAChB,oBAAoB,GACpB,cAAc,GACd,yBAAyB,CAAC;AAE5B,MAAM,MAAM,mBAAmB,GAAG;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,UAAU,CAAC;CAClB"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/types.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,oBAAoB,EACpB,sBAAsB,EACtB,uBAAuB,EACvB,gBAAgB,EAChB,eAAe,EACf,oBAAoB,EACrB,MAAM,qBAAqB,CAAC;AAC7B,OAAO,EAAE,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAC1D,OAAO,EAAE,KAAK,yBAAyB,EAAE,MAAM,uBAAuB,CAAC;AACvE,OAAO,EAAE,KAAK,UAAU,EAAE,MAAM,0BAA0B,CAAC;AAK3D,oBAAY,YAAY;IACtB,MAAM,yCAA4B;IAClC,MAAM,yCAA4B;IAClC,MAAM,yCAA4B;IAClC,cAAc,iDAAoC;IAClD,cAAc,iDAAoC;IAClD,eAAe,kDAAqC;CACrD;AAID,MAAM,MAAM,cAAc,CAAC,CAAC,IAAI;IAC9B,MAAM,EAAE;QACN,SAAS,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,IAAI,CAAC,CAAC;QAClC,UAAU,EAAE,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,CAAC;KACtE,CAAC;CACH,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG,eAAe,GAChD,sBAAsB,GACtB,oBAAoB,GACpB,uBAAuB,GACvB,gBAAgB,GAChB,oBAAoB,GACpB,cAAc,GACd,yBAAyB,CAAC;AAE5B,MAAM,MAAM,mBAAmB,GAAG;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAE,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,UAAU,CAAC;CAClB"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/plugin-thread",
3
- "version": "0.6.10",
3
+ "version": "0.6.11-staging.9c4dc38",
4
4
  "description": "DXOS Threads Surface plugin",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -51,28 +51,28 @@
51
51
  "@preact/signals-core": "^1.6.0",
52
52
  "date-fns": "^3.3.1",
53
53
  "lodash.get": "^4.4.2",
54
- "@dxos/app-framework": "0.6.10",
55
- "@dxos/async": "0.6.10",
56
- "@dxos/display-name": "0.6.10",
57
- "@dxos/echo-schema": "0.6.10",
58
- "@dxos/invariant": "0.6.10",
59
- "@dxos/local-storage": "0.6.10",
60
- "@dxos/log": "0.6.10",
61
- "@dxos/plugin-attention": "0.6.10",
62
- "@dxos/plugin-client": "0.6.10",
63
- "@dxos/plugin-graph": "0.6.10",
64
- "@dxos/plugin-markdown": "0.6.10",
65
- "@dxos/plugin-settings": "0.6.10",
66
- "@dxos/plugin-observability": "0.6.10",
67
- "@dxos/plugin-space": "0.6.10",
68
- "@dxos/react-client": "0.6.10",
69
- "@dxos/react-ui-deck": "0.6.10",
70
- "@dxos/react-ui-attention": "0.6.10",
71
- "@dxos/react-ui-editor": "0.6.10",
72
- "@dxos/react-ui-mosaic": "0.6.10",
73
- "@dxos/react-ui-theme": "0.6.10",
74
- "@dxos/react-ui-thread": "0.6.10",
75
- "@dxos/util": "0.6.10"
54
+ "@dxos/app-framework": "0.6.11-staging.9c4dc38",
55
+ "@dxos/echo-schema": "0.6.11-staging.9c4dc38",
56
+ "@dxos/async": "0.6.11-staging.9c4dc38",
57
+ "@dxos/display-name": "0.6.11-staging.9c4dc38",
58
+ "@dxos/local-storage": "0.6.11-staging.9c4dc38",
59
+ "@dxos/invariant": "0.6.11-staging.9c4dc38",
60
+ "@dxos/log": "0.6.11-staging.9c4dc38",
61
+ "@dxos/plugin-attention": "0.6.11-staging.9c4dc38",
62
+ "@dxos/plugin-markdown": "0.6.11-staging.9c4dc38",
63
+ "@dxos/plugin-client": "0.6.11-staging.9c4dc38",
64
+ "@dxos/plugin-graph": "0.6.11-staging.9c4dc38",
65
+ "@dxos/plugin-observability": "0.6.11-staging.9c4dc38",
66
+ "@dxos/plugin-settings": "0.6.11-staging.9c4dc38",
67
+ "@dxos/plugin-space": "0.6.11-staging.9c4dc38",
68
+ "@dxos/react-client": "0.6.11-staging.9c4dc38",
69
+ "@dxos/react-ui-attention": "0.6.11-staging.9c4dc38",
70
+ "@dxos/react-ui-deck": "0.6.11-staging.9c4dc38",
71
+ "@dxos/react-ui-editor": "0.6.11-staging.9c4dc38",
72
+ "@dxos/react-ui-mosaic": "0.6.11-staging.9c4dc38",
73
+ "@dxos/util": "0.6.11-staging.9c4dc38",
74
+ "@dxos/react-ui-thread": "0.6.11-staging.9c4dc38",
75
+ "@dxos/react-ui-theme": "0.6.11-staging.9c4dc38"
76
76
  },
77
77
  "devDependencies": {
78
78
  "@phosphor-icons/react": "^2.1.5",
@@ -82,16 +82,16 @@
82
82
  "react": "~18.2.0",
83
83
  "react-dom": "~18.2.0",
84
84
  "vite": "^5.3.4",
85
- "@dxos/random": "0.6.10",
86
- "@dxos/react-ui": "0.6.10",
87
- "@dxos/storybook-utils": "0.6.10"
85
+ "@dxos/random": "0.6.11-staging.9c4dc38",
86
+ "@dxos/react-ui": "0.6.11-staging.9c4dc38",
87
+ "@dxos/storybook-utils": "0.6.11-staging.9c4dc38"
88
88
  },
89
89
  "optionalDependencies": {
90
90
  "@phosphor-icons/react": "^2.1.5",
91
91
  "react": "^18.0.0",
92
92
  "react-dom": "^18.0.0",
93
- "@dxos/react-ui": "0.6.10",
94
- "@dxos/react-ui-theme": "0.6.10"
93
+ "@dxos/react-ui": "0.6.11-staging.9c4dc38",
94
+ "@dxos/react-ui-theme": "0.6.11-staging.9c4dc38"
95
95
  },
96
96
  "publishConfig": {
97
97
  "access": "public"
@@ -22,6 +22,7 @@ import {
22
22
  isLayoutParts,
23
23
  parseMetadataResolverPlugin,
24
24
  type IntentDispatcher,
25
+ useResolvePlugins,
25
26
  } from '@dxos/app-framework';
26
27
  import { type UnsubscribeCallback } from '@dxos/async';
27
28
  import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
@@ -40,7 +41,6 @@ import {
40
41
  Filter,
41
42
  createDocAccessor,
42
43
  fullyQualifiedId,
43
- getRangeFromCursor,
44
44
  loadObjectReferences,
45
45
  } from '@dxos/react-client/echo';
46
46
  import { ScrollArea } from '@dxos/react-ui';
@@ -60,9 +60,10 @@ import {
60
60
  } from './components';
61
61
  import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
62
62
  import translations from './translations';
63
- import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
63
+ import { ThreadAction, type ThreadProvides, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
64
64
 
65
65
  type ThreadState = {
66
+ /** An in-memory staging area for threads that are being drafted. */
66
67
  staging: Record<string, ThreadType[]>;
67
68
  current?: string | undefined;
68
69
  focus?: boolean;
@@ -72,6 +73,11 @@ type SubjectId = string;
72
73
  const initialViewState = { showResolvedThreads: false };
73
74
  type ViewStore = Record<SubjectId, typeof initialViewState>;
74
75
 
76
+ // TODO(Zan): More robust runtime check.
77
+ const providesThreadsConfig = (plugin: any): Plugin<ThreadProvides<any>> | undefined =>
78
+ 'thread' in plugin.provides ? (plugin as Plugin<ThreadProvides<any>>) : undefined;
79
+
80
+ // TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
75
81
  export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
76
82
  const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
77
83
  const state = create<ThreadState>({ staging: {} });
@@ -197,7 +203,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
197
203
  const [spaceId, objectId] = docId?.split(':') ?? [];
198
204
  const space = client.spaces.get().find((space) => space.id === spaceId);
199
205
  const doc = space?.db.getObjectById(objectId);
200
-
201
206
  if (!doc || !docId) {
202
207
  return;
203
208
  }
@@ -223,7 +228,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
223
228
  },
224
229
  actions: ({ node }) => {
225
230
  const dataId = node.id.split('~').at(0);
226
-
227
231
  if (!node.id.endsWith('~comments') || !dataId) {
228
232
  return;
229
233
  }
@@ -310,15 +314,15 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
310
314
  case 'article':
311
315
  case 'complementary': {
312
316
  const location = navigationPlugin?.provides.location;
317
+ const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
318
+ const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
313
319
 
314
320
  if (data.object instanceof ChannelType && data.object.threads[0]) {
315
321
  const channel = data.object;
316
322
  // TODO(zan): Maybe we should have utility for positional main object ids.
317
323
  if (isLayoutParts(location?.active) && location.active.main) {
318
324
  const layoutEntries = location.active.main;
319
-
320
325
  const currentPosition = layoutEntries.findIndex((entry) => channel.id === entry.id);
321
-
322
326
  if (currentPosition > 0) {
323
327
  const objectToTheLeft = layoutEntries[currentPosition - 1];
324
328
  const context = getSpace(data.object)?.db.getObjectById(objectToTheLeft.id);
@@ -329,45 +333,41 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
329
333
  return <ThreadArticle thread={data.object.threads[0]} />;
330
334
  }
331
335
 
332
- if (data.subject instanceof DocumentType) {
333
- const doc = data.subject;
334
- const accessor = doc.content ? createDocAccessor(doc.content, ['content']) : undefined;
336
+ if (data.subject instanceof ThreadType) {
337
+ return (
338
+ <>
339
+ <ChatHeading attendableId={data.subject.id} />
340
+ <ChatContainer thread={data.subject} />
341
+ </>
342
+ );
343
+ }
335
344
 
336
- if (!accessor) {
337
- return null;
345
+ if (
346
+ data.subject &&
347
+ typeof data.subject === 'object' &&
348
+ 'threads' in data.subject &&
349
+ Array.isArray(data.subject.threads)
350
+ ) {
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));
338
359
  }
339
360
 
340
- const getStartPosition = (cursor: string | undefined) => {
341
- const range = cursor ? getRangeFromCursor(accessor, cursor) : undefined;
342
- return range?.start ?? Number.MAX_SAFE_INTEGER;
343
- };
344
-
345
- const threads = doc.threads
346
- .concat(state.staging[data.subject.id])
347
- .filter(nonNullable)
348
- .sort((a, b) => getStartPosition(a.anchor) - getStartPosition(b.anchor));
349
-
350
361
  const detached = data.subject.threads
351
362
  .filter(nonNullable)
352
363
  .filter(({ anchor }) => !anchor)
353
364
  .map((thread) => thread.id);
354
365
 
355
- const qualifiedSubjectId = fullyQualifiedId(doc);
366
+ const qualifiedSubjectId = fullyQualifiedId(data.subject);
356
367
  const attention = attentionPlugin?.provides.attention?.attended ?? new Set([qualifiedSubjectId]);
357
368
  const attendableAttrs = createAttendableAttributes(qualifiedSubjectId);
358
- const space = getSpace(doc);
359
369
  const { showResolvedThreads } = getViewState(qualifiedSubjectId);
360
370
 
361
- const dispatchAnalytic = (name: string, meta: any) => {
362
- void dispatch?.({
363
- action: ObservabilityAction.SEND_EVENT,
364
- data: {
365
- name,
366
- properties: { ...meta, space: space?.id },
367
- },
368
- });
369
- };
370
-
371
371
  return (
372
372
  <div role='none' className='contents group/attention' {...attendableAttrs}>
373
373
  {role === 'complementary' && <CommentsHeading attendableId={qualifiedSubjectId} />}
@@ -396,14 +396,14 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
396
396
  dispatch?.({
397
397
  plugin: THREAD_PLUGIN,
398
398
  action: ThreadAction.DELETE,
399
- data: { document: data.subject, thread },
399
+ data: { subject: data.subject, thread },
400
400
  })
401
401
  }
402
402
  onMessageDelete={(thread, messageId) =>
403
403
  dispatch?.({
404
404
  plugin: THREAD_PLUGIN,
405
405
  action: ThreadAction.DELETE_MESSAGE,
406
- data: { document: data.subject, thread, messageId },
406
+ data: { subject: data.subject, thread, messageId },
407
407
  })
408
408
  }
409
409
  onThreadToggleResolved={(thread) =>
@@ -414,19 +414,9 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
414
414
  })
415
415
  }
416
416
  onComment={(thread) => {
417
- // TODO(Zan): This might be a bit too much logic for a component. Move to intents?
418
- const doc = data.subject as DocumentType;
419
- if (state.staging[doc.id]?.find((t) => t === thread)) {
420
- // Move thread from staging to document.
421
- thread.status = 'active';
422
- doc.threads ? doc.threads.push(thread) : (doc.threads = [thread]);
423
- state.staging[doc.id] = state.staging[doc.id]?.filter((t) => t.id !== thread.id);
424
- dispatchAnalytic('threads.thread-created', { threadId: thread.id });
425
- }
426
-
427
- dispatchAnalytic('threads.message-added', {
428
- threadId: thread.id,
429
- threadLength: thread.messages.length,
417
+ void intentPlugin?.provides.intent.dispatch({
418
+ action: ThreadAction.ON_MESSAGE_ADD,
419
+ data: { thread, subject: data.subject },
430
420
  });
431
421
  }}
432
422
  />
@@ -438,13 +428,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
438
428
  </ScrollArea.Root>
439
429
  </div>
440
430
  );
441
- } else if (data.subject instanceof ThreadType) {
442
- return (
443
- <>
444
- <ChatHeading attendableId={data.subject.id} />
445
- <ChatContainer thread={data.subject} />
446
- </>
447
- );
448
431
  }
449
432
  }
450
433
  }
@@ -456,7 +439,49 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
456
439
  resolver: async (intent) => {
457
440
  switch (intent.action) {
458
441
  case ThreadAction.CREATE: {
459
- return { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
442
+ if (intent.data && intent.data.cursor !== undefined) {
443
+ const { cursor, name, subject } = intent.data;
444
+
445
+ const subjectId = fullyQualifiedId(subject);
446
+ const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
447
+ const stagingArea = state.staging[subjectId];
448
+ if (stagingArea) {
449
+ stagingArea.push(thread);
450
+ } else {
451
+ state.staging[subjectId] = [thread];
452
+ }
453
+
454
+ return {
455
+ data: thread,
456
+ intents: [
457
+ [
458
+ {
459
+ action: NavigationAction.OPEN,
460
+ data: {
461
+ activeParts: {
462
+ complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
463
+ },
464
+ },
465
+ },
466
+ {
467
+ action: ThreadAction.SELECT,
468
+ data: { current: thread.id, focus: true },
469
+ },
470
+ {
471
+ action: LayoutAction.SET_LAYOUT,
472
+ data: {
473
+ element: 'complementary',
474
+ subject: subjectId,
475
+ state: true,
476
+ },
477
+ },
478
+ ],
479
+ ],
480
+ };
481
+ } else {
482
+ // NOTE: This is the standalone thread creation case.
483
+ return { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
484
+ }
460
485
  }
461
486
 
462
487
  case ThreadAction.SELECT: {
@@ -493,32 +518,33 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
493
518
  }
494
519
 
495
520
  case ThreadAction.DELETE: {
496
- const { document: doc, thread } = intent.data ?? {};
497
- if (!(doc instanceof DocumentType) || !(thread instanceof ThreadType)) {
521
+ const { subject, thread } = intent.data ?? {};
522
+ if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType)) {
498
523
  return;
499
524
  }
500
525
 
501
- const stagingArea = state.staging[doc.id];
526
+ const subjectId = fullyQualifiedId(subject);
527
+ const stagingArea = state.staging[subjectId];
502
528
  if (stagingArea) {
503
529
  // Check if we're deleting a thread that's in the staging area.
504
530
  // If so, remove it from the staging area without ceremony.
505
- const index = state.staging[doc.id]?.findIndex((t) => t.id === thread.id);
531
+ const index = stagingArea.findIndex((t) => t.id === thread.id);
506
532
  if (index !== -1) {
507
- state.staging[doc.id]?.splice(index, 1);
533
+ stagingArea.splice(index, 1);
508
534
  return;
509
535
  }
510
536
  }
511
537
 
512
538
  const space = getSpace(thread);
513
- if (!space || !doc.threads) {
539
+ if (!space || !subject.threads) {
514
540
  return;
515
541
  }
516
542
 
517
543
  if (!intent.undo) {
518
- const index = doc.threads.findIndex((t) => t?.id === thread.id);
519
- const cursor = doc.threads[index]?.anchor;
544
+ const index = subject.threads.findIndex((t) => t?.id === thread.id);
545
+ const cursor = subject.threads[index]?.anchor;
520
546
  if (index !== -1) {
521
- doc.threads?.splice(index, 1);
547
+ subject.threads?.splice(index, 1);
522
548
  }
523
549
 
524
550
  space.db.remove(thread);
@@ -540,7 +566,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
540
566
  } else if (intent.undo) {
541
567
  // TODO(wittjosiah): SDK should do this automatically.
542
568
  const savedThread = space.db.add(thread);
543
- doc.threads.push(savedThread);
569
+ subject.threads.push(savedThread);
544
570
 
545
571
  return {
546
572
  data: true,
@@ -556,11 +582,52 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
556
582
  }
557
583
  break;
558
584
  }
585
+
586
+ case ThreadAction.ON_MESSAGE_ADD: {
587
+ const { thread, subject } = intent.data ?? {};
588
+ if (
589
+ !(thread instanceof ThreadType) ||
590
+ !(subject && typeof subject === 'object' && 'threads' in subject)
591
+ ) {
592
+ return;
593
+ }
594
+
595
+ const subjectId = fullyQualifiedId(subject);
596
+ const space = getSpace(subject);
597
+ const intents = [];
598
+ const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
599
+
600
+ if (state.staging[subjectId]?.find((t) => t === thread)) {
601
+ // Move thread from staging to document.
602
+ thread.status = 'active';
603
+ subject.threads ? subject.threads.push(thread) : (subject.threads = [thread]);
604
+ state.staging[subjectId] = state.staging[subjectId]?.filter((t) => t.id !== thread.id);
605
+
606
+ intents.push({
607
+ action: ObservabilityAction.SEND_EVENT,
608
+ data: { name: 'threads.thread-created', properties: analyticsProperties },
609
+ });
610
+ }
611
+
612
+ intents.push({
613
+ action: ObservabilityAction.SEND_EVENT,
614
+ data: {
615
+ name: 'threads.message-added',
616
+ properties: { ...analyticsProperties, threadLength: thread.messages.length },
617
+ },
618
+ });
619
+
620
+ return {
621
+ data: thread,
622
+ intents: [intents],
623
+ };
624
+ }
625
+
559
626
  case ThreadAction.DELETE_MESSAGE: {
560
- const { document: doc, thread, messageId } = intent.data ?? {};
561
- const space = getSpace(doc);
627
+ const { subject, thread, messageId } = intent.data ?? {};
628
+ const space = getSpace(subject);
562
629
 
563
- if (!(doc instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
630
+ if (!(subject instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
564
631
  return;
565
632
  }
566
633
 
@@ -574,7 +641,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
574
641
  if (messageIndex === 0 && thread.messages.length === 1) {
575
642
  // If the message is the only message in the thread, delete the thread.
576
643
  return {
577
- intents: [[{ action: ThreadAction.DELETE, data: { document: doc, thread } }]],
644
+ intents: [[{ action: ThreadAction.DELETE, data: { subject, thread } }]],
578
645
  };
579
646
  } else {
580
647
  thread.messages.splice(messageIndex, 1);
@@ -638,7 +705,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
638
705
 
639
706
  // TODO(Zan): When we have the deepsignal specific equivalent of this we should use that instead.
640
707
  const threads = computed(() =>
641
- [...doc.threads.filter(nonNullable), ...(state.staging[doc.id] ?? [])].filter(
708
+ [...doc.threads.filter(nonNullable), ...(state.staging[fullyQualifiedId(doc)] ?? [])].filter(
642
709
  (thread) => !(thread?.status === 'resolved'),
643
710
  ),
644
711
  );
@@ -673,43 +740,19 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
673
740
  onCreate: ({ cursor }) => {
674
741
  const [start, end] = cursor.split(':');
675
742
  const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
676
- const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
677
743
 
678
- const stagingArea = state.staging[doc.id];
679
- if (stagingArea) {
680
- stagingArea.push(thread);
681
- } else {
682
- state.staging[doc.id] = [thread];
683
- }
684
-
685
- void intentPlugin?.provides.intent.dispatch([
686
- {
687
- action: NavigationAction.OPEN,
688
- data: {
689
- activeParts: {
690
- complementary: `${fullyQualifiedId(doc)}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
691
- },
692
- },
693
- },
694
- {
695
- action: ThreadAction.SELECT,
696
- data: { current: thread.id, focus: true },
697
- },
698
- {
699
- action: LayoutAction.SET_LAYOUT,
700
- data: {
701
- element: 'complementary',
702
- subject: doc,
703
- state: true,
704
- },
744
+ void intentPlugin?.provides.intent.dispatch({
745
+ action: ThreadAction.CREATE,
746
+ data: {
747
+ cursor,
748
+ name,
749
+ subject: doc,
705
750
  },
706
- ]);
707
-
708
- return thread.id;
751
+ });
709
752
  },
710
753
  onDelete: ({ id }) => {
711
754
  // If the thread is in the staging area, remove it.
712
- const stagingArea = state.staging[doc.id];
755
+ const stagingArea = state.staging[fullyQualifiedId(doc)];
713
756
  if (stagingArea) {
714
757
  const index = stagingArea.findIndex((thread) => thread.id === id);
715
758
  if (index !== -1) {
@@ -724,7 +767,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
724
767
  },
725
768
  onUpdate: ({ id, cursor }) => {
726
769
  const thread =
727
- state.staging[doc.id]?.find((thread) => thread.id === id) ??
770
+ state.staging[fullyQualifiedId(doc)]?.find((thread) => thread.id === id) ??
728
771
  doc.threads.find((thread) => thread?.id === id);
729
772
 
730
773
  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 = {};
@@ -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
11
  import { Filter, 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));
@@ -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