@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.
- package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs → ThreadArticle-6L6BGE4Z.mjs} +2 -2
- package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs → ThreadMain-DU6LBSN3.mjs} +2 -2
- package/dist/lib/browser/{chunk-P7ZE5N5M.mjs → chunk-AVC4CGJM.mjs} +6 -6
- package/dist/lib/browser/chunk-AVC4CGJM.mjs.map +7 -0
- package/dist/lib/browser/{chunk-GKPZHUT3.mjs → chunk-U5UVX4ZP.mjs} +1 -1
- package/dist/lib/browser/{chunk-GKPZHUT3.mjs.map → chunk-U5UVX4ZP.mjs.map} +1 -1
- package/dist/lib/browser/index.mjs +217 -153
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/types.mjs +1 -1
- package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs → ThreadArticle-7BWXVIJ4.cjs} +6 -6
- package/dist/lib/node/{ThreadMain-XJX75GHG.cjs → ThreadMain-XSRB6NKJ.cjs} +6 -6
- package/dist/lib/node/{chunk-D5GOVXHS.cjs → chunk-4B2ARV7L.cjs} +4 -4
- package/dist/lib/node/{chunk-D5GOVXHS.cjs.map → chunk-4B2ARV7L.cjs.map} +1 -1
- package/dist/lib/node/{chunk-5PZNPPQF.cjs → chunk-T7TXCNHL.cjs} +8 -8
- package/dist/lib/node/chunk-T7TXCNHL.cjs.map +7 -0
- package/dist/lib/node/index.cjs +258 -196
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types.cjs +2 -2
- package/dist/lib/node/types.cjs.map +1 -1
- package/dist/lib/node-esm/ThreadArticle-6L6BGE4Z.mjs +18 -0
- package/dist/lib/node-esm/ThreadArticle-6L6BGE4Z.mjs.map +7 -0
- package/dist/lib/node-esm/ThreadMain-DU6LBSN3.mjs +26 -0
- package/dist/lib/node-esm/ThreadMain-DU6LBSN3.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-5IBJO53M.mjs +21 -0
- package/dist/lib/node-esm/chunk-5IBJO53M.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-AVC4CGJM.mjs +410 -0
- package/dist/lib/node-esm/chunk-AVC4CGJM.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-U5UVX4ZP.mjs +20 -0
- package/dist/lib/node-esm/chunk-U5UVX4ZP.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1166 -0
- package/dist/lib/node-esm/index.mjs.map +7 -0
- package/dist/lib/node-esm/meta.json +1 -0
- package/dist/lib/node-esm/meta.mjs +11 -0
- package/dist/lib/node-esm/meta.mjs.map +7 -0
- package/dist/lib/node-esm/types.mjs +8 -0
- package/dist/lib/node-esm/types.mjs.map +7 -0
- package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
- package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
- package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
- package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
- package/dist/types/src/components/ThreadComplementary.d.ts +11 -0
- package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +1 -1
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +35 -32
- package/src/ThreadPlugin.tsx +46 -111
- package/src/components/ChatContainer.tsx +4 -4
- package/src/components/CommentContainer.tsx +10 -7
- package/src/components/Comments.stories.tsx +2 -2
- package/src/components/CommentsContainer.tsx +18 -14
- package/src/components/MessageContainer.tsx +2 -1
- package/src/components/ThreadComplementary.tsx +120 -0
- package/src/types.ts +1 -1
- package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
- package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
- package/dist/types/src/types.test.d.ts +0 -2
- package/dist/types/src/types.test.d.ts.map +0 -1
- package/src/types.test.ts +0 -13
- /package/dist/lib/browser/{ThreadArticle-O64ZECMF.mjs.map → ThreadArticle-6L6BGE4Z.mjs.map} +0 -0
- /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-DU6LBSN3.mjs.map} +0 -0
- /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-7BWXVIJ4.cjs.map} +0 -0
- /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-XSRB6NKJ.cjs.map} +0 -0
package/src/ThreadPlugin.tsx
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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) =>
|
|
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
|
|
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
|
-
<
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
|