@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
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nyaruka/temba-components",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.162.0",
|
|
4
4
|
"description": "Web components to support rapidpro and related projects",
|
|
5
5
|
"author": "Nyaruka <code@nyaruka.coim>",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
"@jsplumb/browser-ui": "^6.2.10",
|
|
40
40
|
"@lit/localize": "^0.12.2",
|
|
41
41
|
"@open-wc/lit-helpers": "^0.7.0",
|
|
42
|
+
"centrifuge": "^5.7.0",
|
|
42
43
|
"chart.js": "^4.5.1",
|
|
43
44
|
"chartjs-adapter-luxon": "^1.3.1",
|
|
44
45
|
"chartjs-plugin-datalabels": "^2.2.0",
|
|
@@ -68,6 +69,7 @@
|
|
|
68
69
|
"@rollup/plugin-terser": "^0.4.4",
|
|
69
70
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
70
71
|
"@types/leaflet": "1.4.4",
|
|
72
|
+
"@types/mocha": "^10.0.0",
|
|
71
73
|
"@types/node": "22.13.0",
|
|
72
74
|
"@types/remarkable": "^2.0.1",
|
|
73
75
|
"@typescript-eslint/eslint-plugin": "^7.5.0",
|
|
@@ -107,7 +109,6 @@
|
|
|
107
109
|
"sinon": "^21.0.0",
|
|
108
110
|
"svgstore": "^3.0.1",
|
|
109
111
|
"tslib": "2.8.1",
|
|
110
|
-
"@types/mocha": "^10.0.0",
|
|
111
112
|
"typescript": "5.9.3",
|
|
112
113
|
"yargs": "^17.7.2"
|
|
113
114
|
},
|
package/src/display/Chat.ts
CHANGED
|
@@ -838,6 +838,10 @@ export class Chat extends RapidElement {
|
|
|
838
838
|
private msgMap = new Map<string, ContactEvent>();
|
|
839
839
|
private metadataCache = new Map<string, ContactEvent>();
|
|
840
840
|
|
|
841
|
+
// bumped on reset so deferred addMessages work from before a reset is
|
|
842
|
+
// discarded rather than re-merged into the fresh view
|
|
843
|
+
private resetGeneration = 0;
|
|
844
|
+
|
|
841
845
|
public firstUpdated(
|
|
842
846
|
changed: PropertyValueMap<any> | Map<PropertyKey, unknown>
|
|
843
847
|
): void {
|
|
@@ -858,10 +862,16 @@ export class Chat extends RapidElement {
|
|
|
858
862
|
startTime = new Date();
|
|
859
863
|
}
|
|
860
864
|
|
|
865
|
+
const generation = this.resetGeneration;
|
|
861
866
|
const elapsed = new Date().getTime() - startTime.getTime();
|
|
862
867
|
window.setTimeout(
|
|
863
868
|
() => {
|
|
864
869
|
this.fetching = false;
|
|
870
|
+
|
|
871
|
+
// the chat was reset while we were waiting, our messages are stale
|
|
872
|
+
if (generation !== this.resetGeneration) {
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
865
875
|
// first add messages to the map
|
|
866
876
|
const newMessages = [];
|
|
867
877
|
for (const m of messages) {
|
|
@@ -905,6 +915,10 @@ export class Chat extends RapidElement {
|
|
|
905
915
|
}
|
|
906
916
|
|
|
907
917
|
window.setTimeout(() => {
|
|
918
|
+
if (generation !== this.resetGeneration) {
|
|
919
|
+
return;
|
|
920
|
+
}
|
|
921
|
+
|
|
908
922
|
// when appending (new messages at bottom), adjust scroll to maintain visible content
|
|
909
923
|
// with column-reverse, new content at bottom increases scrollHeight
|
|
910
924
|
if (append && (isScrolledAway || maintainScroll)) {
|
|
@@ -1486,7 +1500,9 @@ export class Chat extends RapidElement {
|
|
|
1486
1500
|
}
|
|
1487
1501
|
|
|
1488
1502
|
public reset() {
|
|
1503
|
+
this.resetGeneration++;
|
|
1489
1504
|
this.msgMap.clear();
|
|
1505
|
+
this.metadataCache.clear();
|
|
1490
1506
|
this.messageGroups = [];
|
|
1491
1507
|
this.hideBottomScroll = true;
|
|
1492
1508
|
this.hideTopScroll = true;
|
package/src/display/TembaUser.ts
CHANGED
|
@@ -116,6 +116,13 @@ export class TembaUser extends RapidElement {
|
|
|
116
116
|
if (changed.has('avatar')) {
|
|
117
117
|
if (this.avatar) {
|
|
118
118
|
this.bgimage = `url('${this.avatar}') center / contain no-repeat`;
|
|
119
|
+
} else {
|
|
120
|
+
// clearing the avatar must also clear the image, otherwise a reused
|
|
121
|
+
// element keeps showing the previous user's avatar - fall back to the
|
|
122
|
+
// system default or the initials/contact-icon branch
|
|
123
|
+
this.bgimage = this.system
|
|
124
|
+
? `url('${DEFAULT_AVATAR}') center / contain no-repeat`
|
|
125
|
+
: null;
|
|
119
126
|
}
|
|
120
127
|
}
|
|
121
128
|
}
|
|
@@ -200,7 +200,10 @@ export const split_by_webhook: NodeConfig = {
|
|
|
200
200
|
callWebhookAction?.headers ||
|
|
201
201
|
getDefaultHeadersRecord(callWebhookAction?.method || 'GET'),
|
|
202
202
|
body: callWebhookAction?.body || '',
|
|
203
|
-
|
|
203
|
+
// Legacy webhooks store result_name on the action; modern ones store it
|
|
204
|
+
// on the router. Prefer the action so legacy nodes round-trip correctly.
|
|
205
|
+
result_name:
|
|
206
|
+
callWebhookAction?.result_name || node.router?.result_name || ''
|
|
204
207
|
};
|
|
205
208
|
},
|
|
206
209
|
fromFormData: (formData: FormData, originalNode: Node): Node => {
|
|
@@ -213,9 +216,17 @@ export const split_by_webhook: NodeConfig = {
|
|
|
213
216
|
// Find existing call_webhook action to preserve its UUID
|
|
214
217
|
const existingCallWebhookAction = originalNode.actions?.find(
|
|
215
218
|
(action) => action.type === 'call_webhook'
|
|
216
|
-
);
|
|
219
|
+
) as CallWebhook | undefined;
|
|
217
220
|
const callWebhookUuid = existingCallWebhookAction?.uuid || generateUUID();
|
|
218
221
|
|
|
222
|
+
// Legacy webhooks keep the result_name on the action rather than the
|
|
223
|
+
// router. Detect them by the presence of result_name on the existing
|
|
224
|
+
// action so they continue to round-trip in their original format instead
|
|
225
|
+
// of being silently migrated (which would change how results are
|
|
226
|
+
// referenced downstream of the flow).
|
|
227
|
+
const isLegacyResultName = !!existingCallWebhookAction?.result_name;
|
|
228
|
+
const trimmedResultName = formData.result_name?.trim() || '';
|
|
229
|
+
|
|
219
230
|
// Create call_webhook action
|
|
220
231
|
const callWebhookAction: CallWebhook = {
|
|
221
232
|
type: 'call_webhook',
|
|
@@ -226,6 +237,11 @@ export const split_by_webhook: NodeConfig = {
|
|
|
226
237
|
body: formData.body || ''
|
|
227
238
|
};
|
|
228
239
|
|
|
240
|
+
// Preserve the legacy placement: result_name stays on the action.
|
|
241
|
+
if (isLegacyResultName && trimmedResultName !== '') {
|
|
242
|
+
callWebhookAction.result_name = trimmedResultName;
|
|
243
|
+
}
|
|
244
|
+
|
|
229
245
|
// Create categories and exits for Success and Failure
|
|
230
246
|
const existingCategories = originalNode.router?.categories || [];
|
|
231
247
|
const existingExits = originalNode.exits || [];
|
|
@@ -247,9 +263,11 @@ export const split_by_webhook: NodeConfig = {
|
|
|
247
263
|
...router
|
|
248
264
|
};
|
|
249
265
|
|
|
250
|
-
//
|
|
251
|
-
|
|
252
|
-
|
|
266
|
+
// Modern webhooks store result_name on the router. For legacy webhooks the
|
|
267
|
+
// result_name lives on the action (set above), so we leave it off the
|
|
268
|
+
// router to avoid storing it in both places.
|
|
269
|
+
if (!isLegacyResultName && trimmedResultName !== '') {
|
|
270
|
+
finalRouter.result_name = trimmedResultName;
|
|
253
271
|
}
|
|
254
272
|
|
|
255
273
|
// Return the complete node
|
package/src/list/FlowList.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { css, html, TemplateResult } from 'lit';
|
|
2
2
|
import { ContentList, ContentListColumn } from './ContentList';
|
|
3
3
|
import { Icon } from '../Icons';
|
|
4
|
-
import { Flow, ObjectReference } from '../interfaces';
|
|
4
|
+
import { CustomEventType, Flow, ObjectReference } from '../interfaces';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Flow CRUDL list — drop-in replacement for the rapidpro
|
|
@@ -89,27 +89,28 @@ export class FlowList extends ContentList<Flow> {
|
|
|
89
89
|
key: 'name',
|
|
90
90
|
label: 'Name',
|
|
91
91
|
sortable: true,
|
|
92
|
-
|
|
92
|
+
minWidth: '160px',
|
|
93
|
+
maxWidth: '280px',
|
|
93
94
|
pinned: true
|
|
94
95
|
},
|
|
95
96
|
{
|
|
96
97
|
key: 'runs',
|
|
97
98
|
label: 'Runs',
|
|
98
99
|
sortable: true,
|
|
99
|
-
|
|
100
|
+
minWidth: '64px',
|
|
100
101
|
align: 'right'
|
|
101
102
|
},
|
|
102
103
|
{
|
|
103
104
|
key: 'ongoing',
|
|
104
105
|
label: 'Ongoing',
|
|
105
106
|
sortable: true,
|
|
106
|
-
|
|
107
|
+
minWidth: '64px',
|
|
107
108
|
align: 'right'
|
|
108
109
|
},
|
|
109
110
|
{
|
|
110
111
|
key: 'completion',
|
|
111
112
|
label: 'Completion',
|
|
112
|
-
|
|
113
|
+
minWidth: '110px',
|
|
113
114
|
align: 'right'
|
|
114
115
|
},
|
|
115
116
|
{ key: 'activity', label: 'Activity', width: '120px', align: 'right' }
|
|
@@ -146,6 +147,25 @@ export class FlowList extends ContentList<Flow> {
|
|
|
146
147
|
return item?.uuid ? `/flow/editor/${item.uuid}/` : null;
|
|
147
148
|
}
|
|
148
149
|
|
|
150
|
+
/** Clicking a label chip opens that label's filtered flow list rather
|
|
151
|
+
* than falling through to the row click (which opens the flow editor). */
|
|
152
|
+
private handleLabelClick(label: ObjectReference, event: MouseEvent): void {
|
|
153
|
+
// Stop the click from bubbling to the row's navigation handler.
|
|
154
|
+
event.stopPropagation();
|
|
155
|
+
if (!label?.uuid) return;
|
|
156
|
+
const href = `/flow/filter/${label.uuid}/`;
|
|
157
|
+
// Guard the JSON-driven href against open-redirect, same as the
|
|
158
|
+
// row-click path in ContentList.handleRowClick.
|
|
159
|
+
if (!this.isSafeHref(href)) return;
|
|
160
|
+
// Meta/ctrl-click opens a new tab, matching ordinary links and the
|
|
161
|
+
// row-click behavior.
|
|
162
|
+
if (event.metaKey || event.ctrlKey) {
|
|
163
|
+
window.open(href, '_blank');
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
this.fireCustomEvent(CustomEventType.Redirected, { url: href });
|
|
167
|
+
}
|
|
168
|
+
|
|
149
169
|
protected renderCell(
|
|
150
170
|
item: Flow,
|
|
151
171
|
column: ContentListColumn
|
|
@@ -166,7 +186,11 @@ export class FlowList extends ContentList<Flow> {
|
|
|
166
186
|
? html`<span class="flow-labels">
|
|
167
187
|
${labels.map(
|
|
168
188
|
(l: ObjectReference) =>
|
|
169
|
-
html`<temba-label
|
|
189
|
+
html`<temba-label
|
|
190
|
+
type="label"
|
|
191
|
+
icon=${Icon.label}
|
|
192
|
+
clickable
|
|
193
|
+
@click=${(e: MouseEvent) => this.handleLabelClick(l, e)}
|
|
170
194
|
>${l.name}</temba-label
|
|
171
195
|
>`
|
|
172
196
|
)}
|
package/src/live/ContactChat.ts
CHANGED
|
@@ -36,14 +36,7 @@ import {
|
|
|
36
36
|
renderTicketAction,
|
|
37
37
|
renderTicketAssigneeChanged
|
|
38
38
|
} from '../events/eventRenderers';
|
|
39
|
-
|
|
40
|
-
/*
|
|
41
|
-
export const SCROLL_THRESHOLD = 100;
|
|
42
|
-
export const SIMULATED_WEB_SLOWNESS = 0;
|
|
43
|
-
export const MAX_CHAT_REFRESH = 10000;
|
|
44
|
-
export const MIN_CHAT_REFRESH = 500;
|
|
45
|
-
export const BODY_SNIPPET_LENGTH = 250;
|
|
46
|
-
*/
|
|
39
|
+
import { subscribeToSocket, SocketSubscription } from './SocketService';
|
|
47
40
|
|
|
48
41
|
// re-export for backwards compatibility
|
|
49
42
|
export { renderTicketAction, renderTicketAssigneeChanged };
|
|
@@ -527,11 +520,11 @@ export class ContactChat extends ContactStoreElement {
|
|
|
527
520
|
|
|
528
521
|
ticket = null;
|
|
529
522
|
beforeUUID: string = null; // for scrolling back through history
|
|
530
|
-
afterUUID: string = null; // for
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
523
|
+
afterUUID: string = null; // newest event seen, for catch-up fetches
|
|
524
|
+
|
|
525
|
+
// live socket subscriptions by channel for the current contact (and ticket)
|
|
526
|
+
private subscriptions = new Map<string, SocketSubscription>();
|
|
527
|
+
private fetchingMissed = false;
|
|
535
528
|
|
|
536
529
|
constructor() {
|
|
537
530
|
super();
|
|
@@ -562,28 +555,22 @@ export class ContactChat extends ContactStoreElement {
|
|
|
562
555
|
public connectedCallback() {
|
|
563
556
|
super.connectedCallback();
|
|
564
557
|
this.chat = this.shadowRoot.querySelector('temba-chat');
|
|
558
|
+
this.updateSubscriptions();
|
|
565
559
|
}
|
|
566
560
|
|
|
567
561
|
public disconnectedCallback() {
|
|
568
562
|
super.disconnectedCallback();
|
|
569
|
-
|
|
570
|
-
clearInterval(this.refreshId);
|
|
571
|
-
}
|
|
563
|
+
this.unsubscribeAll();
|
|
572
564
|
}
|
|
573
565
|
|
|
574
566
|
public updated(changedProperties: Map<string, any>) {
|
|
575
567
|
super.updated(changedProperties);
|
|
576
568
|
|
|
577
|
-
// if we don't have an endpoint infer one
|
|
578
569
|
if (
|
|
579
|
-
changedProperties.has('
|
|
580
|
-
changedProperties.has('
|
|
570
|
+
changedProperties.has('currentContact') ||
|
|
571
|
+
changedProperties.has('currentTicket')
|
|
581
572
|
) {
|
|
582
|
-
|
|
583
|
-
if (this.refreshId) {
|
|
584
|
-
clearTimeout(this.refreshId);
|
|
585
|
-
this.refreshId = null;
|
|
586
|
-
}
|
|
573
|
+
this.updateSubscriptions();
|
|
587
574
|
}
|
|
588
575
|
|
|
589
576
|
if (changedProperties.has('currentContact') && this.currentContact) {
|
|
@@ -594,12 +581,74 @@ export class ContactChat extends ContactStoreElement {
|
|
|
594
581
|
) {
|
|
595
582
|
this.reset();
|
|
596
583
|
} else {
|
|
597
|
-
|
|
584
|
+
this.fetchMissedEvents();
|
|
598
585
|
}
|
|
599
586
|
this.fetchPreviousMessages();
|
|
600
587
|
}
|
|
601
588
|
}
|
|
602
589
|
|
|
590
|
+
private unsubscribeAll() {
|
|
591
|
+
this.subscriptions.forEach((sub) => sub.unsubscribe());
|
|
592
|
+
this.subscriptions.clear();
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/**
|
|
596
|
+
* Keeps our socket subscriptions in sync with the current contact and
|
|
597
|
+
* ticket. New events arrive as they happen on "history:<contact-uuid>";
|
|
598
|
+
* ticket detail events (notes, assignment, topic) only arrive on
|
|
599
|
+
* "history:<contact-uuid>:<ticket-uuid>" so we subscribe to both when
|
|
600
|
+
* viewing a ticket. Only the channels that actually changed are touched,
|
|
601
|
+
* so switching tickets on the same contact leaves the contact channel
|
|
602
|
+
* continuously subscribed with no gap in delivery.
|
|
603
|
+
*/
|
|
604
|
+
private updateSubscriptions() {
|
|
605
|
+
const channels = new Set<string>();
|
|
606
|
+
if (this.isConnected && this.currentContact) {
|
|
607
|
+
channels.add(`history:${this.currentContact.uuid}`);
|
|
608
|
+
if (this.currentTicket) {
|
|
609
|
+
channels.add(
|
|
610
|
+
`history:${this.currentContact.uuid}:${this.currentTicket.uuid}`
|
|
611
|
+
);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// drop subscriptions we no longer need
|
|
616
|
+
this.subscriptions.forEach((sub, channel) => {
|
|
617
|
+
if (!channels.has(channel)) {
|
|
618
|
+
sub.unsubscribe();
|
|
619
|
+
this.subscriptions.delete(channel);
|
|
620
|
+
}
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
// add any new ones
|
|
624
|
+
channels.forEach((channel) => {
|
|
625
|
+
if (!this.subscriptions.has(channel)) {
|
|
626
|
+
this.subscriptions.set(
|
|
627
|
+
channel,
|
|
628
|
+
subscribeToSocket(
|
|
629
|
+
channel,
|
|
630
|
+
(data: any) => this.handleSocketEvent(data),
|
|
631
|
+
// on every (re)subscribe fetch anything we might have missed
|
|
632
|
+
() => this.fetchMissedEvents()
|
|
633
|
+
)
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
private handleSocketEvent(event: any) {
|
|
640
|
+
// while searching we're viewing an arbitrary point in history, new
|
|
641
|
+
// events will be picked up by the refetch when search closes
|
|
642
|
+
if (!this.chat || this.searchMode || !this.currentContact) {
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
const messages = this.createMessages({ events: [event], next: null });
|
|
647
|
+
if (messages.length > 0) {
|
|
648
|
+
this.chat.addMessages(messages, null, true);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
603
652
|
private reset() {
|
|
604
653
|
if (this.chat) {
|
|
605
654
|
this.chat.reset();
|
|
@@ -607,10 +656,7 @@ export class ContactChat extends ContactStoreElement {
|
|
|
607
656
|
this.ticket = null;
|
|
608
657
|
this.beforeUUID = null;
|
|
609
658
|
this.afterUUID = null;
|
|
610
|
-
this.
|
|
611
|
-
this.polling = false;
|
|
612
|
-
this.pollingInterval = 2000;
|
|
613
|
-
this.lastFetchTime = null;
|
|
659
|
+
this.fetchingMissed = false;
|
|
614
660
|
|
|
615
661
|
const compose = this.shadowRoot.querySelector('temba-compose') as Compose;
|
|
616
662
|
if (compose) {
|
|
@@ -628,11 +674,6 @@ export class ContactChat extends ContactStoreElement {
|
|
|
628
674
|
this.searchIndex = -1;
|
|
629
675
|
this.searchLoading = false;
|
|
630
676
|
this.searchNoResults = false;
|
|
631
|
-
// stop polling while searching
|
|
632
|
-
if (this.refreshId) {
|
|
633
|
-
clearTimeout(this.refreshId);
|
|
634
|
-
this.refreshId = null;
|
|
635
|
-
}
|
|
636
677
|
window.setTimeout(() => {
|
|
637
678
|
const input = this.shadowRoot.querySelector('.search-input') as any;
|
|
638
679
|
if (input) {
|
|
@@ -953,10 +994,8 @@ export class ContactChat extends ContactStoreElement {
|
|
|
953
994
|
if (response.status < 400) {
|
|
954
995
|
const event = response.json.event;
|
|
955
996
|
event.created_on = new Date(event.created_on);
|
|
997
|
+
this.resolveUserAvatar(event);
|
|
956
998
|
this.chat.addMessages([event], null, true);
|
|
957
|
-
// reset polling interval to 2 seconds after sending a message
|
|
958
|
-
this.pollingInterval = 2000;
|
|
959
|
-
this.checkForNewMessages();
|
|
960
999
|
composeEle.reset();
|
|
961
1000
|
this.fireCustomEvent(CustomEventType.MessageSent, {
|
|
962
1001
|
msg: payload,
|
|
@@ -978,25 +1017,6 @@ export class ContactChat extends ContactStoreElement {
|
|
|
978
1017
|
return null;
|
|
979
1018
|
}
|
|
980
1019
|
|
|
981
|
-
private scheduleRefresh(hasNewEvents = false) {
|
|
982
|
-
if (this.refreshId) {
|
|
983
|
-
clearTimeout(this.refreshId);
|
|
984
|
-
this.refreshId = null;
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
// reset to 2 seconds if we received new events
|
|
988
|
-
if (hasNewEvents) {
|
|
989
|
-
this.pollingInterval = 2000;
|
|
990
|
-
} else {
|
|
991
|
-
// increase interval by 1 second up to max of 15 seconds
|
|
992
|
-
this.pollingInterval = Math.min(this.pollingInterval + 1000, 15000);
|
|
993
|
-
}
|
|
994
|
-
|
|
995
|
-
this.refreshId = setTimeout(() => {
|
|
996
|
-
this.checkForNewMessages();
|
|
997
|
-
}, this.pollingInterval);
|
|
998
|
-
}
|
|
999
|
-
|
|
1000
1020
|
public prerender(event: ContactEvent) {
|
|
1001
1021
|
// use the unified renderEvent function with isSimulation = false
|
|
1002
1022
|
const rendered = renderEvent(event, false);
|
|
@@ -1009,10 +1029,27 @@ export class ContactChat extends ContactStoreElement {
|
|
|
1009
1029
|
}
|
|
1010
1030
|
}
|
|
1011
1031
|
|
|
1032
|
+
/**
|
|
1033
|
+
* Keeps the store's avatar cache fresh from server-hydrated user refs and
|
|
1034
|
+
* fills in refs that arrive without one - events published over sockets
|
|
1035
|
+
* carry only a user's uuid and name.
|
|
1036
|
+
*/
|
|
1037
|
+
private resolveUserAvatar(event: any) {
|
|
1038
|
+
const user = event._user;
|
|
1039
|
+
if (user && user.uuid && this.store) {
|
|
1040
|
+
if (user.avatar) {
|
|
1041
|
+
this.store.setUserAvatar(user.uuid, user.avatar);
|
|
1042
|
+
} else {
|
|
1043
|
+
user.avatar = this.store.getUserAvatar(user.uuid);
|
|
1044
|
+
}
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1012
1048
|
private createMessages(page: ContactHistoryPage): ContactEvent[] {
|
|
1013
1049
|
if (page.events) {
|
|
1014
1050
|
const messages: ContactEvent[] = [];
|
|
1015
1051
|
page.events.forEach((event) => {
|
|
1052
|
+
this.resolveUserAvatar(event);
|
|
1016
1053
|
// track the UUID of the newest event for polling
|
|
1017
1054
|
if (
|
|
1018
1055
|
!this.afterUUID ||
|
|
@@ -1047,40 +1084,48 @@ export class ContactChat extends ContactStoreElement {
|
|
|
1047
1084
|
return [];
|
|
1048
1085
|
}
|
|
1049
1086
|
|
|
1050
|
-
|
|
1087
|
+
/**
|
|
1088
|
+
* Fetches any events newer than the last one we've seen. New events
|
|
1089
|
+
* normally arrive over the socket, so this is only needed to cover gaps -
|
|
1090
|
+
* events published between our initial history fetch and the subscription
|
|
1091
|
+
* becoming active, or while a dropped connection was reconnecting.
|
|
1092
|
+
*/
|
|
1093
|
+
private fetchMissedEvents() {
|
|
1051
1094
|
// we are already working on it or in search mode
|
|
1052
|
-
if (this.
|
|
1095
|
+
if (this.fetchingMissed || this.searchMode) {
|
|
1053
1096
|
return;
|
|
1054
1097
|
}
|
|
1055
1098
|
|
|
1056
1099
|
const chat = this.chat;
|
|
1057
1100
|
if (this.currentContact && this.afterUUID) {
|
|
1058
|
-
this.
|
|
1059
|
-
this.lastFetchTime = Date.now();
|
|
1101
|
+
this.fetchingMissed = true;
|
|
1060
1102
|
const endpoint = this.getEndpoint();
|
|
1061
1103
|
if (!endpoint) {
|
|
1104
|
+
this.fetchingMissed = false;
|
|
1062
1105
|
return;
|
|
1063
1106
|
}
|
|
1064
1107
|
|
|
1065
1108
|
const fetchContact = this.currentContact.uuid;
|
|
1109
|
+
const fetchTicket = this.currentTicket?.uuid;
|
|
1066
1110
|
|
|
1067
|
-
fetchContactHistory(
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1111
|
+
fetchContactHistory(endpoint, fetchTicket, null, this.afterUUID).then(
|
|
1112
|
+
(page: ContactHistoryPage) => {
|
|
1113
|
+
this.fetchingMissed = false;
|
|
1114
|
+
|
|
1115
|
+
// things may have changed while we were fetching
|
|
1116
|
+
if (
|
|
1117
|
+
this.searchMode ||
|
|
1118
|
+
fetchContact !== this.currentContact?.uuid ||
|
|
1119
|
+
fetchTicket !== this.currentTicket?.uuid
|
|
1120
|
+
) {
|
|
1121
|
+
return;
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
const messages = this.createMessages(page);
|
|
1125
|
+
messages.reverse();
|
|
1077
1126
|
chat.addMessages(messages, null, true);
|
|
1078
|
-
this.polling = false;
|
|
1079
|
-
this.scheduleRefresh(hasNewEvents);
|
|
1080
|
-
} else {
|
|
1081
|
-
this.polling = false;
|
|
1082
1127
|
}
|
|
1083
|
-
|
|
1128
|
+
);
|
|
1084
1129
|
}
|
|
1085
1130
|
}
|
|
1086
1131
|
|
|
@@ -1130,9 +1175,6 @@ export class ContactChat extends ContactStoreElement {
|
|
|
1130
1175
|
}
|
|
1131
1176
|
|
|
1132
1177
|
chat.addMessages(messages);
|
|
1133
|
-
if (!this.searchMode) {
|
|
1134
|
-
this.scheduleRefresh();
|
|
1135
|
-
}
|
|
1136
1178
|
});
|
|
1137
1179
|
}
|
|
1138
1180
|
}
|