@dxos/plugin-thread 0.6.8-main.046e6cf
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/LICENSE +8 -0
- package/README.md +15 -0
- package/dist/lib/browser/ThreadArticle-J4VCLPMS.mjs +18 -0
- package/dist/lib/browser/ThreadArticle-J4VCLPMS.mjs.map +7 -0
- package/dist/lib/browser/ThreadMain-VV7MGQZJ.mjs +26 -0
- package/dist/lib/browser/ThreadMain-VV7MGQZJ.mjs.map +7 -0
- package/dist/lib/browser/chunk-5IBJO53M.mjs +21 -0
- package/dist/lib/browser/chunk-5IBJO53M.mjs.map +7 -0
- package/dist/lib/browser/chunk-CQ2RYETH.mjs +410 -0
- package/dist/lib/browser/chunk-CQ2RYETH.mjs.map +7 -0
- package/dist/lib/browser/chunk-TL3JITN7.mjs +19 -0
- package/dist/lib/browser/chunk-TL3JITN7.mjs.map +7 -0
- package/dist/lib/browser/index.mjs +1062 -0
- package/dist/lib/browser/index.mjs.map +7 -0
- package/dist/lib/browser/meta.json +1 -0
- package/dist/lib/browser/meta.mjs +11 -0
- package/dist/lib/browser/meta.mjs.map +7 -0
- package/dist/lib/browser/types.mjs +8 -0
- package/dist/lib/browser/types.mjs.map +7 -0
- package/dist/lib/node/ThreadArticle-CSENFNNT.cjs +44 -0
- package/dist/lib/node/ThreadArticle-CSENFNNT.cjs.map +7 -0
- package/dist/lib/node/ThreadMain-XP4YSF7U.cjs +52 -0
- package/dist/lib/node/ThreadMain-XP4YSF7U.cjs.map +7 -0
- package/dist/lib/node/chunk-3VAAUEU5.cjs +431 -0
- package/dist/lib/node/chunk-3VAAUEU5.cjs.map +7 -0
- package/dist/lib/node/chunk-BWBONZGU.cjs +55 -0
- package/dist/lib/node/chunk-BWBONZGU.cjs.map +7 -0
- package/dist/lib/node/chunk-WPPLQNMJ.cjs +38 -0
- package/dist/lib/node/chunk-WPPLQNMJ.cjs.map +7 -0
- package/dist/lib/node/index.cjs +1067 -0
- package/dist/lib/node/index.cjs.map +7 -0
- package/dist/lib/node/meta.cjs +32 -0
- package/dist/lib/node/meta.cjs.map +7 -0
- package/dist/lib/node/meta.json +1 -0
- package/dist/lib/node/types.cjs +30 -0
- package/dist/lib/node/types.cjs.map +7 -0
- package/dist/types/src/ThreadPlugin.d.ts +4 -0
- package/dist/types/src/ThreadPlugin.d.ts.map +1 -0
- package/dist/types/src/components/Chat.stories.d.ts +46 -0
- package/dist/types/src/components/Chat.stories.d.ts.map +1 -0
- package/dist/types/src/components/ChatContainer.d.ts +7 -0
- package/dist/types/src/components/ChatContainer.d.ts.map +1 -0
- package/dist/types/src/components/CommentContainer.d.ts +4 -0
- package/dist/types/src/components/CommentContainer.d.ts.map +1 -0
- package/dist/types/src/components/Comments.stories.d.ts +44 -0
- package/dist/types/src/components/Comments.stories.d.ts.map +1 -0
- package/dist/types/src/components/CommentsContainer.d.ts +26 -0
- package/dist/types/src/components/CommentsContainer.d.ts.map +1 -0
- package/dist/types/src/components/MessageContainer.d.ts +9 -0
- package/dist/types/src/components/MessageContainer.d.ts.map +1 -0
- package/dist/types/src/components/ThreadArticle.d.ts +5 -0
- package/dist/types/src/components/ThreadArticle.d.ts.map +1 -0
- package/dist/types/src/components/ThreadMain.d.ts +7 -0
- package/dist/types/src/components/ThreadMain.d.ts.map +1 -0
- package/dist/types/src/components/ThreadSettings.d.ts +6 -0
- package/dist/types/src/components/ThreadSettings.d.ts.map +1 -0
- package/dist/types/src/components/command-extension.d.ts +3 -0
- package/dist/types/src/components/command-extension.d.ts.map +1 -0
- package/dist/types/src/components/index.d.ts +22 -0
- package/dist/types/src/components/index.d.ts.map +1 -0
- package/dist/types/src/components/testing.d.ts +67 -0
- package/dist/types/src/components/testing.d.ts.map +1 -0
- package/dist/types/src/components/types.d.ts +28 -0
- package/dist/types/src/components/types.d.ts.map +1 -0
- package/dist/types/src/hooks/analytics.d.ts +4 -0
- package/dist/types/src/hooks/analytics.d.ts.map +1 -0
- package/dist/types/src/hooks/index.d.ts +3 -0
- package/dist/types/src/hooks/index.d.ts.map +1 -0
- package/dist/types/src/hooks/useStatus.d.ts +3 -0
- package/dist/types/src/hooks/useStatus.d.ts.map +1 -0
- package/dist/types/src/index.d.ts +6 -0
- package/dist/types/src/index.d.ts.map +1 -0
- package/dist/types/src/meta.d.ts +16 -0
- package/dist/types/src/meta.d.ts.map +1 -0
- package/dist/types/src/translations.d.ts +34 -0
- package/dist/types/src/translations.d.ts.map +1 -0
- package/dist/types/src/types.d.ts +19 -0
- package/dist/types/src/types.d.ts.map +1 -0
- package/dist/types/src/types.test.d.ts +2 -0
- package/dist/types/src/types.test.d.ts.map +1 -0
- package/dist/types/src/util.d.ts +6 -0
- package/dist/types/src/util.d.ts.map +1 -0
- package/package.json +99 -0
- package/src/ThreadPlugin.tsx +744 -0
- package/src/components/Chat.stories.tsx +76 -0
- package/src/components/ChatContainer.tsx +127 -0
- package/src/components/CommentContainer.tsx +206 -0
- package/src/components/Comments.stories.tsx +66 -0
- package/src/components/CommentsContainer.tsx +114 -0
- package/src/components/MessageContainer.tsx +200 -0
- package/src/components/ThreadArticle.tsx +18 -0
- package/src/components/ThreadMain.tsx +27 -0
- package/src/components/ThreadSettings.tsx +23 -0
- package/src/components/command-extension.ts +36 -0
- package/src/components/index.ts +16 -0
- package/src/components/testing.ts +46 -0
- package/src/components/types.ts +32 -0
- package/src/hooks/analytics.ts +39 -0
- package/src/hooks/index.ts +6 -0
- package/src/hooks/useStatus.ts +39 -0
- package/src/index.ts +12 -0
- package/src/meta.tsx +20 -0
- package/src/translations.ts +41 -0
- package/src/types.test.ts +13 -0
- package/src/types.ts +41 -0
- package/src/util.ts +26 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2023 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Chat, type IconProps } from '@phosphor-icons/react';
|
|
6
|
+
import { computed, effect } from '@preact/signals-core';
|
|
7
|
+
import React from 'react';
|
|
8
|
+
|
|
9
|
+
import {
|
|
10
|
+
type IntentPluginProvides,
|
|
11
|
+
LayoutAction,
|
|
12
|
+
NavigationAction,
|
|
13
|
+
type LocationProvides,
|
|
14
|
+
type Plugin,
|
|
15
|
+
type PluginDefinition,
|
|
16
|
+
parseIntentPlugin,
|
|
17
|
+
parseNavigationPlugin,
|
|
18
|
+
resolvePlugin,
|
|
19
|
+
parseGraphPlugin,
|
|
20
|
+
SLUG_PATH_SEPARATOR,
|
|
21
|
+
SLUG_COLLECTION_INDICATOR,
|
|
22
|
+
isLayoutParts,
|
|
23
|
+
parseMetadataResolverPlugin,
|
|
24
|
+
type IntentDispatcher,
|
|
25
|
+
} from '@dxos/app-framework';
|
|
26
|
+
import { type UnsubscribeCallback } from '@dxos/async';
|
|
27
|
+
import { type EchoReactiveObject, getTypename } from '@dxos/echo-schema';
|
|
28
|
+
import { create } from '@dxos/echo-schema';
|
|
29
|
+
import { LocalStorageStore } from '@dxos/local-storage';
|
|
30
|
+
import { type AttentionPluginProvides, parseAttentionPlugin } from '@dxos/plugin-attention';
|
|
31
|
+
import { parseClientPlugin } from '@dxos/plugin-client';
|
|
32
|
+
import { type ActionGroup, createExtension, isActionGroup } from '@dxos/plugin-graph';
|
|
33
|
+
import { DocumentType } from '@dxos/plugin-markdown/types';
|
|
34
|
+
import { ObservabilityAction } from '@dxos/plugin-observability/meta';
|
|
35
|
+
import { SpaceAction } from '@dxos/plugin-space';
|
|
36
|
+
import { ThreadType, MessageType, ChannelType } from '@dxos/plugin-space/types';
|
|
37
|
+
import {
|
|
38
|
+
getSpace,
|
|
39
|
+
getTextInRange,
|
|
40
|
+
Filter,
|
|
41
|
+
createDocAccessor,
|
|
42
|
+
fullyQualifiedId,
|
|
43
|
+
getRangeFromCursor,
|
|
44
|
+
loadObjectReferences,
|
|
45
|
+
} from '@dxos/react-client/echo';
|
|
46
|
+
import { ScrollArea } from '@dxos/react-ui';
|
|
47
|
+
import { createAttendableAttributes } from '@dxos/react-ui-attention';
|
|
48
|
+
import { comments, createExternalCommentSync, listener } from '@dxos/react-ui-editor';
|
|
49
|
+
import { translations as threadTranslations } from '@dxos/react-ui-thread';
|
|
50
|
+
import { nonNullable } from '@dxos/util';
|
|
51
|
+
|
|
52
|
+
import {
|
|
53
|
+
ThreadMain,
|
|
54
|
+
ThreadSettings,
|
|
55
|
+
CommentsContainer,
|
|
56
|
+
CommentsHeading,
|
|
57
|
+
ChatContainer,
|
|
58
|
+
ChatHeading,
|
|
59
|
+
ThreadArticle,
|
|
60
|
+
} from './components';
|
|
61
|
+
import meta, { THREAD_ITEM, THREAD_PLUGIN } from './meta';
|
|
62
|
+
import translations from './translations';
|
|
63
|
+
import { ThreadAction, type ThreadPluginProvides, type ThreadSettingsProps } from './types';
|
|
64
|
+
|
|
65
|
+
type ThreadState = {
|
|
66
|
+
staging: Record<string, ThreadType[]>;
|
|
67
|
+
current?: string | undefined;
|
|
68
|
+
focus?: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
type SubjectId = string;
|
|
72
|
+
const initialViewState = { showResolvedThreads: false };
|
|
73
|
+
type ViewStore = Record<SubjectId, typeof initialViewState>;
|
|
74
|
+
|
|
75
|
+
export const ThreadPlugin = (): PluginDefinition<ThreadPluginProvides> => {
|
|
76
|
+
const settings = new LocalStorageStore<ThreadSettingsProps>(THREAD_PLUGIN);
|
|
77
|
+
const state = create<ThreadState>({ staging: {} });
|
|
78
|
+
|
|
79
|
+
const viewStore = create<ViewStore>({});
|
|
80
|
+
const getViewState = (subjectId: string) => {
|
|
81
|
+
if (!viewStore[subjectId]) {
|
|
82
|
+
viewStore[subjectId] = { ...initialViewState };
|
|
83
|
+
}
|
|
84
|
+
return viewStore[subjectId];
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
let attentionPlugin: Plugin<AttentionPluginProvides> | undefined;
|
|
88
|
+
let navigationPlugin: Plugin<LocationProvides> | undefined;
|
|
89
|
+
let intentPlugin: Plugin<IntentPluginProvides> | undefined;
|
|
90
|
+
let dispatch: IntentDispatcher | undefined;
|
|
91
|
+
|
|
92
|
+
const unsubscribeCallbacks = [] as UnsubscribeCallback[];
|
|
93
|
+
|
|
94
|
+
return {
|
|
95
|
+
meta,
|
|
96
|
+
ready: async (plugins) => {
|
|
97
|
+
settings.prop({ key: 'standalone', type: LocalStorageStore.bool({ allowUndefined: true }) });
|
|
98
|
+
|
|
99
|
+
attentionPlugin = resolvePlugin(plugins, parseAttentionPlugin);
|
|
100
|
+
navigationPlugin = resolvePlugin(plugins, parseNavigationPlugin);
|
|
101
|
+
intentPlugin = resolvePlugin(plugins, parseIntentPlugin)!;
|
|
102
|
+
dispatch = intentPlugin?.provides.intent.dispatch;
|
|
103
|
+
|
|
104
|
+
const graphPlugin = resolvePlugin(plugins, parseGraphPlugin);
|
|
105
|
+
const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
|
|
106
|
+
if (!client) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// TODO(wittjosiah): This is a hack to make standalone threads work in the c11y sidebar.
|
|
111
|
+
// This should have a better solution when deck is introduced.
|
|
112
|
+
const channelsQuery = client.spaces.query(Filter.schema(ChannelType));
|
|
113
|
+
const queryUnsubscribe = channelsQuery.subscribe();
|
|
114
|
+
const unsubscribe = effect(() => {
|
|
115
|
+
const attention = attentionPlugin?.provides.attention;
|
|
116
|
+
if (!attention?.attended) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const firstAttendedNodeWithComments = Array.from(attention.attended)
|
|
121
|
+
.map((id) => graphPlugin?.provides.graph.findNode(id))
|
|
122
|
+
.find((node) => node?.data instanceof DocumentType && (node.data.threads?.length ?? 0) > 0);
|
|
123
|
+
|
|
124
|
+
if (firstAttendedNodeWithComments) {
|
|
125
|
+
void intentPlugin?.provides.intent.dispatch({
|
|
126
|
+
action: NavigationAction.OPEN,
|
|
127
|
+
data: {
|
|
128
|
+
activeParts: {
|
|
129
|
+
complementary: `${firstAttendedNodeWithComments.id}${SLUG_PATH_SEPARATOR}comments`,
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
unsubscribeCallbacks.push(queryUnsubscribe);
|
|
137
|
+
unsubscribeCallbacks.push(unsubscribe);
|
|
138
|
+
},
|
|
139
|
+
unload: async () => {
|
|
140
|
+
unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe());
|
|
141
|
+
},
|
|
142
|
+
provides: {
|
|
143
|
+
settings: settings.values,
|
|
144
|
+
metadata: {
|
|
145
|
+
records: {
|
|
146
|
+
[ChannelType.typename]: {
|
|
147
|
+
placeholder: ['channel title placeholder', { ns: THREAD_PLUGIN }],
|
|
148
|
+
icon: (props: IconProps) => <Chat {...props} />,
|
|
149
|
+
iconSymbol: 'ph--chat--regular',
|
|
150
|
+
// TODO(wittjosiah): Move out of metadata.
|
|
151
|
+
loadReferences: (channel: ChannelType) => loadObjectReferences(channel, (channel) => channel.threads),
|
|
152
|
+
},
|
|
153
|
+
[ThreadType.typename]: {
|
|
154
|
+
// TODO(wittjosiah): Move out of metadata.
|
|
155
|
+
loadReferences: (thread: ThreadType) => loadObjectReferences(thread, (thread) => thread.messages),
|
|
156
|
+
},
|
|
157
|
+
[MessageType.typename]: {
|
|
158
|
+
// TODO(wittjosiah): Move out of metadata.
|
|
159
|
+
loadReferences: (message: MessageType) => [], // loadObjectReferences(message, (message) => [...message.parts, message.context]),
|
|
160
|
+
},
|
|
161
|
+
[THREAD_ITEM]: {
|
|
162
|
+
parse: (item: EchoReactiveObject<any>, type: string) => {
|
|
163
|
+
switch (type) {
|
|
164
|
+
case 'node':
|
|
165
|
+
return { id: item.id, label: item.title, data: item };
|
|
166
|
+
case 'object':
|
|
167
|
+
return item;
|
|
168
|
+
case 'view-object':
|
|
169
|
+
return { id: `${item.id}-view`, object: item };
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
},
|
|
174
|
+
},
|
|
175
|
+
translations: [...translations, ...threadTranslations],
|
|
176
|
+
echo: { schema: [ChannelType, ThreadType, MessageType] },
|
|
177
|
+
graph: {
|
|
178
|
+
builder: (plugins) => {
|
|
179
|
+
const client = resolvePlugin(plugins, parseClientPlugin)?.provides.client;
|
|
180
|
+
const dispatch = resolvePlugin(plugins, parseIntentPlugin)?.provides.intent.dispatch;
|
|
181
|
+
const metadataResolver = resolvePlugin(plugins, parseMetadataResolverPlugin)?.provides.metadata.resolver;
|
|
182
|
+
|
|
183
|
+
if (!client || !dispatch || !metadataResolver) {
|
|
184
|
+
return [];
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return [
|
|
188
|
+
createExtension({
|
|
189
|
+
id: `${THREAD_PLUGIN}/comments-for-subject`,
|
|
190
|
+
resolver: ({ id }) => {
|
|
191
|
+
if (!id.endsWith('~comments')) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
// TODO(Zan): Find util (or make one)
|
|
196
|
+
const docId = id.split('~').at(0);
|
|
197
|
+
const [spaceId, objectId] = docId?.split(':') ?? [];
|
|
198
|
+
const space = client.spaces.get().find((space) => space.id === spaceId);
|
|
199
|
+
const doc = space?.db.getObjectById(objectId);
|
|
200
|
+
|
|
201
|
+
if (!doc || !docId) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const docMeta = metadataResolver(getTypename(doc) ?? '');
|
|
206
|
+
const label = docMeta.label?.(doc) ||
|
|
207
|
+
doc.name ||
|
|
208
|
+
docMeta.placeholder || ['unnamed object threads label', { ns: THREAD_PLUGIN }];
|
|
209
|
+
// const label = docMeta.label?.(doc) || doc.name || docMeta.placeholder;
|
|
210
|
+
|
|
211
|
+
const viewState = getViewState(docId);
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
id,
|
|
215
|
+
type: 'orphan-comments-for-subject',
|
|
216
|
+
data: doc,
|
|
217
|
+
properties: {
|
|
218
|
+
icon: meta.iconComponent,
|
|
219
|
+
label,
|
|
220
|
+
showResolvedThreads: viewState.showResolvedThreads,
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
},
|
|
224
|
+
actions: ({ node }) => {
|
|
225
|
+
const dataId = node.id.split('~').at(0);
|
|
226
|
+
|
|
227
|
+
if (!node.id.endsWith('~comments') || !dataId) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const [spaceId, objectId] = dataId.split(':');
|
|
232
|
+
|
|
233
|
+
const viewState = getViewState(dataId);
|
|
234
|
+
const toggle = () => {
|
|
235
|
+
const newToggleState = !viewState.showResolvedThreads;
|
|
236
|
+
viewState.showResolvedThreads = newToggleState;
|
|
237
|
+
void dispatch({
|
|
238
|
+
action: ObservabilityAction.SEND_EVENT,
|
|
239
|
+
data: {
|
|
240
|
+
name: 'threads.toggle-show-resolved',
|
|
241
|
+
properties: { spaceId, threadId: objectId, showResolved: newToggleState },
|
|
242
|
+
},
|
|
243
|
+
});
|
|
244
|
+
};
|
|
245
|
+
|
|
246
|
+
return [
|
|
247
|
+
{
|
|
248
|
+
id: `${THREAD_PLUGIN}/toggle-show-resolved/${node.id}`,
|
|
249
|
+
data: toggle,
|
|
250
|
+
properties: {
|
|
251
|
+
label: ['toggle show resolved', { ns: THREAD_PLUGIN }],
|
|
252
|
+
menuItemType: 'toggle',
|
|
253
|
+
isChecked: viewState.showResolvedThreads,
|
|
254
|
+
testId: 'threadPlugin.toggleShowResolved',
|
|
255
|
+
},
|
|
256
|
+
},
|
|
257
|
+
];
|
|
258
|
+
},
|
|
259
|
+
}),
|
|
260
|
+
createExtension({
|
|
261
|
+
id: ThreadAction.CREATE,
|
|
262
|
+
filter: (node): node is ActionGroup =>
|
|
263
|
+
!!settings.values.standalone && isActionGroup(node) && node.id.startsWith(SpaceAction.ADD_OBJECT),
|
|
264
|
+
actions: ({ node }) => {
|
|
265
|
+
const id = node.id.split('/').at(-1);
|
|
266
|
+
const [spaceId, objectId] = id?.split(':') ?? [];
|
|
267
|
+
const space = client.spaces.get().find((space) => space.id === spaceId);
|
|
268
|
+
const object = objectId && space?.db.getObjectById(objectId);
|
|
269
|
+
const target = objectId ? object : space;
|
|
270
|
+
if (!target) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return [
|
|
275
|
+
{
|
|
276
|
+
id: `${THREAD_PLUGIN}/create/${node.id}`,
|
|
277
|
+
data: async () => {
|
|
278
|
+
await dispatch([
|
|
279
|
+
{ plugin: THREAD_PLUGIN, action: ThreadAction.CREATE },
|
|
280
|
+
{ action: SpaceAction.ADD_OBJECT, data: { target } },
|
|
281
|
+
{ action: NavigationAction.OPEN },
|
|
282
|
+
]);
|
|
283
|
+
},
|
|
284
|
+
properties: {
|
|
285
|
+
label: ['create channel label', { ns: THREAD_PLUGIN }],
|
|
286
|
+
icon: (props: IconProps) => <Chat {...props} />,
|
|
287
|
+
iconSymbol: 'ph--chat--regular',
|
|
288
|
+
testId: 'threadPlugin.createObject',
|
|
289
|
+
},
|
|
290
|
+
},
|
|
291
|
+
];
|
|
292
|
+
},
|
|
293
|
+
}),
|
|
294
|
+
];
|
|
295
|
+
},
|
|
296
|
+
},
|
|
297
|
+
surface: {
|
|
298
|
+
component: ({ data, role }) => {
|
|
299
|
+
switch (role) {
|
|
300
|
+
case 'main': {
|
|
301
|
+
return data.active instanceof ChannelType && data.active.threads[0] ? (
|
|
302
|
+
<ThreadMain thread={data.active.threads[0]} />
|
|
303
|
+
) : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
case 'settings': {
|
|
307
|
+
return data.plugin === meta.id ? <ThreadSettings settings={settings.values} /> : null;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
case 'article':
|
|
311
|
+
case 'complementary': {
|
|
312
|
+
const location = navigationPlugin?.provides.location;
|
|
313
|
+
|
|
314
|
+
if (data.object instanceof ChannelType && data.object.threads[0]) {
|
|
315
|
+
const channel = data.object;
|
|
316
|
+
// TODO(zan): Maybe we should have utility for positional main object ids.
|
|
317
|
+
if (isLayoutParts(location?.active) && location.active.main) {
|
|
318
|
+
const layoutEntries = location.active.main;
|
|
319
|
+
|
|
320
|
+
const currentPosition = layoutEntries.findIndex((entry) => channel.id === entry.id);
|
|
321
|
+
|
|
322
|
+
if (currentPosition > 0) {
|
|
323
|
+
const objectToTheLeft = layoutEntries[currentPosition - 1];
|
|
324
|
+
const context = getSpace(data.object)?.db.getObjectById(objectToTheLeft.id);
|
|
325
|
+
return <ThreadArticle thread={data.object.threads[0]} context={context} />;
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
return <ThreadArticle thread={data.object.threads[0]} />;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (data.subject instanceof DocumentType) {
|
|
333
|
+
const doc = data.subject;
|
|
334
|
+
const accessor = doc.content ? createDocAccessor(doc.content, ['content']) : undefined;
|
|
335
|
+
|
|
336
|
+
if (!accessor) {
|
|
337
|
+
return null;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const getStartPosition = (cursor: string | undefined) => {
|
|
341
|
+
const range = cursor ? getRangeFromCursor(accessor, cursor) : undefined;
|
|
342
|
+
return range?.start ?? Number.MAX_SAFE_INTEGER;
|
|
343
|
+
};
|
|
344
|
+
|
|
345
|
+
const threads = doc.threads
|
|
346
|
+
.concat(state.staging[data.subject.id])
|
|
347
|
+
.filter(nonNullable)
|
|
348
|
+
.sort((a, b) => getStartPosition(a.anchor) - getStartPosition(b.anchor));
|
|
349
|
+
|
|
350
|
+
const detached = data.subject.threads
|
|
351
|
+
.filter(nonNullable)
|
|
352
|
+
.filter(({ anchor }) => !anchor)
|
|
353
|
+
.map((thread) => thread.id);
|
|
354
|
+
|
|
355
|
+
const qualifiedSubjectId = fullyQualifiedId(doc);
|
|
356
|
+
const attention = attentionPlugin?.provides.attention?.attended ?? new Set([qualifiedSubjectId]);
|
|
357
|
+
const attendableAttrs = createAttendableAttributes(qualifiedSubjectId);
|
|
358
|
+
const space = getSpace(doc);
|
|
359
|
+
const { showResolvedThreads } = getViewState(qualifiedSubjectId);
|
|
360
|
+
|
|
361
|
+
const dispatchAnalytic = (name: string, meta: any) => {
|
|
362
|
+
void dispatch?.({
|
|
363
|
+
action: ObservabilityAction.SEND_EVENT,
|
|
364
|
+
data: {
|
|
365
|
+
name,
|
|
366
|
+
properties: { ...meta, space: space?.id },
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
return (
|
|
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: { id: thread.id },
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
}}
|
|
391
|
+
onThreadDelete={(thread) =>
|
|
392
|
+
dispatch?.({
|
|
393
|
+
plugin: THREAD_PLUGIN,
|
|
394
|
+
action: ThreadAction.DELETE,
|
|
395
|
+
data: { document: data.subject, thread },
|
|
396
|
+
})
|
|
397
|
+
}
|
|
398
|
+
onMessageDelete={(thread, messageId) =>
|
|
399
|
+
dispatch?.({
|
|
400
|
+
plugin: THREAD_PLUGIN,
|
|
401
|
+
action: ThreadAction.DELETE_MESSAGE,
|
|
402
|
+
data: { document: data.subject, thread, messageId },
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
onThreadToggleResolved={(thread) =>
|
|
406
|
+
dispatch?.({
|
|
407
|
+
plugin: THREAD_PLUGIN,
|
|
408
|
+
action: ThreadAction.TOGGLE_RESOLVED,
|
|
409
|
+
data: { thread },
|
|
410
|
+
})
|
|
411
|
+
}
|
|
412
|
+
onComment={(thread) => {
|
|
413
|
+
// TODO(Zan): This might be a bit too much logic for a component. Move to intents?
|
|
414
|
+
const doc = data.subject as DocumentType;
|
|
415
|
+
if (state.staging[doc.id]?.find((t) => t === thread)) {
|
|
416
|
+
// Move thread from staging to document.
|
|
417
|
+
thread.status = 'active';
|
|
418
|
+
doc.threads ? doc.threads.push(thread) : (doc.threads = [thread]);
|
|
419
|
+
state.staging[doc.id] = state.staging[doc.id]?.filter((t) => t.id !== thread.id);
|
|
420
|
+
dispatchAnalytic('threads.thread-created', { threadId: thread.id });
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
dispatchAnalytic('threads.message-added', {
|
|
424
|
+
threadId: thread.id,
|
|
425
|
+
threadLength: thread.messages.length,
|
|
426
|
+
});
|
|
427
|
+
}}
|
|
428
|
+
/>
|
|
429
|
+
<div role='none' className='bs-10' />
|
|
430
|
+
<ScrollArea.Scrollbar>
|
|
431
|
+
<ScrollArea.Thumb />
|
|
432
|
+
</ScrollArea.Scrollbar>
|
|
433
|
+
</ScrollArea.Viewport>
|
|
434
|
+
</ScrollArea.Root>
|
|
435
|
+
</div>
|
|
436
|
+
);
|
|
437
|
+
} else if (data.subject instanceof ThreadType) {
|
|
438
|
+
return (
|
|
439
|
+
<>
|
|
440
|
+
<ChatHeading attendableId={data.subject.id} />
|
|
441
|
+
<ChatContainer thread={data.subject} />
|
|
442
|
+
</>
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return null;
|
|
449
|
+
},
|
|
450
|
+
},
|
|
451
|
+
intent: {
|
|
452
|
+
resolver: async (intent) => {
|
|
453
|
+
switch (intent.action) {
|
|
454
|
+
case ThreadAction.CREATE: {
|
|
455
|
+
return { data: create(ChannelType, { threads: [create(ThreadType, { messages: [] })] }) };
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
case ThreadAction.SELECT: {
|
|
459
|
+
state.focus = intent.data?.current === state.current ? state.focus : intent.data?.focus;
|
|
460
|
+
state.current = intent.data?.current;
|
|
461
|
+
return { data: true };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
case ThreadAction.TOGGLE_RESOLVED: {
|
|
465
|
+
const { thread } = intent.data ?? {};
|
|
466
|
+
if (!(thread instanceof ThreadType)) {
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if (thread.status === 'active' || thread.status === undefined) {
|
|
471
|
+
thread.status = 'resolved';
|
|
472
|
+
} else if (thread.status === 'resolved') {
|
|
473
|
+
thread.status = 'active';
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
const space = getSpace(thread);
|
|
477
|
+
const spaceId = space?.id;
|
|
478
|
+
|
|
479
|
+
return {
|
|
480
|
+
intents: [
|
|
481
|
+
[
|
|
482
|
+
{
|
|
483
|
+
action: ObservabilityAction.SEND_EVENT,
|
|
484
|
+
data: { name: 'threads.toggle-resolved', properties: { threadId: thread.id, spaceId } },
|
|
485
|
+
},
|
|
486
|
+
],
|
|
487
|
+
],
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
case ThreadAction.DELETE: {
|
|
492
|
+
const { document: doc, thread } = intent.data ?? {};
|
|
493
|
+
if (!(doc instanceof DocumentType) || !(thread instanceof ThreadType)) {
|
|
494
|
+
return;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const stagingArea = state.staging[doc.id];
|
|
498
|
+
if (stagingArea) {
|
|
499
|
+
// Check if we're deleting a thread that's in the staging area.
|
|
500
|
+
// If so, remove it from the staging area without ceremony.
|
|
501
|
+
const index = state.staging[doc.id]?.findIndex((t) => t.id === thread.id);
|
|
502
|
+
if (index !== -1) {
|
|
503
|
+
state.staging[doc.id]?.splice(index, 1);
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
const space = getSpace(thread);
|
|
509
|
+
if (!space || !doc.threads) {
|
|
510
|
+
return;
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
if (!intent.undo) {
|
|
514
|
+
const index = doc.threads.findIndex((t) => t?.id === thread.id);
|
|
515
|
+
const cursor = doc.threads[index]?.anchor;
|
|
516
|
+
if (index !== -1) {
|
|
517
|
+
doc.threads?.splice(index, 1);
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
space.db.remove(thread);
|
|
521
|
+
|
|
522
|
+
return {
|
|
523
|
+
undoable: {
|
|
524
|
+
message: translations[0]['en-US'][THREAD_PLUGIN]['thread deleted label'],
|
|
525
|
+
data: { cursor },
|
|
526
|
+
},
|
|
527
|
+
intents: [
|
|
528
|
+
[
|
|
529
|
+
{
|
|
530
|
+
action: ObservabilityAction.SEND_EVENT,
|
|
531
|
+
data: { name: 'threads.delete', properties: { threadId: thread.id, spaceId: space.id } },
|
|
532
|
+
},
|
|
533
|
+
],
|
|
534
|
+
],
|
|
535
|
+
};
|
|
536
|
+
} else if (intent.undo) {
|
|
537
|
+
// TODO(wittjosiah): SDK should do this automatically.
|
|
538
|
+
const savedThread = space.db.add(thread);
|
|
539
|
+
doc.threads.push(savedThread);
|
|
540
|
+
|
|
541
|
+
return {
|
|
542
|
+
data: true,
|
|
543
|
+
intents: [
|
|
544
|
+
[
|
|
545
|
+
{
|
|
546
|
+
action: ObservabilityAction.SEND_EVENT,
|
|
547
|
+
data: { name: 'threads.undo-delete', properties: { threadId: thread.id, spaceId: space.id } },
|
|
548
|
+
},
|
|
549
|
+
],
|
|
550
|
+
],
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
break;
|
|
554
|
+
}
|
|
555
|
+
case ThreadAction.DELETE_MESSAGE: {
|
|
556
|
+
const { document: doc, thread, messageId } = intent.data ?? {};
|
|
557
|
+
const space = getSpace(doc);
|
|
558
|
+
|
|
559
|
+
if (!(doc instanceof DocumentType) || !(thread instanceof ThreadType) || !space) {
|
|
560
|
+
return;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
if (!intent.undo) {
|
|
564
|
+
const message = thread.messages.find((m) => m?.id === messageId);
|
|
565
|
+
const messageIndex = thread.messages.findIndex((m) => m?.id === messageId);
|
|
566
|
+
if (messageIndex === -1 || !message) {
|
|
567
|
+
return undefined;
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if (messageIndex === 0 && thread.messages.length === 1) {
|
|
571
|
+
// If the message is the only message in the thread, delete the thread.
|
|
572
|
+
return {
|
|
573
|
+
intents: [[{ action: ThreadAction.DELETE, data: { document: doc, thread } }]],
|
|
574
|
+
};
|
|
575
|
+
} else {
|
|
576
|
+
thread.messages.splice(messageIndex, 1);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return {
|
|
580
|
+
undoable: {
|
|
581
|
+
message: translations[0]['en-US'][THREAD_PLUGIN]['message deleted label'],
|
|
582
|
+
data: { message, messageIndex },
|
|
583
|
+
},
|
|
584
|
+
intents: [
|
|
585
|
+
[
|
|
586
|
+
{
|
|
587
|
+
action: ObservabilityAction.SEND_EVENT,
|
|
588
|
+
data: {
|
|
589
|
+
name: 'threads.message.delete',
|
|
590
|
+
properties: { threadId: thread.id, spaceId: space.id },
|
|
591
|
+
},
|
|
592
|
+
},
|
|
593
|
+
],
|
|
594
|
+
],
|
|
595
|
+
};
|
|
596
|
+
} else if (intent.undo) {
|
|
597
|
+
const message = intent.data?.message;
|
|
598
|
+
const messageIndex = intent.data?.messageIndex;
|
|
599
|
+
|
|
600
|
+
if (!(message instanceof MessageType) || !(typeof messageIndex === 'number')) {
|
|
601
|
+
return;
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
thread.messages.splice(messageIndex, 0, message);
|
|
605
|
+
|
|
606
|
+
return {
|
|
607
|
+
data: true,
|
|
608
|
+
intents: [
|
|
609
|
+
[
|
|
610
|
+
{
|
|
611
|
+
action: ObservabilityAction.SEND_EVENT,
|
|
612
|
+
data: {
|
|
613
|
+
name: 'threads.message.undo-delete',
|
|
614
|
+
properties: { threadId: thread.id, spaceId: space.id },
|
|
615
|
+
},
|
|
616
|
+
},
|
|
617
|
+
],
|
|
618
|
+
],
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
markdown: {
|
|
626
|
+
// TODO(burdon): Factor out extension factory into separate file (for simplicity).
|
|
627
|
+
extensions: ({ document: doc }) => {
|
|
628
|
+
const space = doc && getSpace(doc);
|
|
629
|
+
if (!doc || !space) {
|
|
630
|
+
// Include no-op comments extension here to ensure that the facets are always present when they are expected.
|
|
631
|
+
// TODO(wittjosiah): The Editor should only look for these facets when comments are available.
|
|
632
|
+
return [comments()];
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// TODO(Zan): When we have the deepsignal specific equivalent of this we should use that instead.
|
|
636
|
+
const threads = computed(() =>
|
|
637
|
+
[...doc.threads.filter(nonNullable), ...(state.staging[doc.id] ?? [])].filter(
|
|
638
|
+
(thread) => !(thread?.status === 'resolved'),
|
|
639
|
+
),
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
return [
|
|
643
|
+
listener({
|
|
644
|
+
onChange: () => {
|
|
645
|
+
doc.threads.forEach((thread) => {
|
|
646
|
+
if (thread?.anchor) {
|
|
647
|
+
const [start, end] = thread.anchor.split(':');
|
|
648
|
+
const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
649
|
+
// TODO(burdon): This seems unsafe; review.
|
|
650
|
+
// Only update if the name has changed, otherwise this will cause an infinite loop.
|
|
651
|
+
// Skip if the name is empty - this means comment text was deleted, but thread name should remain.
|
|
652
|
+
if (name && name !== thread.name) {
|
|
653
|
+
thread.name = name;
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
});
|
|
657
|
+
},
|
|
658
|
+
}),
|
|
659
|
+
createExternalCommentSync(
|
|
660
|
+
doc.id,
|
|
661
|
+
(sink) => effect(() => sink()),
|
|
662
|
+
() =>
|
|
663
|
+
threads.value
|
|
664
|
+
.filter((thread) => thread?.anchor)
|
|
665
|
+
.map((thread) => ({ id: thread.id, cursor: thread.anchor! })),
|
|
666
|
+
),
|
|
667
|
+
comments({
|
|
668
|
+
id: doc.id,
|
|
669
|
+
onCreate: ({ cursor }) => {
|
|
670
|
+
const [start, end] = cursor.split(':');
|
|
671
|
+
const name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
672
|
+
const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
|
|
673
|
+
|
|
674
|
+
const stagingArea = state.staging[doc.id];
|
|
675
|
+
if (stagingArea) {
|
|
676
|
+
stagingArea.push(thread);
|
|
677
|
+
} else {
|
|
678
|
+
state.staging[doc.id] = [thread];
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
void intentPlugin?.provides.intent.dispatch([
|
|
682
|
+
{
|
|
683
|
+
action: NavigationAction.OPEN,
|
|
684
|
+
data: {
|
|
685
|
+
activeParts: {
|
|
686
|
+
complementary: `${fullyQualifiedId(doc)}${SLUG_PATH_SEPARATOR}comments${SLUG_COLLECTION_INDICATOR}`,
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
{
|
|
691
|
+
action: ThreadAction.SELECT,
|
|
692
|
+
data: { current: thread.id, focus: true },
|
|
693
|
+
},
|
|
694
|
+
{
|
|
695
|
+
action: LayoutAction.SET_LAYOUT,
|
|
696
|
+
data: {
|
|
697
|
+
element: 'complementary',
|
|
698
|
+
subject: doc,
|
|
699
|
+
state: true,
|
|
700
|
+
},
|
|
701
|
+
},
|
|
702
|
+
]);
|
|
703
|
+
|
|
704
|
+
return thread.id;
|
|
705
|
+
},
|
|
706
|
+
onDelete: ({ id }) => {
|
|
707
|
+
// If the thread is in the staging area, remove it.
|
|
708
|
+
const stagingArea = state.staging[doc.id];
|
|
709
|
+
if (stagingArea) {
|
|
710
|
+
const index = stagingArea.findIndex((thread) => thread.id === id);
|
|
711
|
+
if (index !== -1) {
|
|
712
|
+
stagingArea.splice(index, 1);
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
const thread = doc.threads.find((thread) => thread?.id === id);
|
|
717
|
+
if (thread) {
|
|
718
|
+
thread.anchor = undefined;
|
|
719
|
+
}
|
|
720
|
+
},
|
|
721
|
+
onUpdate: ({ id, cursor }) => {
|
|
722
|
+
const thread =
|
|
723
|
+
state.staging[doc.id]?.find((thread) => thread.id === id) ??
|
|
724
|
+
doc.threads.find((thread) => thread?.id === id);
|
|
725
|
+
|
|
726
|
+
if (thread instanceof ThreadType && thread.anchor) {
|
|
727
|
+
const [start, end] = thread.anchor.split(':');
|
|
728
|
+
thread.name = doc.content && getTextInRange(createDocAccessor(doc.content, ['content']), start, end);
|
|
729
|
+
thread.anchor = cursor;
|
|
730
|
+
}
|
|
731
|
+
},
|
|
732
|
+
onSelect: ({ selection: { current, closest } }) => {
|
|
733
|
+
const dispatch = intentPlugin?.provides.intent.dispatch;
|
|
734
|
+
if (dispatch) {
|
|
735
|
+
void dispatch([{ action: ThreadAction.SELECT, data: { current: current ?? closest } }]);
|
|
736
|
+
}
|
|
737
|
+
},
|
|
738
|
+
}),
|
|
739
|
+
];
|
|
740
|
+
},
|
|
741
|
+
},
|
|
742
|
+
},
|
|
743
|
+
};
|
|
744
|
+
};
|