@dxos/plugin-thread 0.7.5-main.499c70c → 0.7.5-main.5ae2ba8

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.
Files changed (188) hide show
  1. package/dist/lib/browser/ThreadContainer-6OP5AG2W.mjs +19 -0
  2. package/dist/lib/browser/ThreadContainer-6OP5AG2W.mjs.map +7 -0
  3. package/dist/lib/browser/{chunk-ENERBDYI.mjs → chunk-636QDHE3.mjs} +3 -3
  4. package/dist/lib/browser/chunk-636QDHE3.mjs.map +7 -0
  5. package/dist/lib/browser/chunk-BNEWNQBO.mjs +71 -0
  6. package/dist/lib/browser/chunk-BNEWNQBO.mjs.map +7 -0
  7. package/dist/lib/browser/chunk-NAO3LZMZ.mjs +16 -0
  8. package/dist/lib/browser/chunk-NAO3LZMZ.mjs.map +7 -0
  9. package/dist/lib/browser/{chunk-OD2IZKRP.mjs → chunk-UPKU23DA.mjs} +29 -105
  10. package/dist/lib/browser/chunk-UPKU23DA.mjs.map +7 -0
  11. package/dist/lib/browser/chunk-VOMIDDNO.mjs +15 -0
  12. package/dist/lib/browser/chunk-VOMIDDNO.mjs.map +7 -0
  13. package/dist/lib/browser/{chunk-6KTMQPL4.mjs → chunk-YK3ICB7G.mjs} +5 -3
  14. package/dist/lib/browser/chunk-YK3ICB7G.mjs.map +7 -0
  15. package/dist/lib/browser/index.mjs +150 -942
  16. package/dist/lib/browser/index.mjs.map +4 -4
  17. package/dist/lib/browser/intent-resolver-FAFQ54NP.mjs +287 -0
  18. package/dist/lib/browser/intent-resolver-FAFQ54NP.mjs.map +7 -0
  19. package/dist/lib/browser/markdown-RPYOUBVR.mjs +112 -0
  20. package/dist/lib/browser/markdown-RPYOUBVR.mjs.map +7 -0
  21. package/dist/lib/browser/meta.json +1 -1
  22. package/dist/lib/browser/react-surface-Y4MUSICN.mjs +433 -0
  23. package/dist/lib/browser/react-surface-Y4MUSICN.mjs.map +7 -0
  24. package/dist/lib/browser/settings-WQTE5AEZ.mjs +22 -0
  25. package/dist/lib/browser/settings-WQTE5AEZ.mjs.map +7 -0
  26. package/dist/lib/browser/state-GNFUURIC.mjs +33 -0
  27. package/dist/lib/browser/state-GNFUURIC.mjs.map +7 -0
  28. package/dist/lib/browser/types.mjs +6 -4
  29. package/dist/lib/node/{ThreadArticle-ZCNMFBAO.cjs → ThreadContainer-LTKKLPXX.cjs} +14 -13
  30. package/dist/lib/node/ThreadContainer-LTKKLPXX.cjs.map +7 -0
  31. package/dist/lib/node/chunk-AWSTZ2KN.cjs +93 -0
  32. package/dist/lib/node/chunk-AWSTZ2KN.cjs.map +7 -0
  33. package/dist/lib/node/{ThreadMain-5AYGCXXP.cjs → chunk-BHFHFTHO.cjs} +21 -23
  34. package/dist/lib/node/chunk-BHFHFTHO.cjs.map +7 -0
  35. package/dist/lib/node/{chunk-VW2P26MZ.cjs → chunk-CMEMMYDL.cjs} +11 -8
  36. package/dist/lib/node/chunk-CMEMMYDL.cjs.map +7 -0
  37. package/dist/lib/node/{chunk-IC2NICNC.cjs → chunk-F735VMTN.cjs} +7 -7
  38. package/dist/lib/node/chunk-F735VMTN.cjs.map +7 -0
  39. package/dist/lib/node/{meta.cjs → chunk-FPWNZ6TB.cjs} +13 -10
  40. package/dist/lib/node/chunk-FPWNZ6TB.cjs.map +7 -0
  41. package/dist/lib/node/{chunk-2CB5AVBL.cjs → chunk-RQR5Z23G.cjs} +74 -151
  42. package/dist/lib/node/chunk-RQR5Z23G.cjs.map +7 -0
  43. package/dist/lib/node/index.cjs +149 -935
  44. package/dist/lib/node/index.cjs.map +4 -4
  45. package/dist/lib/node/intent-resolver-DVBEXJ33.cjs +299 -0
  46. package/dist/lib/node/intent-resolver-DVBEXJ33.cjs.map +7 -0
  47. package/dist/lib/node/markdown-ELRGISXE.cjs +122 -0
  48. package/dist/lib/node/markdown-ELRGISXE.cjs.map +7 -0
  49. package/dist/lib/node/meta.json +1 -1
  50. package/dist/lib/node/react-surface-7HLTN5XS.cjs +435 -0
  51. package/dist/lib/node/react-surface-7HLTN5XS.cjs.map +7 -0
  52. package/dist/lib/node/settings-7VQYARM5.cjs +36 -0
  53. package/dist/lib/node/settings-7VQYARM5.cjs.map +7 -0
  54. package/dist/lib/node/state-TQWEZ6B3.cjs +49 -0
  55. package/dist/lib/node/state-TQWEZ6B3.cjs.map +7 -0
  56. package/dist/lib/node/types.cjs +6 -4
  57. package/dist/lib/node/types.cjs.map +2 -2
  58. package/dist/lib/node-esm/ThreadContainer-SV74JFRH.mjs +20 -0
  59. package/dist/lib/node-esm/ThreadContainer-SV74JFRH.mjs.map +7 -0
  60. package/dist/lib/node-esm/{chunk-RMYATBXM.mjs → chunk-33NDMQQQ.mjs} +5 -3
  61. package/dist/lib/node-esm/chunk-33NDMQQQ.mjs.map +7 -0
  62. package/dist/lib/node-esm/{chunk-AQ74G3GY.mjs → chunk-CISLDOH7.mjs} +29 -105
  63. package/dist/lib/node-esm/chunk-CISLDOH7.mjs.map +7 -0
  64. package/dist/lib/node-esm/{chunk-7QRGG25F.mjs → chunk-FXUATCLW.mjs} +3 -3
  65. package/dist/lib/node-esm/chunk-FXUATCLW.mjs.map +7 -0
  66. package/dist/lib/node-esm/chunk-HLWJTUWZ.mjs +73 -0
  67. package/dist/lib/node-esm/chunk-HLWJTUWZ.mjs.map +7 -0
  68. package/dist/lib/node-esm/chunk-PJPVEO7E.mjs +17 -0
  69. package/dist/lib/node-esm/chunk-PJPVEO7E.mjs.map +7 -0
  70. package/dist/lib/node-esm/chunk-ZJITX3TO.mjs +17 -0
  71. package/dist/lib/node-esm/chunk-ZJITX3TO.mjs.map +7 -0
  72. package/dist/lib/node-esm/index.mjs +150 -942
  73. package/dist/lib/node-esm/index.mjs.map +4 -4
  74. package/dist/lib/node-esm/intent-resolver-DHVLBNCC.mjs +288 -0
  75. package/dist/lib/node-esm/intent-resolver-DHVLBNCC.mjs.map +7 -0
  76. package/dist/lib/node-esm/markdown-23OB7EZG.mjs +113 -0
  77. package/dist/lib/node-esm/markdown-23OB7EZG.mjs.map +7 -0
  78. package/dist/lib/node-esm/meta.json +1 -1
  79. package/dist/lib/node-esm/react-surface-AZHTTR6J.mjs +434 -0
  80. package/dist/lib/node-esm/react-surface-AZHTTR6J.mjs.map +7 -0
  81. package/dist/lib/node-esm/settings-SGMM7UDT.mjs +23 -0
  82. package/dist/lib/node-esm/settings-SGMM7UDT.mjs.map +7 -0
  83. package/dist/lib/node-esm/state-7MLR27YG.mjs +34 -0
  84. package/dist/lib/node-esm/state-7MLR27YG.mjs.map +7 -0
  85. package/dist/lib/node-esm/types.mjs +6 -4
  86. package/dist/types/src/ThreadPlugin.d.ts +1 -3
  87. package/dist/types/src/ThreadPlugin.d.ts.map +1 -1
  88. package/dist/types/src/capabilities/capabilities.d.ts +15 -0
  89. package/dist/types/src/capabilities/capabilities.d.ts.map +1 -0
  90. package/dist/types/src/capabilities/index.d.ts +10 -0
  91. package/dist/types/src/capabilities/index.d.ts.map +1 -0
  92. package/dist/types/src/capabilities/intent-resolver.d.ts +4 -0
  93. package/dist/types/src/capabilities/intent-resolver.d.ts.map +1 -0
  94. package/dist/types/src/capabilities/markdown.d.ts +4 -0
  95. package/dist/types/src/capabilities/markdown.d.ts.map +1 -0
  96. package/dist/types/src/capabilities/react-surface.d.ts +4 -0
  97. package/dist/types/src/capabilities/react-surface.d.ts.map +1 -0
  98. package/dist/types/src/capabilities/settings.d.ts +4 -0
  99. package/dist/types/src/capabilities/settings.d.ts.map +1 -0
  100. package/dist/types/src/capabilities/state.d.ts +7 -0
  101. package/dist/types/src/capabilities/state.d.ts.map +1 -0
  102. package/dist/types/src/components/Chat.stories.d.ts.map +1 -1
  103. package/dist/types/src/components/ChatContainer.d.ts +2 -3
  104. package/dist/types/src/components/ChatContainer.d.ts.map +1 -1
  105. package/dist/types/src/components/CommentContainer.d.ts +1 -2
  106. package/dist/types/src/components/CommentContainer.d.ts.map +1 -1
  107. package/dist/types/src/components/Comments.stories.d.ts.map +1 -1
  108. package/dist/types/src/components/CommentsContainer.d.ts +2 -8
  109. package/dist/types/src/components/CommentsContainer.d.ts.map +1 -1
  110. package/dist/types/src/components/MessageContainer.d.ts +1 -2
  111. package/dist/types/src/components/MessageContainer.d.ts.map +1 -1
  112. package/dist/types/src/components/ThreadComplementary.d.ts +2 -7
  113. package/dist/types/src/components/ThreadComplementary.d.ts.map +1 -1
  114. package/dist/types/src/components/ThreadContainer.d.ts +7 -0
  115. package/dist/types/src/components/ThreadContainer.d.ts.map +1 -0
  116. package/dist/types/src/components/ThreadSettings.d.ts +1 -2
  117. package/dist/types/src/components/ThreadSettings.d.ts.map +1 -1
  118. package/dist/types/src/components/command-extension.d.ts.map +1 -1
  119. package/dist/types/src/components/index.d.ts +3 -15
  120. package/dist/types/src/components/index.d.ts.map +1 -1
  121. package/dist/types/src/components/testing.d.ts +12 -12
  122. package/dist/types/src/components/testing.d.ts.map +1 -1
  123. package/dist/types/src/components/types.d.ts +5 -5
  124. package/dist/types/src/components/types.d.ts.map +1 -1
  125. package/dist/types/src/index.d.ts +1 -3
  126. package/dist/types/src/index.d.ts.map +1 -1
  127. package/dist/types/src/meta.d.ts +1 -2
  128. package/dist/types/src/meta.d.ts.map +1 -1
  129. package/dist/types/src/translations.d.ts +9 -3
  130. package/dist/types/src/translations.d.ts.map +1 -1
  131. package/dist/types/src/types.d.ts +12 -18
  132. package/dist/types/src/types.d.ts.map +1 -1
  133. package/dist/types/tsconfig.tsbuildinfo +1 -1
  134. package/package.json +31 -38
  135. package/src/ThreadPlugin.tsx +106 -475
  136. package/src/capabilities/capabilities.ts +19 -0
  137. package/src/capabilities/index.ts +13 -0
  138. package/src/capabilities/intent-resolver.ts +239 -0
  139. package/src/capabilities/markdown.ts +18 -0
  140. package/src/capabilities/react-surface.tsx +61 -0
  141. package/src/capabilities/settings.ts +14 -0
  142. package/src/capabilities/state.ts +25 -0
  143. package/src/components/Chat.stories.tsx +22 -24
  144. package/src/components/ChatContainer.tsx +2 -2
  145. package/src/components/CommentContainer.tsx +18 -32
  146. package/src/components/Comments.stories.tsx +8 -2
  147. package/src/components/CommentsContainer.tsx +1 -17
  148. package/src/components/MessageContainer.tsx +6 -19
  149. package/src/components/ThreadComplementary.tsx +118 -51
  150. package/src/components/ThreadContainer.tsx +22 -0
  151. package/src/components/ThreadSettings.tsx +5 -11
  152. package/src/components/command-extension.ts +2 -5
  153. package/src/components/index.ts +1 -2
  154. package/src/components/types.ts +5 -5
  155. package/src/index.ts +1 -5
  156. package/src/meta.ts +1 -1
  157. package/src/translations.ts +3 -1
  158. package/src/types.ts +6 -29
  159. package/dist/lib/browser/ThreadArticle-EWLTNU6Z.mjs +0 -18
  160. package/dist/lib/browser/ThreadArticle-EWLTNU6Z.mjs.map +0 -7
  161. package/dist/lib/browser/ThreadMain-ABEDFWHC.mjs +0 -26
  162. package/dist/lib/browser/ThreadMain-ABEDFWHC.mjs.map +0 -7
  163. package/dist/lib/browser/chunk-6KTMQPL4.mjs.map +0 -7
  164. package/dist/lib/browser/chunk-ENERBDYI.mjs.map +0 -7
  165. package/dist/lib/browser/chunk-OD2IZKRP.mjs.map +0 -7
  166. package/dist/lib/browser/meta.mjs +0 -11
  167. package/dist/lib/browser/meta.mjs.map +0 -7
  168. package/dist/lib/node/ThreadArticle-ZCNMFBAO.cjs.map +0 -7
  169. package/dist/lib/node/ThreadMain-5AYGCXXP.cjs.map +0 -7
  170. package/dist/lib/node/chunk-2CB5AVBL.cjs.map +0 -7
  171. package/dist/lib/node/chunk-IC2NICNC.cjs.map +0 -7
  172. package/dist/lib/node/chunk-VW2P26MZ.cjs.map +0 -7
  173. package/dist/lib/node/meta.cjs.map +0 -7
  174. package/dist/lib/node-esm/ThreadArticle-F37HYTOU.mjs +0 -19
  175. package/dist/lib/node-esm/ThreadArticle-F37HYTOU.mjs.map +0 -7
  176. package/dist/lib/node-esm/ThreadMain-OL6ZHNZY.mjs +0 -27
  177. package/dist/lib/node-esm/ThreadMain-OL6ZHNZY.mjs.map +0 -7
  178. package/dist/lib/node-esm/chunk-7QRGG25F.mjs.map +0 -7
  179. package/dist/lib/node-esm/chunk-AQ74G3GY.mjs.map +0 -7
  180. package/dist/lib/node-esm/chunk-RMYATBXM.mjs.map +0 -7
  181. package/dist/lib/node-esm/meta.mjs +0 -12
  182. package/dist/lib/node-esm/meta.mjs.map +0 -7
  183. package/dist/types/src/components/ThreadArticle.d.ts +0 -5
  184. package/dist/types/src/components/ThreadArticle.d.ts.map +0 -1
  185. package/dist/types/src/components/ThreadMain.d.ts +0 -7
  186. package/dist/types/src/components/ThreadMain.d.ts.map +0 -1
  187. package/src/components/ThreadArticle.tsx +0 -18
  188. package/src/components/ThreadMain.tsx +0 -27
@@ -0,0 +1,239 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import {
6
+ Capabilities,
7
+ contributes,
8
+ createIntent,
9
+ createResolver,
10
+ LayoutAction,
11
+ type PluginsContext,
12
+ } from '@dxos/app-framework';
13
+ import { Ref } from '@dxos/echo-schema';
14
+ import { log } from '@dxos/log';
15
+ import { ObservabilityAction } from '@dxos/plugin-observability/types';
16
+ import { ChannelType, ThreadType } from '@dxos/plugin-space/types';
17
+ import { create, fullyQualifiedId, getSpace, makeRef } from '@dxos/react-client/echo';
18
+
19
+ import { ThreadCapabilities } from './capabilities';
20
+ import { THREAD_PLUGIN } from '../meta';
21
+ import { ThreadAction } from '../types';
22
+
23
+ export default (context: PluginsContext) =>
24
+ contributes(Capabilities.IntentResolver, [
25
+ createResolver({
26
+ intent: ThreadAction.Create,
27
+ resolve: ({ name, cursor, subject }) => {
28
+ if (cursor && subject) {
29
+ // Seed the threads array if it does not exist.
30
+ if (subject?.threads === undefined) {
31
+ try {
32
+ // Static schema will throw an error if subject does not support threads array property.
33
+ subject.threads = [];
34
+ } catch (err) {
35
+ log.error('Subject does not support threads array', { typename: subject?.typename });
36
+ return;
37
+ }
38
+ }
39
+
40
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
41
+ const subjectId = fullyQualifiedId(subject);
42
+ const thread = create(ThreadType, { name, anchor: cursor, messages: [], status: 'staged' });
43
+ const draft = state.drafts[subjectId];
44
+ if (draft) {
45
+ draft.push(thread);
46
+ } else {
47
+ state.drafts[subjectId] = [thread];
48
+ }
49
+
50
+ return {
51
+ data: { object: thread },
52
+ intents: [
53
+ createIntent(ThreadAction.Select, { current: fullyQualifiedId(thread) }),
54
+ createIntent(LayoutAction.UpdateComplementary, { part: 'complementary', subject: 'comments' }),
55
+ ],
56
+ };
57
+ } else {
58
+ // TODO(wittjosiah): Create separate intent for channel creation.
59
+ return {
60
+ data: { object: create(ChannelType, { threads: [makeRef(create(ThreadType, { messages: [] }))] }) },
61
+ };
62
+ }
63
+ },
64
+ }),
65
+ createResolver({
66
+ intent: ThreadAction.Select,
67
+ resolve: ({ current, skipOpen }) => {
68
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
69
+ state.current = current;
70
+
71
+ return {
72
+ intents: !skipOpen
73
+ ? [createIntent(LayoutAction.UpdateComplementary, { part: 'complementary', subject: 'comments' })]
74
+ : undefined,
75
+ };
76
+ },
77
+ }),
78
+ createResolver({
79
+ intent: ThreadAction.ToggleResolved,
80
+ resolve: ({ thread }) => {
81
+ if (thread.status === 'active' || thread.status === undefined) {
82
+ thread.status = 'resolved';
83
+ } else if (thread.status === 'resolved') {
84
+ thread.status = 'active';
85
+ }
86
+
87
+ const space = getSpace(thread);
88
+ const spaceId = space?.id;
89
+
90
+ return {
91
+ intents: [
92
+ createIntent(ObservabilityAction.SendEvent, {
93
+ name: 'threads.toggle-resolved',
94
+ properties: { threadId: thread.id, spaceId },
95
+ }),
96
+ ],
97
+ };
98
+ },
99
+ }),
100
+ createResolver({
101
+ intent: ThreadAction.Delete,
102
+ resolve: ({ subject, thread }, undo) => {
103
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
104
+ const subjectId = fullyQualifiedId(subject);
105
+ const draft = state.drafts[subjectId];
106
+ if (draft) {
107
+ // Check if we're deleting a draft; if so, remove it.
108
+ const index = draft.findIndex((t) => t.id === thread.id);
109
+ if (index !== -1) {
110
+ draft.splice(index, 1);
111
+ return;
112
+ }
113
+ }
114
+
115
+ const space = getSpace(thread);
116
+ if (!space || !subject.threads) {
117
+ return;
118
+ }
119
+
120
+ if (!undo) {
121
+ const index = subject.threads.findIndex((t: any) => t?.id === thread.id);
122
+ const cursor = subject.threads[index]?.anchor;
123
+ if (index !== -1) {
124
+ subject.threads?.splice(index, 1);
125
+ }
126
+
127
+ space.db.remove(thread);
128
+
129
+ return {
130
+ undoable: {
131
+ message: ['thread deleted label', { ns: THREAD_PLUGIN }],
132
+ data: { cursor },
133
+ },
134
+ intents: [
135
+ createIntent(ObservabilityAction.SendEvent, {
136
+ name: 'threads.delete',
137
+ properties: { threadId: thread.id, spaceId: space.id },
138
+ }),
139
+ ],
140
+ };
141
+ } else {
142
+ // TODO(wittjosiah): SDK should do this automatically.
143
+ const savedThread = space.db.add(thread);
144
+ subject.threads.push(savedThread);
145
+
146
+ return {
147
+ intents: [
148
+ createIntent(ObservabilityAction.SendEvent, {
149
+ name: 'threads.undo-delete',
150
+ properties: { threadId: thread.id, spaceId: space.id },
151
+ }),
152
+ ],
153
+ };
154
+ }
155
+ },
156
+ }),
157
+ createResolver({
158
+ intent: ThreadAction.OnMessageAdd,
159
+ resolve: ({ thread, subject }) => {
160
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
161
+ const subjectId = fullyQualifiedId(subject);
162
+ const space = getSpace(subject);
163
+ const intents = [];
164
+ const analyticsProperties = { threadId: thread.id, spaceId: space?.id };
165
+
166
+ if (state.drafts[subjectId]?.find((t) => t === thread)) {
167
+ // Move draft to document.
168
+ thread.status = 'active';
169
+ subject.threads ? subject.threads.push(makeRef(thread)) : (subject.threads = [makeRef(thread)]);
170
+ state.drafts[subjectId] = state.drafts[subjectId]?.filter(({ id }) => id !== thread.id);
171
+ intents.push(
172
+ createIntent(ObservabilityAction.SendEvent, {
173
+ name: 'threads.thread-created',
174
+ properties: analyticsProperties,
175
+ }),
176
+ );
177
+ }
178
+
179
+ intents.push(
180
+ createIntent(ObservabilityAction.SendEvent, {
181
+ name: 'threads.message-added',
182
+ properties: { ...analyticsProperties, threadLength: thread.messages.length },
183
+ }),
184
+ );
185
+
186
+ return { intents };
187
+ },
188
+ }),
189
+ createResolver({
190
+ intent: ThreadAction.DeleteMessage,
191
+ resolve: ({ subject, thread, messageId, message, messageIndex }, undo) => {
192
+ const space = getSpace(subject);
193
+
194
+ if (!undo) {
195
+ const messageIndex = thread.messages.findIndex(Ref.hasObjectId(messageId));
196
+ const message = thread.messages[messageIndex]?.target;
197
+ if (!message) {
198
+ return;
199
+ }
200
+
201
+ if (messageIndex === 0 && thread.messages.length === 1) {
202
+ // If the message is the only message in the thread, delete the thread.
203
+ return {
204
+ intents: [createIntent(ThreadAction.Delete, { subject, thread })],
205
+ };
206
+ } else {
207
+ thread.messages.splice(messageIndex, 1);
208
+ }
209
+
210
+ return {
211
+ undoable: {
212
+ message: ['message deleted label', { ns: THREAD_PLUGIN }],
213
+ data: { message, messageIndex },
214
+ },
215
+ intents: [
216
+ createIntent(ObservabilityAction.SendEvent, {
217
+ name: 'threads.message.delete',
218
+ properties: { threadId: thread.id, spaceId: space?.id },
219
+ }),
220
+ ],
221
+ };
222
+ } else {
223
+ if (!messageIndex || !message) {
224
+ return;
225
+ }
226
+
227
+ thread.messages.splice(messageIndex, 0, makeRef(message));
228
+ return {
229
+ intents: [
230
+ createIntent(ObservabilityAction.SendEvent, {
231
+ name: 'threads.message.undo-delete',
232
+ properties: { threadId: thread.id, spaceId: space?.id },
233
+ }),
234
+ ],
235
+ };
236
+ }
237
+ },
238
+ }),
239
+ ]);
@@ -0,0 +1,18 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capabilities, contributes, type PluginsContext } from '@dxos/app-framework';
6
+ import { MarkdownCapabilities } from '@dxos/plugin-markdown';
7
+
8
+ import { ThreadCapabilities } from './capabilities';
9
+ import { threads } from '../extensions';
10
+
11
+ export default (context: PluginsContext) =>
12
+ contributes(MarkdownCapabilities.Extensions, [
13
+ ({ document: doc }) => {
14
+ const { dispatchPromise: dispatch } = context.requestCapability(Capabilities.IntentDispatcher);
15
+ const { state } = context.requestCapability(ThreadCapabilities.MutableState);
16
+ return threads(state, doc, dispatch);
17
+ },
18
+ ]);
@@ -0,0 +1,61 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import React from 'react';
6
+
7
+ import { Capabilities, contributes, createSurface, useLayout } from '@dxos/app-framework';
8
+ import { type Ref } from '@dxos/echo-schema';
9
+ import { SettingsStore } from '@dxos/local-storage';
10
+ import { ChannelType, type ThreadType } from '@dxos/plugin-space/types';
11
+ import { getSpace } from '@dxos/react-client/echo';
12
+
13
+ import { ThreadContainer, ThreadComplementary, ThreadSettings } from '../components';
14
+ import { THREAD_PLUGIN } from '../meta';
15
+ import { type ThreadSettingsProps } from '../types';
16
+
17
+ export default () =>
18
+ contributes(Capabilities.ReactSurface, [
19
+ createSurface({
20
+ id: `${THREAD_PLUGIN}/channel`,
21
+ role: 'article',
22
+ filter: (data): data is { subject: ChannelType } =>
23
+ data.subject instanceof ChannelType && !!data.subject.threads[0],
24
+ component: ({ data, role }) => {
25
+ const layout = useLayout();
26
+ const channel = data.subject;
27
+ void channel.threads[0].load();
28
+ const thread = channel.threads[0].target;
29
+ if (!thread) {
30
+ return null;
31
+ }
32
+
33
+ const currentPosition = layout.active.findIndex((id) => id === channel.id);
34
+ if (currentPosition > 0) {
35
+ const objectToTheLeft = layout.active[currentPosition - 1];
36
+ const context = getSpace(channel)?.db.getObjectById(objectToTheLeft);
37
+ return <ThreadContainer role={role} thread={thread} context={context} />;
38
+ }
39
+
40
+ return <ThreadContainer role={role} thread={thread} />;
41
+ },
42
+ }),
43
+ createSurface({
44
+ id: `${THREAD_PLUGIN}/thread`,
45
+ role: 'complementary--comments',
46
+ filter: (data): data is { subject: { threads: Ref<ThreadType>[] } } =>
47
+ !!data.subject &&
48
+ typeof data.subject === 'object' &&
49
+ 'threads' in data.subject &&
50
+ Array.isArray(data.subject.threads) &&
51
+ !(data.subject instanceof ChannelType),
52
+ component: ({ data }) => <ThreadComplementary subject={data.subject} />,
53
+ }),
54
+ createSurface({
55
+ id: `${THREAD_PLUGIN}/settings`,
56
+ role: 'article',
57
+ filter: (data): data is { subject: SettingsStore<ThreadSettingsProps> } =>
58
+ data.subject instanceof SettingsStore && data.subject.prefix === THREAD_PLUGIN,
59
+ component: ({ data: { subject } }) => <ThreadSettings settings={subject.value} />,
60
+ }),
61
+ ]);
@@ -0,0 +1,14 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Capabilities, contributes } from '@dxos/app-framework';
6
+ import { create } from '@dxos/live-object';
7
+
8
+ import { THREAD_PLUGIN } from '../meta';
9
+ import { type ThreadSettingsProps, ThreadSettingsSchema } from '../types';
10
+
11
+ export default () => {
12
+ const settings = create<ThreadSettingsProps>({});
13
+ return contributes(Capabilities.Settings, { schema: ThreadSettingsSchema, prefix: THREAD_PLUGIN, value: settings });
14
+ };
@@ -0,0 +1,25 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { contributes } from '@dxos/app-framework';
6
+ import { create } from '@dxos/live-object';
7
+
8
+ import { ThreadCapabilities } from './capabilities';
9
+ import { type ThreadState, type ViewStore } from '../types';
10
+
11
+ const initialViewState = { showResolvedThreads: false };
12
+
13
+ export default () => {
14
+ const state = create<ThreadState>({ drafts: {} });
15
+ const viewStore = create<ViewStore>({});
16
+
17
+ const getViewState = (subjectId: string) => {
18
+ if (!viewStore[subjectId]) {
19
+ viewStore[subjectId] = { ...initialViewState };
20
+ }
21
+ return viewStore[subjectId];
22
+ };
23
+
24
+ return contributes(ThreadCapabilities.State, { state, getViewState });
25
+ };
@@ -7,14 +7,14 @@ import '@dxos-theme';
7
7
  import { type Meta } from '@storybook/react';
8
8
  import React, { useEffect, useState } from 'react';
9
9
 
10
- import { createSurface, SurfaceProvider } from '@dxos/app-framework';
10
+ import { Capabilities, contributes, createSurface } from '@dxos/app-framework';
11
+ import { withPluginManager } from '@dxos/app-framework/testing';
11
12
  import { MessageType, ThreadType } from '@dxos/plugin-space/types';
12
13
  import { faker } from '@dxos/random';
13
14
  import { useClient } from '@dxos/react-client';
14
15
  import { type Space } from '@dxos/react-client/echo';
15
16
  import { useIdentity } from '@dxos/react-client/halo';
16
- import { ClientRepeater } from '@dxos/react-client/testing';
17
- import { Mosaic } from '@dxos/react-ui-mosaic';
17
+ import { withClientProvider } from '@dxos/react-client/testing';
18
18
  import { Thread } from '@dxos/react-ui-thread';
19
19
  import { withLayout, withTheme } from '@dxos/storybook-utils';
20
20
 
@@ -46,24 +46,7 @@ const Story = () => {
46
46
  }
47
47
 
48
48
  return (
49
- <SurfaceProvider
50
- value={{
51
- surfaces: {
52
- ObjectMessage: createSurface({
53
- id: 'test',
54
- role: 'card',
55
- component: ({ role }) => <span>{JSON.stringify({ role })}</span>,
56
- }),
57
- },
58
- }}
59
- >
60
- <Mosaic.Root debug>
61
- <main className='max-is-prose mli-auto bs-dvh overflow-hidden'>
62
- {space && <ChatContainer thread={thread} />}
63
- </main>
64
- <Mosaic.DragOverlay />
65
- </Mosaic.Root>
66
- </SurfaceProvider>
49
+ <main className='max-is-prose mli-auto bs-dvh overflow-hidden'>{space && <ChatContainer thread={thread} />}</main>
67
50
  );
68
51
  };
69
52
 
@@ -72,9 +55,24 @@ export const Default = {};
72
55
  const meta: Meta = {
73
56
  title: 'plugins/plugin-thread/Chat',
74
57
  component: Thread,
75
- // TODO(burdon): Use decorator.
76
- render: () => <ClientRepeater component={Story} createIdentity createSpace types={[ThreadType, MessageType]} />,
77
- decorators: [withTheme, withLayout({ fullscreen: true, tooltips: true })],
58
+ render: () => <Story />,
59
+ decorators: [
60
+ withPluginManager({
61
+ capabilities: [
62
+ contributes(
63
+ Capabilities.ReactSurface,
64
+ createSurface({
65
+ id: 'test',
66
+ role: 'card',
67
+ component: ({ role }) => <span>{JSON.stringify({ role })}</span>,
68
+ }),
69
+ ),
70
+ ],
71
+ }),
72
+ withTheme,
73
+ withLayout({ fullscreen: true, tooltips: true }),
74
+ withClientProvider({ createSpace: true, types: [ThreadType, MessageType] }),
75
+ ],
78
76
  parameters: { translations },
79
77
  };
80
78
 
@@ -7,7 +7,7 @@ import React, { useLayoutEffect, useMemo, useRef, useState } from 'react';
7
7
  import { Ref } from '@dxos/echo-schema';
8
8
  import { create, makeRef, RefArray } from '@dxos/live-object';
9
9
  import { MessageType } from '@dxos/plugin-space/types';
10
- import { fullyQualifiedId, getSpace, useMembers, type Expando } 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 { Icon, ScrollArea, useThemeContext, useTranslation } from '@dxos/react-ui';
13
13
  import { createBasicExtensions, createThemeExtensions, listener } from '@dxos/react-ui-editor';
@@ -82,7 +82,7 @@ export const ChatContainer = ({ thread, context, current, autoFocusTextbox }: Th
82
82
  sender: { identityKey: identity.identityKey.toHex() },
83
83
  timestamp: new Date().toISOString(),
84
84
  text: messageRef.current,
85
- context: makeRef(context as Expando),
85
+ context: context ? makeRef(context) : undefined,
86
86
  }),
87
87
  ),
88
88
  );
@@ -5,8 +5,7 @@
5
5
  import { CheckCircle, X } from '@phosphor-icons/react';
6
6
  import React, { useCallback, useMemo, useRef, useState } from 'react';
7
7
 
8
- import { create, makeRef, RefArray } from '@dxos/live-object';
9
- import { MessageType } from '@dxos/plugin-space/types';
8
+ import { RefArray } from '@dxos/live-object';
10
9
  import { fullyQualifiedId, getSpace, useMembers } from '@dxos/react-client/echo';
11
10
  import { useIdentity } from '@dxos/react-client/halo';
12
11
  import { Button, Tag, Tooltip, useThemeContext, useTranslation } from '@dxos/react-ui';
@@ -19,7 +18,7 @@ import {
19
18
  mx,
20
19
  } from '@dxos/react-ui-theme';
21
20
  import { MessageTextbox, type MessageTextboxProps, Thread, ThreadFooter, ThreadHeading } from '@dxos/react-ui-thread';
22
- import { nonNullable } from '@dxos/util';
21
+ import { isNonNullable } from '@dxos/util';
23
22
 
24
23
  import { MessageContainer } from './MessageContainer';
25
24
  import { command } from './command-extension';
@@ -94,13 +93,12 @@ const DeleteThreadButton = ({ onDelete }: { onDelete: () => void }) => {
94
93
  export const CommentContainer = ({
95
94
  thread,
96
95
  detached,
97
- context,
98
96
  current,
99
97
  onAttend,
100
- onThreadDelete,
101
- onMessageDelete,
102
- onResolve,
103
98
  onComment,
99
+ onResolve,
100
+ onMessageDelete,
101
+ onThreadDelete,
104
102
  }: ThreadContainerProps) => {
105
103
  const identity = useIdentity()!;
106
104
  const space = getSpace(thread);
@@ -129,33 +127,26 @@ export const CommentContainer = ({
129
127
  const scrollToEnd = (behavior: ScrollBehavior) =>
130
128
  setTimeout(() => threadScrollRef.current?.scrollIntoView({ behavior, block: 'end' }), 10);
131
129
 
132
- const handleCreate: MessageTextboxProps['onSend'] = useCallback(() => {
130
+ const handleAttend = useCallback(() => onAttend?.(thread), [onAttend, thread]);
131
+ const handleResolve = useCallback(() => onResolve?.(thread), [onResolve, thread]);
132
+ const handleMessageDelete = useCallback((id: string) => onMessageDelete?.(thread, id), [onMessageDelete, thread]);
133
+ const handleThreadDelete = useCallback(() => onThreadDelete?.(thread), [onThreadDelete, thread]);
134
+
135
+ const handleComment: MessageTextboxProps['onSend'] = useCallback(() => {
133
136
  if (!messageRef.current) {
134
137
  return false;
135
138
  }
136
139
 
137
- thread.messages.push(
138
- makeRef(
139
- create(MessageType, {
140
- sender: { identityKey: identity.identityKey.toHex() },
141
- timestamp: new Date().toISOString(),
142
- text: messageRef.current,
143
- context: context ? makeRef(context) : undefined,
144
- }),
145
- ),
146
- );
147
- onComment?.(thread);
148
-
140
+ onComment?.(thread, messageRef.current);
149
141
  messageRef.current = '';
150
142
  scrollToEnd('instant');
151
143
  rerenderEditor();
152
144
 
153
- // TODO(burdon): Scroll to bottom.
154
145
  return true;
155
146
  }, [thread, identity]);
156
147
 
157
148
  return (
158
- <Thread onClickCapture={onAttend} onFocusCapture={onAttend} current={current} id={fullyQualifiedId(thread)}>
149
+ <Thread onClickCapture={handleAttend} onFocusCapture={handleAttend} current={current} id={fullyQualifiedId(thread)}>
159
150
  <div
160
151
  role='none'
161
152
  className={mx(
@@ -182,19 +173,14 @@ export const CommentContainer = ({
182
173
  <div className='flex flex-row items-center pli-1'>
183
174
  {thread.status === 'staged' && <Tag palette='neutral'>{t('draft button')}</Tag>}
184
175
  {onResolve && !(thread?.status === 'staged') && (
185
- <ToggleResolvedButton isResolved={thread?.status === 'resolved'} onResolve={onResolve} />
176
+ <ToggleResolvedButton isResolved={thread?.status === 'resolved'} onResolve={handleResolve} />
186
177
  )}
187
- {onThreadDelete && <DeleteThreadButton onDelete={onThreadDelete} />}
178
+ {onThreadDelete && <DeleteThreadButton onDelete={handleThreadDelete} />}
188
179
  </div>
189
180
  </div>
190
181
  {/** TODO(dmaretskyi): How's `thread.messages` undefined? */}
191
- {RefArray.allResolvedTargets(thread.messages?.filter(nonNullable) ?? []).map((message) => (
192
- <MessageContainer
193
- key={message.id}
194
- message={message}
195
- members={members}
196
- onDelete={(id: string) => onMessageDelete?.(id)}
197
- />
182
+ {RefArray.allResolvedTargets(thread.messages?.filter(isNonNullable) ?? []).map((message) => (
183
+ <MessageContainer key={message.id} message={message} members={members} onDelete={handleMessageDelete} />
198
184
  ))}
199
185
  {/*
200
186
  TODO(wittjosiah): Can't autofocus this generally.
@@ -202,7 +188,7 @@ export const CommentContainer = ({
202
188
  Also, it steals focus from documents when first rendered.
203
189
  Need to find a way to autofocus in one scenario only: when a new thread is created.
204
190
  */}
205
- <MessageTextbox extensions={extensions} onSend={handleCreate} {...textboxMetadata} />
191
+ <MessageTextbox extensions={extensions} onSend={handleComment} {...textboxMetadata} />
206
192
  <ThreadFooter activity={activity}>{t('activity message')}</ThreadFooter>
207
193
  {/* NOTE(thure): This can’t also be the `overflow-anchor` because `ScrollArea` injects an interceding node that contains this necessary ref’d element. */}
208
194
  <div role='none' className='bs-px -mbs-px' ref={threadScrollRef} />
@@ -7,6 +7,8 @@ import '@dxos-theme';
7
7
  import { type Meta } from '@storybook/react';
8
8
  import React, { useEffect, useState } from 'react';
9
9
 
10
+ import { IntentPlugin } from '@dxos/app-framework';
11
+ import { withPluginManager } from '@dxos/app-framework/testing';
10
12
  import { MessageType, ThreadType } from '@dxos/plugin-space/types';
11
13
  import { faker } from '@dxos/random';
12
14
  import { Filter, fullyQualifiedId, useQuery, useSpace } from '@dxos/react-client/echo';
@@ -44,7 +46,7 @@ const Story = ({ spaceKey }: ClientRepeatedComponentProps) => {
44
46
 
45
47
  return (
46
48
  <div className='flex justify-center overflow-y-auto bg-white dark:bg-black'>
47
- <div className='flex flex-col w-[400px]'>
49
+ <div className='flex flex-col w-[30rem]'>
48
50
  <CommentsContainer threads={threads} detached={detached} onThreadDelete={console.log} />
49
51
  </div>
50
52
  </div>
@@ -57,7 +59,11 @@ const meta: Meta = {
57
59
  title: 'plugins/plugin-thread/Comments',
58
60
  // TODO(wittjosiah): Use decorator.
59
61
  render: () => <ClientRepeater component={Story} createIdentity createSpace types={[ThreadType, MessageType]} />,
60
- decorators: [withTheme, withLayout({ fullscreen: true, tooltips: true })],
62
+ decorators: [
63
+ withTheme,
64
+ withLayout({ fullscreen: true, tooltips: true }),
65
+ withPluginManager({ plugins: [IntentPlugin()] }),
66
+ ],
61
67
  parameters: { translations },
62
68
  };
63
69
 
@@ -14,10 +14,7 @@ import { CommentContainer } from './CommentContainer';
14
14
  import { type ThreadContainerProps } from './types';
15
15
  import { THREAD_PLUGIN } from '../meta';
16
16
 
17
- export type ThreadsContainerProps = Omit<
18
- ThreadContainerProps,
19
- 'thread' | 'detached' | 'onAttend' | 'onThreadDelete' | 'onMessageDelete' | 'current' | 'autoFocus'
20
- > & {
17
+ export type ThreadsContainerProps = Omit<ThreadContainerProps, 'thread' | 'detached' | 'current' | 'autoFocus'> & {
21
18
  threads: ThreadType[];
22
19
  /**
23
20
  * Threads that are no longer anchored to a position in the object.
@@ -25,11 +22,6 @@ export type ThreadsContainerProps = Omit<
25
22
  detached?: string[];
26
23
  currentId?: string;
27
24
  showResolvedThreads?: boolean;
28
- onThreadAttend?: (thread: ThreadType) => void;
29
- onThreadDelete?: (thread: ThreadType) => void;
30
- onMessageDelete?: (thread: ThreadType, messageId: string) => void;
31
- onThreadToggleResolved?: (thread: ThreadType) => void;
32
- onComment?: (thread: ThreadType) => void;
33
25
  };
34
26
 
35
27
  /**
@@ -40,10 +32,6 @@ export const CommentsContainer = ({
40
32
  detached = [],
41
33
  currentId,
42
34
  showResolvedThreads,
43
- onThreadAttend,
44
- onThreadDelete,
45
- onMessageDelete,
46
- onThreadToggleResolved,
47
35
  ...props
48
36
  }: ThreadsContainerProps) => {
49
37
  const { t } = useTranslation(THREAD_PLUGIN);
@@ -66,10 +54,6 @@ export const CommentsContainer = ({
66
54
  thread={thread}
67
55
  current={currentId === threadId}
68
56
  detached={detached.includes(threadId)}
69
- {...(onThreadAttend && { onAttend: () => onThreadAttend(thread) })}
70
- {...(onThreadDelete && { onThreadDelete: () => onThreadDelete(thread) })}
71
- {...(onMessageDelete && { onMessageDelete: (messageId: string) => onMessageDelete(thread, messageId) })}
72
- {...(onThreadToggleResolved && { onResolve: () => onThreadToggleResolved(thread) })}
73
57
  {...props}
74
58
  />
75
59
  );