@droponair/sdk-js 0.13.0 → 0.14.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 +22 -0
- package/README.md +47 -2
- package/dist/core/messaging-client.d.ts +20 -2
- package/dist/core/messaging-client.js +136 -2
- package/dist/core/types.d.ts +77 -5
- package/dist/index.d.ts +1 -1
- package/dist/transport/protobuf-codec.d.ts +2 -0
- package/dist/transport/protobuf-codec.js +2 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,28 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.14.0], 2026-05-21
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Scheduled & persistent rooms.** New `createRoom` / `listRooms` / `getRoom` / `updateRoom` / `deleteRoom` REST methods and `joinRoom(roomId)` / `leaveRoom(callId)`. A room is an addressable container a live multi-party call runs inside - it is not tied to a group's member roster. Per-room `policy` knobs (`waitingRoom`, `requireHost`, `maxParticipants`, `autoCloseWhenEmpty`) and optional `scheduledStartAt` / `scheduledEndAt` metadata let you compose scheduled or persistent meetings; the platform imposes no meeting model.
|
|
14
|
+
- `joinRoom` resolves with a callId that works with the existing group-call signaling methods (`sendGroupCallSignal`, screen-share, moderation, `leaveGroupCall`) - pass an empty string for `groupId`. Room-call events arrive via `onGroupCallEvent` and carry `roomId`.
|
|
15
|
+
- New `Room`, `RoomPolicy`, `CreateRoomOptions`, `UpdateRoomOptions` types.
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
|
|
19
|
+
- Additive only. The live room call reuses the group-call mesh; on the wire it is the existing `GroupCallFrame` with the new optional `roomId` field. No PROTOCOL_VERSION change.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## [0.13.1], 2026-05-21
|
|
24
|
+
|
|
25
|
+
### Changed
|
|
26
|
+
|
|
27
|
+
- **`revokeAttachment` accepts an `AttachmentRef`.** Calling `revokeAttachment(ref)` now also revokes the linked preview thumbnail (`ref.thumbnailAttachmentId`) in the same call. `revokeAttachment(attachmentId)` with a plain id string is unchanged and revokes only that one attachment. Additive — no breaking change.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
9
31
|
## [0.13.0], 2026-05-21
|
|
10
32
|
|
|
11
33
|
### Added
|
package/README.md
CHANGED
|
@@ -121,6 +121,16 @@ client.onReadReceipt(e => {
|
|
|
121
121
|
});
|
|
122
122
|
```
|
|
123
123
|
|
|
124
|
+
**Group read receipts.** Since SDK `0.13.0`, a group can opt into sharing read receipts with every member. Call `updateGroup(groupId, { readReceiptsVisibleToGroup: true })` (owner/admin); after that, a member's `markRead()` on a group message is broadcast to every other member, and `onReadReceipt` fires with `e.fromUserId` set to the member who read it. It is **opt-in per group** — the platform never enables it for you, and it has no effect unless read receipts are also enabled for your app.
|
|
125
|
+
|
|
126
|
+
```typescript
|
|
127
|
+
await client.updateGroup(groupId, { readReceiptsVisibleToGroup: true });
|
|
128
|
+
|
|
129
|
+
client.onReadReceipt(e => {
|
|
130
|
+
// e.fromUserId is the group member who read e.messageId
|
|
131
|
+
});
|
|
132
|
+
```
|
|
133
|
+
|
|
124
134
|
### Notification clear & draft sync
|
|
125
135
|
|
|
126
136
|
Available since SDK `0.12.0`, both own-device only. `clearNotification()` tells your user's other devices a conversation's notifications were dismissed — always available. `syncDraft()` pushes a draft so the user can keep typing on another device — **opt-in** (the app owner enables it in the dashboard) and the draft text crosses the relay in cleartext.
|
|
@@ -231,11 +241,11 @@ client.onMessage(async (msg) => {
|
|
|
231
241
|
| `createUploadSession(options)` | `Promise<UploadSession>` | Low-level: presigned PUT URL only |
|
|
232
242
|
| `finalizeAttachment(attachmentId, sha256)` | `Promise<void>` | Low-level: commit integrity hash |
|
|
233
243
|
| `downloadAttachment(ref)` | `Promise<DownloadedAttachment>` | Get presigned URL + download + decrypt |
|
|
234
|
-
| `revokeAttachment(attachmentId)` | `Promise<void>` | Revoke an attachment you sent; recipients get `ATTACHMENT_REVOKED` |
|
|
244
|
+
| `revokeAttachment(attachmentId \| ref)` | `Promise<void>` | Revoke an attachment you sent; recipients get `ATTACHMENT_REVOKED` |
|
|
235
245
|
| `sendMessage(toUserId, text, { attachments })` | `Promise<{ messageId }>` | Send with attachments |
|
|
236
246
|
|
|
237
247
|
- **Preview thumbnails (optional).** Pass `thumbnail` (bytes) in `prepareAttachmentAndUpload` options and the SDK uploads it as a separate encrypted attachment, linked via `AttachmentRef.thumbnailAttachmentId`. Purely the developer's choice; omit it and there is no thumbnail. Download the thumbnail like any attachment.
|
|
238
|
-
- **Revoke.** `revokeAttachment()` stops the platform issuing new download URLs and notifies recipients. Bytes a recipient already downloaded cannot be recalled. For GROUP attachments, download is authorized against *current* group membership, so a removed member loses access.
|
|
248
|
+
- **Revoke.** `revokeAttachment()` stops the platform issuing new download URLs and notifies recipients. Bytes a recipient already downloaded cannot be recalled. For GROUP attachments, download is authorized against *current* group membership, so a removed member loses access. Pass an `AttachmentRef` instead of an id (`revokeAttachment(ref)`, since 0.13.1) to revoke the linked preview thumbnail in the same call; a thumbnail is a separate attachment, so revoking a bare id leaves its thumbnail untouched.
|
|
239
249
|
|
|
240
250
|
- E2EE: a random AES-256-GCM file key encrypts the bytes; the file key is wrapped per recipient device using X25519 + HKDF (same model as message payloads). Server never sees the unwrapped file key.
|
|
241
251
|
- Availability and per-file / per-month limits depend on your plan and are enforced server-side before the presigned URL is issued. See the [pricing page](https://www.droponair.com/pricing) and your dashboard Subscription page for what's enabled on your app.
|
|
@@ -259,6 +269,7 @@ client.onMessage(async (msg) => {
|
|
|
259
269
|
| `getGroup(groupId)` | `Promise<GroupInfo>` | Get group details |
|
|
260
270
|
| `addGroupMembers(groupId, userIds)` | `Promise<GroupInfo>` | Add members (owner/admin) |
|
|
261
271
|
| `removeGroupMember(groupId, userId)` | `Promise<GroupInfo>` | Remove a member (owner/admin) |
|
|
272
|
+
| `updateGroup(groupId, { name?, readReceiptsVisibleToGroup? })` | `Promise<GroupInfo>` | Update group settings (owner/admin) |
|
|
262
273
|
| `deleteGroup(groupId)` | `Promise<void>` | Delete a group (owner only) |
|
|
263
274
|
| `sendGroupMessage(groupId, plaintext, memberUserIds, options?)` | `Promise<{ messageId }>` | Send an end-to-end encrypted group message (sender-side per-recipient fan-out). `options.attachments` for E2EE group attachments. |
|
|
264
275
|
| `sendCleartextGroupMessage(groupId, plaintext)` | `Promise<{ messageId }>` | Send a plaintext (non-encrypted) group message; server fans out to every other member. |
|
|
@@ -288,6 +299,40 @@ client.onMessage(async (msg) => {
|
|
|
288
299
|
| `sendGroupCallSignal(type, callId, groupId, targetUserId, payload)` | `void` | Send SDP/ICE to a peer |
|
|
289
300
|
| `onGroupCallEvent(callback)` | `() => void` | Listen for group call events |
|
|
290
301
|
|
|
302
|
+
### Scheduled & Persistent Rooms
|
|
303
|
+
|
|
304
|
+
Available since SDK `0.14.0`. A **room** is an addressable container a live multi-party call runs inside - unlike a group, it is not tied to a member roster. Anyone in your app with the room id can attempt to join; the room's policy is the gate. "Scheduled" vs "persistent" is just how you compose the knobs - the platform imposes no meeting model.
|
|
305
|
+
|
|
306
|
+
| Method | Returns | Description |
|
|
307
|
+
|--------|---------|-------------|
|
|
308
|
+
| `createRoom(options?)` | `Promise<Room>` | Create a room (name, schedule, policy, hosts) |
|
|
309
|
+
| `listRooms()` | `Promise<Room[]>` | Rooms you created or host |
|
|
310
|
+
| `getRoom(roomId)` | `Promise<Room>` | One room, including live-call state |
|
|
311
|
+
| `updateRoom(roomId, update)` | `Promise<Room>` | Update name/schedule/policy/hosts (creator only) |
|
|
312
|
+
| `deleteRoom(roomId)` | `Promise<void>` | Delete a room (creator only) |
|
|
313
|
+
| `joinRoom(roomId)` | `Promise<string>` | Join the room's live call, returns callId |
|
|
314
|
+
| `leaveRoom(callId)` | `Promise<void>` | Leave the room's live call |
|
|
315
|
+
|
|
316
|
+
Room policy (`RoomPolicy`, all optional): `waitingRoom` (non-hosts wait for host admit), `requireHost` (non-hosts cannot open the call), `maxParticipants` (per-room cap), `autoCloseWhenEmpty` (room flips to `CLOSED` when the last participant leaves).
|
|
317
|
+
|
|
318
|
+
```typescript
|
|
319
|
+
const room = await client.createRoom({
|
|
320
|
+
name: 'Weekly sync',
|
|
321
|
+
scheduledStartAt: Date.now() + 3_600_000, // metadata - your app drives the countdown UX
|
|
322
|
+
policy: { waitingRoom: true, requireHost: true },
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Join the live call. The returned callId works with every group-call
|
|
326
|
+
// signaling method - pass '' for groupId.
|
|
327
|
+
const callId = await client.joinRoom(room.roomId);
|
|
328
|
+
client.onGroupCallEvent((e) => {
|
|
329
|
+
// room-call events carry e.roomId; e.type GROUP_CALL_PARTICIPANT_JOINED, etc.
|
|
330
|
+
});
|
|
331
|
+
client.sendGroupCallSignal('GROUP_CALL_SDP_OFFER', callId, '', peerUserId, sdp);
|
|
332
|
+
```
|
|
333
|
+
|
|
334
|
+
`joinRoom` rejects with `HOST_REQUIRED`, `ROOM_CLOSED`, or `WAITING_ROOM_PENDING` (listen for `GROUP_CALL_WAITING_ROOM_ADMITTED`, then call `joinRoom` again).
|
|
335
|
+
|
|
291
336
|
### Screen Sharing
|
|
292
337
|
|
|
293
338
|
Available since SDK `0.6.0`. The SDK only signals start/stop - capture (via the browser's `getDisplayMedia()`) and adding the resulting `MediaStreamTrack` to the existing peer connection are the app's responsibility.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CryptoService } from '../crypto/crypto-service';
|
|
2
2
|
import { SessionManager } from './session-manager';
|
|
3
|
-
import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, GroupCallEventCallback, GroupInfo, GroupMessageCallback, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback } from './types';
|
|
3
|
+
import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback } from './types';
|
|
4
4
|
import { AttachmentRef, CreateUploadSessionOptions, DownloadedAttachment, PrepareAttachmentOptions, UploadSession } from '../attachment/attachment-types';
|
|
5
5
|
export declare class MessagingClient implements DropOnAirClient {
|
|
6
6
|
private readonly options;
|
|
@@ -37,6 +37,8 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
37
37
|
/** Pending startGroupCall resolver. */
|
|
38
38
|
private pendingGroupInviteResolve;
|
|
39
39
|
private pendingGroupInviteReject;
|
|
40
|
+
/** Pending joinRoom resolver (Feature 3.1), keyed by roomId. */
|
|
41
|
+
private readonly pendingRoomJoins;
|
|
40
42
|
private reconnectDelayMs;
|
|
41
43
|
/**
|
|
42
44
|
* Register a document visibilitychange listener so that when the iOS app
|
|
@@ -93,7 +95,7 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
93
95
|
createUploadSession(options: CreateUploadSessionOptions): Promise<UploadSession>;
|
|
94
96
|
finalizeAttachment(attachmentId: string, sha256: string): Promise<void>;
|
|
95
97
|
downloadAttachment(ref: AttachmentRef): Promise<DownloadedAttachment>;
|
|
96
|
-
revokeAttachment(
|
|
98
|
+
revokeAttachment(attachment: string | AttachmentRef): Promise<void>;
|
|
97
99
|
connect(): Promise<void>;
|
|
98
100
|
disconnect(): void;
|
|
99
101
|
sendMessage(toUserId: string, plaintextMessage: string, options?: {
|
|
@@ -226,6 +228,22 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
226
228
|
addGroupMembers(groupId: string, userIds: string[]): Promise<GroupInfo>;
|
|
227
229
|
removeGroupMember(groupId: string, userId: string): Promise<GroupInfo>;
|
|
228
230
|
deleteGroup(groupId: string): Promise<void>;
|
|
231
|
+
createRoom(options?: CreateRoomOptions): Promise<Room>;
|
|
232
|
+
listRooms(): Promise<Room[]>;
|
|
233
|
+
getRoom(roomId: string): Promise<Room>;
|
|
234
|
+
updateRoom(roomId: string, update: UpdateRoomOptions): Promise<Room>;
|
|
235
|
+
deleteRoom(roomId: string): Promise<void>;
|
|
236
|
+
/**
|
|
237
|
+
* Join the live call in a room. Resolves with the callId once joined; that
|
|
238
|
+
* callId works with the existing group-call signaling methods (pass an empty
|
|
239
|
+
* string for groupId). Rejects with an Error whose message is the reason when
|
|
240
|
+
* the room is closed (`ROOM_CLOSED`), a host is required but absent
|
|
241
|
+
* (`HOST_REQUIRED`), or the joiner was placed in the waiting room
|
|
242
|
+
* (`WAITING_ROOM_PENDING` - listen for GROUP_CALL_WAITING_ROOM_ADMITTED, then
|
|
243
|
+
* call joinRoom again).
|
|
244
|
+
*/
|
|
245
|
+
joinRoom(roomId: string): Promise<string>;
|
|
246
|
+
leaveRoom(callId: string): Promise<void>;
|
|
229
247
|
/**
|
|
230
248
|
* Send an end-to-end encrypted group message. Mirrors the Android and iOS
|
|
231
249
|
* SDKs: the SDK encrypts the plaintext per recipient device (sender-side
|
|
@@ -216,6 +216,8 @@ class MessagingClient {
|
|
|
216
216
|
/** Pending startGroupCall resolver. */
|
|
217
217
|
this.pendingGroupInviteResolve = null;
|
|
218
218
|
this.pendingGroupInviteReject = null;
|
|
219
|
+
/** Pending joinRoom resolver (Feature 3.1), keyed by roomId. */
|
|
220
|
+
this.pendingRoomJoins = new Map();
|
|
219
221
|
this.messageListeners = new Set();
|
|
220
222
|
this.eventListeners = new Set();
|
|
221
223
|
this.broadcastListeners = new Set();
|
|
@@ -270,8 +272,16 @@ class MessagingClient {
|
|
|
270
272
|
async downloadAttachment(ref) {
|
|
271
273
|
return this.attachmentClient.downloadAttachment(ref);
|
|
272
274
|
}
|
|
273
|
-
async revokeAttachment(
|
|
274
|
-
|
|
275
|
+
async revokeAttachment(attachment) {
|
|
276
|
+
if (typeof attachment === 'string') {
|
|
277
|
+
return this.attachmentClient.revokeAttachment(attachment);
|
|
278
|
+
}
|
|
279
|
+
// Revoke the main attachment first so the primary intent always lands,
|
|
280
|
+
// then cascade to the linked preview thumbnail if there is one.
|
|
281
|
+
await this.attachmentClient.revokeAttachment(attachment.attachmentId);
|
|
282
|
+
if (attachment.thumbnailAttachmentId) {
|
|
283
|
+
await this.attachmentClient.revokeAttachment(attachment.thumbnailAttachmentId);
|
|
284
|
+
}
|
|
275
285
|
}
|
|
276
286
|
async connect() {
|
|
277
287
|
this.log('connect_start');
|
|
@@ -929,6 +939,112 @@ class MessagingClient {
|
|
|
929
939
|
throw new Error(`deleteGroup failed (HTTP ${res.status})`);
|
|
930
940
|
}
|
|
931
941
|
// ---------------------------------------------------------------------------
|
|
942
|
+
// Rooms (Feature 3.1 - scheduled & persistent meeting rooms)
|
|
943
|
+
// ---------------------------------------------------------------------------
|
|
944
|
+
async createRoom(options) {
|
|
945
|
+
const jwt = await this.getValidDropOnAirJwt(false);
|
|
946
|
+
const body = {};
|
|
947
|
+
if (options?.name !== undefined)
|
|
948
|
+
body.name = options.name;
|
|
949
|
+
if (options?.scheduledStartAt !== undefined) {
|
|
950
|
+
body.scheduledStartAt = new Date(options.scheduledStartAt).toISOString();
|
|
951
|
+
}
|
|
952
|
+
if (options?.scheduledEndAt !== undefined) {
|
|
953
|
+
body.scheduledEndAt = new Date(options.scheduledEndAt).toISOString();
|
|
954
|
+
}
|
|
955
|
+
if (options?.policy !== undefined)
|
|
956
|
+
body.policy = options.policy;
|
|
957
|
+
if (options?.hostUserIds !== undefined)
|
|
958
|
+
body.hostUserIds = options.hostUserIds;
|
|
959
|
+
const res = await this.fetchFn(`${this.httpUrl}/api/rooms`, {
|
|
960
|
+
method: 'POST',
|
|
961
|
+
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
|
962
|
+
body: JSON.stringify(body),
|
|
963
|
+
});
|
|
964
|
+
if (!res.ok)
|
|
965
|
+
throw new Error(`createRoom failed (HTTP ${res.status})`);
|
|
966
|
+
return res.json();
|
|
967
|
+
}
|
|
968
|
+
async listRooms() {
|
|
969
|
+
const jwt = await this.getValidDropOnAirJwt(false);
|
|
970
|
+
const res = await this.fetchFn(`${this.httpUrl}/api/rooms`, {
|
|
971
|
+
method: 'GET',
|
|
972
|
+
headers: { Authorization: `Bearer ${jwt}` },
|
|
973
|
+
});
|
|
974
|
+
if (!res.ok)
|
|
975
|
+
throw new Error(`listRooms failed (HTTP ${res.status})`);
|
|
976
|
+
return res.json();
|
|
977
|
+
}
|
|
978
|
+
async getRoom(roomId) {
|
|
979
|
+
const jwt = await this.getValidDropOnAirJwt(false);
|
|
980
|
+
const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}`, {
|
|
981
|
+
method: 'GET',
|
|
982
|
+
headers: { Authorization: `Bearer ${jwt}` },
|
|
983
|
+
});
|
|
984
|
+
if (!res.ok)
|
|
985
|
+
throw new Error(`getRoom failed (HTTP ${res.status})`);
|
|
986
|
+
return res.json();
|
|
987
|
+
}
|
|
988
|
+
async updateRoom(roomId, update) {
|
|
989
|
+
const jwt = await this.getValidDropOnAirJwt(false);
|
|
990
|
+
const body = {};
|
|
991
|
+
if (update.name !== undefined)
|
|
992
|
+
body.name = update.name;
|
|
993
|
+
if (update.scheduledStartAt !== undefined) {
|
|
994
|
+
body.scheduledStartAt = new Date(update.scheduledStartAt).toISOString();
|
|
995
|
+
}
|
|
996
|
+
if (update.scheduledEndAt !== undefined) {
|
|
997
|
+
body.scheduledEndAt = new Date(update.scheduledEndAt).toISOString();
|
|
998
|
+
}
|
|
999
|
+
if (update.policy !== undefined)
|
|
1000
|
+
body.policy = update.policy;
|
|
1001
|
+
if (update.hostUserIds !== undefined)
|
|
1002
|
+
body.hostUserIds = update.hostUserIds;
|
|
1003
|
+
const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}`, {
|
|
1004
|
+
method: 'PATCH',
|
|
1005
|
+
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
|
1006
|
+
body: JSON.stringify(body),
|
|
1007
|
+
});
|
|
1008
|
+
if (!res.ok)
|
|
1009
|
+
throw new Error(`updateRoom failed (HTTP ${res.status})`);
|
|
1010
|
+
return res.json();
|
|
1011
|
+
}
|
|
1012
|
+
async deleteRoom(roomId) {
|
|
1013
|
+
const jwt = await this.getValidDropOnAirJwt(false);
|
|
1014
|
+
const res = await this.fetchFn(`${this.httpUrl}/api/rooms/${encodeURIComponent(roomId)}`, {
|
|
1015
|
+
method: 'DELETE',
|
|
1016
|
+
headers: { Authorization: `Bearer ${jwt}` },
|
|
1017
|
+
});
|
|
1018
|
+
if (!res.ok)
|
|
1019
|
+
throw new Error(`deleteRoom failed (HTTP ${res.status})`);
|
|
1020
|
+
}
|
|
1021
|
+
/**
|
|
1022
|
+
* Join the live call in a room. Resolves with the callId once joined; that
|
|
1023
|
+
* callId works with the existing group-call signaling methods (pass an empty
|
|
1024
|
+
* string for groupId). Rejects with an Error whose message is the reason when
|
|
1025
|
+
* the room is closed (`ROOM_CLOSED`), a host is required but absent
|
|
1026
|
+
* (`HOST_REQUIRED`), or the joiner was placed in the waiting room
|
|
1027
|
+
* (`WAITING_ROOM_PENDING` - listen for GROUP_CALL_WAITING_ROOM_ADMITTED, then
|
|
1028
|
+
* call joinRoom again).
|
|
1029
|
+
*/
|
|
1030
|
+
joinRoom(roomId) {
|
|
1031
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
1032
|
+
return Promise.reject(new Error('DropOnAir websocket is not connected'));
|
|
1033
|
+
}
|
|
1034
|
+
return new Promise((resolve, reject) => {
|
|
1035
|
+
this.pendingRoomJoins.set(roomId, { resolve, reject });
|
|
1036
|
+
this.ws.send(this.codec.encodeGroupCallFrame({
|
|
1037
|
+
type: 'GROUP_CALL_JOIN',
|
|
1038
|
+
callId: '',
|
|
1039
|
+
groupId: '',
|
|
1040
|
+
roomId,
|
|
1041
|
+
}));
|
|
1042
|
+
});
|
|
1043
|
+
}
|
|
1044
|
+
async leaveRoom(callId) {
|
|
1045
|
+
this.sendGroupCallFrame({ type: 'GROUP_CALL_LEAVE', callId, groupId: '' });
|
|
1046
|
+
}
|
|
1047
|
+
// ---------------------------------------------------------------------------
|
|
932
1048
|
// Group messaging
|
|
933
1049
|
// ---------------------------------------------------------------------------
|
|
934
1050
|
/**
|
|
@@ -1379,10 +1495,28 @@ class MessagingClient {
|
|
|
1379
1495
|
this.pendingGroupInviteReject = null;
|
|
1380
1496
|
reject(new Error(`GROUP_CALL_ALREADY_ACTIVE:${wire.payload ?? ''}`));
|
|
1381
1497
|
}
|
|
1498
|
+
// Resolve / reject a pending joinRoom() (Feature 3.1). Room-call frames
|
|
1499
|
+
// carry roomId; the join outcome is one of these four types.
|
|
1500
|
+
if (wire.roomId) {
|
|
1501
|
+
const pending = this.pendingRoomJoins.get(wire.roomId);
|
|
1502
|
+
if (pending) {
|
|
1503
|
+
if (wire.type === 'GROUP_CALL_JOINED') {
|
|
1504
|
+
this.pendingRoomJoins.delete(wire.roomId);
|
|
1505
|
+
pending.resolve(wire.callId);
|
|
1506
|
+
}
|
|
1507
|
+
else if (wire.type === 'GROUP_CALL_HOST_REQUIRED' ||
|
|
1508
|
+
wire.type === 'GROUP_CALL_ROOM_CLOSED' ||
|
|
1509
|
+
wire.type === 'GROUP_CALL_WAITING_ROOM_PENDING') {
|
|
1510
|
+
this.pendingRoomJoins.delete(wire.roomId);
|
|
1511
|
+
pending.reject(new Error(wire.type.replace('GROUP_CALL_', '')));
|
|
1512
|
+
}
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1382
1515
|
const event = {
|
|
1383
1516
|
type: wire.type,
|
|
1384
1517
|
callId: wire.callId,
|
|
1385
1518
|
groupId: wire.groupId,
|
|
1519
|
+
roomId: wire.roomId,
|
|
1386
1520
|
targetUserId: wire.targetUserId,
|
|
1387
1521
|
payload: wire.payload,
|
|
1388
1522
|
};
|
package/dist/core/types.d.ts
CHANGED
|
@@ -135,15 +135,63 @@ export interface DecryptedGroupMessage {
|
|
|
135
135
|
plaintext: string;
|
|
136
136
|
}
|
|
137
137
|
export type GroupMessageCallback = (message: DecryptedGroupMessage) => void;
|
|
138
|
-
export type GroupCallEventType = 'GROUP_CALL_INVITE' | 'GROUP_CALL_RINGING' | 'GROUP_CALL_JOIN' | 'GROUP_CALL_LEAVE' | 'GROUP_CALL_END' | 'GROUP_CALL_ENDED' | 'GROUP_CALL_PARTICIPANT_JOINED' | 'GROUP_CALL_PARTICIPANT_LEFT' | 'GROUP_CALL_PARTICIPANT_REMOVED' | 'GROUP_CALL_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE' | 'GROUP_CALL_SCREEN_SHARE_STARTED' | 'GROUP_CALL_SCREEN_SHARE_STOPPED' | 'GROUP_CALL_HOST_TRANSFER' | 'GROUP_CALL_COHOST_APPOINT' | 'GROUP_CALL_COHOST_REVOKE' | 'GROUP_CALL_ROLE_CHANGED' | 'GROUP_CALL_MUTE_PARTICIPANT' | 'GROUP_CALL_REMOVE_PARTICIPANT' | 'GROUP_CALL_WAITING_ROOM_REQUEST' | 'GROUP_CALL_WAITING_ROOM_JOINED' | 'GROUP_CALL_WAITING_ROOM_ADMIT' | 'GROUP_CALL_WAITING_ROOM_ADMITTED' | 'GROUP_CALL_WAITING_ROOM_REJECT' | 'GROUP_CALL_WAITING_ROOM_REJECTED';
|
|
138
|
+
export type GroupCallEventType = 'GROUP_CALL_INVITE' | 'GROUP_CALL_RINGING' | 'GROUP_CALL_JOIN' | 'GROUP_CALL_LEAVE' | 'GROUP_CALL_END' | 'GROUP_CALL_ENDED' | 'GROUP_CALL_PARTICIPANT_JOINED' | 'GROUP_CALL_PARTICIPANT_LEFT' | 'GROUP_CALL_PARTICIPANT_REMOVED' | 'GROUP_CALL_ALREADY_ACTIVE' | 'GROUP_CALL_SDP_OFFER' | 'GROUP_CALL_SDP_ANSWER' | 'GROUP_CALL_ICE_CANDIDATE' | 'GROUP_CALL_SCREEN_SHARE_STARTED' | 'GROUP_CALL_SCREEN_SHARE_STOPPED' | 'GROUP_CALL_HOST_TRANSFER' | 'GROUP_CALL_COHOST_APPOINT' | 'GROUP_CALL_COHOST_REVOKE' | 'GROUP_CALL_ROLE_CHANGED' | 'GROUP_CALL_MUTE_PARTICIPANT' | 'GROUP_CALL_REMOVE_PARTICIPANT' | 'GROUP_CALL_WAITING_ROOM_REQUEST' | 'GROUP_CALL_WAITING_ROOM_JOINED' | 'GROUP_CALL_WAITING_ROOM_ADMIT' | 'GROUP_CALL_WAITING_ROOM_ADMITTED' | 'GROUP_CALL_WAITING_ROOM_REJECT' | 'GROUP_CALL_WAITING_ROOM_REJECTED' | 'GROUP_CALL_JOINED' | 'GROUP_CALL_WAITING_ROOM_PENDING' | 'GROUP_CALL_HOST_REQUIRED' | 'GROUP_CALL_ROOM_CLOSED';
|
|
139
139
|
export interface GroupCallEvent {
|
|
140
140
|
type: GroupCallEventType | string;
|
|
141
141
|
callId: string;
|
|
142
142
|
groupId: string;
|
|
143
|
+
/** Set instead of groupId when the event belongs to a room call (Feature 3.1). */
|
|
144
|
+
roomId?: string;
|
|
143
145
|
targetUserId?: string;
|
|
144
146
|
payload?: string;
|
|
145
147
|
}
|
|
146
148
|
export type GroupCallEventCallback = (event: GroupCallEvent) => void;
|
|
149
|
+
/**
|
|
150
|
+
* Per-room policy. Each flag is a capability you compose per room; the platform
|
|
151
|
+
* imposes no meeting model of its own.
|
|
152
|
+
*/
|
|
153
|
+
export interface RoomPolicy {
|
|
154
|
+
/** Non-host joiners are held in the waiting room until a host admits them. */
|
|
155
|
+
waitingRoom?: boolean;
|
|
156
|
+
/** Non-host joiners cannot open the call; they wait until a host is present. */
|
|
157
|
+
requireHost?: boolean;
|
|
158
|
+
/** Per-room participant cap. Omit to use the plan default. */
|
|
159
|
+
maxParticipants?: number;
|
|
160
|
+
/** When true, the room flips to CLOSED once the last participant leaves the call. */
|
|
161
|
+
autoCloseWhenEmpty?: boolean;
|
|
162
|
+
}
|
|
163
|
+
export interface Room {
|
|
164
|
+
roomId: string;
|
|
165
|
+
name: string;
|
|
166
|
+
createdBy: string;
|
|
167
|
+
hostUserIds: string[];
|
|
168
|
+
/** Schedule metadata, Unix epoch millis. The platform does not enforce a join window. */
|
|
169
|
+
scheduledStartAt?: number | null;
|
|
170
|
+
scheduledEndAt?: number | null;
|
|
171
|
+
policy: RoomPolicy;
|
|
172
|
+
status: 'ACTIVE' | 'CLOSED';
|
|
173
|
+
/** True when a live call is currently running in the room. */
|
|
174
|
+
isLive: boolean;
|
|
175
|
+
participantCount: number;
|
|
176
|
+
createdAt: number;
|
|
177
|
+
}
|
|
178
|
+
/** Options for creating a room. All fields optional except none are required. */
|
|
179
|
+
export interface CreateRoomOptions {
|
|
180
|
+
name?: string;
|
|
181
|
+
scheduledStartAt?: number;
|
|
182
|
+
scheduledEndAt?: number;
|
|
183
|
+
policy?: RoomPolicy;
|
|
184
|
+
/** Additional designated hosts. The creator is always a host. */
|
|
185
|
+
hostUserIds?: string[];
|
|
186
|
+
}
|
|
187
|
+
/** Patch for updateRoom. Any omitted field is left unchanged. */
|
|
188
|
+
export interface UpdateRoomOptions {
|
|
189
|
+
name?: string;
|
|
190
|
+
scheduledStartAt?: number;
|
|
191
|
+
scheduledEndAt?: number;
|
|
192
|
+
policy?: RoomPolicy;
|
|
193
|
+
hostUserIds?: string[];
|
|
194
|
+
}
|
|
147
195
|
export interface KeyStorageAdapter {
|
|
148
196
|
get(key: string): Promise<string | null>;
|
|
149
197
|
set(key: string, value: string): Promise<void>;
|
|
@@ -286,11 +334,15 @@ export interface DropOnAirClient {
|
|
|
286
334
|
/** Download (and for E2EE decrypt) an attachment referenced inside a received message. */
|
|
287
335
|
downloadAttachment(ref: import('../attachment/attachment-types').AttachmentRef): Promise<import('../attachment/attachment-types').DownloadedAttachment>;
|
|
288
336
|
/**
|
|
289
|
-
* Revoke an attachment you sent
|
|
290
|
-
*
|
|
291
|
-
*
|
|
337
|
+
* Revoke an attachment you sent. The platform stops issuing download URLs
|
|
338
|
+
* for it and notifies recipients; bytes already downloaded cannot be
|
|
339
|
+
* recalled.
|
|
340
|
+
*
|
|
341
|
+
* Pass an attachment id to revoke that single attachment. Pass an
|
|
342
|
+
* `AttachmentRef` to also revoke its linked preview thumbnail
|
|
343
|
+
* (`thumbnailAttachmentId`) in the same call, if it has one.
|
|
292
344
|
*/
|
|
293
|
-
revokeAttachment(
|
|
345
|
+
revokeAttachment(attachment: string | import('../attachment/attachment-types').AttachmentRef): Promise<void>;
|
|
294
346
|
/** Send a cleartext message to a user (no encryption). */
|
|
295
347
|
sendCleartextMessage(toUserId: string, plaintext: string): Promise<{
|
|
296
348
|
messageId: string;
|
|
@@ -414,4 +466,24 @@ export interface DropOnAirClient {
|
|
|
414
466
|
admitFromWaitingRoom(callId: string, groupId: string, userId: string): void;
|
|
415
467
|
/** Reject a user waiting to join (host / co-host). */
|
|
416
468
|
rejectFromWaitingRoom(callId: string, groupId: string, userId: string): void;
|
|
469
|
+
/** Create a scheduled or persistent room. */
|
|
470
|
+
createRoom(options?: CreateRoomOptions): Promise<Room>;
|
|
471
|
+
/** List rooms you created or are a designated host of. */
|
|
472
|
+
listRooms(): Promise<Room[]>;
|
|
473
|
+
/** Fetch a single room, including its live-call state. */
|
|
474
|
+
getRoom(roomId: string): Promise<Room>;
|
|
475
|
+
/** Update a room's name, schedule, policy, or hosts (creator only). */
|
|
476
|
+
updateRoom(roomId: string, update: UpdateRoomOptions): Promise<Room>;
|
|
477
|
+
/** Delete a room (creator only); any live call is ended. */
|
|
478
|
+
deleteRoom(roomId: string): Promise<void>;
|
|
479
|
+
/**
|
|
480
|
+
* Join the live call in a room. Resolves with the callId once joined; that
|
|
481
|
+
* callId is then used with the group-call signaling methods (sendGroupCallSignal,
|
|
482
|
+
* screen-share, moderation, leaveGroupCall) - pass an empty string for groupId.
|
|
483
|
+
* Rejects when the room is closed, a host is required but absent, or the
|
|
484
|
+
* joiner was placed in the waiting room (error message carries the reason).
|
|
485
|
+
*/
|
|
486
|
+
joinRoom(roomId: string): Promise<string>;
|
|
487
|
+
/** Leave the live call in a room. */
|
|
488
|
+
leaveRoom(callId: string): Promise<void>;
|
|
417
489
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { InitializeOptions, DropOnAirClient } from './core/types';
|
|
2
2
|
export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version';
|
|
3
3
|
export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
|
|
4
|
-
export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
|
|
4
|
+
export type { InitializeOptions, DropOnAirClient, DropOnAirEvent, MessageCallback, EventCallback, DecryptedMessage, KeyStorageAdapter, CallEvent, CallEventType, CallEventCallback, TurnCredentials, BroadcastMessage, BroadcastCallback, GroupInfo, GroupMemberInfo, DecryptedGroupMessage, GroupMessageCallback, GroupCallEvent, GroupCallEventType, GroupCallEventCallback, Room, RoomPolicy, CreateRoomOptions, UpdateRoomOptions, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
|
|
5
5
|
export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
|
|
6
6
|
declare const _default: {
|
|
7
7
|
initialize: typeof initialize;
|
|
@@ -116,6 +116,8 @@ export interface WireGroupCallFrame {
|
|
|
116
116
|
groupId: string;
|
|
117
117
|
targetUserId?: string;
|
|
118
118
|
payload?: string;
|
|
119
|
+
/** Set instead of groupId when the multi-party call belongs to a Room (Feature 3.1). */
|
|
120
|
+
roomId?: string;
|
|
119
121
|
}
|
|
120
122
|
/**
|
|
121
123
|
* Edit frame, sender re-encrypts new content for all recipient devices.
|
|
@@ -167,7 +167,8 @@ const GroupCallFrameType = new protobuf.Type('GroupCallFrame')
|
|
|
167
167
|
.add(new protobuf.Field('callId', 2, 'string'))
|
|
168
168
|
.add(new protobuf.Field('groupId', 3, 'string'))
|
|
169
169
|
.add(new protobuf.Field('targetUserId', 4, 'string'))
|
|
170
|
-
.add(new protobuf.Field('payload', 5, 'string'))
|
|
170
|
+
.add(new protobuf.Field('payload', 5, 'string'))
|
|
171
|
+
.add(new protobuf.Field('roomId', 7, 'string'));
|
|
171
172
|
// ---------------------------------------------------------------------------
|
|
172
173
|
// Message Edit and Delete (PROTOCOL_VERSION 3+)
|
|
173
174
|
// ---------------------------------------------------------------------------
|
package/dist/version.d.ts
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* MINOR, additive feature (e.g. multi-device payloads, new call event type)
|
|
8
8
|
* PATCH, bug-fix / perf improvement with no wire or API change
|
|
9
9
|
*/
|
|
10
|
-
export declare const SDK_VERSION = "0.
|
|
10
|
+
export declare const SDK_VERSION = "0.14.0";
|
|
11
11
|
/**
|
|
12
12
|
* Binary encrypted-payload format version.
|
|
13
13
|
* Included as the first byte of every encrypted payload so receivers can
|
package/dist/version.js
CHANGED
|
@@ -10,7 +10,7 @@ exports.PROTOCOL_VERSION = exports.PAYLOAD_FORMAT_VERSION = exports.SDK_VERSION
|
|
|
10
10
|
* MINOR, additive feature (e.g. multi-device payloads, new call event type)
|
|
11
11
|
* PATCH, bug-fix / perf improvement with no wire or API change
|
|
12
12
|
*/
|
|
13
|
-
exports.SDK_VERSION = '0.
|
|
13
|
+
exports.SDK_VERSION = '0.14.0';
|
|
14
14
|
/**
|
|
15
15
|
* Binary encrypted-payload format version.
|
|
16
16
|
* Included as the first byte of every encrypted payload so receivers can
|