@meshagent/meshagent-react 0.4.0
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 +3 -0
- package/LICENSE +201 -0
- package/README.md +0 -0
- package/dist/cjs/chat.d.ts +29 -0
- package/dist/cjs/chat.js +178 -0
- package/dist/cjs/client-toolkits.d.ts +7 -0
- package/dist/cjs/client-toolkits.js +11 -0
- package/dist/cjs/document-connection-scope.d.ts +26 -0
- package/dist/cjs/document-connection-scope.js +70 -0
- package/dist/cjs/file-upload.d.ts +43 -0
- package/dist/cjs/file-upload.js +167 -0
- package/dist/cjs/index.d.ts +5 -0
- package/dist/cjs/index.js +21 -0
- package/dist/cjs/room-connection-scope.d.ts +47 -0
- package/dist/cjs/room-connection-scope.js +148 -0
- package/dist/cjs/subscribe-async-gen.d.ts +10 -0
- package/dist/cjs/subscribe-async-gen.js +35 -0
- package/dist/esm/chat.d.ts +29 -0
- package/dist/esm/chat.js +172 -0
- package/dist/esm/client-toolkits.d.ts +7 -0
- package/dist/esm/client-toolkits.js +7 -0
- package/dist/esm/document-connection-scope.d.ts +26 -0
- package/dist/esm/document-connection-scope.js +66 -0
- package/dist/esm/file-upload.d.ts +43 -0
- package/dist/esm/file-upload.js +162 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +5 -0
- package/dist/esm/room-connection-scope.d.ts +47 -0
- package/dist/esm/room-connection-scope.js +141 -0
- package/dist/esm/subscribe-async-gen.d.ts +10 -0
- package/dist/esm/subscribe-async-gen.js +32 -0
- package/package.json +39 -0
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MeshagentFileUpload = exports.FileUpload = exports.UploadStatus = void 0;
|
|
4
|
+
const meshagent_1 = require("@meshagent/meshagent");
|
|
5
|
+
var UploadStatus;
|
|
6
|
+
(function (UploadStatus) {
|
|
7
|
+
UploadStatus["Initial"] = "initial";
|
|
8
|
+
UploadStatus["Uploading"] = "uploading";
|
|
9
|
+
UploadStatus["Completed"] = "completed";
|
|
10
|
+
UploadStatus["Failed"] = "failed";
|
|
11
|
+
})(UploadStatus || (exports.UploadStatus = UploadStatus = {}));
|
|
12
|
+
class FileUpload extends meshagent_1.EventEmitter {
|
|
13
|
+
constructor(path, size = 0) {
|
|
14
|
+
super();
|
|
15
|
+
Object.defineProperty(this, "path", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: path
|
|
20
|
+
});
|
|
21
|
+
Object.defineProperty(this, "size", {
|
|
22
|
+
enumerable: true,
|
|
23
|
+
configurable: true,
|
|
24
|
+
writable: true,
|
|
25
|
+
value: size
|
|
26
|
+
});
|
|
27
|
+
Object.defineProperty(this, "_status", {
|
|
28
|
+
enumerable: true,
|
|
29
|
+
configurable: true,
|
|
30
|
+
writable: true,
|
|
31
|
+
value: UploadStatus.Initial
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
get status() {
|
|
35
|
+
return this._status;
|
|
36
|
+
}
|
|
37
|
+
set status(value) {
|
|
38
|
+
if (this._status !== value) {
|
|
39
|
+
this._status = value;
|
|
40
|
+
this.emit("status", {
|
|
41
|
+
status: value,
|
|
42
|
+
progress: this.bytesUploaded,
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
get filename() {
|
|
47
|
+
return this.path.split("/").pop() ?? "";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.FileUpload = FileUpload;
|
|
51
|
+
class MeshagentFileUpload extends FileUpload {
|
|
52
|
+
constructor(room, path, dataStream, size = 0, autoStart = true) {
|
|
53
|
+
super(path, size);
|
|
54
|
+
Object.defineProperty(this, "room", {
|
|
55
|
+
enumerable: true,
|
|
56
|
+
configurable: true,
|
|
57
|
+
writable: true,
|
|
58
|
+
value: room
|
|
59
|
+
});
|
|
60
|
+
Object.defineProperty(this, "dataStream", {
|
|
61
|
+
enumerable: true,
|
|
62
|
+
configurable: true,
|
|
63
|
+
writable: true,
|
|
64
|
+
value: dataStream
|
|
65
|
+
});
|
|
66
|
+
Object.defineProperty(this, "_bytesUploaded", {
|
|
67
|
+
enumerable: true,
|
|
68
|
+
configurable: true,
|
|
69
|
+
writable: true,
|
|
70
|
+
value: 0
|
|
71
|
+
});
|
|
72
|
+
Object.defineProperty(this, "_done", {
|
|
73
|
+
enumerable: true,
|
|
74
|
+
configurable: true,
|
|
75
|
+
writable: true,
|
|
76
|
+
value: void 0
|
|
77
|
+
});
|
|
78
|
+
Object.defineProperty(this, "_resolveDone", {
|
|
79
|
+
enumerable: true,
|
|
80
|
+
configurable: true,
|
|
81
|
+
writable: true,
|
|
82
|
+
value: void 0
|
|
83
|
+
});
|
|
84
|
+
Object.defineProperty(this, "_rejectDone", {
|
|
85
|
+
enumerable: true,
|
|
86
|
+
configurable: true,
|
|
87
|
+
writable: true,
|
|
88
|
+
value: void 0
|
|
89
|
+
});
|
|
90
|
+
Object.defineProperty(this, "_downloadUrl", {
|
|
91
|
+
enumerable: true,
|
|
92
|
+
configurable: true,
|
|
93
|
+
writable: true,
|
|
94
|
+
value: void 0
|
|
95
|
+
});
|
|
96
|
+
Object.defineProperty(this, "_resolveUrl", {
|
|
97
|
+
enumerable: true,
|
|
98
|
+
configurable: true,
|
|
99
|
+
writable: true,
|
|
100
|
+
value: void 0
|
|
101
|
+
});
|
|
102
|
+
Object.defineProperty(this, "_rejectUrl", {
|
|
103
|
+
enumerable: true,
|
|
104
|
+
configurable: true,
|
|
105
|
+
writable: true,
|
|
106
|
+
value: void 0
|
|
107
|
+
});
|
|
108
|
+
this._done = new Promise((res, rej) => {
|
|
109
|
+
this._resolveDone = res;
|
|
110
|
+
this._rejectDone = rej;
|
|
111
|
+
});
|
|
112
|
+
this._downloadUrl = new Promise((res, rej) => {
|
|
113
|
+
this._resolveUrl = res;
|
|
114
|
+
this._rejectUrl = rej;
|
|
115
|
+
});
|
|
116
|
+
if (autoStart)
|
|
117
|
+
this._upload();
|
|
118
|
+
}
|
|
119
|
+
static deferred(room, path, dataStream, size = 0) {
|
|
120
|
+
return new MeshagentFileUpload(room, path, dataStream, size, false);
|
|
121
|
+
}
|
|
122
|
+
get bytesUploaded() {
|
|
123
|
+
return this._bytesUploaded;
|
|
124
|
+
}
|
|
125
|
+
get done() {
|
|
126
|
+
return this._done;
|
|
127
|
+
}
|
|
128
|
+
/** Resolves to the server’s public download URL – like Dart version. */
|
|
129
|
+
get downloadUrl() {
|
|
130
|
+
return this._downloadUrl;
|
|
131
|
+
}
|
|
132
|
+
startUpload() {
|
|
133
|
+
this._upload(); // idempotent guard inside _upload()
|
|
134
|
+
}
|
|
135
|
+
async _upload() {
|
|
136
|
+
if (this.status !== UploadStatus.Initial) {
|
|
137
|
+
throw new Error("upload already started or completed");
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
const handle = await this.room.storage.open(this.path, { overwrite: true });
|
|
141
|
+
try {
|
|
142
|
+
this.status = UploadStatus.Uploading;
|
|
143
|
+
for await (const chunk of this.dataStream) {
|
|
144
|
+
await this.room.storage.write(handle, chunk);
|
|
145
|
+
this._bytesUploaded += chunk.length;
|
|
146
|
+
this.emit("progress", {
|
|
147
|
+
status: UploadStatus.Uploading,
|
|
148
|
+
progress: this.bytesUploaded / this.size,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
finally {
|
|
153
|
+
await this.room.storage.close(handle);
|
|
154
|
+
}
|
|
155
|
+
this._resolveDone();
|
|
156
|
+
this.status = UploadStatus.Completed;
|
|
157
|
+
const urlStr = await this.room.storage.downloadUrl(this.path);
|
|
158
|
+
this._resolveUrl(new URL(urlStr));
|
|
159
|
+
}
|
|
160
|
+
catch (err) {
|
|
161
|
+
this.status = UploadStatus.Failed;
|
|
162
|
+
this._rejectDone(err);
|
|
163
|
+
this._rejectUrl(err);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
exports.MeshagentFileUpload = MeshagentFileUpload;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./chat"), exports);
|
|
18
|
+
__exportStar(require("./client-toolkits"), exports);
|
|
19
|
+
__exportStar(require("./document-connection-scope"), exports);
|
|
20
|
+
__exportStar(require("./file-upload"), exports);
|
|
21
|
+
__exportStar(require("./room-connection-scope"), exports);
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { RoomClient } from '@meshagent/meshagent';
|
|
2
|
+
export interface RoomConnectionInfo {
|
|
3
|
+
url: string;
|
|
4
|
+
jwt: string;
|
|
5
|
+
}
|
|
6
|
+
export declare const developmentAuthorization: ({ url, projectId, apiKeyId, participantName, roomName, secret, }: {
|
|
7
|
+
url: string;
|
|
8
|
+
projectId: string;
|
|
9
|
+
apiKeyId: string;
|
|
10
|
+
participantName: string;
|
|
11
|
+
roomName: string;
|
|
12
|
+
secret: string;
|
|
13
|
+
}) => (() => Promise<RoomConnectionInfo>);
|
|
14
|
+
export declare const staticAuthorization: ({ url, jwt, }: {
|
|
15
|
+
url: string;
|
|
16
|
+
jwt: string;
|
|
17
|
+
}) => (() => Promise<RoomConnectionInfo>);
|
|
18
|
+
export interface UseRoomConnectionOptions {
|
|
19
|
+
/** Async function that returns `{ url, jwt }` for the room. */
|
|
20
|
+
authorization: () => Promise<{
|
|
21
|
+
url: string;
|
|
22
|
+
jwt: string;
|
|
23
|
+
}>;
|
|
24
|
+
/** Enable the optional messaging layer (default = `true`). */
|
|
25
|
+
enableMessaging?: boolean;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Shape of the object returned by the hook.
|
|
29
|
+
*/
|
|
30
|
+
export interface UseRoomConnectionResult {
|
|
31
|
+
client: RoomClient | null;
|
|
32
|
+
state: 'authorizing' | 'connecting' | 'ready' | 'done';
|
|
33
|
+
ready: boolean;
|
|
34
|
+
done: boolean;
|
|
35
|
+
error: unknown;
|
|
36
|
+
dispose: () => void;
|
|
37
|
+
}
|
|
38
|
+
export declare function useRoomConnection(props: UseRoomConnectionOptions): UseRoomConnectionResult;
|
|
39
|
+
export interface UseRoomIndicatorsResult {
|
|
40
|
+
typing: boolean;
|
|
41
|
+
thinking: boolean;
|
|
42
|
+
}
|
|
43
|
+
export interface UseRoomIndicatorsProps {
|
|
44
|
+
room: RoomClient | null;
|
|
45
|
+
path: string;
|
|
46
|
+
}
|
|
47
|
+
export declare function useRoomIndicators({ room, path }: UseRoomIndicatorsProps): UseRoomIndicatorsResult;
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.staticAuthorization = exports.developmentAuthorization = void 0;
|
|
4
|
+
exports.useRoomConnection = useRoomConnection;
|
|
5
|
+
exports.useRoomIndicators = useRoomIndicators;
|
|
6
|
+
const react_1 = require("react");
|
|
7
|
+
const subscribe_async_gen_1 = require("./subscribe-async-gen");
|
|
8
|
+
const meshagent_1 = require("@meshagent/meshagent");
|
|
9
|
+
const meshagent_2 = require("@meshagent/meshagent");
|
|
10
|
+
/* -------------------------------------------------
|
|
11
|
+
* Authorization helpers
|
|
12
|
+
* ------------------------------------------------- */
|
|
13
|
+
const developmentAuthorization = ({ url, projectId, apiKeyId, participantName, roomName, secret, }) => async () => {
|
|
14
|
+
const token = new meshagent_2.ParticipantToken({
|
|
15
|
+
name: participantName,
|
|
16
|
+
projectId,
|
|
17
|
+
apiKeyId,
|
|
18
|
+
});
|
|
19
|
+
token.addRoomGrant(roomName);
|
|
20
|
+
token.addRoleGrant('user');
|
|
21
|
+
const jwt = await token.toJwt({ token: secret });
|
|
22
|
+
return { url, jwt };
|
|
23
|
+
};
|
|
24
|
+
exports.developmentAuthorization = developmentAuthorization;
|
|
25
|
+
const staticAuthorization = ({ url, jwt, }) => async () => ({ url, jwt });
|
|
26
|
+
exports.staticAuthorization = staticAuthorization;
|
|
27
|
+
function useRoomConnection(props) {
|
|
28
|
+
const { authorization, enableMessaging = true, } = props;
|
|
29
|
+
const [client, setClient] = (0, react_1.useState)(null);
|
|
30
|
+
const [ready, setReady] = (0, react_1.useState)(false);
|
|
31
|
+
const [state, setState] = (0, react_1.useState)('authorizing');
|
|
32
|
+
const [error, setError] = (0, react_1.useState)(null);
|
|
33
|
+
// Keep the latest client in a ref so we can call `dispose` in cleanup.
|
|
34
|
+
const clientRef = (0, react_1.useRef)(null);
|
|
35
|
+
clientRef.current = client;
|
|
36
|
+
// Instance method exposed to consumers (rarely needed).
|
|
37
|
+
const dispose = () => {
|
|
38
|
+
clientRef.current?.dispose();
|
|
39
|
+
setState('done');
|
|
40
|
+
};
|
|
41
|
+
(0, react_1.useEffect)(() => {
|
|
42
|
+
let cancelled = false;
|
|
43
|
+
const connect = async () => {
|
|
44
|
+
try {
|
|
45
|
+
// 1️⃣ Get connection credentials
|
|
46
|
+
const { url, jwt } = await authorization();
|
|
47
|
+
if (cancelled)
|
|
48
|
+
return;
|
|
49
|
+
const room = new meshagent_2.RoomClient({
|
|
50
|
+
protocol: new meshagent_2.Protocol({
|
|
51
|
+
channel: new meshagent_2.WebSocketProtocolChannel({ url, jwt }),
|
|
52
|
+
}),
|
|
53
|
+
});
|
|
54
|
+
setClient(room);
|
|
55
|
+
setState('connecting');
|
|
56
|
+
await room.start({
|
|
57
|
+
onDone: () => {
|
|
58
|
+
if (cancelled)
|
|
59
|
+
return;
|
|
60
|
+
setState('done');
|
|
61
|
+
},
|
|
62
|
+
onError: (e) => {
|
|
63
|
+
if (cancelled)
|
|
64
|
+
return;
|
|
65
|
+
setError(e);
|
|
66
|
+
setState('done');
|
|
67
|
+
},
|
|
68
|
+
});
|
|
69
|
+
if (enableMessaging) {
|
|
70
|
+
await room.messaging.enable();
|
|
71
|
+
}
|
|
72
|
+
if (cancelled)
|
|
73
|
+
return;
|
|
74
|
+
setState('ready');
|
|
75
|
+
setReady(true);
|
|
76
|
+
}
|
|
77
|
+
catch (e) {
|
|
78
|
+
if (cancelled)
|
|
79
|
+
return;
|
|
80
|
+
setError(e);
|
|
81
|
+
setState('done');
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
connect();
|
|
85
|
+
return () => {
|
|
86
|
+
// React unmount or deps change → cancel & dispose
|
|
87
|
+
cancelled = true;
|
|
88
|
+
dispose();
|
|
89
|
+
};
|
|
90
|
+
// eslint‑disable‑next‑line react-hooks/exhaustive-deps
|
|
91
|
+
}, []); // run once, just like componentDidMount
|
|
92
|
+
return {
|
|
93
|
+
client,
|
|
94
|
+
state,
|
|
95
|
+
ready,
|
|
96
|
+
done: state === 'done',
|
|
97
|
+
error,
|
|
98
|
+
dispose,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function useRoomIndicators({ room, path }) {
|
|
102
|
+
const typingMap = (0, react_1.useRef)({});
|
|
103
|
+
const thinkingSet = (0, react_1.useRef)(new Set());
|
|
104
|
+
const [typing, setState] = (0, react_1.useState)(false);
|
|
105
|
+
const [thinking, setThinking] = (0, react_1.useState)(false);
|
|
106
|
+
(0, react_1.useEffect)(() => {
|
|
107
|
+
if (!room)
|
|
108
|
+
return;
|
|
109
|
+
const s = (0, subscribe_async_gen_1.subscribe)(room.listen(), {
|
|
110
|
+
next: (event) => {
|
|
111
|
+
if (event instanceof meshagent_1.RoomMessageEvent) {
|
|
112
|
+
const { message } = event;
|
|
113
|
+
// Ignore messages from ourselves
|
|
114
|
+
if (message.fromParticipantId === room.localParticipant?.id) {
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
// Ignore messages not for this path
|
|
118
|
+
if (message.message.path !== path) {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (message.type === "typing") {
|
|
122
|
+
// Clear any existing timer for this participant
|
|
123
|
+
clearTimeout(typingMap.current[message.fromParticipantId]);
|
|
124
|
+
// Set a new timer to remove typing after 1 second
|
|
125
|
+
typingMap.current[message.fromParticipantId] = setTimeout(() => {
|
|
126
|
+
delete typingMap.current[message.fromParticipantId];
|
|
127
|
+
setState(Object.keys(typingMap.current).length > 0);
|
|
128
|
+
}, 1000);
|
|
129
|
+
// Update typing state
|
|
130
|
+
setState(Object.keys(typingMap.current).length > 0);
|
|
131
|
+
}
|
|
132
|
+
else if (message.type === "thinking") {
|
|
133
|
+
if (message.message.thinking) {
|
|
134
|
+
thinkingSet.current.add(message.fromParticipantId);
|
|
135
|
+
}
|
|
136
|
+
else {
|
|
137
|
+
thinkingSet.current.delete(message.fromParticipantId);
|
|
138
|
+
}
|
|
139
|
+
// Update thinking state
|
|
140
|
+
setThinking(thinkingSet.current.size > 0);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
return () => s.unsubscribe();
|
|
146
|
+
}, [room, path]);
|
|
147
|
+
return { typing, thinking };
|
|
148
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
type Subscriber<T> = {
|
|
2
|
+
next: (value: T) => void;
|
|
3
|
+
error?: (err: unknown) => void;
|
|
4
|
+
complete?: () => void;
|
|
5
|
+
};
|
|
6
|
+
export interface Subscription {
|
|
7
|
+
unsubscribe: () => void;
|
|
8
|
+
}
|
|
9
|
+
export declare function subscribe<T>(iterable: AsyncIterable<T>, { next, error, complete }: Subscriber<T>): Subscription;
|
|
10
|
+
export {};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.subscribe = subscribe;
|
|
4
|
+
function abortableNext(iterator, signal) {
|
|
5
|
+
const nextP = iterator.next();
|
|
6
|
+
const abortP = new Promise((_, rej) => {
|
|
7
|
+
signal.addEventListener("abort", () => rej(new Error("aborted")), { once: true });
|
|
8
|
+
});
|
|
9
|
+
return Promise.race([nextP, abortP]);
|
|
10
|
+
}
|
|
11
|
+
function subscribe(iterable, { next, error, complete }) {
|
|
12
|
+
const controller = new AbortController();
|
|
13
|
+
const it = iterable[Symbol.asyncIterator]();
|
|
14
|
+
(async () => {
|
|
15
|
+
try {
|
|
16
|
+
while (!controller.signal.aborted) {
|
|
17
|
+
const { value, done } = await abortableNext(it, controller.signal);
|
|
18
|
+
if (done)
|
|
19
|
+
break;
|
|
20
|
+
next(value);
|
|
21
|
+
}
|
|
22
|
+
if (!controller.signal.aborted && complete) {
|
|
23
|
+
complete();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
catch (err) {
|
|
27
|
+
if (!controller.signal.aborted && error) {
|
|
28
|
+
error(err);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
})();
|
|
32
|
+
return {
|
|
33
|
+
unsubscribe: () => controller.abort(),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
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
|
+
}
|
|
28
|
+
export declare function fileToAsyncIterable(file: File): AsyncIterable<Uint8Array>;
|
|
29
|
+
export declare function useChat({ room, path, participants, participantNames, initialMessage, includeLocalParticipant }: UseMessageChatProps): UseMessageChatResult;
|
package/dist/esm/chat.js
ADDED
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
import { useCallback, useState, useEffect } from "react";
|
|
2
|
+
import { MeshagentFileUpload } from "./file-upload";
|
|
3
|
+
import { useDocumentConnection, useDocumentChanged, } from "./document-connection-scope";
|
|
4
|
+
export class ChatMessage {
|
|
5
|
+
constructor({ id, text, attachments }) {
|
|
6
|
+
Object.defineProperty(this, "id", {
|
|
7
|
+
enumerable: true,
|
|
8
|
+
configurable: true,
|
|
9
|
+
writable: true,
|
|
10
|
+
value: void 0
|
|
11
|
+
});
|
|
12
|
+
Object.defineProperty(this, "text", {
|
|
13
|
+
enumerable: true,
|
|
14
|
+
configurable: true,
|
|
15
|
+
writable: true,
|
|
16
|
+
value: void 0
|
|
17
|
+
});
|
|
18
|
+
Object.defineProperty(this, "attachments", {
|
|
19
|
+
enumerable: true,
|
|
20
|
+
configurable: true,
|
|
21
|
+
writable: true,
|
|
22
|
+
value: void 0
|
|
23
|
+
});
|
|
24
|
+
this.id = id;
|
|
25
|
+
this.text = text;
|
|
26
|
+
this.attachments = attachments ?? [];
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function ensureParticipants(document, localParticipant, includeLocalParticipant, participants, participantNames) {
|
|
30
|
+
const retParticipants = [
|
|
31
|
+
...(participants ?? []),
|
|
32
|
+
...(includeLocalParticipant ? [localParticipant] : []),
|
|
33
|
+
];
|
|
34
|
+
const existing = new Set();
|
|
35
|
+
for (const child of document.root.getChildren()
|
|
36
|
+
.filter((c) => c.tagName !== undefined)) {
|
|
37
|
+
if (child.tagName === "members") {
|
|
38
|
+
for (const member of child.getChildren()
|
|
39
|
+
.filter((c) => c.tagName !== undefined)) {
|
|
40
|
+
const name = member.getAttribute("name");
|
|
41
|
+
if (name)
|
|
42
|
+
existing.add(name);
|
|
43
|
+
}
|
|
44
|
+
for (const part of retParticipants) {
|
|
45
|
+
const name = part.getAttribute("name");
|
|
46
|
+
if (name && !existing.has(name)) {
|
|
47
|
+
child.createChildElement("member", { name });
|
|
48
|
+
existing.add(name);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (participantNames != null) {
|
|
52
|
+
for (const name of participantNames) {
|
|
53
|
+
if (!existing.has(name)) {
|
|
54
|
+
child.createChildElement("member", { name });
|
|
55
|
+
existing.add(name);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function mapMessages(doc) {
|
|
63
|
+
const children = doc.root.getChildren() || [];
|
|
64
|
+
const thread = children.find((c) => c.tagName === "messages");
|
|
65
|
+
const threadChildren = thread?.getChildren() || [];
|
|
66
|
+
return threadChildren.filter((el) => el.tagName === "message");
|
|
67
|
+
}
|
|
68
|
+
function* getParticipantNames(document) {
|
|
69
|
+
const children = document.root.getChildren() || [];
|
|
70
|
+
const memberNode = children.find((c) => c.tagName === "members");
|
|
71
|
+
const members = memberNode?.getChildren() || [];
|
|
72
|
+
for (const member of members) {
|
|
73
|
+
const name = member.getAttribute("name");
|
|
74
|
+
if (name) {
|
|
75
|
+
yield name;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function* getOnlineParticipants(room, document) {
|
|
80
|
+
for (const participantName of getParticipantNames(document)) {
|
|
81
|
+
if (participantName === room.localParticipant?.getAttribute("name")) {
|
|
82
|
+
yield room.localParticipant;
|
|
83
|
+
}
|
|
84
|
+
for (const remoteParticipant of room.messaging.remoteParticipants) {
|
|
85
|
+
if (remoteParticipant.getAttribute("name") === participantName) {
|
|
86
|
+
yield remoteParticipant;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
const chunkSize = 64 * 1024; // 64 KB
|
|
92
|
+
export function fileToAsyncIterable(file) {
|
|
93
|
+
const hasNativeStream = typeof file.stream === 'function';
|
|
94
|
+
async function* nativeStream() {
|
|
95
|
+
const reader = file.stream().getReader();
|
|
96
|
+
try {
|
|
97
|
+
while (true) {
|
|
98
|
+
const { done, value } = await reader.read();
|
|
99
|
+
if (done)
|
|
100
|
+
break;
|
|
101
|
+
yield value;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
reader.releaseLock();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
async function* sliceStream() {
|
|
109
|
+
let offset = 0;
|
|
110
|
+
while (offset < file.size) {
|
|
111
|
+
const blob = file.slice(offset, offset + chunkSize);
|
|
112
|
+
const buffer = await blob.arrayBuffer();
|
|
113
|
+
yield new Uint8Array(buffer);
|
|
114
|
+
offset += chunkSize;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return (hasNativeStream ? nativeStream : sliceStream)();
|
|
118
|
+
}
|
|
119
|
+
export function useChat({ room, path, participants, participantNames, initialMessage, includeLocalParticipant }) {
|
|
120
|
+
const { document } = useDocumentConnection({ room, path });
|
|
121
|
+
const [messages, setMessages] = useState(() => document ? mapMessages(document) : []);
|
|
122
|
+
const [attachments, setAttachments] = useState([]);
|
|
123
|
+
useDocumentChanged({
|
|
124
|
+
document,
|
|
125
|
+
onChanged: (doc) => {
|
|
126
|
+
setMessages(mapMessages(doc));
|
|
127
|
+
},
|
|
128
|
+
});
|
|
129
|
+
const selectAttachments = useCallback((files) => {
|
|
130
|
+
const attachmentsToUpload = files.map((file) => new MeshagentFileUpload(room, `uploaded-files/${file.name}`, fileToAsyncIterable(file), file.size));
|
|
131
|
+
setAttachments(attachmentsToUpload);
|
|
132
|
+
}, [room, document]);
|
|
133
|
+
const sendMessage = useCallback((message) => {
|
|
134
|
+
const children = document?.root.getChildren() || [];
|
|
135
|
+
const thread = children.find((c) => c.tagName === "messages");
|
|
136
|
+
if (!thread) {
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
thread.createChildElement("message", {
|
|
140
|
+
id: message.id,
|
|
141
|
+
text: message.text,
|
|
142
|
+
created_at: new Date().toISOString(),
|
|
143
|
+
author_name: room.localParticipant.getAttribute("name"),
|
|
144
|
+
author_ref: null,
|
|
145
|
+
});
|
|
146
|
+
for (const participant of getOnlineParticipants(room, document)) {
|
|
147
|
+
room.messaging.sendMessage({
|
|
148
|
+
to: participant,
|
|
149
|
+
type: "chat",
|
|
150
|
+
message: {
|
|
151
|
+
path,
|
|
152
|
+
text: message.text,
|
|
153
|
+
attachments: message.attachments,
|
|
154
|
+
},
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}, [document]);
|
|
158
|
+
useEffect(() => {
|
|
159
|
+
if (document) {
|
|
160
|
+
ensureParticipants(document, room.localParticipant, includeLocalParticipant ?? true, participants ?? [], participantNames ?? []);
|
|
161
|
+
if (initialMessage) {
|
|
162
|
+
sendMessage(initialMessage);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
}, [document]);
|
|
166
|
+
return {
|
|
167
|
+
messages,
|
|
168
|
+
sendMessage,
|
|
169
|
+
selectAttachments,
|
|
170
|
+
attachments,
|
|
171
|
+
};
|
|
172
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
export const useClientToolkits = ({ toolkits, public: isPublic = false, }) => {
|
|
3
|
+
useEffect(() => {
|
|
4
|
+
toolkits.forEach(toolkit => toolkit.start({ public_: isPublic }));
|
|
5
|
+
return () => toolkits.forEach(toolkit => toolkit.stop());
|
|
6
|
+
}, []);
|
|
7
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { MeshDocument, RoomClient } from '@meshagent/meshagent';
|
|
2
|
+
export interface UseDocumentConnectionProps {
|
|
3
|
+
room: RoomClient;
|
|
4
|
+
path: string;
|
|
5
|
+
}
|
|
6
|
+
export interface UseDocumentConnectionResult {
|
|
7
|
+
document: MeshDocument | null;
|
|
8
|
+
error: unknown;
|
|
9
|
+
loading: boolean;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Connects to a Mesh document inside an existing RoomClient and keeps it in sync.
|
|
13
|
+
*
|
|
14
|
+
* The function retries with an exponential back‑off (capped at 60 s) until the
|
|
15
|
+
* document becomes available or the component unmounts.
|
|
16
|
+
*
|
|
17
|
+
* @param room An already‑connected RoomClient.
|
|
18
|
+
* @param path Path to the document inside the room.
|
|
19
|
+
*/
|
|
20
|
+
export declare function useDocumentConnection({ room, path }: UseDocumentConnectionProps): UseDocumentConnectionResult;
|
|
21
|
+
type onChangedHandler = (document: MeshDocument) => void;
|
|
22
|
+
export declare function useDocumentChanged({ document, onChanged }: {
|
|
23
|
+
document: MeshDocument | null;
|
|
24
|
+
onChanged: onChangedHandler;
|
|
25
|
+
}): void;
|
|
26
|
+
export {};
|