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