@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.
@@ -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
 
@@ -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>