@dxos/plugin-thread 0.6.12-main.15a606f → 0.6.12-main.2d19bf1
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-4VU752F4.mjs → ThreadArticle-GBPIH47W.mjs} +3 -3
- package/dist/lib/browser/{ThreadMain-ML3P4XSD.mjs → ThreadMain-PGF7W5EV.mjs} +3 -3
- package/dist/lib/browser/{chunk-7NUYW5IW.mjs → chunk-5G3KL6TC.mjs} +4 -6
- package/dist/lib/browser/chunk-5G3KL6TC.mjs.map +7 -0
- package/dist/lib/browser/{chunk-GJRBBPXI.mjs → chunk-JFQZ5W2I.mjs} +2 -2
- package/dist/lib/browser/{chunk-GJRBBPXI.mjs.map → chunk-JFQZ5W2I.mjs.map} +1 -1
- package/dist/lib/browser/{chunk-XF5QKI75.mjs → chunk-O4YRHIZC.mjs} +2 -2
- package/dist/lib/browser/index.mjs +185 -196
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/browser/meta.mjs +1 -1
- package/dist/lib/browser/types.mjs +2 -2
- package/dist/lib/node/{ThreadArticle-ERUHPHVT.cjs → ThreadArticle-GH5UBWFU.cjs} +7 -7
- package/dist/lib/node/{ThreadMain-6DN7FDNB.cjs → ThreadMain-CO6I3DNX.cjs} +7 -7
- package/dist/lib/node/{chunk-PZVUW4W2.cjs → chunk-FDYRG7HE.cjs} +9 -9
- package/dist/lib/node/{chunk-FXD3DYMO.cjs → chunk-FOZUQZF4.cjs} +6 -6
- package/dist/lib/node/{chunk-FXD3DYMO.cjs.map → chunk-FOZUQZF4.cjs.map} +1 -1
- package/dist/lib/node/{chunk-6EVXCSDS.cjs → chunk-JIJ7Z6NO.cjs} +6 -8
- package/dist/lib/node/chunk-JIJ7Z6NO.cjs.map +7 -0
- package/dist/lib/node/index.cjs +207 -220
- package/dist/lib/node/index.cjs.map +4 -4
- package/dist/lib/node/meta.cjs +4 -4
- package/dist/lib/node/meta.cjs.map +1 -1
- package/dist/lib/node/meta.json +1 -1
- package/dist/lib/node/types.cjs +3 -3
- package/dist/lib/node/types.cjs.map +1 -1
- package/dist/lib/node-esm/{ThreadArticle-NKI5F4W5.mjs → ThreadArticle-ZTYD52TR.mjs} +3 -3
- package/dist/lib/node-esm/{ThreadMain-2A3VQLCW.mjs → ThreadMain-LEHGOF7R.mjs} +3 -3
- package/dist/lib/node-esm/{chunk-Y475KZTX.mjs → chunk-DC5ZA6H5.mjs} +4 -6
- package/dist/lib/node-esm/chunk-DC5ZA6H5.mjs.map +7 -0
- package/dist/lib/node-esm/{chunk-A44C4MTQ.mjs → chunk-XOVJ65DG.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-YO46JGAE.mjs → chunk-ZXIPFXSA.mjs} +2 -2
- package/dist/lib/node-esm/{chunk-YO46JGAE.mjs.map → chunk-ZXIPFXSA.mjs.map} +1 -1
- package/dist/lib/node-esm/index.mjs +185 -196
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/lib/node-esm/meta.mjs +1 -1
- package/dist/lib/node-esm/types.mjs +2 -2
- package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
- package/dist/types/src/components/CommentContainer.d.ts +1 -1
- package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
- package/dist/types/src/components/CommentsContainer.d.ts +1 -5
- package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
- package/dist/types/src/components/ThreadComplementary.d.ts +2 -3
- package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -1
- package/dist/types/src/extensions/index.d.ts +2 -0
- package/dist/types/src/extensions/index.d.ts.map +1 -0
- package/dist/types/src/extensions/threads.d.ts +9 -0
- package/dist/types/src/extensions/threads.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +3 -6
- package/dist/types/src/meta.d.ts.map +1 -1
- package/dist/types/src/types.d.ts +5 -0
- package/dist/types/src/types.d.ts.map +1 -1
- package/package.json +31 -31
- package/src/ThreadPlugin.tsx +58 -142
- package/src/components/CommentContainer.tsx +8 -12
- package/src/components/CommentsContainer.tsx +1 -18
- package/src/components/ThreadComplementary.tsx +9 -16
- package/src/extensions/index.ts +5 -0
- package/src/extensions/threads.ts +117 -0
- package/src/{meta.tsx → meta.ts} +3 -4
- package/src/types.ts +7 -2
- package/dist/lib/browser/chunk-7NUYW5IW.mjs.map +0 -7
- package/dist/lib/node/chunk-6EVXCSDS.cjs.map +0 -7
- package/dist/lib/node-esm/chunk-Y475KZTX.mjs.map +0 -7
- /package/dist/lib/browser/{ThreadArticle-4VU752F4.mjs.map → ThreadArticle-GBPIH47W.mjs.map} +0 -0
- /package/dist/lib/browser/{ThreadMain-ML3P4XSD.mjs.map → ThreadMain-PGF7W5EV.mjs.map} +0 -0
- /package/dist/lib/browser/{chunk-XF5QKI75.mjs.map → chunk-O4YRHIZC.mjs.map} +0 -0
- /package/dist/lib/node/{ThreadArticle-ERUHPHVT.cjs.map → ThreadArticle-GH5UBWFU.cjs.map} +0 -0
- /package/dist/lib/node/{ThreadMain-6DN7FDNB.cjs.map → ThreadMain-CO6I3DNX.cjs.map} +0 -0
- /package/dist/lib/node/{chunk-PZVUW4W2.cjs.map → chunk-FDYRG7HE.cjs.map} +0 -0
- /package/dist/lib/node-esm/{ThreadArticle-NKI5F4W5.mjs.map → ThreadArticle-ZTYD52TR.mjs.map} +0 -0
- /package/dist/lib/node-esm/{ThreadMain-2A3VQLCW.mjs.map → ThreadMain-LEHGOF7R.mjs.map} +0 -0
- /package/dist/lib/node-esm/{chunk-A44C4MTQ.mjs.map → chunk-XOVJ65DG.mjs.map} +0 -0
package/src/ThreadPlugin.tsx
CHANGED
|
@@ -2,12 +2,10 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { computed, effect } from '@preact/signals-core';
|
|
6
5
|
import React from 'react';
|
|
7
6
|
|
|
8
7
|
import {
|
|
9
8
|
type IntentPluginProvides,
|
|
10
|
-
LayoutAction,
|
|
11
9
|
NavigationAction,
|
|
12
10
|
type LocationProvides,
|
|
13
11
|
type Plugin,
|
|
@@ -28,40 +26,27 @@ import { type ActionGroup, createExtension, isActionGroup, toSignal } from '@dxo
|
|
|
28
26
|
import { ObservabilityAction } from '@dxos/plugin-observability/meta';
|
|
29
27
|
import { SpaceAction } from '@dxos/plugin-space';
|
|
30
28
|
import { ThreadType, MessageType, ChannelType } from '@dxos/plugin-space/types';
|
|
31
|
-
import {
|
|
32
|
-
getSpace,
|
|
33
|
-
getTextInRange,
|
|
34
|
-
createDocAccessor,
|
|
35
|
-
fullyQualifiedId,
|
|
36
|
-
loadObjectReferences,
|
|
37
|
-
} from '@dxos/react-client/echo';
|
|
38
|
-
import { comments, createExternalCommentSync, listener } from '@dxos/react-ui-editor';
|
|
29
|
+
import { getSpace, fullyQualifiedId, loadObjectReferences } from '@dxos/react-client/echo';
|
|
39
30
|
import { translations as threadTranslations } from '@dxos/react-ui-thread';
|
|
40
|
-
import { nonNullable } from '@dxos/util';
|
|
41
31
|
|
|
42
32
|
import { ThreadMain, ThreadSettings, ChatContainer, ChatHeading, ThreadArticle } from './components';
|
|
43
33
|
import { ThreadComplementary } from './components/ThreadComplementary';
|
|
34
|
+
import { threads } from './extensions';
|
|
44
35
|
import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
|
|
45
36
|
import translations from './translations';
|
|
46
|
-
import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
|
|
47
|
-
|
|
48
|
-
type ThreadState = {
|
|
49
|
-
/** An in-memory staging area for threads that are being drafted. */
|
|
50
|
-
staging: Record<string, ThreadType[]>;
|
|
51
|
-
current?: string | undefined;
|
|
52
|
-
focus?: boolean;
|
|
53
|
-
};
|
|
37
|
+
import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps, type ThreadState } from './types';
|
|
54
38
|
|
|
55
39
|
type SubjectId = string;
|
|
40
|
+
|
|
56
41
|
const initialViewState = { showResolvedThreads: false };
|
|
57
|
-
type ViewStore = Record<SubjectId, typeof initialViewState>;
|
|
58
42
|
|
|
59
|
-
|
|
43
|
+
type ViewStore = Record<SubjectId, typeof initialViewState>;
|
|
60
44
|
|
|
61
45
|
// TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
|
|
46
|
+
// NOTE(burdon): Review/discuss CursorConverter semantics.
|
|
62
47
|
export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
63
48
|
const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
|
|
64
|
-
const state = create<ThreadState>({
|
|
49
|
+
const state = create<ThreadState>({ drafts: {} });
|
|
65
50
|
|
|
66
51
|
const viewStore = create<ViewStore>({});
|
|
67
52
|
const getViewState = (subjectId: string) => {
|
|
@@ -153,6 +138,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
153
138
|
return () => clearTimeout(timeout);
|
|
154
139
|
},
|
|
155
140
|
() => space?.db.getObjectById(objectId),
|
|
141
|
+
subjectId,
|
|
156
142
|
);
|
|
157
143
|
if (!object || !subjectId) {
|
|
158
144
|
return;
|
|
@@ -168,11 +154,12 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
168
154
|
return {
|
|
169
155
|
id,
|
|
170
156
|
type: 'orphan-comments-for-subject',
|
|
171
|
-
data:
|
|
157
|
+
data: null,
|
|
172
158
|
properties: {
|
|
173
|
-
icon:
|
|
159
|
+
icon: 'ph--chat-text--regular',
|
|
174
160
|
label,
|
|
175
161
|
showResolvedThreads: viewState.showResolvedThreads,
|
|
162
|
+
object,
|
|
176
163
|
},
|
|
177
164
|
};
|
|
178
165
|
},
|
|
@@ -261,8 +248,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
261
248
|
return data.plugin === meta.id ? <ThreadSettings settings={settings.values} /> : null;
|
|
262
249
|
}
|
|
263
250
|
|
|
264
|
-
case 'article':
|
|
265
|
-
case 'complementary': {
|
|
251
|
+
case 'article': {
|
|
266
252
|
const location = navigationPlugin?.provides.location;
|
|
267
253
|
|
|
268
254
|
if (data.object instanceof ChannelType && data.object.threads[0]) {
|
|
@@ -290,6 +276,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
290
276
|
);
|
|
291
277
|
}
|
|
292
278
|
|
|
279
|
+
break;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
case 'complementary--comments': {
|
|
293
283
|
if (
|
|
294
284
|
data.subject &&
|
|
295
285
|
typeof data.subject === 'object' &&
|
|
@@ -302,13 +292,14 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
302
292
|
<ThreadComplementary
|
|
303
293
|
role={role}
|
|
304
294
|
subject={data.subject}
|
|
305
|
-
|
|
295
|
+
drafts={state.drafts[fullyQualifiedId(data.subject)]}
|
|
306
296
|
current={state.current}
|
|
307
|
-
focus
|
|
308
297
|
showResolvedThreads={showResolvedThreads}
|
|
309
298
|
/>
|
|
310
299
|
);
|
|
311
300
|
}
|
|
301
|
+
|
|
302
|
+
break;
|
|
312
303
|
}
|
|
313
304
|
}
|
|
314
305
|
|
|
@@ -327,7 +318,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
327
318
|
try {
|
|
328
319
|
// Static schema will throw an error if subject does not support threads array property.
|
|
329
320
|
subject.threads = [];
|
|
330
|
-
} catch (
|
|
321
|
+
} catch (err) {
|
|
331
322
|
log.error('Subject does not support threads array', subject?.typename);
|
|
332
323
|
return;
|
|
333
324
|
}
|
|
@@ -335,11 +326,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
335
326
|
|
|
336
327
|
const subjectId = fullyQualifiedId(subject);
|
|
337
328
|
const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
|
|
338
|
-
const
|
|
339
|
-
if (
|
|
340
|
-
|
|
329
|
+
const draft = state.drafts[subjectId];
|
|
330
|
+
if (draft) {
|
|
331
|
+
draft.push(thread);
|
|
341
332
|
} else {
|
|
342
|
-
state.
|
|
333
|
+
state.drafts[subjectId] = [thread];
|
|
343
334
|
}
|
|
344
335
|
|
|
345
336
|
return {
|
|
@@ -348,14 +339,14 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
348
339
|
[
|
|
349
340
|
{
|
|
350
341
|
action: ThreadAction.SELECT,
|
|
351
|
-
data: { current: fullyQualifiedId(thread)
|
|
342
|
+
data: { current: fullyQualifiedId(thread) },
|
|
352
343
|
},
|
|
344
|
+
],
|
|
345
|
+
[
|
|
353
346
|
{
|
|
354
|
-
action:
|
|
347
|
+
action: NavigationAction.OPEN,
|
|
355
348
|
data: {
|
|
356
|
-
|
|
357
|
-
subject: subjectId,
|
|
358
|
-
state: true,
|
|
349
|
+
activeParts: { complementary: 'comments' },
|
|
359
350
|
},
|
|
360
351
|
},
|
|
361
352
|
],
|
|
@@ -363,14 +354,30 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
363
354
|
};
|
|
364
355
|
} else {
|
|
365
356
|
// NOTE: This is the standalone thread creation case.
|
|
366
|
-
return {
|
|
357
|
+
return {
|
|
358
|
+
data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }),
|
|
359
|
+
};
|
|
367
360
|
}
|
|
368
361
|
}
|
|
369
362
|
|
|
370
363
|
case ThreadAction.SELECT: {
|
|
371
|
-
state.focus = intent.data?.current === state.current ? state.focus : intent.data?.focus;
|
|
372
364
|
state.current = intent.data?.current;
|
|
373
|
-
|
|
365
|
+
|
|
366
|
+
return {
|
|
367
|
+
data: true,
|
|
368
|
+
intents: !intent.data?.skipOpen
|
|
369
|
+
? [
|
|
370
|
+
[
|
|
371
|
+
{
|
|
372
|
+
action: NavigationAction.OPEN,
|
|
373
|
+
data: {
|
|
374
|
+
activeParts: { complementary: 'comments' },
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
]
|
|
379
|
+
: [],
|
|
380
|
+
};
|
|
374
381
|
}
|
|
375
382
|
|
|
376
383
|
case ThreadAction.TOGGLE_RESOLVED: {
|
|
@@ -389,6 +396,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
389
396
|
const spaceId = space?.id;
|
|
390
397
|
|
|
391
398
|
return {
|
|
399
|
+
data: true,
|
|
392
400
|
intents: [
|
|
393
401
|
[
|
|
394
402
|
{
|
|
@@ -410,13 +418,12 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
410
418
|
}
|
|
411
419
|
|
|
412
420
|
const subjectId = fullyQualifiedId(subject);
|
|
413
|
-
const
|
|
414
|
-
if (
|
|
415
|
-
// Check if we're deleting a
|
|
416
|
-
|
|
417
|
-
const index = stagingArea.findIndex((t) => t.id === thread.id);
|
|
421
|
+
const draft = state.drafts[subjectId];
|
|
422
|
+
if (draft) {
|
|
423
|
+
// Check if we're deleting a draft; if so, remove it.
|
|
424
|
+
const index = draft.findIndex((t) => t.id === thread.id);
|
|
418
425
|
if (index !== -1) {
|
|
419
|
-
|
|
426
|
+
draft.splice(index, 1);
|
|
420
427
|
return { data: true };
|
|
421
428
|
}
|
|
422
429
|
}
|
|
@@ -483,12 +490,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
483
490
|
const intents = [];
|
|
484
491
|
const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
|
|
485
492
|
|
|
486
|
-
if (state.
|
|
487
|
-
// Move
|
|
493
|
+
if (state.drafts[subjectId]?.find((t) => t === thread)) {
|
|
494
|
+
// Move draft to document.
|
|
488
495
|
thread.status = 'active';
|
|
489
496
|
subject.threads ? subject.threads.push(thread) : (subject.threads = [thread]);
|
|
490
|
-
state.
|
|
491
|
-
|
|
497
|
+
state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
|
|
492
498
|
intents.push({
|
|
493
499
|
action: ObservabilityAction.SEND_EVENT,
|
|
494
500
|
data: { name: 'threads.thread-created', properties: analyticsProperties },
|
|
@@ -556,13 +562,11 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
556
562
|
} else if (intent.undo) {
|
|
557
563
|
const message = intent.data?.message;
|
|
558
564
|
const messageIndex = intent.data?.messageIndex;
|
|
559
|
-
|
|
560
565
|
if (!(message instanceof MessageType) || !(typeof messageIndex === 'number')) {
|
|
561
566
|
return;
|
|
562
567
|
}
|
|
563
568
|
|
|
564
569
|
thread.messages.splice(messageIndex, 0, message);
|
|
565
|
-
|
|
566
570
|
return {
|
|
567
571
|
data: true,
|
|
568
572
|
intents: [
|
|
@@ -583,96 +587,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
583
587
|
},
|
|
584
588
|
},
|
|
585
589
|
markdown: {
|
|
586
|
-
// TODO(burdon): Factor out extension factory into separate file (for simplicity).
|
|
587
590
|
extensions: ({ document: doc }) => {
|
|
588
|
-
|
|
589
|
-
if (!doc || !space) {
|
|
590
|
-
// Include no-op comments extension here to ensure that the facets are always present when they are expected.
|
|
591
|
-
// TODO(wittjosiah): The Editor should only look for these facets when comments are available.
|
|
592
|
-
return [comments()];
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
// TODO(Zan): When we have the deepsignal specific equivalent of this we should use that instead.
|
|
596
|
-
const threads = computed(() =>
|
|
597
|
-
[...doc.threads.filter(nonNullable), ...(state.staging[fullyQualifiedId(doc)] ?? [])].filter(
|
|
598
|
-
(thread) => !(thread?.status === 'resolved'),
|
|
599
|
-
),
|
|
600
|
-
);
|
|
601
|
-
|
|
602
|
-
return [
|
|
603
|
-
listener({
|
|
604
|
-
onChange: () => {
|
|
605
|
-
doc.threads.forEach((thread) => {
|
|
606
|
-
if (thread?.anchor) {
|
|
607
|
-
const [start, end] = thread.anchor.split(':');
|
|
608
|
-
const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
609
|
-
// TODO(burdon): This seems unsafe; review.
|
|
610
|
-
// Only update if the name has changed, otherwise this will cause an infinite loop.
|
|
611
|
-
// Skip if the name is empty - this means comment text was deleted, but thread name should remain.
|
|
612
|
-
if (name && name !== thread.name) {
|
|
613
|
-
thread.name = name;
|
|
614
|
-
}
|
|
615
|
-
}
|
|
616
|
-
});
|
|
617
|
-
},
|
|
618
|
-
}),
|
|
619
|
-
createExternalCommentSync(
|
|
620
|
-
doc.id,
|
|
621
|
-
(sink) => effect(() => sink()),
|
|
622
|
-
() =>
|
|
623
|
-
threads.value
|
|
624
|
-
.filter((thread) => thread?.anchor)
|
|
625
|
-
.map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
|
|
626
|
-
),
|
|
627
|
-
comments({
|
|
628
|
-
id: doc.id,
|
|
629
|
-
onCreate: ({ cursor }) => {
|
|
630
|
-
const [start, end] = cursor.split(':');
|
|
631
|
-
const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
632
|
-
|
|
633
|
-
void intentPlugin?.provides.intent.dispatch({
|
|
634
|
-
action: ThreadAction.CREATE,
|
|
635
|
-
data: {
|
|
636
|
-
cursor,
|
|
637
|
-
name,
|
|
638
|
-
subject: doc,
|
|
639
|
-
},
|
|
640
|
-
});
|
|
641
|
-
},
|
|
642
|
-
onDelete: ({ id }) => {
|
|
643
|
-
// If the thread is in the staging area, remove it.
|
|
644
|
-
const stagingArea = state.staging[fullyQualifiedId(doc)];
|
|
645
|
-
if (stagingArea) {
|
|
646
|
-
const index = stagingArea.findIndex((thread) => fullyQualifiedId(thread) === id);
|
|
647
|
-
if (index !== -1) {
|
|
648
|
-
stagingArea.splice(index, 1);
|
|
649
|
-
}
|
|
650
|
-
}
|
|
651
|
-
|
|
652
|
-
const thread = doc.threads.find((thread) => thread?.id === id);
|
|
653
|
-
if (thread) {
|
|
654
|
-
thread.anchor = undefined;
|
|
655
|
-
}
|
|
656
|
-
},
|
|
657
|
-
onUpdate: ({ id, cursor }) => {
|
|
658
|
-
const thread =
|
|
659
|
-
state.staging[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
|
|
660
|
-
doc.threads.find((thread) => thread?.id === id);
|
|
661
|
-
|
|
662
|
-
if (thread instanceof ThreadType && thread.anchor) {
|
|
663
|
-
const [start, end] = thread.anchor.split(':');
|
|
664
|
-
thread.name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
665
|
-
thread.anchor = cursor;
|
|
666
|
-
}
|
|
667
|
-
},
|
|
668
|
-
onSelect: ({ selection: { current, closest } }) => {
|
|
669
|
-
const dispatch = intentPlugin?.provides.intent.dispatch;
|
|
670
|
-
if (dispatch) {
|
|
671
|
-
void dispatch([{ action: ThreadAction.SELECT, data: { current: current ?? closest } }]);
|
|
672
|
-
}
|
|
673
|
-
},
|
|
674
|
-
}),
|
|
675
|
-
];
|
|
591
|
+
return threads(state, doc, intentPlugin?.provides.intent.dispatch);
|
|
676
592
|
},
|
|
677
593
|
},
|
|
678
594
|
},
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
//
|
|
4
4
|
|
|
5
5
|
import { CheckCircle, X } from '@phosphor-icons/react';
|
|
6
|
-
import React, { useCallback,
|
|
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';
|
|
@@ -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,
|
|
@@ -106,7 +105,6 @@ export const CommentContainer = ({
|
|
|
106
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
110
|
const textboxMetadata = getMessageMetadata(fullyQualifiedId(thread), identity);
|
|
@@ -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
|
|
|
@@ -200,7 +190,13 @@ export const CommentContainer = ({
|
|
|
200
190
|
onDelete={(id: string) => onMessageDelete?.(id)}
|
|
201
191
|
/>
|
|
202
192
|
))}
|
|
203
|
-
|
|
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} />
|
|
204
200
|
<ThreadFooter activity={activity}>{t('activity message')}</ThreadFooter>
|
|
205
201
|
{/* NOTE(thure): This can’t also be the `overflow-anchor` because `ScrollArea` injects an interceding node that contains this necessary ref’d element. */}
|
|
206
202
|
<div role='none' className='bs-px -mbs-px' ref={threadScrollRef} />
|
|
@@ -7,8 +7,7 @@ import React, { useEffect } from 'react';
|
|
|
7
7
|
|
|
8
8
|
import { type ThreadType } from '@dxos/plugin-space/types';
|
|
9
9
|
import { fullyQualifiedId } from '@dxos/react-client/echo';
|
|
10
|
-
import { useTranslation,
|
|
11
|
-
import { PlankHeading } from '@dxos/react-ui-deck';
|
|
10
|
+
import { useTranslation, Trans } from '@dxos/react-ui';
|
|
12
11
|
import { descriptionText, mx } from '@dxos/react-ui-theme';
|
|
13
12
|
|
|
14
13
|
import { CommentContainer } from './CommentContainer';
|
|
@@ -25,7 +24,6 @@ export type ThreadsContainerProps = Omit<
|
|
|
25
24
|
*/
|
|
26
25
|
detached?: string[];
|
|
27
26
|
currentId?: string;
|
|
28
|
-
autoFocusCurrentTextbox?: boolean;
|
|
29
27
|
showResolvedThreads?: boolean;
|
|
30
28
|
onThreadAttend?: (thread: ThreadType) => void;
|
|
31
29
|
onThreadDelete?: (thread: ThreadType) => void;
|
|
@@ -34,19 +32,6 @@ export type ThreadsContainerProps = Omit<
|
|
|
34
32
|
onComment?: (thread: ThreadType) => void;
|
|
35
33
|
};
|
|
36
34
|
|
|
37
|
-
export const CommentsHeading = ({ attendableId }: { attendableId?: string }) => {
|
|
38
|
-
const { t } = useTranslation(THREAD_PLUGIN);
|
|
39
|
-
|
|
40
|
-
return (
|
|
41
|
-
<div role='none' className='flex items-center'>
|
|
42
|
-
<PlankHeading.Button attendableId={attendableId}>
|
|
43
|
-
<Icon icon='ph--quotes--regular' size={5} />
|
|
44
|
-
</PlankHeading.Button>
|
|
45
|
-
<PlankHeading.Label attendableId={attendableId}>{t('comments heading')}</PlankHeading.Label>
|
|
46
|
-
</div>
|
|
47
|
-
);
|
|
48
|
-
};
|
|
49
|
-
|
|
50
35
|
/**
|
|
51
36
|
* Comment threads.
|
|
52
37
|
*/
|
|
@@ -54,7 +39,6 @@ export const CommentsContainer = ({
|
|
|
54
39
|
threads,
|
|
55
40
|
detached = [],
|
|
56
41
|
currentId,
|
|
57
|
-
autoFocusCurrentTextbox,
|
|
58
42
|
showResolvedThreads,
|
|
59
43
|
onThreadAttend,
|
|
60
44
|
onThreadDelete,
|
|
@@ -82,7 +66,6 @@ export const CommentsContainer = ({
|
|
|
82
66
|
thread={thread}
|
|
83
67
|
current={currentId === threadId}
|
|
84
68
|
detached={detached.includes(threadId)}
|
|
85
|
-
autoFocusTextbox={autoFocusCurrentTextbox && currentId === threadId}
|
|
86
69
|
{...(onThreadAttend && { onAttend: () => onThreadAttend(thread) })}
|
|
87
70
|
{...(onThreadDelete && { onThreadDelete: () => onThreadDelete(thread) })}
|
|
88
71
|
{...(onMessageDelete && { onMessageDelete: (messageId: string) => onMessageDelete(thread, messageId) })}
|
|
@@ -8,10 +8,10 @@ import { LayoutAction, type Plugin, useIntentDispatcher, useResolvePlugins } fro
|
|
|
8
8
|
import { type ThreadType } from '@dxos/plugin-space';
|
|
9
9
|
import { fullyQualifiedId } from '@dxos/react-client/echo';
|
|
10
10
|
import { ScrollArea } from '@dxos/react-ui';
|
|
11
|
-
import {
|
|
11
|
+
import { useAttended } from '@dxos/react-ui-attention';
|
|
12
12
|
import { nonNullable } from '@dxos/util';
|
|
13
13
|
|
|
14
|
-
import { CommentsContainer
|
|
14
|
+
import { CommentsContainer } from '../components';
|
|
15
15
|
import { THREAD_PLUGIN } from '../meta';
|
|
16
16
|
import { ThreadAction, type ThreadProvides } from '../types';
|
|
17
17
|
|
|
@@ -21,16 +21,14 @@ const providesThreadsConfig = (plugin: any): Plugin<ThreadProvides<any>> | undef
|
|
|
21
21
|
export const ThreadComplementary = ({
|
|
22
22
|
role,
|
|
23
23
|
subject,
|
|
24
|
-
|
|
24
|
+
drafts,
|
|
25
25
|
current,
|
|
26
|
-
focus,
|
|
27
26
|
showResolvedThreads,
|
|
28
27
|
}: {
|
|
29
28
|
role: string;
|
|
30
29
|
subject: any;
|
|
31
|
-
|
|
30
|
+
drafts: ThreadType[] | undefined;
|
|
32
31
|
current?: string;
|
|
33
|
-
focus?: boolean;
|
|
34
32
|
showResolvedThreads?: boolean;
|
|
35
33
|
}) => {
|
|
36
34
|
const dispatch = useIntentDispatcher();
|
|
@@ -44,8 +42,8 @@ export const ThreadComplementary = ({
|
|
|
44
42
|
const sort = useMemo(() => createSort?.(subject), [createSort, subject]);
|
|
45
43
|
|
|
46
44
|
const threads = useMemo(() => {
|
|
47
|
-
return subject.threads.concat(
|
|
48
|
-
}, [JSON.stringify(subject.threads), JSON.stringify(
|
|
45
|
+
return subject.threads.concat(drafts ?? []).filter(nonNullable) as ThreadType[];
|
|
46
|
+
}, [JSON.stringify(subject.threads), JSON.stringify(drafts)]);
|
|
49
47
|
|
|
50
48
|
const detachedIds = useMemo(() => {
|
|
51
49
|
return threads.filter(({ anchor }) => !anchor).map((thread) => fullyQualifiedId(thread));
|
|
@@ -58,22 +56,17 @@ export const ThreadComplementary = ({
|
|
|
58
56
|
threads.sort((a, b) => sort(a?.anchor, b?.anchor));
|
|
59
57
|
}, [sort, threads]);
|
|
60
58
|
|
|
61
|
-
|
|
62
|
-
const { attended } = useAttentionContext('Thread Complementary');
|
|
59
|
+
const attended = useAttended();
|
|
63
60
|
const qualifiedSubjectId = fullyQualifiedId(subject);
|
|
64
61
|
|
|
65
|
-
const attendableAttrs = createAttendableAttributes(qualifiedSubjectId);
|
|
66
|
-
|
|
67
62
|
return (
|
|
68
|
-
<div role='none' className='contents
|
|
69
|
-
{role === 'complementary' && <CommentsHeading attendableId={qualifiedSubjectId} />}
|
|
63
|
+
<div role='none' className='contents'>
|
|
70
64
|
<ScrollArea.Root classNames='row-span-2'>
|
|
71
65
|
<ScrollArea.Viewport>
|
|
72
66
|
<CommentsContainer
|
|
73
67
|
threads={threads}
|
|
74
68
|
detached={detachedIds}
|
|
75
|
-
currentId={attended.
|
|
76
|
-
autoFocusCurrentTextbox={focus}
|
|
69
|
+
currentId={attended.includes(qualifiedSubjectId) ? current : undefined}
|
|
77
70
|
showResolvedThreads={showResolvedThreads}
|
|
78
71
|
onThreadAttend={(thread) => {
|
|
79
72
|
const threadId = fullyQualifiedId(thread);
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2024 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type Extension } from '@codemirror/state';
|
|
6
|
+
import { EditorView } from '@codemirror/view';
|
|
7
|
+
import { computed, effect } from '@preact/signals-core';
|
|
8
|
+
|
|
9
|
+
import { type IntentDispatcher } from '@dxos/app-framework';
|
|
10
|
+
import { type DocumentType } from '@dxos/plugin-markdown/types';
|
|
11
|
+
import { ThreadType } from '@dxos/plugin-space/types';
|
|
12
|
+
import { getSpace, getTextInRange, createDocAccessor, fullyQualifiedId } from '@dxos/react-client/echo';
|
|
13
|
+
import { comments, createExternalCommentSync } from '@dxos/react-ui-editor';
|
|
14
|
+
import { nonNullable } from '@dxos/util';
|
|
15
|
+
|
|
16
|
+
import { ThreadAction, type ThreadState } from '../types';
|
|
17
|
+
|
|
18
|
+
// TODO(burdon): Factor out.
|
|
19
|
+
const getName = (doc: DocumentType, anchor: string): string | undefined => {
|
|
20
|
+
if (doc.content) {
|
|
21
|
+
const [start, end] = anchor.split(':');
|
|
22
|
+
return getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
23
|
+
}
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Construct plugins.
|
|
28
|
+
*/
|
|
29
|
+
export const threads = (state: ThreadState, doc?: DocumentType, dispatch?: IntentDispatcher): Extension => {
|
|
30
|
+
const space = doc && getSpace(doc);
|
|
31
|
+
if (!doc || !space || !dispatch) {
|
|
32
|
+
// Include no-op comments extension here to ensure that the facets are always present when they are expected.
|
|
33
|
+
// TODO(wittjosiah): The Editor should only look for these facets when comments are available.
|
|
34
|
+
return [comments()];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// TODO(Zan): When we have the deepsignal specific equivalent of this we should use that instead.
|
|
38
|
+
const threads = computed(() =>
|
|
39
|
+
[...doc.threads.filter(nonNullable), ...(state.drafts[fullyQualifiedId(doc)] ?? [])].filter(
|
|
40
|
+
(thread) => !(thread?.status === 'resolved'),
|
|
41
|
+
),
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
return [
|
|
45
|
+
EditorView.updateListener.of((update) => {
|
|
46
|
+
if (update.docChanged) {
|
|
47
|
+
doc.threads.forEach((thread) => {
|
|
48
|
+
if (thread?.anchor) {
|
|
49
|
+
// Only update if the name has changed, otherwise this will cause an infinite loop.
|
|
50
|
+
// Skip if the name is empty; this means comment text was deleted, but thread name should remain.
|
|
51
|
+
const name = getName(doc, thread.anchor);
|
|
52
|
+
if (name && name !== thread.name) {
|
|
53
|
+
thread.name = name;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}),
|
|
59
|
+
|
|
60
|
+
createExternalCommentSync(
|
|
61
|
+
fullyQualifiedId(doc),
|
|
62
|
+
(sink) => effect(() => sink()),
|
|
63
|
+
() =>
|
|
64
|
+
threads.value
|
|
65
|
+
.filter((thread) => thread?.anchor)
|
|
66
|
+
.map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
|
|
67
|
+
),
|
|
68
|
+
|
|
69
|
+
comments({
|
|
70
|
+
id: fullyQualifiedId(doc),
|
|
71
|
+
onCreate: ({ cursor }) => {
|
|
72
|
+
const name = getName(doc, cursor);
|
|
73
|
+
void dispatch({
|
|
74
|
+
action: ThreadAction.CREATE,
|
|
75
|
+
data: {
|
|
76
|
+
cursor,
|
|
77
|
+
name,
|
|
78
|
+
subject: doc,
|
|
79
|
+
},
|
|
80
|
+
});
|
|
81
|
+
},
|
|
82
|
+
onDelete: ({ id }) => {
|
|
83
|
+
const draft = state.drafts[fullyQualifiedId(doc)];
|
|
84
|
+
if (draft) {
|
|
85
|
+
const index = draft.findIndex((thread) => fullyQualifiedId(thread) === id);
|
|
86
|
+
if (index !== -1) {
|
|
87
|
+
draft.splice(index, 1);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const thread = doc.threads.find((thread) => thread?.id === id);
|
|
92
|
+
if (thread) {
|
|
93
|
+
thread.anchor = undefined;
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
onUpdate: ({ id, cursor }) => {
|
|
97
|
+
const thread =
|
|
98
|
+
state.drafts[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
|
|
99
|
+
doc.threads.find((thread) => thread?.id === id);
|
|
100
|
+
|
|
101
|
+
if (thread instanceof ThreadType && thread.anchor) {
|
|
102
|
+
thread.name = getName(doc, thread.anchor);
|
|
103
|
+
thread.anchor = cursor;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
onSelect: ({ selection: { current, closest } }) => {
|
|
107
|
+
void dispatch({
|
|
108
|
+
action: ThreadAction.SELECT,
|
|
109
|
+
data: {
|
|
110
|
+
current: current ?? closest,
|
|
111
|
+
skipOpen: true,
|
|
112
|
+
},
|
|
113
|
+
});
|
|
114
|
+
},
|
|
115
|
+
}),
|
|
116
|
+
];
|
|
117
|
+
};
|
package/src/{meta.tsx → meta.ts}
RENAMED
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { type PluginMeta } from '@dxos/app-framework';
|
|
6
6
|
|
|
7
7
|
export const THREAD_PLUGIN = 'dxos.org/plugin/thread';
|
|
8
8
|
export const THREAD_ITEM = `${THREAD_PLUGIN}/item`;
|
|
9
9
|
|
|
10
|
-
export default
|
|
10
|
+
export default {
|
|
11
11
|
id: THREAD_PLUGIN,
|
|
12
12
|
name: 'Threads',
|
|
13
|
-
// TODO(wittjosiah): Update once scope of threads expands.
|
|
14
13
|
description: 'Comment threads on documents.',
|
|
15
14
|
icon: 'ph--chat--regular',
|
|
16
|
-
}
|
|
15
|
+
} satisfies PluginMeta;
|