@droponair/sdk-js 0.10.0 → 0.12.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 +30 -0
- package/README.md +32 -0
- package/dist/core/messaging-client.d.ts +44 -1
- package/dist/core/messaging-client.js +132 -0
- package/dist/core/types.d.ts +54 -0
- package/dist/index.d.ts +1 -1
- package/dist/transport/protobuf-codec.d.ts +16 -0
- package/dist/transport/protobuf-codec.js +29 -0
- package/dist/version.d.ts +2 -2
- package/dist/version.js +2 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,36 @@ This project follows [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
---
|
|
8
8
|
|
|
9
|
+
## [0.12.0], 2026-05-20
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
|
|
13
|
+
- **Notification clear sync.** `client.clearNotification(conversationId)` tells the user's other devices a conversation's notifications were dismissed; `client.onNotificationCleared(cb)` listens for it. Always available (no opt-out) — it is plain own-device hygiene.
|
|
14
|
+
- **Draft sync.** `client.syncDraft(conversationId, draftText)` pushes a conversation draft to the user's other devices; `client.onDraftSync(cb)` listens for it. **Opt-in:** the app owner enables it in the dashboard, and the draft text crosses the relay in cleartext (fanned out, never stored). When disabled the server drops the frame.
|
|
15
|
+
- New exported types: `NotificationClearEvent` / `NotificationClearCallback`, `DraftSyncEvent` / `DraftSyncCallback`.
|
|
16
|
+
|
|
17
|
+
### Notes
|
|
18
|
+
|
|
19
|
+
- Both ride the existing `SyncFrame` wire type (own-device fan-out only, never delivered to a different user). No PROTOCOL_VERSION change.
|
|
20
|
+
- Your app decides when to call `clearNotification()` / `syncDraft()` — the platform never infers dismissal or tracks drafts.
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
|
|
24
|
+
## [0.11.0], 2026-05-20
|
|
25
|
+
|
|
26
|
+
### Added
|
|
27
|
+
|
|
28
|
+
- **Cross-device read receipts.** New `client.markRead(messageId, conversationId?)` reports a message as read; the receipt is relayed to the user's *other* devices (never to the message's sender). New `client.onReadReceipt(callback)` listens for receipts reported by the user's other devices — use it to clear unread state your app maintains.
|
|
29
|
+
- New exported `ReadReceiptEvent` type and `ReadReceiptCallback`.
|
|
30
|
+
- **PROTOCOL_VERSION bumped to 6.** Additive only: new `SyncFrame` wire type. Existing 0.10.x clients keep working against the new server.
|
|
31
|
+
|
|
32
|
+
### Notes
|
|
33
|
+
|
|
34
|
+
- The platform is deliberately unopinionated: **your app decides when `markRead()` is called** — the server never infers read state, and unread counts stay client-side. The app owner can disable read receipts entirely from the dashboard, in which case the server silently drops the frame.
|
|
35
|
+
- This release is own-device sync only: the *sender* of a message is not told when a recipient read it. Sender-side read notifications are a separate future capability.
|
|
36
|
+
|
|
37
|
+
---
|
|
38
|
+
|
|
9
39
|
## [0.10.0], 2026-05-20
|
|
10
40
|
|
|
11
41
|
### Added
|
package/README.md
CHANGED
|
@@ -100,6 +100,38 @@ const client = await initialize(options);
|
|
|
100
100
|
| `unregisterPushToken({ platform })` | `Promise<void>` | Unregister this device for push notifications (e.g. on logout). |
|
|
101
101
|
| `listMyDevices()` | `Promise<DeviceInfo[]>` | List the current user's registered devices. |
|
|
102
102
|
| `revokeMyDevice(deviceId)` | `Promise<DeviceInfo>` | Revoke one of the current user's devices. Permanent. |
|
|
103
|
+
| `markRead(messageId, conversationId?)` | `void` | Mark a message as read; relays a receipt to the user's other devices. |
|
|
104
|
+
| `onReadReceipt(callback)` | `() => void` | Listen for read receipts from the user's other devices. |
|
|
105
|
+
| `clearNotification(conversationId)` | `void` | Tell the user's other devices a conversation's notifications were dismissed. |
|
|
106
|
+
| `onNotificationCleared(callback)` | `() => void` | Listen for notification-clear syncs from the user's other devices. |
|
|
107
|
+
| `syncDraft(conversationId, draftText)` | `void` | Push a conversation draft to the user's other devices (opt-in, cleartext). |
|
|
108
|
+
| `onDraftSync(callback)` | `() => void` | Listen for draft syncs from the user's other devices. |
|
|
109
|
+
|
|
110
|
+
### Cross-device read receipts
|
|
111
|
+
|
|
112
|
+
Available since SDK `0.11.0`. **Your app decides when a message is read** — the platform never infers it. Call `markRead()` at that moment; the receipt syncs to the user's *other* devices so they can clear their unread UI. It is not sent to the message's sender. The app owner can switch read receipts off entirely from the dashboard.
|
|
113
|
+
|
|
114
|
+
```typescript
|
|
115
|
+
// When your UI decides the message has been read
|
|
116
|
+
client.markRead(messageId, peerUserId);
|
|
117
|
+
|
|
118
|
+
// On the user's other devices
|
|
119
|
+
client.onReadReceipt(e => {
|
|
120
|
+
// e.messageId was read elsewhere — clear your unread state for it
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### Notification clear & draft sync
|
|
125
|
+
|
|
126
|
+
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.
|
|
127
|
+
|
|
128
|
+
```typescript
|
|
129
|
+
client.clearNotification(conversationId);
|
|
130
|
+
client.onNotificationCleared(e => { /* clear badge for e.conversationId */ });
|
|
131
|
+
|
|
132
|
+
client.syncDraft(conversationId, composerText);
|
|
133
|
+
client.onDraftSync(e => { /* pre-fill composer with e.draftText */ });
|
|
134
|
+
```
|
|
103
135
|
|
|
104
136
|
### Device trust
|
|
105
137
|
|
|
@@ -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 } from './types';
|
|
3
|
+
import { BroadcastCallback, CallEventCallback, DropOnAirClient, EventCallback, GroupCallEventCallback, GroupInfo, GroupMessageCallback, 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;
|
|
@@ -71,6 +71,9 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
71
71
|
private readonly eventListeners;
|
|
72
72
|
private readonly broadcastListeners;
|
|
73
73
|
private readonly messageEditListeners;
|
|
74
|
+
private readonly readReceiptListeners;
|
|
75
|
+
private readonly notificationClearListeners;
|
|
76
|
+
private readonly draftSyncListeners;
|
|
74
77
|
private readonly messageDeleteListeners;
|
|
75
78
|
/** ------------------------------------------------------------------
|
|
76
79
|
* Lightweight structured logger. Only active when options.debug === true.
|
|
@@ -102,6 +105,12 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
102
105
|
onEvent(callback: EventCallback): () => void;
|
|
103
106
|
onMessageEdit(callback: MessageEditCallback): () => void;
|
|
104
107
|
onMessageDelete(callback: MessageDeleteCallback): () => void;
|
|
108
|
+
/**
|
|
109
|
+
* Register a listener for read receipts that this user's OTHER devices
|
|
110
|
+
* reported. Fires when another device of the same user calls markRead();
|
|
111
|
+
* use it to clear the unread state your app maintains for that message.
|
|
112
|
+
*/
|
|
113
|
+
onReadReceipt(callback: ReadReceiptCallback): () => void;
|
|
105
114
|
editMessage(originalMessageId: string, toUserId: string, newPlaintext: string): Promise<{
|
|
106
115
|
editId: string;
|
|
107
116
|
}>;
|
|
@@ -157,6 +166,40 @@ export declare class MessagingClient implements DropOnAirClient {
|
|
|
157
166
|
* show a re-register prompt.
|
|
158
167
|
*/
|
|
159
168
|
private handleDeviceRevoked;
|
|
169
|
+
/**
|
|
170
|
+
* Mark a message as read. Call this when YOUR app decides a message has
|
|
171
|
+
* been read - the platform never infers read state for you. The receipt
|
|
172
|
+
* is relayed to this user's other devices so they can clear their unread
|
|
173
|
+
* UI for the same message; it is not delivered to the message's sender.
|
|
174
|
+
*
|
|
175
|
+
* If the app owner has disabled read receipts in the dashboard, the server
|
|
176
|
+
* silently drops the frame.
|
|
177
|
+
*
|
|
178
|
+
* @param messageId the message that was read
|
|
179
|
+
* @param conversationId optional peer userId / group id, echoed back to
|
|
180
|
+
* other devices so they can bucket the receipt
|
|
181
|
+
*/
|
|
182
|
+
markRead(messageId: string, conversationId?: string): void;
|
|
183
|
+
/**
|
|
184
|
+
* Tell this user's other devices that the notification(s) for a
|
|
185
|
+
* conversation have been dismissed - call this when YOUR app dismisses a
|
|
186
|
+
* notification or the user opens the conversation. The other devices clear
|
|
187
|
+
* the matching badge. The relay never decides what "dismissed" means.
|
|
188
|
+
*/
|
|
189
|
+
clearNotification(conversationId: string): void;
|
|
190
|
+
/**
|
|
191
|
+
* Push the current draft text for a conversation to this user's other
|
|
192
|
+
* devices so the user can continue typing on another device. Draft sync is
|
|
193
|
+
* opt-in: the app owner must enable it in the dashboard, and the draft text
|
|
194
|
+
* crosses the relay in cleartext (it is fanned out, never stored). If the
|
|
195
|
+
* feature is disabled the server silently drops the frame.
|
|
196
|
+
*/
|
|
197
|
+
syncDraft(conversationId: string, draftText: string): void;
|
|
198
|
+
/** Register a listener for notification-clear syncs from the user's other devices. */
|
|
199
|
+
onNotificationCleared(callback: NotificationClearCallback): () => void;
|
|
200
|
+
/** Register a listener for draft syncs from the user's other devices. */
|
|
201
|
+
onDraftSync(callback: DraftSyncCallback): () => void;
|
|
202
|
+
private handleIncomingSync;
|
|
160
203
|
sendCleartextMessage(toUserId: string, plaintext: string): Promise<{
|
|
161
204
|
messageId: string;
|
|
162
205
|
}>;
|
|
@@ -220,6 +220,9 @@ class MessagingClient {
|
|
|
220
220
|
this.eventListeners = new Set();
|
|
221
221
|
this.broadcastListeners = new Set();
|
|
222
222
|
this.messageEditListeners = new Set();
|
|
223
|
+
this.readReceiptListeners = new Set();
|
|
224
|
+
this.notificationClearListeners = new Set();
|
|
225
|
+
this.draftSyncListeners = new Set();
|
|
223
226
|
this.messageDeleteListeners = new Set();
|
|
224
227
|
this.wsUrl = options.messagingWsUrl ?? 'wss://sdk.droponair.com/ws';
|
|
225
228
|
this.httpUrl = options.messagingHttpUrl ?? 'https://sdk.droponair.com';
|
|
@@ -410,6 +413,15 @@ class MessagingClient {
|
|
|
410
413
|
this.messageDeleteListeners.add(callback);
|
|
411
414
|
return () => this.messageDeleteListeners.delete(callback);
|
|
412
415
|
}
|
|
416
|
+
/**
|
|
417
|
+
* Register a listener for read receipts that this user's OTHER devices
|
|
418
|
+
* reported. Fires when another device of the same user calls markRead();
|
|
419
|
+
* use it to clear the unread state your app maintains for that message.
|
|
420
|
+
*/
|
|
421
|
+
onReadReceipt(callback) {
|
|
422
|
+
this.readReceiptListeners.add(callback);
|
|
423
|
+
return () => this.readReceiptListeners.delete(callback);
|
|
424
|
+
}
|
|
413
425
|
// ---------------------------------------------------------------------------
|
|
414
426
|
// Message edit and delete (PROTOCOL_VERSION 3+)
|
|
415
427
|
// ---------------------------------------------------------------------------
|
|
@@ -630,6 +642,122 @@ class MessagingClient {
|
|
|
630
642
|
}
|
|
631
643
|
}
|
|
632
644
|
// ---------------------------------------------------------------------------
|
|
645
|
+
// Cross-device read receipts (PROTOCOL_VERSION 6+ / Phase 2c)
|
|
646
|
+
// ---------------------------------------------------------------------------
|
|
647
|
+
/**
|
|
648
|
+
* Mark a message as read. Call this when YOUR app decides a message has
|
|
649
|
+
* been read - the platform never infers read state for you. The receipt
|
|
650
|
+
* is relayed to this user's other devices so they can clear their unread
|
|
651
|
+
* UI for the same message; it is not delivered to the message's sender.
|
|
652
|
+
*
|
|
653
|
+
* If the app owner has disabled read receipts in the dashboard, the server
|
|
654
|
+
* silently drops the frame.
|
|
655
|
+
*
|
|
656
|
+
* @param messageId the message that was read
|
|
657
|
+
* @param conversationId optional peer userId / group id, echoed back to
|
|
658
|
+
* other devices so they can bucket the receipt
|
|
659
|
+
*/
|
|
660
|
+
markRead(messageId, conversationId) {
|
|
661
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
662
|
+
throw new Error('DropOnAir websocket is not connected');
|
|
663
|
+
}
|
|
664
|
+
const frame = {
|
|
665
|
+
type: 'SYNC_READ_RECEIPT',
|
|
666
|
+
messageId,
|
|
667
|
+
conversationId: conversationId ?? '',
|
|
668
|
+
timestamp: Date.now(),
|
|
669
|
+
};
|
|
670
|
+
this.ws.send(this.codec.encodeSyncFrame(frame));
|
|
671
|
+
}
|
|
672
|
+
/**
|
|
673
|
+
* Tell this user's other devices that the notification(s) for a
|
|
674
|
+
* conversation have been dismissed - call this when YOUR app dismisses a
|
|
675
|
+
* notification or the user opens the conversation. The other devices clear
|
|
676
|
+
* the matching badge. The relay never decides what "dismissed" means.
|
|
677
|
+
*/
|
|
678
|
+
clearNotification(conversationId) {
|
|
679
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
680
|
+
throw new Error('DropOnAir websocket is not connected');
|
|
681
|
+
}
|
|
682
|
+
const frame = {
|
|
683
|
+
type: 'SYNC_CLEAR_NOTIFICATION',
|
|
684
|
+
messageId: '',
|
|
685
|
+
conversationId,
|
|
686
|
+
timestamp: Date.now(),
|
|
687
|
+
};
|
|
688
|
+
this.ws.send(this.codec.encodeSyncFrame(frame));
|
|
689
|
+
}
|
|
690
|
+
/**
|
|
691
|
+
* Push the current draft text for a conversation to this user's other
|
|
692
|
+
* devices so the user can continue typing on another device. Draft sync is
|
|
693
|
+
* opt-in: the app owner must enable it in the dashboard, and the draft text
|
|
694
|
+
* crosses the relay in cleartext (it is fanned out, never stored). If the
|
|
695
|
+
* feature is disabled the server silently drops the frame.
|
|
696
|
+
*/
|
|
697
|
+
syncDraft(conversationId, draftText) {
|
|
698
|
+
if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
|
|
699
|
+
throw new Error('DropOnAir websocket is not connected');
|
|
700
|
+
}
|
|
701
|
+
const frame = {
|
|
702
|
+
type: 'SYNC_DRAFT',
|
|
703
|
+
messageId: '',
|
|
704
|
+
conversationId,
|
|
705
|
+
timestamp: Date.now(),
|
|
706
|
+
payload: draftText,
|
|
707
|
+
};
|
|
708
|
+
this.ws.send(this.codec.encodeSyncFrame(frame));
|
|
709
|
+
}
|
|
710
|
+
/** Register a listener for notification-clear syncs from the user's other devices. */
|
|
711
|
+
onNotificationCleared(callback) {
|
|
712
|
+
this.notificationClearListeners.add(callback);
|
|
713
|
+
return () => this.notificationClearListeners.delete(callback);
|
|
714
|
+
}
|
|
715
|
+
/** Register a listener for draft syncs from the user's other devices. */
|
|
716
|
+
onDraftSync(callback) {
|
|
717
|
+
this.draftSyncListeners.add(callback);
|
|
718
|
+
return () => this.draftSyncListeners.delete(callback);
|
|
719
|
+
}
|
|
720
|
+
handleIncomingSync(frame) {
|
|
721
|
+
if (frame.type === 'SYNC_READ_RECEIPT') {
|
|
722
|
+
const event = {
|
|
723
|
+
messageId: frame.messageId,
|
|
724
|
+
conversationId: frame.conversationId || undefined,
|
|
725
|
+
timestamp: frame.timestamp,
|
|
726
|
+
};
|
|
727
|
+
for (const listener of this.readReceiptListeners) {
|
|
728
|
+
try {
|
|
729
|
+
listener(event);
|
|
730
|
+
}
|
|
731
|
+
catch { /* listener errors must not break the socket */ }
|
|
732
|
+
}
|
|
733
|
+
}
|
|
734
|
+
else if (frame.type === 'SYNC_CLEAR_NOTIFICATION') {
|
|
735
|
+
const event = {
|
|
736
|
+
conversationId: frame.conversationId || '',
|
|
737
|
+
timestamp: frame.timestamp,
|
|
738
|
+
};
|
|
739
|
+
for (const listener of this.notificationClearListeners) {
|
|
740
|
+
try {
|
|
741
|
+
listener(event);
|
|
742
|
+
}
|
|
743
|
+
catch { /* listener errors must not break the socket */ }
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
else if (frame.type === 'SYNC_DRAFT') {
|
|
747
|
+
const event = {
|
|
748
|
+
conversationId: frame.conversationId || '',
|
|
749
|
+
draftText: frame.payload || '',
|
|
750
|
+
timestamp: frame.timestamp,
|
|
751
|
+
};
|
|
752
|
+
for (const listener of this.draftSyncListeners) {
|
|
753
|
+
try {
|
|
754
|
+
listener(event);
|
|
755
|
+
}
|
|
756
|
+
catch { /* listener errors must not break the socket */ }
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
// ---------------------------------------------------------------------------
|
|
633
761
|
// Cleartext messaging (no E2EE key exchange required)
|
|
634
762
|
// ---------------------------------------------------------------------------
|
|
635
763
|
async sendCleartextMessage(toUserId, plaintext) {
|
|
@@ -1389,6 +1517,10 @@ class MessagingClient {
|
|
|
1389
1517
|
this.handleIncomingMessageDelete(frame.data);
|
|
1390
1518
|
return;
|
|
1391
1519
|
}
|
|
1520
|
+
if (frame.kind === 'sync') {
|
|
1521
|
+
this.handleIncomingSync(frame.data);
|
|
1522
|
+
return;
|
|
1523
|
+
}
|
|
1392
1524
|
if (frame.kind === 'envelope') {
|
|
1393
1525
|
await this.handleIncomingEnvelope(frame.data);
|
|
1394
1526
|
}
|
package/dist/core/types.d.ts
CHANGED
|
@@ -48,6 +48,38 @@ export interface MessageDeleteEvent {
|
|
|
48
48
|
}
|
|
49
49
|
export type MessageEditCallback = (event: MessageEditEvent) => void;
|
|
50
50
|
export type MessageDeleteCallback = (event: MessageDeleteEvent) => void;
|
|
51
|
+
/**
|
|
52
|
+
* Delivered to the user's OTHER devices when one device calls markRead().
|
|
53
|
+
* Use it to clear whatever unread state your app keeps for that message.
|
|
54
|
+
* It is NOT delivered to the original sender of the message.
|
|
55
|
+
*/
|
|
56
|
+
export interface ReadReceiptEvent {
|
|
57
|
+
messageId: string;
|
|
58
|
+
/** Optional peer userId / group id, echoed from markRead() for bucketing. */
|
|
59
|
+
conversationId?: string;
|
|
60
|
+
timestamp: number;
|
|
61
|
+
}
|
|
62
|
+
export type ReadReceiptCallback = (event: ReadReceiptEvent) => void;
|
|
63
|
+
/**
|
|
64
|
+
* Delivered to the user's OTHER devices when one device calls
|
|
65
|
+
* clearNotification(). Use it to clear the conversation's notification badge.
|
|
66
|
+
*/
|
|
67
|
+
export interface NotificationClearEvent {
|
|
68
|
+
conversationId: string;
|
|
69
|
+
timestamp: number;
|
|
70
|
+
}
|
|
71
|
+
export type NotificationClearCallback = (event: NotificationClearEvent) => void;
|
|
72
|
+
/**
|
|
73
|
+
* Delivered to the user's OTHER devices when one device calls syncDraft().
|
|
74
|
+
* Use it to pre-fill the message composer for that conversation. Draft sync
|
|
75
|
+
* is opt-in and the text crosses the relay in cleartext.
|
|
76
|
+
*/
|
|
77
|
+
export interface DraftSyncEvent {
|
|
78
|
+
conversationId: string;
|
|
79
|
+
draftText: string;
|
|
80
|
+
timestamp: number;
|
|
81
|
+
}
|
|
82
|
+
export type DraftSyncCallback = (event: DraftSyncEvent) => void;
|
|
51
83
|
export interface BroadcastMessage {
|
|
52
84
|
broadcastId: string;
|
|
53
85
|
channelId: string;
|
|
@@ -183,6 +215,28 @@ export interface DropOnAirClient {
|
|
|
183
215
|
onMessageEdit(callback: MessageEditCallback): () => void;
|
|
184
216
|
/** Register a listener for incoming message-delete notifications. */
|
|
185
217
|
onMessageDelete(callback: MessageDeleteCallback): () => void;
|
|
218
|
+
/**
|
|
219
|
+
* Mark a message as read. The integrating app decides when this is called;
|
|
220
|
+
* the platform never infers read state. The receipt is relayed to the
|
|
221
|
+
* user's other devices only, never to the message's sender.
|
|
222
|
+
*/
|
|
223
|
+
markRead(messageId: string, conversationId?: string): void;
|
|
224
|
+
/** Register a listener for read receipts reported by this user's other devices. */
|
|
225
|
+
onReadReceipt(callback: ReadReceiptCallback): () => void;
|
|
226
|
+
/**
|
|
227
|
+
* Tell the user's other devices a conversation's notifications were
|
|
228
|
+
* dismissed. The app decides what "dismissed" means.
|
|
229
|
+
*/
|
|
230
|
+
clearNotification(conversationId: string): void;
|
|
231
|
+
/** Listen for notification-clear syncs from the user's other devices. */
|
|
232
|
+
onNotificationCleared(callback: NotificationClearCallback): () => void;
|
|
233
|
+
/**
|
|
234
|
+
* Push a conversation draft to the user's other devices. Opt-in; the draft
|
|
235
|
+
* text crosses the relay in cleartext and is never stored.
|
|
236
|
+
*/
|
|
237
|
+
syncDraft(conversationId: string, draftText: string): void;
|
|
238
|
+
/** Listen for draft syncs from the user's other devices. */
|
|
239
|
+
onDraftSync(callback: DraftSyncCallback): () => void;
|
|
186
240
|
/**
|
|
187
241
|
* Register this device's push notification token. The platform delivers a
|
|
188
242
|
* push via APNs / FCM / Web Push when a sender attaches a pushPayload to
|
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, } 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, 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;
|
|
@@ -148,6 +148,17 @@ export interface WirePushRegistrationFrame {
|
|
|
148
148
|
voipToken?: string;
|
|
149
149
|
deviceId: string;
|
|
150
150
|
}
|
|
151
|
+
/**
|
|
152
|
+
* Cross-device state sync frame (PROTOCOL_VERSION 6+). Same shape sent
|
|
153
|
+
* client -> server and relayed server -> the user's other devices.
|
|
154
|
+
*/
|
|
155
|
+
export interface WireSyncFrame {
|
|
156
|
+
type: string;
|
|
157
|
+
messageId: string;
|
|
158
|
+
conversationId?: string;
|
|
159
|
+
timestamp: number;
|
|
160
|
+
payload?: string;
|
|
161
|
+
}
|
|
151
162
|
/** Tombstone frame, scope FOR_EVERYONE or FOR_ME. */
|
|
152
163
|
export interface WireMessageDeleteFrame {
|
|
153
164
|
type: string;
|
|
@@ -190,6 +201,9 @@ export type InboundFrame = {
|
|
|
190
201
|
} | {
|
|
191
202
|
kind: 'messageDelete';
|
|
192
203
|
data: WireMessageDeleteFrame;
|
|
204
|
+
} | {
|
|
205
|
+
kind: 'sync';
|
|
206
|
+
data: WireSyncFrame;
|
|
193
207
|
};
|
|
194
208
|
export declare class ProtobufCodec {
|
|
195
209
|
encodeEnvelope(value: WireEnvelope): Uint8Array;
|
|
@@ -202,6 +216,7 @@ export declare class ProtobufCodec {
|
|
|
202
216
|
encodeMessageEditFrame(value: WireMessageEditFrame): Uint8Array;
|
|
203
217
|
encodeMessageDeleteFrame(value: WireMessageDeleteFrame): Uint8Array;
|
|
204
218
|
encodePushRegistrationFrame(value: WirePushRegistrationFrame): Uint8Array;
|
|
219
|
+
encodeSyncFrame(value: WireSyncFrame): Uint8Array;
|
|
205
220
|
decodeFrame(payload: Uint8Array): InboundFrame;
|
|
206
221
|
private tryDecodeEnvelope;
|
|
207
222
|
private tryDecodeAck;
|
|
@@ -211,6 +226,7 @@ export declare class ProtobufCodec {
|
|
|
211
226
|
private tryDecodeGroupMessageNotification;
|
|
212
227
|
private tryDecodeGroupAck;
|
|
213
228
|
private tryDecodeGroupCallFrame;
|
|
229
|
+
private tryDecodeSyncFrame;
|
|
214
230
|
private tryDecodeMessageEditFrame;
|
|
215
231
|
private tryDecodeMessageDeleteFrame;
|
|
216
232
|
}
|
|
@@ -217,6 +217,15 @@ const PushRegistrationFrameType = new protobuf.Type('PushRegistrationFrame')
|
|
|
217
217
|
.add(new protobuf.Field('token', 3, 'string'))
|
|
218
218
|
.add(new protobuf.Field('voipToken', 4, 'string'))
|
|
219
219
|
.add(new protobuf.Field('deviceId', 5, 'string'));
|
|
220
|
+
// Cross-device state sync frame (PROTOCOL_VERSION 6+). Same shape both
|
|
221
|
+
// directions: client -> server (request) and server -> the user's other
|
|
222
|
+
// devices (notification). Discriminator is `type` at field 1 ("SYNC_").
|
|
223
|
+
const SyncFrameType = new protobuf.Type('SyncFrame')
|
|
224
|
+
.add(new protobuf.Field('type', 1, 'string'))
|
|
225
|
+
.add(new protobuf.Field('messageId', 2, 'string'))
|
|
226
|
+
.add(new protobuf.Field('conversationId', 3, 'string'))
|
|
227
|
+
.add(new protobuf.Field('timestamp', 4, 'int64'))
|
|
228
|
+
.add(new protobuf.Field('payload', 5, 'string'));
|
|
220
229
|
class ProtobufCodec {
|
|
221
230
|
encodeEnvelope(value) {
|
|
222
231
|
return EnvelopeType.encode(value).finish();
|
|
@@ -248,6 +257,9 @@ class ProtobufCodec {
|
|
|
248
257
|
encodePushRegistrationFrame(value) {
|
|
249
258
|
return PushRegistrationFrameType.encode(value).finish();
|
|
250
259
|
}
|
|
260
|
+
encodeSyncFrame(value) {
|
|
261
|
+
return SyncFrameType.encode(value).finish();
|
|
262
|
+
}
|
|
251
263
|
decodeFrame(payload) {
|
|
252
264
|
// Edit and delete frames must be probed BEFORE Envelope, the type
|
|
253
265
|
// discriminator at field 1 ("MESSAGE_EDIT" / "MESSAGE_DELETE") would
|
|
@@ -260,6 +272,11 @@ class ProtobufCodec {
|
|
|
260
272
|
if (asDelete) {
|
|
261
273
|
return { kind: 'messageDelete', data: asDelete };
|
|
262
274
|
}
|
|
275
|
+
// SyncFrame: discriminator "SYNC_" at field 1, probed before Envelope.
|
|
276
|
+
const asSync = this.tryDecodeSyncFrame(payload);
|
|
277
|
+
if (asSync) {
|
|
278
|
+
return { kind: 'sync', data: asSync };
|
|
279
|
+
}
|
|
263
280
|
const asEnvelope = this.tryDecodeEnvelope(payload);
|
|
264
281
|
if (asEnvelope) {
|
|
265
282
|
return { kind: 'envelope', data: asEnvelope };
|
|
@@ -446,6 +463,18 @@ class ProtobufCodec {
|
|
|
446
463
|
return null;
|
|
447
464
|
}
|
|
448
465
|
}
|
|
466
|
+
tryDecodeSyncFrame(payload) {
|
|
467
|
+
try {
|
|
468
|
+
const decoded = SyncFrameType.decode(payload);
|
|
469
|
+
if (!decoded.type || !decoded.type.startsWith('SYNC_')) {
|
|
470
|
+
return null;
|
|
471
|
+
}
|
|
472
|
+
return { ...decoded, timestamp: Number(decoded.timestamp) };
|
|
473
|
+
}
|
|
474
|
+
catch {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
449
478
|
tryDecodeMessageEditFrame(payload) {
|
|
450
479
|
try {
|
|
451
480
|
const decoded = MessageEditFrameType.decode(payload);
|
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.12.0";
|
|
11
11
|
/**
|
|
12
12
|
* Binary encrypted-payload format version.
|
|
13
13
|
* Included as the first byte of every encrypted payload so receivers can
|
|
@@ -19,4 +19,4 @@ export declare const PAYLOAD_FORMAT_VERSION = 1;
|
|
|
19
19
|
* Increment when adding required proto fields or changing frame semantics.
|
|
20
20
|
* The server advertises its supported range via GET /api/info.
|
|
21
21
|
*/
|
|
22
|
-
export declare const PROTOCOL_VERSION =
|
|
22
|
+
export declare const PROTOCOL_VERSION = 6;
|
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.12.0';
|
|
14
14
|
/**
|
|
15
15
|
* Binary encrypted-payload format version.
|
|
16
16
|
* Included as the first byte of every encrypted payload so receivers can
|
|
@@ -22,4 +22,4 @@ exports.PAYLOAD_FORMAT_VERSION = 1;
|
|
|
22
22
|
* Increment when adding required proto fields or changing frame semantics.
|
|
23
23
|
* The server advertises its supported range via GET /api/info.
|
|
24
24
|
*/
|
|
25
|
-
exports.PROTOCOL_VERSION =
|
|
25
|
+
exports.PROTOCOL_VERSION = 6;
|