@nyaruka/temba-components 0.160.1 → 0.162.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 +13 -0
- package/dist/locales/es.js +2 -0
- package/dist/locales/es.js.map +1 -1
- package/dist/locales/fr.js +2 -0
- package/dist/locales/fr.js.map +1 -1
- package/dist/locales/pt.js +2 -0
- package/dist/locales/pt.js.map +1 -1
- package/dist/temba-components.js +469 -465
- package/dist/temba-components.js.map +1 -1
- package/package.json +3 -2
- package/src/display/Chat.ts +16 -0
- package/src/display/TembaUser.ts +7 -0
- package/src/flow/nodes/split_by_webhook.ts +23 -5
- package/src/list/FlowList.ts +30 -6
- package/src/live/ContactChat.ts +120 -78
- package/src/live/SocketService.ts +158 -0
- package/src/locales/es.ts +2 -0
- package/src/locales/fr.ts +2 -0
- package/src/locales/pt.ts +2 -0
- package/src/store/Store.ts +18 -0
- package/xliff/es.xlf +6 -0
- package/xliff/fr.xlf +6 -0
- package/xliff/pt.xlf +6 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { Centrifuge, Subscription, SubscriptionState } from 'centrifuge';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Access to our realtime messaging socket (centrifugo). The server lives
|
|
5
|
+
* behind the same origin at /ws/connect and authenticates connections with
|
|
6
|
+
* the browser's session cookie (via a server-side connect proxy), so no
|
|
7
|
+
* token handling is needed here.
|
|
8
|
+
*
|
|
9
|
+
* The connection is owned by a SocketManager which is page-scoped - it hangs
|
|
10
|
+
* off `window` rather than any component, so it survives components mounting
|
|
11
|
+
* and unmounting, and vanilla js on the containing page can share the same
|
|
12
|
+
* connection:
|
|
13
|
+
*
|
|
14
|
+
* const sub = window.sockets.subscribe('notifications:<org>:<user>', (event) => {
|
|
15
|
+
* ...
|
|
16
|
+
* });
|
|
17
|
+
* sub.unsubscribe();
|
|
18
|
+
*
|
|
19
|
+
* Any number of subscribers (components or page js) can watch the same
|
|
20
|
+
* channel - the underlying centrifugo subscription is created on first use
|
|
21
|
+
* and torn down when the last subscriber leaves. The connection itself stays
|
|
22
|
+
* open for the life of the page. Each published event arrives as raw JSON.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export interface SocketSubscription {
|
|
26
|
+
unsubscribe(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type PublicationHandler = (data: any) => void;
|
|
30
|
+
|
|
31
|
+
export interface SocketProvider {
|
|
32
|
+
subscribe(
|
|
33
|
+
channel: string,
|
|
34
|
+
onPublication: PublicationHandler,
|
|
35
|
+
onSubscribed?: () => void
|
|
36
|
+
): SocketSubscription;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
interface ChannelEntry {
|
|
40
|
+
sub: Subscription;
|
|
41
|
+
count: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export class SocketManager implements SocketProvider {
|
|
45
|
+
private socket: Centrifuge = null;
|
|
46
|
+
private channels = new Map<string, ChannelEntry>();
|
|
47
|
+
private createSocket: () => Centrifuge;
|
|
48
|
+
|
|
49
|
+
constructor(createSocket?: () => Centrifuge) {
|
|
50
|
+
this.createSocket =
|
|
51
|
+
createSocket ||
|
|
52
|
+
(() => {
|
|
53
|
+
const protocol = window.location.protocol === 'https:' ? 'wss' : 'ws';
|
|
54
|
+
const socket = new Centrifuge(
|
|
55
|
+
`${protocol}://${window.location.host}/ws/connect`
|
|
56
|
+
);
|
|
57
|
+
socket.connect();
|
|
58
|
+
return socket;
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
public subscribe(
|
|
63
|
+
channel: string,
|
|
64
|
+
onPublication: PublicationHandler,
|
|
65
|
+
onSubscribed?: () => void
|
|
66
|
+
): SocketSubscription {
|
|
67
|
+
if (!this.socket) {
|
|
68
|
+
this.socket = this.createSocket();
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
let entry = this.channels.get(channel);
|
|
72
|
+
if (!entry) {
|
|
73
|
+
entry = {
|
|
74
|
+
sub:
|
|
75
|
+
this.socket.getSubscription(channel) ||
|
|
76
|
+
this.socket.newSubscription(channel),
|
|
77
|
+
count: 0
|
|
78
|
+
};
|
|
79
|
+
this.channels.set(channel, entry);
|
|
80
|
+
entry.sub.subscribe();
|
|
81
|
+
}
|
|
82
|
+
entry.count++;
|
|
83
|
+
|
|
84
|
+
const sub = entry.sub;
|
|
85
|
+
const pubHandler = (ctx: { data: any }) => onPublication(ctx.data);
|
|
86
|
+
sub.on('publication', pubHandler);
|
|
87
|
+
|
|
88
|
+
let subHandler: () => void = null;
|
|
89
|
+
if (onSubscribed) {
|
|
90
|
+
// fires on every (re)subscribe, including after reconnects, so
|
|
91
|
+
// subscribers can catch up on anything missed while offline
|
|
92
|
+
subHandler = () => onSubscribed();
|
|
93
|
+
sub.on('subscribed', subHandler);
|
|
94
|
+
|
|
95
|
+
// late joiners on an already-live channel won't see a subscribed
|
|
96
|
+
// event, so give them their initial one
|
|
97
|
+
if (sub.state === SubscriptionState.Subscribed) {
|
|
98
|
+
window.setTimeout(() => subHandler && subHandler(), 0);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
let active = true;
|
|
103
|
+
return {
|
|
104
|
+
unsubscribe: () => {
|
|
105
|
+
if (!active) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
active = false;
|
|
109
|
+
|
|
110
|
+
sub.off('publication', pubHandler);
|
|
111
|
+
if (subHandler) {
|
|
112
|
+
sub.off('subscribed', subHandler);
|
|
113
|
+
subHandler = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
entry.count--;
|
|
117
|
+
if (entry.count === 0) {
|
|
118
|
+
this.channels.delete(channel);
|
|
119
|
+
sub.unsubscribe();
|
|
120
|
+
this.socket.removeSubscription(sub);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// the page-scoped manager, shared with vanilla js as window.sockets and
|
|
128
|
+
// reused if another copy of this module already created it
|
|
129
|
+
const getManager = (): SocketManager => {
|
|
130
|
+
const w = window as any;
|
|
131
|
+
if (!w.sockets) {
|
|
132
|
+
w.sockets = new SocketManager();
|
|
133
|
+
}
|
|
134
|
+
return w.sockets;
|
|
135
|
+
};
|
|
136
|
+
getManager();
|
|
137
|
+
|
|
138
|
+
// when set, components subscribe through this instead of the page manager
|
|
139
|
+
let provider: SocketProvider = null;
|
|
140
|
+
|
|
141
|
+
export const subscribeToSocket = (
|
|
142
|
+
channel: string,
|
|
143
|
+
onPublication: PublicationHandler,
|
|
144
|
+
onSubscribed?: () => void
|
|
145
|
+
): SocketSubscription => {
|
|
146
|
+
return (provider || getManager()).subscribe(
|
|
147
|
+
channel,
|
|
148
|
+
onPublication,
|
|
149
|
+
onSubscribed
|
|
150
|
+
);
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// for tests to swap in a mock provider, returns the previous provider
|
|
154
|
+
export const setSocketProvider = (newProvider: SocketProvider) => {
|
|
155
|
+
const previous = provider;
|
|
156
|
+
provider = newProvider;
|
|
157
|
+
return previous;
|
|
158
|
+
};
|
package/src/locales/es.ts
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
'scf1453991c986b25': `Tab para completar, enter para seleccionar`,
|
|
13
13
|
's73b4d70c02f4b4e0': `No options`,
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
|
+
's81f17cfc89a04338': `Interrupt flow`,
|
|
15
16
|
's8f02e3a18ffc083a': `Are not currently in a flow`,
|
|
16
17
|
's638236250662c6b3': `Have sent a message in the last`,
|
|
17
18
|
's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
|
|
19
|
+
'sea4f08110bb8f15d': `Remove`,
|
|
18
20
|
};
|
|
19
21
|
|
package/src/locales/fr.ts
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
's73b4d70c02f4b4e0': `No options`,
|
|
13
13
|
'scf1453991c986b25': `Tab to complete, enter to select`,
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
|
+
's81f17cfc89a04338': `Interrupt flow`,
|
|
15
16
|
's8f02e3a18ffc083a': `Are not currently in a flow`,
|
|
16
17
|
's638236250662c6b3': `Have sent a message in the last`,
|
|
17
18
|
's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
|
|
19
|
+
'sea4f08110bb8f15d': `Remove`,
|
|
18
20
|
};
|
|
19
21
|
|
package/src/locales/pt.ts
CHANGED
|
@@ -12,8 +12,10 @@
|
|
|
12
12
|
's73b4d70c02f4b4e0': `No options`,
|
|
13
13
|
'scf1453991c986b25': `Tab to complete, enter to select`,
|
|
14
14
|
'sbc913d7dc0f33877': `to add`,
|
|
15
|
+
's81f17cfc89a04338': `Interrupt flow`,
|
|
15
16
|
's8f02e3a18ffc083a': `Are not currently in a flow`,
|
|
16
17
|
's638236250662c6b3': `Have sent a message in the last`,
|
|
17
18
|
's4788ee206c4570c7': `Have not started this flow in the last 90 days`,
|
|
19
|
+
'sea4f08110bb8f15d': `Remove`,
|
|
18
20
|
};
|
|
19
21
|
|
package/src/store/Store.ts
CHANGED
|
@@ -158,6 +158,23 @@ export class Store extends RapidElement {
|
|
|
158
158
|
}
|
|
159
159
|
|
|
160
160
|
private cache: any;
|
|
161
|
+
|
|
162
|
+
// user avatar urls by user uuid, seeded from server-hydrated user
|
|
163
|
+
// references so ones that arrive without an avatar (e.g. events published
|
|
164
|
+
// over sockets) can be filled in from users we've already seen. LRU-bounded
|
|
165
|
+
// so long-lived tabs don't accumulate every user ever seen.
|
|
166
|
+
private userAvatars = lru<string>(500);
|
|
167
|
+
|
|
168
|
+
public setUserAvatar(uuid: string, avatar: string) {
|
|
169
|
+
if (uuid && avatar) {
|
|
170
|
+
this.userAvatars.set(uuid, avatar);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
public getUserAvatar(uuid: string): string {
|
|
175
|
+
return this.userAvatars.get(uuid);
|
|
176
|
+
}
|
|
177
|
+
|
|
161
178
|
public getLocale() {
|
|
162
179
|
return this.locale[0];
|
|
163
180
|
}
|
|
@@ -561,6 +578,7 @@ export class Store extends RapidElement {
|
|
|
561
578
|
const results = response.json.results;
|
|
562
579
|
if (results && results.length === 1) {
|
|
563
580
|
const user = results[0];
|
|
581
|
+
this.setUserAvatar(user.uuid, user.avatar);
|
|
564
582
|
|
|
565
583
|
items.forEach((item) => {
|
|
566
584
|
// replace each key with a matching user
|
package/xliff/es.xlf
CHANGED
|
@@ -21,6 +21,12 @@
|
|
|
21
21
|
<trans-unit id="sbc913d7dc0f33877">
|
|
22
22
|
<source>to add</source>
|
|
23
23
|
</trans-unit>
|
|
24
|
+
<trans-unit id="sea4f08110bb8f15d">
|
|
25
|
+
<source>Remove</source>
|
|
26
|
+
</trans-unit>
|
|
27
|
+
<trans-unit id="s81f17cfc89a04338">
|
|
28
|
+
<source>Interrupt flow</source>
|
|
29
|
+
</trans-unit>
|
|
24
30
|
</body>
|
|
25
31
|
</file>
|
|
26
32
|
</xliff>
|
package/xliff/fr.xlf
CHANGED
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
<trans-unit id="sbc913d7dc0f33877">
|
|
21
21
|
<source>to add</source>
|
|
22
22
|
</trans-unit>
|
|
23
|
+
<trans-unit id="sea4f08110bb8f15d">
|
|
24
|
+
<source>Remove</source>
|
|
25
|
+
</trans-unit>
|
|
26
|
+
<trans-unit id="s81f17cfc89a04338">
|
|
27
|
+
<source>Interrupt flow</source>
|
|
28
|
+
</trans-unit>
|
|
23
29
|
</body>
|
|
24
30
|
</file>
|
|
25
31
|
</xliff>
|
package/xliff/pt.xlf
CHANGED
|
@@ -20,6 +20,12 @@
|
|
|
20
20
|
<trans-unit id="sbc913d7dc0f33877">
|
|
21
21
|
<source>to add</source>
|
|
22
22
|
</trans-unit>
|
|
23
|
+
<trans-unit id="sea4f08110bb8f15d">
|
|
24
|
+
<source>Remove</source>
|
|
25
|
+
</trans-unit>
|
|
26
|
+
<trans-unit id="s81f17cfc89a04338">
|
|
27
|
+
<source>Interrupt flow</source>
|
|
28
|
+
</trans-unit>
|
|
23
29
|
</body>
|
|
24
30
|
</file>
|
|
25
31
|
</xliff>
|