@droponair/sdk-js 0.28.0 → 0.30.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 +33 -0
- package/README.md +22 -0
- package/dist/core/messaging-client.d.ts +24 -1
- package/dist/core/messaging-client.js +85 -1
- package/dist/core/types.d.ts +18 -0
- package/dist/index.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,38 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.30.0
|
|
4
|
+
|
|
5
|
+
### Added
|
|
6
|
+
|
|
7
|
+
- **Typing indicators.** `sendTyping(conversationId, isTyping)` and `onTyping(callback)`. Nothing in the platform carried this before, so an
|
|
8
|
+
app that wanted to show someone composing had nowhere to send it.
|
|
9
|
+
- Typing is never stored. It describes this moment and is worth nothing after it,
|
|
10
|
+
so it reaches only those connected at the time and is never replayed. Treat a
|
|
11
|
+
start as expiring on its own after a few seconds rather than waiting for the
|
|
12
|
+
matching stop, which is lost if the sender disconnects mid-sentence.
|
|
13
|
+
- Gated separately from read receipts, which name a message that was read and are
|
|
14
|
+
opted into per group. Typing says only that somebody is composing and discloses
|
|
15
|
+
nothing about what, so it needs no per-group opt-in.
|
|
16
|
+
|
|
17
|
+
## 0.29.0
|
|
18
|
+
|
|
19
|
+
### Fixed
|
|
20
|
+
|
|
21
|
+
- **Delivered and seen no longer depend on the sender being online.** A receipt
|
|
22
|
+
existed only as a frame on a live socket, so a sender who was away when someone
|
|
23
|
+
fetched or read the message never learned it and the mark stayed where it was
|
|
24
|
+
for good. An app that disconnects in the background, which is what an app has to
|
|
25
|
+
do to be pushed at all, missed nearly all of them. The SDK now catches up on
|
|
26
|
+
connect and reports the state as the ordinary delivered and read callbacks, so
|
|
27
|
+
there is no new API to adopt: existing receipt handling starts working.
|
|
28
|
+
|
|
29
|
+
### Notes
|
|
30
|
+
|
|
31
|
+
- Catch-up covers the messages you sent most recently and replays their current
|
|
32
|
+
state, so handling must be idempotent. Marks should only ever move forward.
|
|
33
|
+
- Additive and backward compatible. A relay that predates this answers 404 and
|
|
34
|
+
the SDK treats that as nothing to catch up on.
|
|
35
|
+
|
|
3
36
|
## 0.28.0
|
|
4
37
|
|
|
5
38
|
### Fixed
|
package/README.md
CHANGED
|
@@ -363,6 +363,12 @@ client.onMessage(async (msg) => {
|
|
|
363
363
|
| `sendCleartextGroupMessage(groupId, plaintext)` | `Promise<{ messageId }>` | Send a plaintext (non-encrypted) group message; server fans out to every other member. |
|
|
364
364
|
| `onGroupMessage(callback)` | `() => void` | Listen for group messages |
|
|
365
365
|
|
|
366
|
+
Delivered and seen are caught up on every connect, so a sender that was away
|
|
367
|
+
while someone fetched or read still learns about it. The state arrives through
|
|
368
|
+
the same delivered and read callbacks a live receipt uses, so there is nothing
|
|
369
|
+
extra to handle; make sure your handling is idempotent and only ever moves a
|
|
370
|
+
mark forward, since catch-up replays the current state rather than the changes.
|
|
371
|
+
|
|
366
372
|
**Group delivery is per member.** A group message has many recipients, so the platform
|
|
367
373
|
reports delivery as a set rather than a single flag and leaves the presentation to you.
|
|
368
374
|
`DELIVERED` and `SEEN` arrive as events whose `metadata` carries `messageId`, `groupId`
|
|
@@ -374,6 +380,22 @@ on `SERVER_RECEIVED`, which is about the relay rather than any member.
|
|
|
374
380
|
was closed or disconnected is drained automatically when it reconnects and delivered
|
|
375
381
|
through `onGroupMessage` just like a live message. There is nothing to call.
|
|
376
382
|
|
|
383
|
+
### Typing indicators
|
|
384
|
+
|
|
385
|
+
Say when someone starts and stops composing, and hear it about others. Nothing is
|
|
386
|
+
stored: it reaches only those connected at the time and is never replayed, so
|
|
387
|
+
treat a start as expiring on its own rather than waiting for the stop, which is
|
|
388
|
+
lost if the sender disconnects mid-sentence.
|
|
389
|
+
|
|
390
|
+
```typescript
|
|
391
|
+
client.sendTyping(groupId, true);
|
|
392
|
+
client.sendTyping(groupId, false);
|
|
393
|
+
|
|
394
|
+
client.onTyping((event) => {
|
|
395
|
+
// event.conversationId, event.fromUserId, event.isTyping
|
|
396
|
+
});
|
|
397
|
+
```
|
|
398
|
+
|
|
377
399
|
### 1-to-1 Calls
|
|
378
400
|
|
|
379
401
|
| Method | Returns | Description |
|
|
@@ -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, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, SfuToken, SfuRecording, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, NotificationClearCallback, DraftSyncCallback, KeyCustody, PushPayload } from './types';
|
|
3
|
+
import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, CreateRoomOptions, GroupCallEventCallback, GroupInfo, GroupMessageCallback, Room, UpdateRoomOptions, SfuToken, SfuRecording, InitializeOptions, MessageCallback, MessageDeleteCallback, MessageEditCallback, TurnCredentials, DeviceInfo, ReadReceiptCallback, TypingCallback, NotificationClearCallback, DraftSyncCallback, KeyCustody, PushPayload } 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;
|
|
@@ -90,6 +90,7 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
90
90
|
private readonly broadcastListeners;
|
|
91
91
|
private readonly messageEditListeners;
|
|
92
92
|
private readonly readReceiptListeners;
|
|
93
|
+
private readonly typingListeners;
|
|
93
94
|
private readonly notificationClearListeners;
|
|
94
95
|
private readonly draftSyncListeners;
|
|
95
96
|
private readonly messageDeleteListeners;
|
|
@@ -146,6 +147,11 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
146
147
|
* reported. Fires when another device of the same user calls markRead();
|
|
147
148
|
* use it to clear the unread state your app maintains for that message.
|
|
148
149
|
*/
|
|
150
|
+
/**
|
|
151
|
+
* Listen for someone starting or stopping typing in a conversation. Nothing is
|
|
152
|
+
* stored, so this only fires while both sides are connected.
|
|
153
|
+
*/
|
|
154
|
+
onTyping(callback: TypingCallback): () => void;
|
|
149
155
|
onReadReceipt(callback: ReadReceiptCallback): () => void;
|
|
150
156
|
editMessage(originalMessageId: string, toUserId: string, newPlaintext: string): Promise<{
|
|
151
157
|
editId: string;
|
|
@@ -217,6 +223,14 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
217
223
|
* @param conversationId optional peer userId / group id, echoed back to
|
|
218
224
|
* other devices so they can bucket the receipt
|
|
219
225
|
*/
|
|
226
|
+
/**
|
|
227
|
+
* Say this user started or stopped composing in a conversation.
|
|
228
|
+
*
|
|
229
|
+
* Never stored, so it reaches only those connected at the time. Send `false`
|
|
230
|
+
* when they stop, but treat the indicator as expiring on its own: if the sender
|
|
231
|
+
* disconnects mid-sentence, the stop never arrives.
|
|
232
|
+
*/
|
|
233
|
+
sendTyping(conversationId: string, isTyping: boolean): void;
|
|
220
234
|
markRead(messageId: string, conversationId?: string): void;
|
|
221
235
|
/**
|
|
222
236
|
* Tell this user's other devices that the notification(s) for a
|
|
@@ -388,6 +402,15 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
388
402
|
* live at that instant and is unreachable afterwards, so anyone closed loses it.
|
|
389
403
|
* Fetching is also what tells the sender the message reached this member.
|
|
390
404
|
*/
|
|
405
|
+
/**
|
|
406
|
+
* Catches up on who has the messages this client sent.
|
|
407
|
+
*
|
|
408
|
+
* A receipt used to be a frame on a live socket and nothing else, so a sender
|
|
409
|
+
* that was away when someone fetched or read never found out and the tick
|
|
410
|
+
* stayed where it was. The state is kept per member, and this replays it as the
|
|
411
|
+
* events that would have arrived live, so an app needs no separate path for it.
|
|
412
|
+
*/
|
|
413
|
+
private fetchGroupReceipts;
|
|
391
414
|
private fetchAndProcessOfflineGroupMessages;
|
|
392
415
|
private fetchAndProcessOfflineMessages;
|
|
393
416
|
private getValidDropOnAirJwt;
|
|
@@ -282,6 +282,7 @@ class MessagingClient {
|
|
|
282
282
|
this.broadcastListeners = new Set();
|
|
283
283
|
this.messageEditListeners = new Set();
|
|
284
284
|
this.readReceiptListeners = new Set();
|
|
285
|
+
this.typingListeners = new Set();
|
|
285
286
|
this.notificationClearListeners = new Set();
|
|
286
287
|
this.draftSyncListeners = new Set();
|
|
287
288
|
this.messageDeleteListeners = new Set();
|
|
@@ -519,6 +520,14 @@ class MessagingClient {
|
|
|
519
520
|
* reported. Fires when another device of the same user calls markRead();
|
|
520
521
|
* use it to clear the unread state your app maintains for that message.
|
|
521
522
|
*/
|
|
523
|
+
/**
|
|
524
|
+
* Listen for someone starting or stopping typing in a conversation. Nothing is
|
|
525
|
+
* stored, so this only fires while both sides are connected.
|
|
526
|
+
*/
|
|
527
|
+
onTyping(callback) {
|
|
528
|
+
this.typingListeners.add(callback);
|
|
529
|
+
return () => this.typingListeners.delete(callback);
|
|
530
|
+
}
|
|
522
531
|
onReadReceipt(callback) {
|
|
523
532
|
this.readReceiptListeners.add(callback);
|
|
524
533
|
return () => this.readReceiptListeners.delete(callback);
|
|
@@ -777,6 +786,26 @@ class MessagingClient {
|
|
|
777
786
|
* @param conversationId optional peer userId / group id, echoed back to
|
|
778
787
|
* other devices so they can bucket the receipt
|
|
779
788
|
*/
|
|
789
|
+
/**
|
|
790
|
+
* Say this user started or stopped composing in a conversation.
|
|
791
|
+
*
|
|
792
|
+
* Never stored, so it reaches only those connected at the time. Send `false`
|
|
793
|
+
* when they stop, but treat the indicator as expiring on its own: if the sender
|
|
794
|
+
* disconnects mid-sentence, the stop never arrives.
|
|
795
|
+
*/
|
|
796
|
+
sendTyping(conversationId, isTyping) {
|
|
797
|
+
if (!this.transport?.isOpen()) {
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
const frame = {
|
|
801
|
+
type: 'SYNC_TYPING',
|
|
802
|
+
messageId: '',
|
|
803
|
+
conversationId,
|
|
804
|
+
timestamp: Date.now(),
|
|
805
|
+
payload: isTyping ? 'start' : 'stop',
|
|
806
|
+
};
|
|
807
|
+
this.transport.send(this.codec.encodeSyncFrame(frame));
|
|
808
|
+
}
|
|
780
809
|
markRead(messageId, conversationId) {
|
|
781
810
|
if (!this.transport?.isOpen()) {
|
|
782
811
|
throw new Error('DropOnAir websocket is not connected');
|
|
@@ -838,7 +867,24 @@ class MessagingClient {
|
|
|
838
867
|
return () => this.draftSyncListeners.delete(callback);
|
|
839
868
|
}
|
|
840
869
|
handleIncomingSync(frame) {
|
|
841
|
-
if (frame.type === '
|
|
870
|
+
if (frame.type === 'SYNC_TYPING') {
|
|
871
|
+
if (!frame.conversationId || !frame.fromUserId) {
|
|
872
|
+
return;
|
|
873
|
+
}
|
|
874
|
+
const event = {
|
|
875
|
+
conversationId: frame.conversationId,
|
|
876
|
+
fromUserId: frame.fromUserId,
|
|
877
|
+
isTyping: frame.payload !== 'stop',
|
|
878
|
+
timestamp: frame.timestamp,
|
|
879
|
+
};
|
|
880
|
+
for (const listener of this.typingListeners) {
|
|
881
|
+
try {
|
|
882
|
+
listener(event);
|
|
883
|
+
}
|
|
884
|
+
catch { /* listener errors must not break the socket */ }
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
else if (frame.type === 'SYNC_READ_RECEIPT') {
|
|
842
888
|
const event = {
|
|
843
889
|
messageId: frame.messageId,
|
|
844
890
|
conversationId: frame.conversationId || undefined,
|
|
@@ -1806,6 +1852,9 @@ class MessagingClient {
|
|
|
1806
1852
|
this.sendPushRegistration().catch((err) => {
|
|
1807
1853
|
this.logError('push_registration_failed', { error: String(err) });
|
|
1808
1854
|
});
|
|
1855
|
+
this.fetchGroupReceipts().catch((err) => {
|
|
1856
|
+
this.logError('group_receipts_fetch_failed', { error: String(err) });
|
|
1857
|
+
});
|
|
1809
1858
|
this.fetchAndProcessOfflineGroupMessages().catch((err) => {
|
|
1810
1859
|
this.logError('group_offline_fetch_failed', { error: String(err) });
|
|
1811
1860
|
});
|
|
@@ -2277,6 +2326,41 @@ class MessagingClient {
|
|
|
2277
2326
|
* live at that instant and is unreachable afterwards, so anyone closed loses it.
|
|
2278
2327
|
* Fetching is also what tells the sender the message reached this member.
|
|
2279
2328
|
*/
|
|
2329
|
+
/**
|
|
2330
|
+
* Catches up on who has the messages this client sent.
|
|
2331
|
+
*
|
|
2332
|
+
* A receipt used to be a frame on a live socket and nothing else, so a sender
|
|
2333
|
+
* that was away when someone fetched or read never found out and the tick
|
|
2334
|
+
* stayed where it was. The state is kept per member, and this replays it as the
|
|
2335
|
+
* events that would have arrived live, so an app needs no separate path for it.
|
|
2336
|
+
*/
|
|
2337
|
+
async fetchGroupReceipts() {
|
|
2338
|
+
const jwt = await this.getValidDropOnAirJwt(false);
|
|
2339
|
+
const response = await this.fetchFn(`${this.httpUrl}/v1/groups/messages/receipts?limit=100`, {
|
|
2340
|
+
method: 'GET',
|
|
2341
|
+
headers: { Authorization: `Bearer ${jwt}`, 'Content-Type': 'application/json' },
|
|
2342
|
+
});
|
|
2343
|
+
// A relay that predates this endpoint answers 404: nothing to catch up on.
|
|
2344
|
+
if (!response.ok) {
|
|
2345
|
+
return;
|
|
2346
|
+
}
|
|
2347
|
+
const body = await response.json();
|
|
2348
|
+
for (const entry of body.messages ?? []) {
|
|
2349
|
+
for (const memberUserId of entry.deliveredTo ?? []) {
|
|
2350
|
+
this.emitEvent({
|
|
2351
|
+
type: 'DELIVERED',
|
|
2352
|
+
metadata: JSON.stringify({ messageId: entry.messageId, groupId: entry.groupId, memberUserId }),
|
|
2353
|
+
});
|
|
2354
|
+
}
|
|
2355
|
+
for (const memberUserId of entry.seenBy ?? []) {
|
|
2356
|
+
this.emitEvent({
|
|
2357
|
+
type: 'SEEN',
|
|
2358
|
+
metadata: JSON.stringify({ messageId: entry.messageId, groupId: entry.groupId, memberUserId }),
|
|
2359
|
+
});
|
|
2360
|
+
}
|
|
2361
|
+
}
|
|
2362
|
+
this.log('group_receipts_fetched', { messages: (body.messages ?? []).length });
|
|
2363
|
+
}
|
|
2280
2364
|
async fetchAndProcessOfflineGroupMessages() {
|
|
2281
2365
|
const jwt = await this.getValidDropOnAirJwt(false);
|
|
2282
2366
|
let page = 0;
|
package/dist/core/types.d.ts
CHANGED
|
@@ -109,6 +109,24 @@ export interface ReadReceiptEvent {
|
|
|
109
109
|
fromUserId?: string;
|
|
110
110
|
}
|
|
111
111
|
export type ReadReceiptCallback = (event: ReadReceiptEvent) => void;
|
|
112
|
+
/**
|
|
113
|
+
* Someone in a conversation started or stopped composing.
|
|
114
|
+
*
|
|
115
|
+
* Ephemeral by design: the platform never stores it, so it arrives only while
|
|
116
|
+
* both sides are connected and nothing is replayed afterwards. Treat a `true` as
|
|
117
|
+
* expiring on its own after a few seconds rather than waiting for the matching
|
|
118
|
+
* `false`, which is lost if the sender disconnects mid-sentence.
|
|
119
|
+
*/
|
|
120
|
+
export interface TypingEvent {
|
|
121
|
+
/** The group or peer the typing is happening in. */
|
|
122
|
+
conversationId: string;
|
|
123
|
+
/** Who is typing. */
|
|
124
|
+
fromUserId: string;
|
|
125
|
+
/** Whether they started or stopped. */
|
|
126
|
+
isTyping: boolean;
|
|
127
|
+
timestamp: number;
|
|
128
|
+
}
|
|
129
|
+
export type TypingCallback = (event: TypingEvent) => void;
|
|
112
130
|
/**
|
|
113
131
|
* Delivered to the user's OTHER devices when one device calls
|
|
114
132
|
* clearNotification(). Use it to clear the conversation's notification badge.
|
package/dist/index.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export { SDK_VERSION, PROTOCOL_VERSION, PAYLOAD_FORMAT_VERSION } from './version
|
|
|
22
22
|
*/
|
|
23
23
|
export declare function createSecureIdentity(store?: IdentityRecordStore): Promise<WebCryptoIdentityProvider | null>;
|
|
24
24
|
export declare function initialize(options: InitializeOptions): Promise<DropOnAirClient>;
|
|
25
|
-
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, SfuToken, SfuRecording, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
|
|
25
|
+
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, SfuToken, SfuRecording, MessageEditEvent, MessageEditCallback, MessageDeleteEvent, MessageDeleteCallback, DeviceInfo, ReadReceiptEvent, ReadReceiptCallback, TypingEvent, TypingCallback, NotificationClearEvent, NotificationClearCallback, DraftSyncEvent, DraftSyncCallback, } from './core/types';
|
|
26
26
|
export type { AttachmentEncryptionType, AttachmentConversationType, AttachmentRef, DeviceWrappedKey, CreateUploadSessionOptions, UploadSession, PrepareAttachmentOptions, DownloadedAttachment, } from './attachment/attachment-types';
|
|
27
27
|
export { SseTransport, type SseTransportOptions, type SseFrameHandler, type SseStateHandler, } from './transport/sse-transport';
|
|
28
28
|
export { WebTransportTransport, type WebTransportTransportOptions, type WTFrameHandler, type WTStateHandler, } from './transport/webtransport-transport';
|
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.30.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.30.0';
|
|
14
14
|
/**
|
|
15
15
|
* Binary encrypted-payload format version.
|
|
16
16
|
* Included as the first byte of every encrypted payload so receivers can
|
package/package.json
CHANGED