@meshagent/meshagent-react 0.38.2 → 0.38.3
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/CHANGELOG.md +14 -1
- package/README.md +78 -41
- package/dist/cjs/client-toolkits.d.ts +2 -2
- package/dist/cjs/client-toolkits.js +143 -10
- package/dist/cjs/document-connection-scope.d.ts +7 -12
- package/dist/cjs/document-connection-scope.js +88 -82
- package/dist/cjs/index.d.ts +3 -2
- package/dist/cjs/index.js +3 -2
- package/dist/cjs/livekit-client.d.ts +23 -0
- package/dist/cjs/livekit-client.js +66 -0
- package/dist/cjs/livekit-protocol.d.ts +21 -0
- package/dist/cjs/livekit-protocol.js +97 -0
- package/dist/cjs/room-connection-scope.d.ts +16 -16
- package/dist/cjs/room-connection-scope.js +207 -90
- package/dist/cjs/room-participants.d.ts +2 -0
- package/dist/cjs/room-participants.js +46 -0
- package/dist/esm/client-toolkits.d.ts +2 -2
- package/dist/esm/client-toolkits.js +145 -12
- package/dist/esm/document-connection-scope.d.ts +7 -12
- package/dist/esm/document-connection-scope.js +88 -81
- package/dist/esm/index.d.ts +3 -2
- package/dist/esm/index.js +3 -2
- package/dist/esm/livekit-client.d.ts +23 -0
- package/dist/esm/livekit-client.js +61 -0
- package/dist/esm/livekit-protocol.d.ts +21 -0
- package/dist/esm/livekit-protocol.js +60 -0
- package/dist/esm/room-connection-scope.d.ts +16 -16
- package/dist/esm/room-connection-scope.js +205 -89
- package/dist/esm/room-participants.d.ts +2 -0
- package/dist/esm/room-participants.js +43 -0
- package/package.json +2 -2
- package/dist/cjs/chat.d.ts +0 -33
- package/dist/cjs/chat.js +0 -207
- package/dist/cjs/file-upload.d.ts +0 -43
- package/dist/cjs/file-upload.js +0 -168
- package/dist/esm/chat.d.ts +0 -33
- package/dist/esm/chat.js +0 -201
- package/dist/esm/file-upload.d.ts +0 -43
- package/dist/esm/file-upload.js +0 -163
|
@@ -1,7 +1,14 @@
|
|
|
1
|
-
import { useEffect,
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { ParticipantToken, RoomClient, RoomMessageEvent, RoomServerException, WebSocketClientProtocol, } from '@meshagent/meshagent';
|
|
2
3
|
import { subscribe } from './subscribe-async-gen';
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
const retryBaseDelayMs = 500;
|
|
5
|
+
const retryMaxDelayMs = 30000;
|
|
6
|
+
function getRetryDelayMs(retryCount) {
|
|
7
|
+
return Math.min(retryMaxDelayMs, retryBaseDelayMs * 2 ** retryCount);
|
|
8
|
+
}
|
|
9
|
+
function isRetryableConnectionError(error) {
|
|
10
|
+
return error instanceof RoomServerException && error.retryable;
|
|
11
|
+
}
|
|
5
12
|
/* -------------------------------------------------
|
|
6
13
|
* Authorization helpers
|
|
7
14
|
* ------------------------------------------------- */
|
|
@@ -14,71 +21,169 @@ export const developmentAuthorization = ({ url, projectId, apiKeyId, participant
|
|
|
14
21
|
token.addRoomGrant(roomName);
|
|
15
22
|
token.addRoleGrant('user');
|
|
16
23
|
const jwt = await token.toJwt({ token: secret });
|
|
17
|
-
return {
|
|
24
|
+
return {
|
|
25
|
+
jwt,
|
|
26
|
+
projectId,
|
|
27
|
+
roomName,
|
|
28
|
+
roomUrl: url,
|
|
29
|
+
};
|
|
18
30
|
};
|
|
19
|
-
export
|
|
20
|
-
|
|
31
|
+
export const staticAuthorization = ({ projectId, roomName, url, jwt, }) => async () => ({
|
|
32
|
+
jwt,
|
|
33
|
+
projectId,
|
|
34
|
+
roomName,
|
|
35
|
+
roomUrl: url,
|
|
36
|
+
});
|
|
37
|
+
export function useRoomConnection({ reconnectKey, authorization, enableMessaging = true, onReady, oauthTokenRequestHandler, secretRequestHandler, roomClientFactory, }) {
|
|
21
38
|
const [client, setClient] = useState(null);
|
|
22
39
|
const [ready, setReady] = useState(false);
|
|
23
40
|
const [state, setState] = useState('authorizing');
|
|
24
41
|
const [error, setError] = useState(null);
|
|
25
|
-
// Keep the latest client in a ref so we can call `dispose` in cleanup.
|
|
26
42
|
const clientRef = useRef(null);
|
|
27
|
-
|
|
28
|
-
// Instance method exposed to consumers (rarely needed).
|
|
29
|
-
const dispose = () => {
|
|
30
|
-
clientRef.current?.dispose();
|
|
31
|
-
setState('done');
|
|
32
|
-
};
|
|
43
|
+
const cancelConnectionRef = useRef(() => { });
|
|
33
44
|
useEffect(() => {
|
|
34
45
|
let cancelled = false;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
46
|
+
let retryTimeout = null;
|
|
47
|
+
const clearRetryTimeout = () => {
|
|
48
|
+
if (retryTimeout != null) {
|
|
49
|
+
clearTimeout(retryTimeout);
|
|
50
|
+
retryTimeout = null;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
const disposeClient = (room) => {
|
|
54
|
+
if (room == null) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
if (clientRef.current === room) {
|
|
58
|
+
clientRef.current = null;
|
|
59
|
+
}
|
|
60
|
+
room.dispose();
|
|
61
|
+
};
|
|
62
|
+
const waitForRetry = (delayMs) => new Promise((resolve) => {
|
|
63
|
+
retryTimeout = setTimeout(() => {
|
|
64
|
+
retryTimeout = null;
|
|
65
|
+
resolve();
|
|
66
|
+
}, delayMs);
|
|
67
|
+
});
|
|
68
|
+
setClient(null);
|
|
69
|
+
setReady(false);
|
|
70
|
+
const cancelConnection = ({ updateState }) => {
|
|
71
|
+
if (cancelled) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
cancelled = true;
|
|
75
|
+
clearRetryTimeout();
|
|
76
|
+
const currentClient = clientRef.current;
|
|
77
|
+
clientRef.current = null;
|
|
78
|
+
disposeClient(currentClient);
|
|
79
|
+
if (updateState) {
|
|
80
|
+
setClient(null);
|
|
81
|
+
setReady(false);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
cancelConnectionRef.current = () => cancelConnection({ updateState: true });
|
|
85
|
+
void (async () => {
|
|
86
|
+
let retryCount = 0;
|
|
87
|
+
while (!cancelled) {
|
|
88
|
+
let connectionInfo;
|
|
89
|
+
try {
|
|
90
|
+
setState('authorizing');
|
|
91
|
+
setError(null);
|
|
92
|
+
connectionInfo = await authorization();
|
|
93
|
+
}
|
|
94
|
+
catch (nextError) {
|
|
95
|
+
if (cancelled) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
setError(nextError);
|
|
99
|
+
setState('done');
|
|
100
|
+
setReady(false);
|
|
40
101
|
return;
|
|
41
|
-
const room = new RoomClient({
|
|
42
|
-
protocolFactory: WebSocketClientProtocol.createFactory({ url, token: jwt }),
|
|
43
|
-
});
|
|
44
|
-
setClient(room);
|
|
45
|
-
setState('connecting');
|
|
46
|
-
await room.start({
|
|
47
|
-
onDone: () => {
|
|
48
|
-
if (cancelled)
|
|
49
|
-
return;
|
|
50
|
-
setState('done');
|
|
51
|
-
},
|
|
52
|
-
onError: (e) => {
|
|
53
|
-
if (cancelled)
|
|
54
|
-
return;
|
|
55
|
-
setError(e);
|
|
56
|
-
setState('done');
|
|
57
|
-
},
|
|
58
|
-
});
|
|
59
|
-
if (enableMessaging) {
|
|
60
|
-
room.messaging.enable();
|
|
61
102
|
}
|
|
62
|
-
if (cancelled)
|
|
103
|
+
if (cancelled) {
|
|
63
104
|
return;
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
105
|
+
}
|
|
106
|
+
let nextClient;
|
|
107
|
+
if (roomClientFactory != null) {
|
|
108
|
+
nextClient = roomClientFactory(connectionInfo);
|
|
109
|
+
}
|
|
110
|
+
else {
|
|
111
|
+
nextClient = new RoomClient({
|
|
112
|
+
protocolFactory: WebSocketClientProtocol.createFactory({
|
|
113
|
+
url: connectionInfo.roomUrl,
|
|
114
|
+
token: connectionInfo.jwt,
|
|
115
|
+
}),
|
|
116
|
+
oauthTokenRequestHandler: oauthTokenRequestHandler == null
|
|
117
|
+
? undefined
|
|
118
|
+
: (request) => oauthTokenRequestHandler(nextClient, request),
|
|
119
|
+
secretRequestHandler: secretRequestHandler == null
|
|
120
|
+
? undefined
|
|
121
|
+
: (request) => secretRequestHandler(nextClient, request),
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
clientRef.current = nextClient;
|
|
125
|
+
setClient(nextClient);
|
|
126
|
+
setReady(false);
|
|
127
|
+
setState('connecting');
|
|
128
|
+
try {
|
|
129
|
+
await nextClient.start({
|
|
130
|
+
onDone: () => {
|
|
131
|
+
if (cancelled || clientRef.current !== nextClient) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
setReady(false);
|
|
135
|
+
setState('done');
|
|
136
|
+
},
|
|
137
|
+
onError: (nextError) => {
|
|
138
|
+
if (cancelled || clientRef.current !== nextClient) {
|
|
139
|
+
return;
|
|
140
|
+
}
|
|
141
|
+
setError(nextError);
|
|
142
|
+
setReady(false);
|
|
143
|
+
setState('done');
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
if (enableMessaging) {
|
|
147
|
+
nextClient.messaging.enable();
|
|
148
|
+
}
|
|
149
|
+
if (cancelled || clientRef.current !== nextClient) {
|
|
150
|
+
disposeClient(nextClient);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
retryCount = 0;
|
|
154
|
+
setError(null);
|
|
155
|
+
setReady(true);
|
|
156
|
+
setState('ready');
|
|
157
|
+
onReady?.(nextClient);
|
|
69
158
|
return;
|
|
70
|
-
|
|
71
|
-
|
|
159
|
+
}
|
|
160
|
+
catch (nextError) {
|
|
161
|
+
disposeClient(nextClient);
|
|
162
|
+
if (cancelled) {
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
setClient((currentClient) => currentClient === nextClient ? null : currentClient);
|
|
166
|
+
setReady(false);
|
|
167
|
+
setError(nextError);
|
|
168
|
+
if (!isRetryableConnectionError(nextError)) {
|
|
169
|
+
setState('done');
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
setState('retrying');
|
|
173
|
+
await waitForRetry(getRetryDelayMs(retryCount));
|
|
174
|
+
retryCount += 1;
|
|
175
|
+
}
|
|
72
176
|
}
|
|
73
|
-
};
|
|
74
|
-
connect();
|
|
177
|
+
})();
|
|
75
178
|
return () => {
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
dispose();
|
|
179
|
+
cancelConnection({ updateState: false });
|
|
180
|
+
cancelConnectionRef.current = () => { };
|
|
79
181
|
};
|
|
80
|
-
|
|
81
|
-
|
|
182
|
+
}, [reconnectKey, enableMessaging, roomClientFactory]);
|
|
183
|
+
const dispose = () => {
|
|
184
|
+
cancelConnectionRef.current();
|
|
185
|
+
setState('done');
|
|
186
|
+
};
|
|
82
187
|
return {
|
|
83
188
|
client,
|
|
84
189
|
state,
|
|
@@ -94,48 +199,59 @@ export function useRoomIndicators({ room, path }) {
|
|
|
94
199
|
const [typing, setTyping] = useState(false);
|
|
95
200
|
const [thinking, setThinking] = useState(false);
|
|
96
201
|
useEffect(() => {
|
|
97
|
-
if (!room)
|
|
202
|
+
if (!room) {
|
|
98
203
|
return;
|
|
99
|
-
|
|
204
|
+
}
|
|
205
|
+
setTyping(false);
|
|
206
|
+
setThinking(false);
|
|
207
|
+
const subscription = subscribe(room.listen(), {
|
|
100
208
|
next: (event) => {
|
|
101
|
-
if (event instanceof RoomMessageEvent) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
typingMap.current[message.fromParticipantId] = setTimeout(() => {
|
|
116
|
-
delete typingMap.current[message.fromParticipantId];
|
|
117
|
-
setTyping(Object.keys(typingMap.current).length > 0);
|
|
118
|
-
}, 1000);
|
|
119
|
-
// Update typing state
|
|
209
|
+
if (!(event instanceof RoomMessageEvent)) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
const { message } = event;
|
|
213
|
+
if (message.fromParticipantId === room.localParticipant?.id) {
|
|
214
|
+
return;
|
|
215
|
+
}
|
|
216
|
+
if (message.message.path !== path) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
if (message.type === 'typing') {
|
|
220
|
+
clearTimeout(typingMap.current[message.fromParticipantId]);
|
|
221
|
+
typingMap.current[message.fromParticipantId] = setTimeout(() => {
|
|
222
|
+
delete typingMap.current[message.fromParticipantId];
|
|
120
223
|
setTyping(Object.keys(typingMap.current).length > 0);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
224
|
+
}, 1000);
|
|
225
|
+
setTyping(Object.keys(typingMap.current).length > 0);
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
if (message.type !== 'thinking') {
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
clearTimeout(thinkingMap.current[message.fromParticipantId]);
|
|
232
|
+
if (message.message.thinking) {
|
|
233
|
+
thinkingMap.current[message.fromParticipantId] = setTimeout(() => {
|
|
234
|
+
delete thinkingMap.current[message.fromParticipantId];
|
|
133
235
|
setThinking(Object.keys(thinkingMap.current).length > 0);
|
|
134
|
-
}
|
|
236
|
+
}, 5000);
|
|
237
|
+
}
|
|
238
|
+
else {
|
|
239
|
+
delete thinkingMap.current[message.fromParticipantId];
|
|
135
240
|
}
|
|
241
|
+
setThinking(Object.keys(thinkingMap.current).length > 0);
|
|
136
242
|
},
|
|
137
243
|
});
|
|
138
|
-
return () =>
|
|
139
|
-
|
|
244
|
+
return () => {
|
|
245
|
+
subscription.unsubscribe();
|
|
246
|
+
for (const timeout of Object.values(typingMap.current)) {
|
|
247
|
+
clearTimeout(timeout);
|
|
248
|
+
}
|
|
249
|
+
for (const timeout of Object.values(thinkingMap.current)) {
|
|
250
|
+
clearTimeout(timeout);
|
|
251
|
+
}
|
|
252
|
+
typingMap.current = {};
|
|
253
|
+
thinkingMap.current = {};
|
|
254
|
+
};
|
|
255
|
+
}, [path, room]);
|
|
140
256
|
return { typing, thinking };
|
|
141
257
|
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
function sameParticipantIds(currentParticipants, nextParticipants) {
|
|
3
|
+
if (currentParticipants.length !== nextParticipants.length) {
|
|
4
|
+
return false;
|
|
5
|
+
}
|
|
6
|
+
const currentParticipantIds = new Set(currentParticipants.map((participant) => participant.id));
|
|
7
|
+
for (const participant of nextParticipants) {
|
|
8
|
+
if (!currentParticipantIds.has(participant.id)) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
return true;
|
|
13
|
+
}
|
|
14
|
+
export function useRoomParticipants(room) {
|
|
15
|
+
const [participants, setParticipants] = useState([]);
|
|
16
|
+
useEffect(() => {
|
|
17
|
+
if (room == null) {
|
|
18
|
+
setParticipants([]);
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const updateParticipants = () => {
|
|
22
|
+
const nextParticipants = room.messaging.remoteParticipants;
|
|
23
|
+
setParticipants((currentParticipants) => {
|
|
24
|
+
if (sameParticipantIds(currentParticipants, nextParticipants)) {
|
|
25
|
+
return currentParticipants;
|
|
26
|
+
}
|
|
27
|
+
return [...nextParticipants];
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
room.messaging.on('participant_added', updateParticipants);
|
|
31
|
+
room.messaging.on('participant_removed', updateParticipants);
|
|
32
|
+
room.messaging.on('messaging_enabled', updateParticipants);
|
|
33
|
+
room.on('disconnected', updateParticipants);
|
|
34
|
+
updateParticipants();
|
|
35
|
+
return () => {
|
|
36
|
+
room.messaging.off('participant_added', updateParticipants);
|
|
37
|
+
room.messaging.off('participant_removed', updateParticipants);
|
|
38
|
+
room.messaging.off('messaging_enabled', updateParticipants);
|
|
39
|
+
room.off('disconnected', updateParticipants);
|
|
40
|
+
};
|
|
41
|
+
}, [room]);
|
|
42
|
+
return participants;
|
|
43
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@meshagent/meshagent-react",
|
|
3
|
-
"version": "0.38.
|
|
3
|
+
"version": "0.38.3",
|
|
4
4
|
"description": "Meshagent React Client",
|
|
5
5
|
"homepage": "https://github.com/meshagent/meshagent-react",
|
|
6
6
|
"scripts": {
|
|
@@ -36,7 +36,7 @@
|
|
|
36
36
|
"base-64": "^1.0.0",
|
|
37
37
|
"livekit-client": "^2.15.5",
|
|
38
38
|
"react": "^19.1.0",
|
|
39
|
-
"@meshagent/meshagent": "^0.38.
|
|
39
|
+
"@meshagent/meshagent": "^0.38.3",
|
|
40
40
|
"react-dom": "^19.1.0",
|
|
41
41
|
"typescript": "^5.8.3",
|
|
42
42
|
"uuid": "^11.1.0",
|
package/dist/cjs/chat.d.ts
DELETED
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
import { RoomClient, Element, Participant } from "@meshagent/meshagent";
|
|
2
|
-
import { FileUpload } from "./file-upload";
|
|
3
|
-
export interface ChatMessageArgs {
|
|
4
|
-
id: string;
|
|
5
|
-
text: string;
|
|
6
|
-
attachments?: string[];
|
|
7
|
-
}
|
|
8
|
-
export declare class ChatMessage {
|
|
9
|
-
id: string;
|
|
10
|
-
text: string;
|
|
11
|
-
attachments: string[];
|
|
12
|
-
constructor({ id, text, attachments }: ChatMessageArgs);
|
|
13
|
-
}
|
|
14
|
-
export interface UseMessageChatProps {
|
|
15
|
-
room: RoomClient;
|
|
16
|
-
path: string;
|
|
17
|
-
participants?: Participant[];
|
|
18
|
-
participantNames?: string[];
|
|
19
|
-
includeLocalParticipant?: boolean;
|
|
20
|
-
initialMessage?: ChatMessage;
|
|
21
|
-
}
|
|
22
|
-
export interface UseMessageChatResult {
|
|
23
|
-
messages: Element[];
|
|
24
|
-
sendMessage: (message: ChatMessage) => void;
|
|
25
|
-
selectAttachments: (files: File[]) => void;
|
|
26
|
-
attachments: FileUpload[];
|
|
27
|
-
setAttachments: (attachments: FileUpload[]) => void;
|
|
28
|
-
schemaFileExists: boolean;
|
|
29
|
-
onlineParticipants: Participant[];
|
|
30
|
-
cancelRequest?: () => void;
|
|
31
|
-
}
|
|
32
|
-
export declare function fileToAsyncIterable(file: File): AsyncIterable<Uint8Array>;
|
|
33
|
-
export declare function useChat({ room, path, participants, participantNames, initialMessage, includeLocalParticipant }: UseMessageChatProps): UseMessageChatResult;
|
package/dist/cjs/chat.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ChatMessage = void 0;
|
|
4
|
-
exports.fileToAsyncIterable = fileToAsyncIterable;
|
|
5
|
-
exports.useChat = useChat;
|
|
6
|
-
const react_1 = require("react");
|
|
7
|
-
const meshagent_1 = require("@meshagent/meshagent");
|
|
8
|
-
const file_upload_1 = require("./file-upload");
|
|
9
|
-
const document_connection_scope_1 = require("./document-connection-scope");
|
|
10
|
-
const document_connection_scope_2 = require("./document-connection-scope");
|
|
11
|
-
class ChatMessage {
|
|
12
|
-
constructor({ id, text, attachments }) {
|
|
13
|
-
Object.defineProperty(this, "id", {
|
|
14
|
-
enumerable: true,
|
|
15
|
-
configurable: true,
|
|
16
|
-
writable: true,
|
|
17
|
-
value: void 0
|
|
18
|
-
});
|
|
19
|
-
Object.defineProperty(this, "text", {
|
|
20
|
-
enumerable: true,
|
|
21
|
-
configurable: true,
|
|
22
|
-
writable: true,
|
|
23
|
-
value: void 0
|
|
24
|
-
});
|
|
25
|
-
Object.defineProperty(this, "attachments", {
|
|
26
|
-
enumerable: true,
|
|
27
|
-
configurable: true,
|
|
28
|
-
writable: true,
|
|
29
|
-
value: void 0
|
|
30
|
-
});
|
|
31
|
-
this.id = id;
|
|
32
|
-
this.text = text;
|
|
33
|
-
this.attachments = attachments ?? [];
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
exports.ChatMessage = ChatMessage;
|
|
37
|
-
function getParticipantName(participant) {
|
|
38
|
-
const name = participant.getAttribute("name");
|
|
39
|
-
return typeof name === "string" && name.length > 0 ? name : null;
|
|
40
|
-
}
|
|
41
|
-
function ensureParticipants(document, localParticipant, includeLocalParticipant, participants, participantNames) {
|
|
42
|
-
const retParticipants = [
|
|
43
|
-
...(participants ?? []),
|
|
44
|
-
...(includeLocalParticipant ? [localParticipant] : []),
|
|
45
|
-
];
|
|
46
|
-
const existing = new Set();
|
|
47
|
-
for (const child of document.root.getChildren()
|
|
48
|
-
.filter((c) => c instanceof meshagent_1.Element)) {
|
|
49
|
-
if (child.tagName === "members") {
|
|
50
|
-
for (const member of child.getChildren()
|
|
51
|
-
.filter((c) => c instanceof meshagent_1.Element)) {
|
|
52
|
-
const name = getParticipantName(member);
|
|
53
|
-
if (name)
|
|
54
|
-
existing.add(name);
|
|
55
|
-
}
|
|
56
|
-
for (const part of retParticipants) {
|
|
57
|
-
const name = getParticipantName(part);
|
|
58
|
-
if (name && !existing.has(name)) {
|
|
59
|
-
child.createChildElement("member", { name });
|
|
60
|
-
existing.add(name);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
if (participantNames != null) {
|
|
64
|
-
for (const name of participantNames) {
|
|
65
|
-
if (!existing.has(name)) {
|
|
66
|
-
child.createChildElement("member", { name });
|
|
67
|
-
existing.add(name);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
function mapMessages(doc) {
|
|
75
|
-
const children = doc.root.getChildren() || [];
|
|
76
|
-
const thread = children.find((c) => c.tagName === "messages");
|
|
77
|
-
const threadChildren = thread?.getChildren() || [];
|
|
78
|
-
return threadChildren.filter((el) => el.tagName === "message");
|
|
79
|
-
}
|
|
80
|
-
function* getParticipantNames(document) {
|
|
81
|
-
const children = document.root.getChildren() || [];
|
|
82
|
-
const memberNode = children.find((c) => c.tagName === "members");
|
|
83
|
-
const members = memberNode?.getChildren() || [];
|
|
84
|
-
for (const member of members) {
|
|
85
|
-
const name = getParticipantName(member);
|
|
86
|
-
if (name) {
|
|
87
|
-
yield name;
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
function* getOnlineParticipants(roomParticipants, participantNames) {
|
|
92
|
-
for (const participantName of participantNames) {
|
|
93
|
-
for (const remoteParticipant of roomParticipants) {
|
|
94
|
-
if (getParticipantName(remoteParticipant) === participantName) {
|
|
95
|
-
yield remoteParticipant;
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
const chunkSize = 64 * 1024; // 64 KB
|
|
101
|
-
function fileToAsyncIterable(file) {
|
|
102
|
-
const hasNativeStream = typeof file.stream === 'function';
|
|
103
|
-
async function* nativeStream() {
|
|
104
|
-
const reader = file.stream().getReader();
|
|
105
|
-
try {
|
|
106
|
-
while (true) {
|
|
107
|
-
const { done, value } = await reader.read();
|
|
108
|
-
if (done)
|
|
109
|
-
break;
|
|
110
|
-
yield value;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
finally {
|
|
114
|
-
reader.releaseLock();
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
async function* sliceStream() {
|
|
118
|
-
let offset = 0;
|
|
119
|
-
while (offset < file.size) {
|
|
120
|
-
const blob = file.slice(offset, offset + chunkSize);
|
|
121
|
-
const buffer = await blob.arrayBuffer();
|
|
122
|
-
yield new Uint8Array(buffer);
|
|
123
|
-
offset += chunkSize;
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
return (hasNativeStream ? nativeStream : sliceStream)();
|
|
127
|
-
}
|
|
128
|
-
function useChat({ room, path, participants, participantNames, initialMessage, includeLocalParticipant }) {
|
|
129
|
-
const { document, schemaFileExists } = (0, document_connection_scope_2.useDocumentConnection)({
|
|
130
|
-
room,
|
|
131
|
-
path,
|
|
132
|
-
onConnected: (doc) => {
|
|
133
|
-
ensureParticipants(doc, room.localParticipant, includeLocalParticipant ?? true, participants ?? [], participantNames ?? []);
|
|
134
|
-
if (initialMessage) {
|
|
135
|
-
sendMessage(initialMessage);
|
|
136
|
-
}
|
|
137
|
-
},
|
|
138
|
-
onError: (error) => {
|
|
139
|
-
console.error("Failed to connect to document:", error);
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
const [messages, setMessages] = (0, react_1.useState)(() => document ? mapMessages(document) : []);
|
|
143
|
-
const [attachments, setAttachments] = (0, react_1.useState)([]);
|
|
144
|
-
const [documentMembers, setDocumentMembers] = (0, react_1.useState)(() => document ? getParticipantNames(document) : []);
|
|
145
|
-
(0, document_connection_scope_2.useDocumentChanged)({
|
|
146
|
-
document,
|
|
147
|
-
onChanged: (doc) => {
|
|
148
|
-
setMessages(mapMessages(doc));
|
|
149
|
-
setDocumentMembers(getParticipantNames(doc));
|
|
150
|
-
},
|
|
151
|
-
});
|
|
152
|
-
const selectAttachments = (0, react_1.useCallback)((files) => {
|
|
153
|
-
const attachmentsToUpload = files.map((file) => new file_upload_1.MeshagentFileUpload(room, `uploaded-files/${file.name}`, fileToAsyncIterable(file), file.size));
|
|
154
|
-
setAttachments(attachmentsToUpload);
|
|
155
|
-
}, [room]);
|
|
156
|
-
const roomParticipants = (0, document_connection_scope_1.useRoomParticipants)(room);
|
|
157
|
-
const onlineParticipants = (0, react_1.useMemo)(() => Array.from(getOnlineParticipants(roomParticipants, documentMembers)), [roomParticipants, documentMembers]);
|
|
158
|
-
const sendMessage = (0, react_1.useCallback)((message) => {
|
|
159
|
-
const children = document?.root.getChildren() || [];
|
|
160
|
-
const thread = children.find((c) => c.tagName === "messages");
|
|
161
|
-
if (!thread) {
|
|
162
|
-
return;
|
|
163
|
-
}
|
|
164
|
-
const m = thread.createChildElement("message", {
|
|
165
|
-
id: message.id,
|
|
166
|
-
text: message.text,
|
|
167
|
-
created_at: new Date().toISOString(),
|
|
168
|
-
author_name: getParticipantName(room.localParticipant) ?? "",
|
|
169
|
-
author_ref: null,
|
|
170
|
-
});
|
|
171
|
-
for (const path of message.attachments) {
|
|
172
|
-
m.createChildElement("file", { path });
|
|
173
|
-
}
|
|
174
|
-
for (const participant of onlineParticipants) {
|
|
175
|
-
room.messaging.sendMessage({
|
|
176
|
-
to: participant,
|
|
177
|
-
type: "chat",
|
|
178
|
-
message: {
|
|
179
|
-
path,
|
|
180
|
-
text: message.text,
|
|
181
|
-
attachments: message.attachments.map(path => ({ path })),
|
|
182
|
-
},
|
|
183
|
-
});
|
|
184
|
-
}
|
|
185
|
-
}, [document, path, attachments, onlineParticipants, room]);
|
|
186
|
-
const cancelRequest = (0, react_1.useCallback)(() => {
|
|
187
|
-
for (const participant of onlineParticipants) {
|
|
188
|
-
if (participant instanceof meshagent_1.RemoteParticipant && participant.role === 'agent') {
|
|
189
|
-
room.messaging.sendMessage({
|
|
190
|
-
to: participant,
|
|
191
|
-
type: "cancel",
|
|
192
|
-
message: { path },
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
}, [room, path, onlineParticipants]);
|
|
197
|
-
return {
|
|
198
|
-
messages,
|
|
199
|
-
sendMessage,
|
|
200
|
-
selectAttachments,
|
|
201
|
-
attachments,
|
|
202
|
-
setAttachments,
|
|
203
|
-
schemaFileExists,
|
|
204
|
-
onlineParticipants,
|
|
205
|
-
cancelRequest,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { EventEmitter, RoomClient } from "@meshagent/meshagent";
|
|
2
|
-
export declare enum UploadStatus {
|
|
3
|
-
Initial = "initial",
|
|
4
|
-
Uploading = "uploading",
|
|
5
|
-
Completed = "completed",
|
|
6
|
-
Failed = "failed"
|
|
7
|
-
}
|
|
8
|
-
interface UploadStatusEvent {
|
|
9
|
-
status: UploadStatus;
|
|
10
|
-
progress?: number;
|
|
11
|
-
}
|
|
12
|
-
export declare abstract class FileUpload extends EventEmitter<UploadStatusEvent> {
|
|
13
|
-
path: string;
|
|
14
|
-
size: number;
|
|
15
|
-
protected _status: UploadStatus;
|
|
16
|
-
protected constructor(path: string, size?: number);
|
|
17
|
-
get status(): UploadStatus;
|
|
18
|
-
protected set status(value: UploadStatus);
|
|
19
|
-
abstract get bytesUploaded(): number;
|
|
20
|
-
abstract get done(): Promise<void>;
|
|
21
|
-
get filename(): string;
|
|
22
|
-
abstract startUpload(): void;
|
|
23
|
-
}
|
|
24
|
-
export declare class MeshagentFileUpload extends FileUpload {
|
|
25
|
-
readonly room: RoomClient;
|
|
26
|
-
readonly dataStream: AsyncIterable<Uint8Array>;
|
|
27
|
-
private _bytesUploaded;
|
|
28
|
-
private _done;
|
|
29
|
-
private _resolveDone;
|
|
30
|
-
private _rejectDone;
|
|
31
|
-
private _downloadUrl;
|
|
32
|
-
private _resolveUrl;
|
|
33
|
-
private _rejectUrl;
|
|
34
|
-
constructor(room: RoomClient, path: string, dataStream: AsyncIterable<Uint8Array>, size?: number, autoStart?: boolean);
|
|
35
|
-
static deferred(room: RoomClient, path: string, dataStream: AsyncIterable<Uint8Array>, size?: number): MeshagentFileUpload;
|
|
36
|
-
get bytesUploaded(): number;
|
|
37
|
-
get done(): Promise<void>;
|
|
38
|
-
/** Resolves to the server’s public download URL – like Dart version. */
|
|
39
|
-
get downloadUrl(): Promise<URL>;
|
|
40
|
-
startUpload(): void;
|
|
41
|
-
private _upload;
|
|
42
|
-
}
|
|
43
|
-
export {};
|