@dxos/plugin-thread 0.6.11-staging.e6894a4 → 0.6.12-main.15a606f
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-4VU752F4.mjs} +3 -3
- package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs → ThreadMain-ML3P4XSD.mjs} +3 -3
- package/dist/lib/browser/{chunk-5IBJO53M.mjs → chunk-7NUYW5IW.mjs} +2 -5
- package/dist/lib/browser/chunk-7NUYW5IW.mjs.map +7 -0
- package/dist/lib/browser/{chunk-GKPZHUT3.mjs → chunk-GJRBBPXI.mjs} +2 -2
- package/dist/lib/browser/{chunk-GKPZHUT3.mjs.map → chunk-GJRBBPXI.mjs.map} +1 -1
- package/dist/lib/browser/{chunk-P7ZE5N5M.mjs → chunk-XF5QKI75.mjs} +13 -11
- package/dist/lib/browser/chunk-XF5QKI75.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +237 -210
- 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-DO56JXBU.cjs → ThreadArticle-ERUHPHVT.cjs} +7 -7
- package/dist/lib/node/{ThreadMain-XJX75GHG.cjs → ThreadMain-6DN7FDNB.cjs} +7 -7
- package/dist/lib/node/{chunk-BWBONZGU.cjs → chunk-6EVXCSDS.cjs} +5 -18
- package/dist/lib/node/chunk-6EVXCSDS.cjs.map +7 -0
- package/dist/lib/node/{chunk-D5GOVXHS.cjs → chunk-FXD3DYMO.cjs} +6 -6
- package/dist/lib/node/{chunk-D5GOVXHS.cjs.map → chunk-FXD3DYMO.cjs.map} +1 -1
- package/dist/lib/node/{chunk-5PZNPPQF.cjs → chunk-PZVUW4W2.cjs} +65 -63
- package/dist/lib/node/chunk-PZVUW4W2.cjs.map +7 -0
- package/dist/lib/node/index.cjs +324 -299
- 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 +19 -0
- package/dist/lib/node-esm/ThreadArticle-NKI5F4W5.mjs.map +7 -0
- package/dist/lib/node-esm/ThreadMain-2A3VQLCW.mjs +27 -0
- package/dist/lib/node-esm/ThreadMain-2A3VQLCW.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-A44C4MTQ.mjs +413 -0
- package/dist/lib/node-esm/chunk-A44C4MTQ.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-Y475KZTX.mjs +20 -0
- package/dist/lib/node-esm/chunk-Y475KZTX.mjs.map +7 -0
- package/dist/lib/node-esm/chunk-YO46JGAE.mjs +21 -0
- package/dist/lib/node-esm/chunk-YO46JGAE.mjs.map +7 -0
- package/dist/lib/node-esm/index.mjs +1133 -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 +12 -0
- package/dist/lib/node-esm/meta.mjs.map +7 -0
- package/dist/lib/node-esm/types.mjs +9 -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/Chat.stories.d.ts +0 -1
- package/dist/types/src/components/Chat.stories.d.ts.map +1 -1
- package/dist/types/src/components/ChatContainer.d.ts.map +1 -1
- package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
- package/dist/types/src/components/Comments.stories.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/components/testing.d.ts +2 -0
- package/dist/types/src/components/testing.d.ts.map +1 -1
- package/dist/types/src/meta.d.ts +1 -4
- 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/package.json +37 -34
- package/src/ThreadPlugin.tsx +70 -190
- package/src/components/Chat.stories.tsx +3 -3
- package/src/components/ChatContainer.tsx +7 -8
- package/src/components/CommentContainer.tsx +10 -7
- package/src/components/Comments.stories.tsx +10 -15
- package/src/components/CommentsContainer.tsx +22 -18
- package/src/components/MessageContainer.tsx +2 -1
- package/src/components/ThreadComplementary.tsx +120 -0
- package/src/meta.tsx +1 -5
- package/src/types.ts +1 -1
- package/dist/lib/browser/chunk-5IBJO53M.mjs.map +0 -7
- package/dist/lib/browser/chunk-P7ZE5N5M.mjs.map +0 -7
- package/dist/lib/node/chunk-5PZNPPQF.cjs.map +0 -7
- package/dist/lib/node/chunk-BWBONZGU.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-4VU752F4.mjs.map} +0 -0
- /package/dist/lib/browser/{ThreadMain-ZDCRC6RG.mjs.map → ThreadMain-ML3P4XSD.mjs.map} +0 -0
- /package/dist/lib/node/{ThreadArticle-DO56JXBU.cjs.map → ThreadArticle-ERUHPHVT.cjs.map} +0 -0
- /package/dist/lib/node/{ThreadMain-XJX75GHG.cjs.map → ThreadMain-6DN7FDNB.cjs.map} +0 -0
package/src/ThreadPlugin.tsx
CHANGED
|
@@ -2,9 +2,8 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Chat, type IconProps } from '@phosphor-icons/react';
|
|
6
5
|
import { computed, effect } from '@preact/signals-core';
|
|
7
|
-
import React
|
|
6
|
+
import React from 'react';
|
|
8
7
|
|
|
9
8
|
import {
|
|
10
9
|
type IntentPluginProvides,
|
|
@@ -16,51 +15,35 @@ import {
|
|
|
16
15
|
parseIntentPlugin,
|
|
17
16
|
parseNavigationPlugin,
|
|
18
17
|
resolvePlugin,
|
|
19
|
-
parseGraphPlugin,
|
|
20
|
-
SLUG_PATH_SEPARATOR,
|
|
21
|
-
SLUG_COLLECTION_INDICATOR,
|
|
22
18
|
isLayoutParts,
|
|
23
19
|
parseMetadataResolverPlugin,
|
|
24
|
-
type IntentDispatcher,
|
|
25
|
-
useResolvePlugins,
|
|
26
20
|
} from '@dxos/app-framework';
|
|
27
21
|
import { type UnsubscribeCallback } from '@dxos/async';
|
|
28
22
|
import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
|
|
29
23
|
import { create } from '@dxos/echo-schema';
|
|
30
24
|
import { LocalStorageStore } from '@dxos/local-storage';
|
|
31
|
-
import {
|
|
25
|
+
import { log } from '@dxos/log';
|
|
32
26
|
import { parseClientPlugin } from '@dxos/plugin-client';
|
|
33
|
-
import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
|
|
34
|
-
import { DocumentType } from '@dxos/plugin-markdown/types';
|
|
27
|
+
import { type ActionGroup, createExtension, isActionGroup, toSignal } from '@dxos/plugin-graph';
|
|
35
28
|
import { ObservabilityAction } from '@dxos/plugin-observability/meta';
|
|
36
29
|
import { SpaceAction } from '@dxos/plugin-space';
|
|
37
30
|
import { ThreadType, MessageType, ChannelType } from '@dxos/plugin-space/types';
|
|
38
31
|
import {
|
|
39
32
|
getSpace,
|
|
40
33
|
getTextInRange,
|
|
41
|
-
Filter,
|
|
42
34
|
createDocAccessor,
|
|
43
35
|
fullyQualifiedId,
|
|
44
36
|
loadObjectReferences,
|
|
45
37
|
} from '@dxos/react-client/echo';
|
|
46
|
-
import { ScrollArea } from '@dxos/react-ui';
|
|
47
|
-
import { createAttendableAttributes } from '@dxos/react-ui-attention';
|
|
48
38
|
import { comments, createExternalCommentSync, listener } from '@dxos/react-ui-editor';
|
|
49
39
|
import { translations as threadTranslations } from '@dxos/react-ui-thread';
|
|
50
40
|
import { nonNullable } from '@dxos/util';
|
|
51
41
|
|
|
52
|
-
import {
|
|
53
|
-
|
|
54
|
-
ThreadSettings,
|
|
55
|
-
CommentsContainer,
|
|
56
|
-
CommentsHeading,
|
|
57
|
-
ChatContainer,
|
|
58
|
-
ChatHeading,
|
|
59
|
-
ThreadArticle,
|
|
60
|
-
} from './components';
|
|
42
|
+
import { ThreadMain, ThreadSettings, ChatContainer, ChatHeading, ThreadArticle } from './components';
|
|
43
|
+
import { ThreadComplementary } from './components/ThreadComplementary';
|
|
61
44
|
import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
|
|
62
45
|
import translations from './translations';
|
|
63
|
-
import { ThreadAction, type
|
|
46
|
+
import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
|
|
64
47
|
|
|
65
48
|
type ThreadState = {
|
|
66
49
|
/** An in-memory staging area for threads that are being drafted. */
|
|
@@ -73,6 +56,8 @@ type SubjectId = string;
|
|
|
73
56
|
const initialViewState = { showResolvedThreads: false };
|
|
74
57
|
type ViewStore = Record<SubjectId, typeof initialViewState>;
|
|
75
58
|
|
|
59
|
+
// TODO(Zan): More robust runtime check.
|
|
60
|
+
|
|
76
61
|
// TODO(Zan): Every instance of `cursor` should be replaced with `anchor`.
|
|
77
62
|
export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
78
63
|
const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
|
|
@@ -86,10 +71,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
86
71
|
return viewStore[subjectId];
|
|
87
72
|
};
|
|
88
73
|
|
|
89
|
-
let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
|
|
90
74
|
let navigationPlugin: Plugin<LocationProvides> | undefined;
|
|
91
75
|
let intentPlugin: Plugin<IntentPluginProvides> | undefined;
|
|
92
|
-
let dispatch: IntentDispatcher | undefined;
|
|
93
76
|
|
|
94
77
|
const unsubscribeCallbacks = [] as UnsubscribeCallback[];
|
|
95
78
|
|
|
@@ -98,45 +81,8 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
98
81
|
ready: async (plugins) => {
|
|
99
82
|
settings.prop({ key: 'standalone', type: LocalStorageStore.bool({ allowUndefined: true }) });
|
|
100
83
|
|
|
101
|
-
attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
|
|
102
84
|
navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
|
|
103
85
|
intentPlugin = resolvePlugin(plugins, parseIntentPlugin)!;
|
|
104
|
-
dispatch = intentPlugin?.provides.intent.dispatch;
|
|
105
|
-
|
|
106
|
-
const graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
|
|
107
|
-
const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
|
|
108
|
-
if (!client) {
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// TODO(wittjosiah): This is a hack to make standalone threads work in the c11y sidebar.
|
|
113
|
-
// This should have a better solution when deck is introduced.
|
|
114
|
-
const channelsQuery = client.spaces.query(Filter.schema(ChannelType));
|
|
115
|
-
const queryUnsubscribe = channelsQuery.subscribe();
|
|
116
|
-
const unsubscribe = effect(() => {
|
|
117
|
-
const attention = attentionPlugin?.provides.attention;
|
|
118
|
-
if (!attention?.attended) {
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
const firstAttendedNodeWithComments = Array.from(attention.attended)
|
|
123
|
-
.map((id) => graphPlugin?.provides.graph.findNode(id))
|
|
124
|
-
.find((node) => node?.data instanceof DocumentType && (node.data.threads?.length ?? 0) > 0);
|
|
125
|
-
|
|
126
|
-
if (firstAttendedNodeWithComments) {
|
|
127
|
-
void intentPlugin?.provides.intent.dispatch({
|
|
128
|
-
action: NavigationAction.OPEN,
|
|
129
|
-
data: {
|
|
130
|
-
activeParts: {
|
|
131
|
-
complementary: `${firstAttendedNodeWithComments.id}${SLUG_PATH_SEPARATOR}comments`,
|
|
132
|
-
},
|
|
133
|
-
},
|
|
134
|
-
});
|
|
135
|
-
}
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
unsubscribeCallbacks.push(queryUnsubscribe);
|
|
139
|
-
unsubscribeCallbacks.push(unsubscribe);
|
|
140
86
|
},
|
|
141
87
|
unload: async () => {
|
|
142
88
|
unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
|
|
@@ -147,8 +93,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
147
93
|
records: {
|
|
148
94
|
[ChannelType.typename]: {
|
|
149
95
|
placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
|
|
150
|
-
icon:
|
|
151
|
-
iconSymbol: 'ph--chat--regular',
|
|
96
|
+
icon: 'ph--chat--regular',
|
|
152
97
|
// TODO(wittjosiah): Move out of metadata.
|
|
153
98
|
loadReferences: (channel: ChannelType) => loadObjectReferences(channel, (channel) => channel.threads),
|
|
154
99
|
},
|
|
@@ -194,30 +139,38 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
194
139
|
return;
|
|
195
140
|
}
|
|
196
141
|
|
|
197
|
-
// TODO(Zan): Find util (or make one)
|
|
198
|
-
const
|
|
199
|
-
const [spaceId, objectId] =
|
|
142
|
+
// TODO(Zan): Find util (or make one).
|
|
143
|
+
const subjectId = id.split('~').at(0);
|
|
144
|
+
const [spaceId, objectId] = subjectId?.split(':') ?? [];
|
|
200
145
|
const space = client.spaces.get().find((space) => space.id === spaceId);
|
|
201
|
-
const
|
|
202
|
-
|
|
203
|
-
|
|
146
|
+
const object = toSignal(
|
|
147
|
+
(onChange) => {
|
|
148
|
+
const timeout = setTimeout(async () => {
|
|
149
|
+
await space?.db.loadObjectById(objectId);
|
|
150
|
+
onChange();
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
return () => clearTimeout(timeout);
|
|
154
|
+
},
|
|
155
|
+
() => space?.db.getObjectById(objectId),
|
|
156
|
+
);
|
|
157
|
+
if (!object || !subjectId) {
|
|
204
158
|
return;
|
|
205
159
|
}
|
|
206
160
|
|
|
207
|
-
const
|
|
208
|
-
const label =
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
// const label = docMeta.label?.(doc) || doc.name || docMeta.placeholder;
|
|
161
|
+
const meta = metadataResolver(getTypename(object) ?? '');
|
|
162
|
+
const label = meta.label?.(object) ||
|
|
163
|
+
object.name ||
|
|
164
|
+
meta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
|
|
212
165
|
|
|
213
|
-
const viewState = getViewState(
|
|
166
|
+
const viewState = getViewState(subjectId);
|
|
214
167
|
|
|
215
168
|
return {
|
|
216
169
|
id,
|
|
217
170
|
type: 'orphan-comments-for-subject',
|
|
218
|
-
data:
|
|
171
|
+
data: object,
|
|
219
172
|
properties: {
|
|
220
|
-
icon: meta.
|
|
173
|
+
icon: meta.icon,
|
|
221
174
|
label,
|
|
222
175
|
showResolvedThreads: viewState.showResolvedThreads,
|
|
223
176
|
},
|
|
@@ -225,7 +178,6 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
225
178
|
},
|
|
226
179
|
actions: ({ node }) => {
|
|
227
180
|
const dataId = node.id.split('~').at(0);
|
|
228
|
-
|
|
229
181
|
if (!node.id.endsWith('~comments') || !dataId) {
|
|
230
182
|
return;
|
|
231
183
|
}
|
|
@@ -254,6 +206,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
254
206
|
menuItemType: 'toggle',
|
|
255
207
|
isChecked: viewState.showResolvedThreads,
|
|
256
208
|
testId: 'threadPlugin.toggleShowResolved',
|
|
209
|
+
icon: viewState.showResolvedThreads ? 'ph--eye-slash--regular' : 'ph--eye--regular',
|
|
257
210
|
},
|
|
258
211
|
},
|
|
259
212
|
];
|
|
@@ -285,8 +238,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
285
238
|
},
|
|
286
239
|
properties: {
|
|
287
240
|
label: ['create channel label', { ns: THREAD_PLUGIN }],
|
|
288
|
-
icon:
|
|
289
|
-
iconSymbol: 'ph--chat--regular',
|
|
241
|
+
icon: 'ph--chat--regular',
|
|
290
242
|
testId: 'threadPlugin.createObject',
|
|
291
243
|
},
|
|
292
244
|
},
|
|
@@ -313,24 +265,12 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
313
265
|
case 'complementary': {
|
|
314
266
|
const location = navigationPlugin?.provides.location;
|
|
315
267
|
|
|
316
|
-
const providesThreadsConfig = useCallback(
|
|
317
|
-
(plugin: any): Plugin<ThreadProvides<any>> | undefined =>
|
|
318
|
-
// TODO(Zan): More robust runtime check.
|
|
319
|
-
'thread' in plugin.provides ? (plugin as Plugin<ThreadProvides<any>>) : undefined,
|
|
320
|
-
[],
|
|
321
|
-
);
|
|
322
|
-
|
|
323
|
-
const threadsIntegrators = useResolvePlugins(providesThreadsConfig);
|
|
324
|
-
const threadProvides = threadsIntegrators.map((p) => p.provides.thread);
|
|
325
|
-
|
|
326
268
|
if (data.object instanceof ChannelType && data.object.threads[0]) {
|
|
327
269
|
const channel = data.object;
|
|
328
270
|
// TODO(zan): Maybe we should have utility for positional main object ids.
|
|
329
271
|
if (isLayoutParts(location?.active) && location.active.main) {
|
|
330
272
|
const layoutEntries = location.active.main;
|
|
331
|
-
|
|
332
273
|
const currentPosition = layoutEntries.findIndex((entry) => channel.id === entry.id);
|
|
333
|
-
|
|
334
274
|
if (currentPosition > 0) {
|
|
335
275
|
const objectToTheLeft = layoutEntries[currentPosition - 1];
|
|
336
276
|
const context = getSpace(data.object)?.db.getObjectById(objectToTheLeft.id);
|
|
@@ -356,85 +296,17 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
356
296
|
'threads' in data.subject &&
|
|
357
297
|
Array.isArray(data.subject.threads)
|
|
358
298
|
) {
|
|
359
|
-
const
|
|
360
|
-
.concat(state.staging[fullyQualifiedId(data.subject)])
|
|
361
|
-
.filter(nonNullable);
|
|
362
|
-
|
|
363
|
-
const createSort = threadProvides.find((p) => p.predicate(data.subject))?.createSort;
|
|
364
|
-
if (createSort) {
|
|
365
|
-
const sort = createSort(data.subject);
|
|
366
|
-
threads.sort((a, b) => sort(a.anchor, b.anchor));
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
const detached = data.subject.threads
|
|
370
|
-
.filter(nonNullable)
|
|
371
|
-
.filter(({ anchor }) => !anchor)
|
|
372
|
-
.map((thread) => thread.id);
|
|
373
|
-
|
|
374
|
-
const qualifiedSubjectId = fullyQualifiedId(data.subject);
|
|
375
|
-
const attention = attentionPlugin?.provides.attention?.attended ?? new Set([qualifiedSubjectId]);
|
|
376
|
-
const attendableAttrs = createAttendableAttributes(qualifiedSubjectId);
|
|
377
|
-
const { showResolvedThreads } = getViewState(qualifiedSubjectId);
|
|
299
|
+
const { showResolvedThreads } = getViewState(fullyQualifiedId(data.subject));
|
|
378
300
|
|
|
379
301
|
return (
|
|
380
|
-
<
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
autoFocusCurrentTextbox={state.focus}
|
|
389
|
-
showResolvedThreads={showResolvedThreads}
|
|
390
|
-
onThreadAttend={(thread) => {
|
|
391
|
-
if (state.current !== thread.id) {
|
|
392
|
-
state.current = thread.id;
|
|
393
|
-
void dispatch?.({
|
|
394
|
-
action: LayoutAction.SCROLL_INTO_VIEW,
|
|
395
|
-
data: {
|
|
396
|
-
id: fullyQualifiedId(data.subject!),
|
|
397
|
-
thread: fullyQualifiedId(thread),
|
|
398
|
-
cursor: thread.anchor,
|
|
399
|
-
},
|
|
400
|
-
});
|
|
401
|
-
}
|
|
402
|
-
}}
|
|
403
|
-
onThreadDelete={(thread) =>
|
|
404
|
-
dispatch?.({
|
|
405
|
-
plugin: THREAD_PLUGIN,
|
|
406
|
-
action: ThreadAction.DELETE,
|
|
407
|
-
data: { subject: data.subject, thread },
|
|
408
|
-
})
|
|
409
|
-
}
|
|
410
|
-
onMessageDelete={(thread, messageId) =>
|
|
411
|
-
dispatch?.({
|
|
412
|
-
plugin: THREAD_PLUGIN,
|
|
413
|
-
action: ThreadAction.DELETE_MESSAGE,
|
|
414
|
-
data: { subject: data.subject, thread, messageId },
|
|
415
|
-
})
|
|
416
|
-
}
|
|
417
|
-
onThreadToggleResolved={(thread) =>
|
|
418
|
-
dispatch?.({
|
|
419
|
-
plugin: THREAD_PLUGIN,
|
|
420
|
-
action: ThreadAction.TOGGLE_RESOLVED,
|
|
421
|
-
data: { thread },
|
|
422
|
-
})
|
|
423
|
-
}
|
|
424
|
-
onComment={(thread) => {
|
|
425
|
-
void intentPlugin?.provides.intent.dispatch({
|
|
426
|
-
action: ThreadAction.ON_MESSAGE_ADD,
|
|
427
|
-
data: { thread, subject: data.subject },
|
|
428
|
-
});
|
|
429
|
-
}}
|
|
430
|
-
/>
|
|
431
|
-
<div role='none' className='bs-10' />
|
|
432
|
-
<ScrollArea.Scrollbar>
|
|
433
|
-
<ScrollArea.Thumb />
|
|
434
|
-
</ScrollArea.Scrollbar>
|
|
435
|
-
</ScrollArea.Viewport>
|
|
436
|
-
</ScrollArea.Root>
|
|
437
|
-
</div>
|
|
302
|
+
<ThreadComplementary
|
|
303
|
+
role={role}
|
|
304
|
+
subject={data.subject}
|
|
305
|
+
stagedThreads={state.staging[fullyQualifiedId(data.subject)]}
|
|
306
|
+
current={state.current}
|
|
307
|
+
focus
|
|
308
|
+
showResolvedThreads={showResolvedThreads}
|
|
309
|
+
/>
|
|
438
310
|
);
|
|
439
311
|
}
|
|
440
312
|
}
|
|
@@ -450,9 +322,19 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
450
322
|
if (intent.data && intent.data.cursor !== undefined) {
|
|
451
323
|
const { cursor, name, subject } = intent.data;
|
|
452
324
|
|
|
325
|
+
// Seed the threads array if it does not exist.
|
|
326
|
+
if (subject?.threads === undefined) {
|
|
327
|
+
try {
|
|
328
|
+
// Static schema will throw an error if subject does not support threads array property.
|
|
329
|
+
subject.threads = [];
|
|
330
|
+
} catch (e) {
|
|
331
|
+
log.error('Subject does not support threads array', subject?.typename);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
453
336
|
const subjectId = fullyQualifiedId(subject);
|
|
454
337
|
const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
|
|
455
|
-
|
|
456
338
|
const stagingArea = state.staging[subjectId];
|
|
457
339
|
if (stagingArea) {
|
|
458
340
|
stagingArea.push(thread);
|
|
@@ -464,17 +346,9 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
464
346
|
data: thread,
|
|
465
347
|
intents: [
|
|
466
348
|
[
|
|
467
|
-
{
|
|
468
|
-
action: NavigationAction.OPEN,
|
|
469
|
-
data: {
|
|
470
|
-
activeParts: {
|
|
471
|
-
complementary: `${subjectId}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
|
|
472
|
-
},
|
|
473
|
-
},
|
|
474
|
-
},
|
|
475
349
|
{
|
|
476
350
|
action: ThreadAction.SELECT,
|
|
477
|
-
data: { current: thread
|
|
351
|
+
data: { current: fullyQualifiedId(thread), focus: true },
|
|
478
352
|
},
|
|
479
353
|
{
|
|
480
354
|
action: LayoutAction.SET_LAYOUT,
|
|
@@ -528,7 +402,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
528
402
|
|
|
529
403
|
case ThreadAction.DELETE: {
|
|
530
404
|
const { subject, thread } = intent.data ?? {};
|
|
531
|
-
if (
|
|
405
|
+
if (
|
|
406
|
+
!(thread instanceof ThreadType) ||
|
|
407
|
+
!(subject && typeof subject === 'object' && 'threads' in subject)
|
|
408
|
+
) {
|
|
532
409
|
return;
|
|
533
410
|
}
|
|
534
411
|
|
|
@@ -540,7 +417,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
540
417
|
const index = stagingArea.findIndex((t) => t.id === thread.id);
|
|
541
418
|
if (index !== -1) {
|
|
542
419
|
stagingArea.splice(index, 1);
|
|
543
|
-
return;
|
|
420
|
+
return { data: true };
|
|
544
421
|
}
|
|
545
422
|
}
|
|
546
423
|
|
|
@@ -550,7 +427,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
550
427
|
}
|
|
551
428
|
|
|
552
429
|
if (!intent.undo) {
|
|
553
|
-
const index = subject.threads.findIndex((t) => t?.id === thread.id);
|
|
430
|
+
const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
|
|
554
431
|
const cursor = subject.threads[index]?.anchor;
|
|
555
432
|
if (index !== -1) {
|
|
556
433
|
subject.threads?.splice(index, 1);
|
|
@@ -596,7 +473,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
596
473
|
const { thread, subject } = intent.data ?? {};
|
|
597
474
|
if (
|
|
598
475
|
!(thread instanceof ThreadType) ||
|
|
599
|
-
!(subject && typeof subject === 'object' && 'threads' in subject)
|
|
476
|
+
!(subject && typeof subject === 'object' && 'threads' in subject && Array.isArray(subject.threads))
|
|
600
477
|
) {
|
|
601
478
|
return;
|
|
602
479
|
}
|
|
@@ -636,7 +513,10 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
636
513
|
const { subject, thread, messageId } = intent.data ?? {};
|
|
637
514
|
const space = getSpace(subject);
|
|
638
515
|
|
|
639
|
-
if (
|
|
516
|
+
if (
|
|
517
|
+
!(thread instanceof ThreadType) ||
|
|
518
|
+
!(subject && typeof subject === 'object' && 'threads' in subject)
|
|
519
|
+
) {
|
|
640
520
|
return;
|
|
641
521
|
}
|
|
642
522
|
|
|
@@ -667,7 +547,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
667
547
|
action: ObservabilityAction.SEND_EVENT,
|
|
668
548
|
data: {
|
|
669
549
|
name: 'threads.message.delete',
|
|
670
|
-
properties: { threadId: thread.id, spaceId: space
|
|
550
|
+
properties: { threadId: thread.id, spaceId: space?.id },
|
|
671
551
|
},
|
|
672
552
|
},
|
|
673
553
|
],
|
|
@@ -691,7 +571,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
691
571
|
action: ObservabilityAction.SEND_EVENT,
|
|
692
572
|
data: {
|
|
693
573
|
name: 'threads.message.undo-delete',
|
|
694
|
-
properties: { threadId: thread.id, spaceId: space
|
|
574
|
+
properties: { threadId: thread.id, spaceId: space?.id },
|
|
695
575
|
},
|
|
696
576
|
},
|
|
697
577
|
],
|
|
@@ -742,7 +622,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
742
622
|
() =>
|
|
743
623
|
threads.value
|
|
744
624
|
.filter((thread) => thread?.anchor)
|
|
745
|
-
.map((thread) => ({ id: thread
|
|
625
|
+
.map((thread) => ({ id: fullyQualifiedId(thread), cursor: thread.anchor! })),
|
|
746
626
|
),
|
|
747
627
|
comments({
|
|
748
628
|
id: doc.id,
|
|
@@ -763,7 +643,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
763
643
|
// If the thread is in the staging area, remove it.
|
|
764
644
|
const stagingArea = state.staging[fullyQualifiedId(doc)];
|
|
765
645
|
if (stagingArea) {
|
|
766
|
-
const index = stagingArea.findIndex((thread) => thread
|
|
646
|
+
const index = stagingArea.findIndex((thread) => fullyQualifiedId(thread) === id);
|
|
767
647
|
if (index !== -1) {
|
|
768
648
|
stagingArea.splice(index, 1);
|
|
769
649
|
}
|
|
@@ -776,7 +656,7 @@ export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
|
776
656
|
},
|
|
777
657
|
onUpdate: ({ id, cursor }) => {
|
|
778
658
|
const thread =
|
|
779
|
-
state.staging[fullyQualifiedId(doc)]?.find((thread) => thread
|
|
659
|
+
state.staging[fullyQualifiedId(doc)]?.find((thread) => fullyQualifiedId(thread) === id) ??
|
|
780
660
|
doc.threads.find((thread) => thread?.id === id);
|
|
781
661
|
|
|
782
662
|
if (thread instanceof ThreadType && thread.anchor) {
|
|
@@ -15,7 +15,7 @@ import { useIdentity } from '@dxos/react-client/halo';
|
|
|
15
15
|
import { ClientRepeater } from '@dxos/react-client/testing';
|
|
16
16
|
import { Mosaic } from '@dxos/react-ui-mosaic';
|
|
17
17
|
import { Thread } from '@dxos/react-ui-thread';
|
|
18
|
-
import { withTheme } from '@dxos/storybook-utils';
|
|
18
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
19
19
|
|
|
20
20
|
import { ChatContainer } from './ChatContainer';
|
|
21
21
|
import { createChatThread } from './testing';
|
|
@@ -69,8 +69,8 @@ export default {
|
|
|
69
69
|
component: Thread,
|
|
70
70
|
// TODO(wittjosiah): Register schemas.
|
|
71
71
|
render: () => <ClientRepeater component={Story} createIdentity createSpace types={[ThreadType]} />,
|
|
72
|
-
decorators: [withTheme],
|
|
73
|
-
parameters: { translations
|
|
72
|
+
decorators: [withTheme, withLayout({ fullscreen: true })],
|
|
73
|
+
parameters: { translations },
|
|
74
74
|
};
|
|
75
75
|
|
|
76
76
|
export const Default = {};
|
|
@@ -2,15 +2,14 @@
|
|
|
2
2
|
// Copyright 2023 DXOS.org
|
|
3
3
|
//
|
|
4
4
|
|
|
5
|
-
import { Chat } from '@phosphor-icons/react';
|
|
6
5
|
import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
|
|
7
6
|
|
|
8
7
|
import { create } from '@dxos/echo-schema';
|
|
9
8
|
import { MessageType } from '@dxos/plugin-space/types';
|
|
10
|
-
import { getSpace, useMembers } from '@dxos/react-client/echo';
|
|
9
|
+
import { fullyQualifiedId, getSpace, useMembers } from '@dxos/react-client/echo';
|
|
11
10
|
import { useIdentity } from '@dxos/react-client/halo';
|
|
12
|
-
import { ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
|
|
13
|
-
import { PlankHeading
|
|
11
|
+
import { Icon, ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
|
|
12
|
+
import { PlankHeading } from '@dxos/react-ui-deck';
|
|
14
13
|
import { createBasicExtensions, createThemeExtensions, listener } from '@dxos/react-ui-editor';
|
|
15
14
|
import { mx } from '@dxos/react-ui-theme';
|
|
16
15
|
import { MessageTextbox, type MessageTextboxProps, Thread, ThreadFooter, threadLayout } from '@dxos/react-ui-thread';
|
|
@@ -28,7 +27,7 @@ export const ChatHeading = ({ attendableId }: { attendableId?: string }) => {
|
|
|
28
27
|
return (
|
|
29
28
|
<div role='none' className='flex items-center'>
|
|
30
29
|
<PlankHeading.Button attendableId={attendableId}>
|
|
31
|
-
<
|
|
30
|
+
<Icon icon='ph--chat--regular' size={5} />
|
|
32
31
|
</PlankHeading.Button>
|
|
33
32
|
<PlankHeading.Label attendableId={attendableId}>{t('chat heading')}</PlankHeading.Label>
|
|
34
33
|
</div>
|
|
@@ -39,7 +38,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
|
|
|
39
38
|
const identity = useIdentity()!;
|
|
40
39
|
const space = getSpace(thread);
|
|
41
40
|
const members = useMembers(space?.key);
|
|
42
|
-
const activity = useStatus(space, thread
|
|
41
|
+
const activity = useStatus(space, fullyQualifiedId(thread));
|
|
43
42
|
const { t } = useTranslation(THREAD_PLUGIN);
|
|
44
43
|
// TODO(wittjosiah): This is a hack to reset the editor after a message is sent.
|
|
45
44
|
const [_count, _setCount] = useState(0);
|
|
@@ -48,7 +47,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
|
|
|
48
47
|
const threadScrollRef = useRef<HTMLDivElement | null>(null);
|
|
49
48
|
const { themeMode } = useThemeContext();
|
|
50
49
|
|
|
51
|
-
const textboxMetadata = getMessageMetadata(thread
|
|
50
|
+
const textboxMetadata = getMessageMetadata(fullyQualifiedId(thread), identity);
|
|
52
51
|
const messageRef = useRef('');
|
|
53
52
|
const extensions = useMemo(
|
|
54
53
|
() => [
|
|
@@ -103,7 +102,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
|
|
|
103
102
|
return (
|
|
104
103
|
<Thread
|
|
105
104
|
current={current}
|
|
106
|
-
id={thread
|
|
105
|
+
id={fullyQualifiedId(thread)}
|
|
107
106
|
classNames='bs-full grid-rows-[1fr_min-content_min-content] overflow-hidden transition-[padding-block-end] [[data-sidebar-inline-start-state=open]_&]:lg:pbe-0'
|
|
108
107
|
>
|
|
109
108
|
<ScrollArea.Root classNames='col-span-2'>
|
|
@@ -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,12 +8,10 @@ import React, { useEffect, useState } from 'react';
|
|
|
8
8
|
|
|
9
9
|
import { MessageType, ThreadType } from '@dxos/plugin-space/types';
|
|
10
10
|
import { faker } from '@dxos/random';
|
|
11
|
-
import {
|
|
12
|
-
import { Filter, useQuery, useSpace } from '@dxos/react-client/echo';
|
|
11
|
+
import { Filter, fullyQualifiedId, useQuery, useSpace } from '@dxos/react-client/echo';
|
|
13
12
|
import { useIdentity } from '@dxos/react-client/halo';
|
|
14
|
-
import { ClientRepeater } from '@dxos/react-client/testing';
|
|
15
|
-
import {
|
|
16
|
-
import { withFullscreen, withTheme } from '@dxos/storybook-utils';
|
|
13
|
+
import { type ClientRepeatedComponentProps, ClientRepeater } from '@dxos/react-client/testing';
|
|
14
|
+
import { withLayout, withTheme } from '@dxos/storybook-utils';
|
|
17
15
|
|
|
18
16
|
import { CommentsContainer } from './CommentsContainer';
|
|
19
17
|
import { createCommentThread } from './testing';
|
|
@@ -21,7 +19,7 @@ import translations from '../translations';
|
|
|
21
19
|
|
|
22
20
|
faker.seed(1);
|
|
23
21
|
|
|
24
|
-
const Story = ({ spaceKey }:
|
|
22
|
+
const Story = ({ spaceKey }: ClientRepeatedComponentProps) => {
|
|
25
23
|
const identity = useIdentity();
|
|
26
24
|
const space = useSpace(spaceKey);
|
|
27
25
|
const threads = useQuery(space, Filter.schema(ThreadType));
|
|
@@ -32,7 +30,7 @@ const Story = ({ spaceKey }: { spaceKey: PublicKey }) => {
|
|
|
32
30
|
const t = setTimeout(async () => {
|
|
33
31
|
space.db.add(createCommentThread(identity));
|
|
34
32
|
const thread = space.db.add(createCommentThread(identity));
|
|
35
|
-
setDetached([thread
|
|
33
|
+
setDetached([fullyQualifiedId(thread)]);
|
|
36
34
|
});
|
|
37
35
|
|
|
38
36
|
return () => clearTimeout(t);
|
|
@@ -43,15 +41,12 @@ const Story = ({ spaceKey }: { spaceKey: PublicKey }) => {
|
|
|
43
41
|
return null;
|
|
44
42
|
}
|
|
45
43
|
|
|
46
|
-
// TODO(wittjosiah): Include Tooltip.Provider in `withTheme` decorator?
|
|
47
44
|
return (
|
|
48
|
-
<
|
|
49
|
-
<div className='flex
|
|
50
|
-
<
|
|
51
|
-
<CommentsContainer threads={threads} detached={detached} onThreadDelete={console.log} />
|
|
52
|
-
</div>
|
|
45
|
+
<div className='flex justify-center overflow-y-auto bg-white dark:bg-black'>
|
|
46
|
+
<div className='flex flex-col w-[400px]'>
|
|
47
|
+
<CommentsContainer threads={threads} detached={detached} onThreadDelete={console.log} />
|
|
53
48
|
</div>
|
|
54
|
-
</
|
|
49
|
+
</div>
|
|
55
50
|
);
|
|
56
51
|
};
|
|
57
52
|
|
|
@@ -59,7 +54,7 @@ export default {
|
|
|
59
54
|
title: 'plugin-thread/Comments',
|
|
60
55
|
// TODO(wittjosiah): Register schemas.
|
|
61
56
|
render: () => <ClientRepeater component={Story} createIdentity createSpace types={[ThreadType, MessageType]} />,
|
|
62
|
-
decorators: [withTheme,
|
|
57
|
+
decorators: [withTheme, withLayout({ fullscreen: true, tooltips: true })],
|
|
63
58
|
parameters: { translations },
|
|
64
59
|
};
|
|
65
60
|
|