@nyaruka/temba-components 0.171.0 → 0.172.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 +23 -1
- package/dist/temba-components.js +1015 -747
- package/dist/temba-components.js.map +1 -1
- package/package.json +1 -1
- package/src/RapidElement.ts +83 -41
- package/src/display/Button.ts +14 -2
- package/src/display/Chat.ts +41 -22
- package/src/display/Label.ts +18 -3
- package/src/events/eventRenderers.ts +4 -0
- package/src/flow/FlowSearch.ts +12 -392
- package/src/flow/actions/send_msg.ts +2 -1
- package/src/flow/actions/set_contact_field.ts +1 -0
- package/src/flow/actions/set_contact_name.ts +1 -0
- package/src/flow/nodes/split_by_ticket.ts +1 -0
- package/src/flow/nodes/wait_for_dial.ts +1 -0
- package/src/form/RangePicker.ts +10 -2
- package/src/form/select/Select.ts +15 -2
- package/src/layout/SearchModal.ts +564 -0
- package/src/list/BroadcastList.ts +17 -6
- package/src/list/ContentList.ts +29 -13
- package/src/list/FieldList.ts +8 -1
- package/src/list/TembaMenu.ts +71 -13
- package/src/live/CampaignEvents.ts +31 -11
- package/src/live/ContactChat.ts +276 -76
- package/src/live/ContactTimeline.ts +46 -24
- package/src/live/ContactWatch.ts +166 -63
- package/src/live/Realtime.ts +137 -8
- package/src/live/SocketService.ts +115 -10
- package/src/live/TicketSearch.ts +290 -0
- package/src/live/Watchers.ts +93 -0
- package/src/simulator/Simulator.ts +59 -36
- package/src/store/Store.ts +17 -34
- package/src/styles/designTokens.ts +57 -17
- package/src/styles/pillVariants.ts +42 -10
- package/static/css/design-system.css +47 -12
- package/static/css/temba-components.css +31 -9
- package/temba-modules.ts +2 -0
package/src/live/ContactWatch.ts
CHANGED
|
@@ -1,7 +1,19 @@
|
|
|
1
1
|
import { Contact } from '../interfaces';
|
|
2
2
|
import { Events } from '../events/eventRenderers';
|
|
3
3
|
import { getStore } from '../store/Store';
|
|
4
|
-
import {
|
|
4
|
+
import { Watchers } from './Watchers';
|
|
5
|
+
import {
|
|
6
|
+
ContactFieldChangedEvent,
|
|
7
|
+
ContactFlowChangedEvent,
|
|
8
|
+
ContactGroupsChangedEvent,
|
|
9
|
+
ContactHistoryEvent,
|
|
10
|
+
ContactLanguageChangedEvent,
|
|
11
|
+
ContactLastSeenChangedEvent,
|
|
12
|
+
ContactNameChangedEvent,
|
|
13
|
+
ContactStatusChangedEvent,
|
|
14
|
+
RealtimeSubscription,
|
|
15
|
+
subscribeToContactHistory
|
|
16
|
+
} from './Realtime';
|
|
5
17
|
|
|
6
18
|
/**
|
|
7
19
|
* Central interest registry for live contact state. Components declare what
|
|
@@ -28,7 +40,12 @@ import { RealtimeSubscription, subscribeToContactHistory } from './Realtime';
|
|
|
28
40
|
* unsubscribes the socket subscription and cached contact are dropped.
|
|
29
41
|
*
|
|
30
42
|
* Wildcard watchers are event-stream consumers (e.g. chat renders history) -
|
|
31
|
-
* they get every live event but no eventless deliveries
|
|
43
|
+
* they get every live event but no eventless deliveries, and an entry with
|
|
44
|
+
* only those holds no contact and fetches nothing. They keep their own copy
|
|
45
|
+
* instead, applying events to it with applyContactEvent so how an event
|
|
46
|
+
* changes a contact is still defined in one place. Any watcher can pass
|
|
47
|
+
* onSubscribed to be told when the channel goes live, which is where a
|
|
48
|
+
* stream consumer catches up on what it missed while offline.
|
|
32
49
|
*/
|
|
33
50
|
|
|
34
51
|
// same shape components previously fetched themselves - urns are expanded and
|
|
@@ -60,15 +77,19 @@ export const CONTACT_STATE_TYPES = [
|
|
|
60
77
|
Events.CONTACT_LAST_SEEN_CHANGED
|
|
61
78
|
];
|
|
62
79
|
|
|
63
|
-
export type ContactEventHandler = (
|
|
80
|
+
export type ContactEventHandler = (
|
|
81
|
+
event: ContactHistoryEvent | null,
|
|
82
|
+
contact: Contact
|
|
83
|
+
) => void;
|
|
64
84
|
|
|
65
85
|
interface Watcher {
|
|
66
86
|
types: string[] | '*';
|
|
67
87
|
onEvent: ContactEventHandler;
|
|
88
|
+
onSubscribed?: () => void;
|
|
68
89
|
}
|
|
69
90
|
|
|
70
91
|
interface WatchedContact {
|
|
71
|
-
watchers: Watcher
|
|
92
|
+
watchers: Watchers<Watcher>;
|
|
72
93
|
sub: RealtimeSubscription;
|
|
73
94
|
contact: Contact;
|
|
74
95
|
fetchSeq: number;
|
|
@@ -76,10 +97,19 @@ interface WatchedContact {
|
|
|
76
97
|
// events landing while one is outstanding need a refetch to survive
|
|
77
98
|
fetching: number;
|
|
78
99
|
refetchTimer: number;
|
|
100
|
+
// whether the channel is live, so watchers joining an already-subscribed
|
|
101
|
+
// channel still get their initial catch-up call
|
|
102
|
+
subscribed: boolean;
|
|
79
103
|
}
|
|
80
104
|
|
|
81
105
|
const watched = new Map<string, WatchedContact>();
|
|
82
106
|
|
|
107
|
+
const newWatchers = (watcher: Watcher): Watchers<Watcher> => {
|
|
108
|
+
const watchers = new Watchers<Watcher>('contact watcher');
|
|
109
|
+
watchers.add(watcher);
|
|
110
|
+
return watchers;
|
|
111
|
+
};
|
|
112
|
+
|
|
83
113
|
/**
|
|
84
114
|
* Serializes an engine field value from an event the way the read API
|
|
85
115
|
* would - both read the same engine value dict, keyed by the field's type
|
|
@@ -107,19 +137,28 @@ const serializeFieldValue = (key: string, value: any): string | undefined => {
|
|
|
107
137
|
const appliers: {
|
|
108
138
|
[type: string]: (contact: Contact, event: any) => boolean | void;
|
|
109
139
|
} = {
|
|
110
|
-
[Events.CONTACT_NAME_CHANGED]: (contact, event) => {
|
|
140
|
+
[Events.CONTACT_NAME_CHANGED]: (contact, event: ContactNameChangedEvent) => {
|
|
111
141
|
contact.name = event.name;
|
|
112
142
|
},
|
|
113
|
-
[Events.CONTACT_LANGUAGE_CHANGED]: (
|
|
143
|
+
[Events.CONTACT_LANGUAGE_CHANGED]: (
|
|
144
|
+
contact,
|
|
145
|
+
event: ContactLanguageChangedEvent
|
|
146
|
+
) => {
|
|
114
147
|
contact.language = event.language;
|
|
115
148
|
},
|
|
116
|
-
[Events.CONTACT_STATUS_CHANGED]: (
|
|
149
|
+
[Events.CONTACT_STATUS_CHANGED]: (
|
|
150
|
+
contact,
|
|
151
|
+
event: ContactStatusChangedEvent
|
|
152
|
+
) => {
|
|
117
153
|
contact.status = event.status;
|
|
118
154
|
},
|
|
119
|
-
[Events.CONTACT_FLOW_CHANGED]: (contact, event) => {
|
|
155
|
+
[Events.CONTACT_FLOW_CHANGED]: (contact, event: ContactFlowChangedEvent) => {
|
|
120
156
|
contact.flow = event.flow || null;
|
|
121
157
|
},
|
|
122
|
-
[Events.CONTACT_LAST_SEEN_CHANGED]: (
|
|
158
|
+
[Events.CONTACT_LAST_SEEN_CHANGED]: (
|
|
159
|
+
contact,
|
|
160
|
+
event: ContactLastSeenChangedEvent
|
|
161
|
+
) => {
|
|
123
162
|
// last seen only ever moves forward - ignore out of order deliveries
|
|
124
163
|
if (
|
|
125
164
|
!contact.last_seen_on ||
|
|
@@ -128,7 +167,10 @@ const appliers: {
|
|
|
128
167
|
contact.last_seen_on = event.last_seen_on;
|
|
129
168
|
}
|
|
130
169
|
},
|
|
131
|
-
[Events.CONTACT_FIELD_CHANGED]: (
|
|
170
|
+
[Events.CONTACT_FIELD_CHANGED]: (
|
|
171
|
+
contact,
|
|
172
|
+
event: ContactFieldChangedEvent
|
|
173
|
+
) => {
|
|
132
174
|
const key = event.field?.key;
|
|
133
175
|
if (!key) {
|
|
134
176
|
return;
|
|
@@ -141,19 +183,21 @@ const appliers: {
|
|
|
141
183
|
// replace rather than mutate so earlier deliveries keep their values
|
|
142
184
|
contact.fields = { ...contact.fields, [key]: value };
|
|
143
185
|
},
|
|
144
|
-
[Events.CONTACT_GROUPS_CHANGED]: (
|
|
186
|
+
[Events.CONTACT_GROUPS_CHANGED]: (
|
|
187
|
+
contact,
|
|
188
|
+
event: ContactGroupsChangedEvent
|
|
189
|
+
) => {
|
|
145
190
|
const added = event.groups_added || [];
|
|
146
191
|
const removed = new Set(
|
|
147
|
-
(event.groups_removed || []).map((group
|
|
192
|
+
(event.groups_removed || []).map((group) => group.uuid)
|
|
148
193
|
);
|
|
149
194
|
contact.groups = [
|
|
150
195
|
...(contact.groups || []).filter(
|
|
151
196
|
(group) =>
|
|
152
|
-
!removed.has(group.uuid) &&
|
|
153
|
-
!added.some((a: any) => a.uuid === group.uuid)
|
|
197
|
+
!removed.has(group.uuid) && !added.some((a) => a.uuid === group.uuid)
|
|
154
198
|
),
|
|
155
199
|
// group references can arrive without a name, but consumers sort on it
|
|
156
|
-
...added.map((group
|
|
200
|
+
...added.map((group) => ({
|
|
157
201
|
uuid: group.uuid,
|
|
158
202
|
name: group.name || ''
|
|
159
203
|
}))
|
|
@@ -172,14 +216,31 @@ const matches = (watcher: Watcher, type: string): boolean => {
|
|
|
172
216
|
return watcher.types === '*' || watcher.types.includes(type);
|
|
173
217
|
};
|
|
174
218
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
219
|
+
/**
|
|
220
|
+
* Whether anyone is watching this contact's *state*. Wildcard watchers are
|
|
221
|
+
* stream consumers - they take the events and keep their own view of the
|
|
222
|
+
* contact - so an entry with only those has no contact to hold current and
|
|
223
|
+
* nothing to fetch.
|
|
224
|
+
*/
|
|
225
|
+
const needsContact = (entry: WatchedContact): boolean => {
|
|
226
|
+
return entry.watchers.some((watcher) => watcher.types !== '*');
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Applies a live contact event to a contact, the same way the registry
|
|
231
|
+
* applies it to the one it holds. Exposed for stream consumers that keep
|
|
232
|
+
* their own copy, so how an event changes a contact is defined once.
|
|
233
|
+
* Returns false when the event couldn't be applied.
|
|
234
|
+
*/
|
|
235
|
+
export const applyContactEvent = (
|
|
236
|
+
contact: Contact,
|
|
237
|
+
event: ContactHistoryEvent
|
|
238
|
+
): boolean | void => {
|
|
239
|
+
const apply = appliers[event.type];
|
|
240
|
+
if (!contact || !apply) {
|
|
241
|
+
return;
|
|
182
242
|
}
|
|
243
|
+
return apply(contact, event);
|
|
183
244
|
};
|
|
184
245
|
|
|
185
246
|
/**
|
|
@@ -187,11 +248,10 @@ const deliver = (watcher: Watcher, event: any, contact: Contact) => {
|
|
|
187
248
|
* delivery.
|
|
188
249
|
*/
|
|
189
250
|
const primeAll = (entry: WatchedContact) => {
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
251
|
+
entry.watchers.each(
|
|
252
|
+
(watcher) => watcher.onEvent(null, entry.contact),
|
|
253
|
+
(watcher) => watcher.types !== '*'
|
|
254
|
+
);
|
|
195
255
|
};
|
|
196
256
|
|
|
197
257
|
const clearRefetch = (entry: WatchedContact) => {
|
|
@@ -286,7 +346,11 @@ const scheduleRefetch = (uuid: string, entry: WatchedContact) => {
|
|
|
286
346
|
}, REFETCH_DEBOUNCE);
|
|
287
347
|
};
|
|
288
348
|
|
|
289
|
-
const handleEvent = (
|
|
349
|
+
const handleEvent = (
|
|
350
|
+
uuid: string,
|
|
351
|
+
entry: WatchedContact,
|
|
352
|
+
event: ContactHistoryEvent
|
|
353
|
+
) => {
|
|
290
354
|
const apply = appliers[event.type];
|
|
291
355
|
const applied = entry.contact && apply ? apply(entry.contact, event) : null;
|
|
292
356
|
|
|
@@ -294,34 +358,58 @@ const handleEvent = (uuid: string, entry: WatchedContact, event: any) => {
|
|
|
294
358
|
// was no snapshot to patch, a fetch that predates the event is still in
|
|
295
359
|
// flight, or the applier couldn't resolve what it needed. Scheduling ahead
|
|
296
360
|
// of the fan-out keeps a throwing watcher from costing us the refetch, and
|
|
297
|
-
// the debounce collapses a burst into a single fetch
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
361
|
+
// the debounce collapses a burst into a single fetch. Entries nobody needs
|
|
362
|
+
// a contact from have nothing to keep current
|
|
363
|
+
if (needsContact(entry)) {
|
|
364
|
+
const stale =
|
|
365
|
+
!!apply && (!entry.contact || entry.fetching > 0 || applied === false);
|
|
366
|
+
if (stale || refetchTypes.has(event.type)) {
|
|
367
|
+
scheduleRefetch(uuid, entry);
|
|
368
|
+
}
|
|
302
369
|
}
|
|
303
370
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
371
|
+
entry.watchers.each(
|
|
372
|
+
(watcher) => watcher.onEvent(event, entry.contact),
|
|
373
|
+
(watcher) => matches(watcher, event.type)
|
|
374
|
+
);
|
|
375
|
+
};
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* A (re)subscribe on the contact's channel. Watchers that render contact
|
|
379
|
+
* state get a fresh fetch; every watcher that asked for it gets told, so
|
|
380
|
+
* stream consumers can fetch whatever they missed while we were away.
|
|
381
|
+
*/
|
|
382
|
+
const handleSubscribed = (uuid: string, entry: WatchedContact) => {
|
|
383
|
+
entry.subscribed = true;
|
|
384
|
+
if (needsContact(entry)) {
|
|
385
|
+
fetchContact(uuid, entry);
|
|
308
386
|
}
|
|
387
|
+
entry.watchers.each(
|
|
388
|
+
(watcher) => watcher.onSubscribed(),
|
|
389
|
+
(watcher) => !!watcher.onSubscribed
|
|
390
|
+
);
|
|
309
391
|
};
|
|
310
392
|
|
|
311
393
|
export const watchContact = (
|
|
312
394
|
uuid: string,
|
|
313
395
|
types: string[] | '*',
|
|
314
|
-
onEvent: ContactEventHandler
|
|
396
|
+
onEvent: ContactEventHandler,
|
|
397
|
+
onSubscribed?: () => void
|
|
315
398
|
): RealtimeSubscription => {
|
|
316
|
-
|
|
399
|
+
const entry = watched.get(uuid);
|
|
400
|
+
const watcher: Watcher = { types, onEvent, onSubscribed };
|
|
401
|
+
|
|
317
402
|
if (!entry) {
|
|
318
403
|
const newEntry: WatchedContact = {
|
|
319
|
-
|
|
404
|
+
// registered before we subscribe so the first (re)subscribe already
|
|
405
|
+
// knows whether anyone needs a contact held for them
|
|
406
|
+
watchers: newWatchers(watcher),
|
|
320
407
|
sub: null,
|
|
321
408
|
contact: null,
|
|
322
409
|
fetchSeq: 0,
|
|
323
410
|
fetching: 0,
|
|
324
|
-
refetchTimer: null
|
|
411
|
+
refetchTimer: null,
|
|
412
|
+
subscribed: false
|
|
325
413
|
};
|
|
326
414
|
watched.set(uuid, newEntry);
|
|
327
415
|
newEntry.sub = subscribeToContactHistory(
|
|
@@ -330,39 +418,54 @@ export const watchContact = (
|
|
|
330
418
|
(event) => handleEvent(uuid, newEntry, event),
|
|
331
419
|
// fires on every (re)subscribe incl. after reconnects - (re)fetch so
|
|
332
420
|
// watchers see anything that changed while we weren't listening
|
|
333
|
-
() =>
|
|
421
|
+
() => handleSubscribed(uuid, newEntry)
|
|
334
422
|
);
|
|
335
|
-
|
|
423
|
+
return unwatch(uuid, newEntry, watcher);
|
|
336
424
|
}
|
|
337
425
|
|
|
338
|
-
|
|
339
|
-
|
|
426
|
+
entry.watchers.add(watcher);
|
|
427
|
+
|
|
428
|
+
// a watcher that renders contact state joining an entry that was holding
|
|
429
|
+
// none (only stream consumers so far) needs one fetched for it. On a
|
|
430
|
+
// channel that isn't live yet the pending (re)subscribe covers it
|
|
431
|
+
if (types !== '*' && entry.subscribed && !entry.contact && !entry.fetching) {
|
|
432
|
+
fetchContact(uuid, entry);
|
|
433
|
+
}
|
|
340
434
|
|
|
341
435
|
// late joiners on an already-fetched contact get their initial delivery
|
|
342
436
|
// without waiting for another fetch - async so it mirrors the fetch path
|
|
343
437
|
if (entry.contact && types !== '*') {
|
|
344
438
|
const contact = entry.contact;
|
|
345
|
-
|
|
346
|
-
if (entry.watchers.includes(watcher)) {
|
|
347
|
-
deliver(watcher, null, contact);
|
|
348
|
-
}
|
|
349
|
-
});
|
|
439
|
+
entry.watchers.prime(watcher, () => watcher.onEvent(null, contact));
|
|
350
440
|
}
|
|
351
441
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
if (entry.watchers.length === 0) {
|
|
360
|
-
dropEntry(uuid, entry);
|
|
361
|
-
}
|
|
362
|
-
}
|
|
363
|
-
};
|
|
442
|
+
// and joiners on an already-live channel get their initial catch-up call,
|
|
443
|
+
// which they'd otherwise wait for a reconnect to see
|
|
444
|
+
if (entry.subscribed && onSubscribed) {
|
|
445
|
+
entry.watchers.prime(watcher, () => watcher.onSubscribed());
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return unwatch(uuid, entry, watcher);
|
|
364
449
|
};
|
|
365
450
|
|
|
451
|
+
const unwatch = (
|
|
452
|
+
uuid: string,
|
|
453
|
+
entry: WatchedContact,
|
|
454
|
+
watcher: Watcher
|
|
455
|
+
): RealtimeSubscription => ({
|
|
456
|
+
unsubscribe: () => {
|
|
457
|
+
if (entry.watchers.remove(watcher) && entry.watchers.size === 0) {
|
|
458
|
+
dropEntry(uuid, entry);
|
|
459
|
+
} else if (!needsContact(entry)) {
|
|
460
|
+
// only stream consumers left, and nothing keeps their contact current -
|
|
461
|
+
// holding onto this one would leave it drifting until a state watcher
|
|
462
|
+
// joined later and got primed with the drift
|
|
463
|
+
entry.contact = null;
|
|
464
|
+
clearRefetch(entry);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
});
|
|
468
|
+
|
|
366
469
|
/**
|
|
367
470
|
* Pushes a fresh copy of a contact into the registry, priming its watchers.
|
|
368
471
|
* Components that write contact changes call this with the server's response
|
|
@@ -409,7 +512,7 @@ const dropEntry = (uuid: string, entry: WatchedContact) => {
|
|
|
409
512
|
// for tests - real pages just unwatch
|
|
410
513
|
export const resetContactWatches = () => {
|
|
411
514
|
for (const [uuid, entry] of [...watched.entries()]) {
|
|
412
|
-
entry.watchers.
|
|
515
|
+
entry.watchers.clear();
|
|
413
516
|
dropEntry(uuid, entry);
|
|
414
517
|
}
|
|
415
518
|
};
|
package/src/live/Realtime.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
import { Notification } from '../interfaces';
|
|
1
|
+
import { Notification, ObjectReference, User } from '../interfaces';
|
|
2
|
+
import { Events } from '../events/eventRenderers';
|
|
3
|
+
import type { StoreAsset } from '../store/Store';
|
|
2
4
|
import {
|
|
3
5
|
PublicationHandler,
|
|
4
6
|
SocketSubscription,
|
|
@@ -9,18 +11,137 @@ import {
|
|
|
9
11
|
* Typed access to our realtime topics. SocketService owns the shared
|
|
10
12
|
* connection and per-channel fan-out; this module owns how topics map to
|
|
11
13
|
* channel names, including the page identity (org and user uuids) needed to
|
|
12
|
-
* address user-scoped channels.
|
|
14
|
+
* address user-scoped channels, and what each topic publishes.
|
|
13
15
|
*
|
|
14
16
|
* The identity arrives via temba-store (hydrated from the page template), so
|
|
15
17
|
* user-scoped subscriptions requested before the store mounts are queued and
|
|
16
18
|
* activate when the context is set. On pages with no authenticated context
|
|
17
19
|
* they simply never activate.
|
|
20
|
+
*
|
|
21
|
+
* Payload types below describe the wire, which is raw JSON - timestamps are
|
|
22
|
+
* strings here even where the rendered equivalents in events.ts carry Dates.
|
|
18
23
|
*/
|
|
19
24
|
|
|
20
25
|
export interface RealtimeSubscription {
|
|
21
26
|
unsubscribe(): void;
|
|
22
27
|
}
|
|
23
28
|
|
|
29
|
+
/** Anything published on any of our topics. */
|
|
30
|
+
export interface RealtimeEvent {
|
|
31
|
+
type: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* org:<org-uuid> - workspace-wide state every component on the page shares.
|
|
36
|
+
* An asset changed somewhere, so anything displaying it can update.
|
|
37
|
+
*/
|
|
38
|
+
export interface AssetChangedEvent extends RealtimeEvent {
|
|
39
|
+
type: 'asset_changed';
|
|
40
|
+
asset: StoreAsset;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export type OrganizationEvent = AssetChangedEvent;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* flow:<flow-uuid> - published once per committed sprint batch while a flow
|
|
47
|
+
* is running. It carries no counts, it just says they moved: the editor reads
|
|
48
|
+
* the current numbers over http when it sees one.
|
|
49
|
+
*/
|
|
50
|
+
export interface FlowActivityEvent extends RealtimeEvent {
|
|
51
|
+
type: 'activity';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export type FlowEvent = FlowActivityEvent;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* history:<contact-uuid> and history:<contact-uuid>:<ticket-uuid> - the
|
|
58
|
+
* engine's contact events (the payloads in events.ts, before their dates are
|
|
59
|
+
* parsed) plus the ephemeral ones below, which are never persisted.
|
|
60
|
+
*/
|
|
61
|
+
export interface ContactHistoryEvent extends RealtimeEvent {
|
|
62
|
+
uuid?: string;
|
|
63
|
+
created_on?: string;
|
|
64
|
+
_user?: User;
|
|
65
|
+
// the rest of the payload varies by type. The contact-state events - the
|
|
66
|
+
// ones we read fields off rather than hand straight to the renderers - are
|
|
67
|
+
// typed below; events.ts describes the rendered shape of the others.
|
|
68
|
+
// unknown rather than any, so reading a field nobody declared is a type
|
|
69
|
+
// error at the point of use instead of silently spreading
|
|
70
|
+
[key: string]: unknown;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The events that change what a contact *is*, as opposed to recording
|
|
75
|
+
* something that happened to them. These are the ones ContactWatch applies to
|
|
76
|
+
* the contact it holds, so their payloads are what a server-side rename would
|
|
77
|
+
* break.
|
|
78
|
+
*/
|
|
79
|
+
export interface ContactNameChangedEvent extends ContactHistoryEvent {
|
|
80
|
+
type: Events.CONTACT_NAME_CHANGED;
|
|
81
|
+
name: string;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export interface ContactLanguageChangedEvent extends ContactHistoryEvent {
|
|
85
|
+
type: Events.CONTACT_LANGUAGE_CHANGED;
|
|
86
|
+
language: string;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface ContactStatusChangedEvent extends ContactHistoryEvent {
|
|
90
|
+
type: Events.CONTACT_STATUS_CHANGED;
|
|
91
|
+
status: string;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface ContactFlowChangedEvent extends ContactHistoryEvent {
|
|
95
|
+
type: Events.CONTACT_FLOW_CHANGED;
|
|
96
|
+
flow: ObjectReference | null;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ContactLastSeenChangedEvent extends ContactHistoryEvent {
|
|
100
|
+
type: Events.CONTACT_LAST_SEEN_CHANGED;
|
|
101
|
+
last_seen_on: string;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export interface ContactFieldChangedEvent extends ContactHistoryEvent {
|
|
105
|
+
type: Events.CONTACT_FIELD_CHANGED;
|
|
106
|
+
field: { key: string; name: string };
|
|
107
|
+
// engine field values always carry text; typed representations are present
|
|
108
|
+
// when the value parses as that type (see goflow's Value)
|
|
109
|
+
value: { text: string; datetime?: string; number?: string } | null;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
export interface ContactGroupsChangedEvent extends ContactHistoryEvent {
|
|
113
|
+
type: Events.CONTACT_GROUPS_CHANGED;
|
|
114
|
+
groups_added?: ObjectReference[];
|
|
115
|
+
groups_removed?: ObjectReference[];
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
export interface ContactURNsChangedEvent extends ContactHistoryEvent {
|
|
119
|
+
type: Events.CONTACT_URNS_CHANGED;
|
|
120
|
+
urns: string[];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export type ContactStateEvent =
|
|
124
|
+
| ContactNameChangedEvent
|
|
125
|
+
| ContactLanguageChangedEvent
|
|
126
|
+
| ContactStatusChangedEvent
|
|
127
|
+
| ContactFlowChangedEvent
|
|
128
|
+
| ContactLastSeenChangedEvent
|
|
129
|
+
| ContactFieldChangedEvent
|
|
130
|
+
| ContactGroupsChangedEvent
|
|
131
|
+
| ContactURNsChangedEvent;
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Published by agents as they compose, and echoed back to the publisher, so
|
|
135
|
+
* consumers filter out their own by _user.
|
|
136
|
+
*/
|
|
137
|
+
export interface TypingEvent extends ContactHistoryEvent {
|
|
138
|
+
type: Events.TYPING_STARTED | Events.TYPING_STOPPED;
|
|
139
|
+
direction?: string;
|
|
140
|
+
// whatsapp expresses typing as an operation on the contact's last incoming
|
|
141
|
+
// message, so publications carry its external id when we have one
|
|
142
|
+
msg_external_id?: string;
|
|
143
|
+
}
|
|
144
|
+
|
|
24
145
|
export interface RealtimeContext {
|
|
25
146
|
org: string;
|
|
26
147
|
user: string;
|
|
@@ -119,10 +240,14 @@ export const subscribeToNotifications = (
|
|
|
119
240
|
* Workspace-wide state changes shared by every component on the page.
|
|
120
241
|
*/
|
|
121
242
|
export const subscribeToOrganization = (
|
|
122
|
-
onEvent: (event:
|
|
243
|
+
onEvent: (event: OrganizationEvent) => void,
|
|
123
244
|
onSubscribed?: () => void
|
|
124
245
|
): RealtimeSubscription => {
|
|
125
|
-
return subscribeWhenReady(
|
|
246
|
+
return subscribeWhenReady(
|
|
247
|
+
(ctx) => `org:${ctx.org}`,
|
|
248
|
+
(data) => onEvent(data as OrganizationEvent),
|
|
249
|
+
onSubscribed
|
|
250
|
+
);
|
|
126
251
|
};
|
|
127
252
|
|
|
128
253
|
/**
|
|
@@ -131,10 +256,14 @@ export const subscribeToOrganization = (
|
|
|
131
256
|
*/
|
|
132
257
|
export const subscribeToFlow = (
|
|
133
258
|
flow: string,
|
|
134
|
-
onEvent: (event:
|
|
259
|
+
onEvent: (event: FlowEvent) => void,
|
|
135
260
|
onSubscribed?: () => void
|
|
136
261
|
): RealtimeSubscription => {
|
|
137
|
-
return subscribeToSocket(
|
|
262
|
+
return subscribeToSocket(
|
|
263
|
+
`flow:${flow}`,
|
|
264
|
+
(data) => onEvent(data as FlowEvent),
|
|
265
|
+
onSubscribed
|
|
266
|
+
);
|
|
138
267
|
};
|
|
139
268
|
|
|
140
269
|
/**
|
|
@@ -144,12 +273,12 @@ export const subscribeToFlow = (
|
|
|
144
273
|
export const subscribeToContactHistory = (
|
|
145
274
|
contact: string,
|
|
146
275
|
ticket: string | null,
|
|
147
|
-
onEvent: (event:
|
|
276
|
+
onEvent: (event: ContactHistoryEvent) => void,
|
|
148
277
|
onSubscribed?: () => void
|
|
149
278
|
): RealtimeSubscription => {
|
|
150
279
|
return subscribeToSocket(
|
|
151
280
|
ticket ? `history:${contact}:${ticket}` : `history:${contact}`,
|
|
152
|
-
onEvent,
|
|
281
|
+
(data) => onEvent(data as ContactHistoryEvent),
|
|
153
282
|
onSubscribed
|
|
154
283
|
);
|
|
155
284
|
};
|