@meetelise/chat 1.51.4 → 1.51.6
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/dist/src/WebComponent/Scheduler/tour-scheduler.d.ts +2 -0
- package/dist/src/WebComponent/launcher/Launcher.d.ts +1 -0
- package/dist/src/WebComponent/me-chat.d.ts +1 -0
- package/dist/src/WebComponent/pubnub-chat.d.ts +3 -0
- package/dist/src/fetchFeatureFlag.d.ts +1 -0
- package/package.json +1 -1
- package/public/dist/index.js +232 -227
- package/src/WebComponent/Scheduler/tour-scheduler.ts +9 -2
- package/src/WebComponent/launcher/Launcher.ts +3 -0
- package/src/WebComponent/me-chat.ts +16 -6
- package/src/WebComponent/pubnub-chat.ts +20 -1
- package/src/fetchFeatureFlag.ts +22 -0
- package/src/pubnub-chat.test.ts +192 -0
|
@@ -124,6 +124,8 @@ export class TourScheduler extends LitElement {
|
|
|
124
124
|
orgSlug = "";
|
|
125
125
|
@property({ attribute: true })
|
|
126
126
|
hasDynamicSchedulingEnabled = false;
|
|
127
|
+
@property({ attribute: true })
|
|
128
|
+
bedroomPreferenceRequired = false;
|
|
127
129
|
|
|
128
130
|
@property({ attribute: true })
|
|
129
131
|
private leadSourceClient: LeadSourceClient | null = null;
|
|
@@ -660,13 +662,16 @@ export class TourScheduler extends LitElement {
|
|
|
660
662
|
!!this.phoneNumberE164
|
|
661
663
|
);
|
|
662
664
|
},
|
|
665
|
+
layout: (): boolean =>
|
|
666
|
+
!this.bedroomPreferenceRequired || !!this.selectedLayoutValue,
|
|
663
667
|
};
|
|
664
668
|
|
|
665
669
|
formIsValidForSubmission = (): boolean => {
|
|
666
670
|
const isValid =
|
|
667
671
|
this.validators.tourType() &&
|
|
668
672
|
this.validators.dateAndTime() &&
|
|
669
|
-
this.validators.leadInfo()
|
|
673
|
+
this.validators.leadInfo() &&
|
|
674
|
+
this.validators.layout();
|
|
670
675
|
return isValid;
|
|
671
676
|
};
|
|
672
677
|
|
|
@@ -1303,7 +1308,9 @@ export class TourScheduler extends LitElement {
|
|
|
1303
1308
|
${this.layoutOptions.length > 0
|
|
1304
1309
|
? html` <me-select
|
|
1305
1310
|
id="layout"
|
|
1306
|
-
placeholder
|
|
1311
|
+
placeholder=${this.bedroomPreferenceRequired
|
|
1312
|
+
? "Bedroom preference"
|
|
1313
|
+
: "Bedroom preference (optional)"}
|
|
1307
1314
|
.options="${this.layoutOptions.map((o) => ({
|
|
1308
1315
|
label: o.label,
|
|
1309
1316
|
value: layoutValueToCanonical(o.value),
|
|
@@ -151,6 +151,8 @@ export class Launcher extends LitElement {
|
|
|
151
151
|
@property({ attribute: false })
|
|
152
152
|
layoutOptions: LayoutOption[] = [];
|
|
153
153
|
@property({ attribute: true })
|
|
154
|
+
bedroomPreferenceRequired = false;
|
|
155
|
+
@property({ attribute: true })
|
|
154
156
|
hasDynamicSchedulingEnabled = false;
|
|
155
157
|
|
|
156
158
|
@property({ attribute: true })
|
|
@@ -1310,6 +1312,7 @@ export class Launcher extends LitElement {
|
|
|
1310
1312
|
.leadSources="${this.leadSources}"
|
|
1311
1313
|
.tourTypeOptions=${this.tourTypeOptions}
|
|
1312
1314
|
.layoutOptions=${this.layoutOptions}
|
|
1315
|
+
.bedroomPreferenceRequired=${this.bedroomPreferenceRequired}
|
|
1313
1316
|
.orgLegalName=${this.orgLegalName}
|
|
1314
1317
|
.country=${this.country}
|
|
1315
1318
|
buildingId=${this.buildingId}
|
|
@@ -14,6 +14,7 @@ import {
|
|
|
14
14
|
FeatureFlagsShowDropdown,
|
|
15
15
|
fetchFeatureFlagUseApplicationsLinkReplacement,
|
|
16
16
|
fetchFeatureFlagUseOverrideContactUsForm,
|
|
17
|
+
fetchTrackedRolloutBedroomPreferenceRequired,
|
|
17
18
|
} from "../fetchFeatureFlag";
|
|
18
19
|
import { HIDE_DATA_LAYER_PII_ORG_IDS, setGtmPiiRedaction } from "../gtm";
|
|
19
20
|
import fetchPhoneNumberFromSource, {
|
|
@@ -173,6 +174,8 @@ export class MEChat extends LitElement {
|
|
|
173
174
|
private featureFlagShowDropdown: FeatureFlagsShowDropdown =
|
|
174
175
|
FeatureFlagsShowDropdown.always;
|
|
175
176
|
@state()
|
|
177
|
+
private bedroomPreferenceRequired = false;
|
|
178
|
+
@state()
|
|
176
179
|
private phoneNumberForSource: NumberForSelectedSource | null = null;
|
|
177
180
|
@state()
|
|
178
181
|
private hasMounted = false;
|
|
@@ -360,12 +363,17 @@ export class MEChat extends LitElement {
|
|
|
360
363
|
HIDE_DATA_LAYER_PII_ORG_IDS.includes(buildingDetails.orgId)
|
|
361
364
|
);
|
|
362
365
|
|
|
363
|
-
const [
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
366
|
+
const [
|
|
367
|
+
leadSources,
|
|
368
|
+
featureFlagUseApplicationsLinkReplacement,
|
|
369
|
+
isAip,
|
|
370
|
+
bedroomPreferenceRequired,
|
|
371
|
+
] = await Promise.all([
|
|
372
|
+
fetchLeadSources(this.orgSlug, this.buildingSlug),
|
|
373
|
+
fetchFeatureFlagUseApplicationsLinkReplacement(this.buildingSlug),
|
|
374
|
+
fetchBuildingAip(this.buildingSlug),
|
|
375
|
+
fetchTrackedRolloutBedroomPreferenceRequired(this.buildingSlug),
|
|
376
|
+
]);
|
|
369
377
|
|
|
370
378
|
if (this.buildingWebchatView) {
|
|
371
379
|
this.buildingWebchatView.phoneNumber = formatPhoneNumber(
|
|
@@ -376,6 +384,7 @@ export class MEChat extends LitElement {
|
|
|
376
384
|
this.leadSources = leadSources;
|
|
377
385
|
this.featureFlagShowDropdown =
|
|
378
386
|
featureFlagShowDropdown as FeatureFlagsShowDropdown;
|
|
387
|
+
this.bedroomPreferenceRequired = bedroomPreferenceRequired;
|
|
379
388
|
|
|
380
389
|
// The backend is cached for ~4 hours, so falling back to the existing api request until the cache propagates all the changes
|
|
381
390
|
const buildingPhoneNumber = buildingDetails.textWithUsPhoneNumber
|
|
@@ -1022,6 +1031,7 @@ export class MEChat extends LitElement {
|
|
|
1022
1031
|
?.tourTypeOptions ?? []}
|
|
1023
1032
|
.layoutOptions=${this.buildingWebchatView?.layoutOptions ??
|
|
1024
1033
|
[]}
|
|
1034
|
+
.bedroomPreferenceRequired=${this.bedroomPreferenceRequired}
|
|
1025
1035
|
.launcherStyles=${this.launcherStyles}
|
|
1026
1036
|
.primaryColor=${this.primaryColor}
|
|
1027
1037
|
.backgroundColor=${this.backgroundColor}
|
|
@@ -39,6 +39,8 @@ import { BuildingWebchatView } from "../fetchBuildingWebchatView";
|
|
|
39
39
|
|
|
40
40
|
@customElement("pubnub-chat")
|
|
41
41
|
export class PubnubChat extends LitElement {
|
|
42
|
+
private static readonly AUTO_SCROLL_THRESHOLD_PX = 32;
|
|
43
|
+
|
|
42
44
|
static styles = [
|
|
43
45
|
InputStyles,
|
|
44
46
|
pubnubChatStyles,
|
|
@@ -108,6 +110,8 @@ export class PubnubChat extends LitElement {
|
|
|
108
110
|
|
|
109
111
|
private _cleanupMobileListener?: () => void;
|
|
110
112
|
|
|
113
|
+
private shouldAutoScroll = true;
|
|
114
|
+
|
|
111
115
|
@state()
|
|
112
116
|
messages: SimpleChatMessage[] = [];
|
|
113
117
|
|
|
@@ -151,6 +155,7 @@ export class PubnubChat extends LitElement {
|
|
|
151
155
|
if (message.length === 0) {
|
|
152
156
|
return;
|
|
153
157
|
}
|
|
158
|
+
this.shouldAutoScroll = true;
|
|
154
159
|
this.messageInput.value = "";
|
|
155
160
|
this.autoResizeMessageInput();
|
|
156
161
|
|
|
@@ -199,12 +204,23 @@ export class PubnubChat extends LitElement {
|
|
|
199
204
|
}
|
|
200
205
|
|
|
201
206
|
scrollToChatBottom = (): void => {
|
|
207
|
+
if (!this.shouldAutoScroll) return;
|
|
202
208
|
this.messageBody.scrollTo({
|
|
203
209
|
top: this.messageBody.scrollHeight - this.messageBody.clientHeight,
|
|
204
|
-
behavior: "
|
|
210
|
+
behavior: "auto",
|
|
205
211
|
});
|
|
206
212
|
};
|
|
207
213
|
|
|
214
|
+
private handleConversationScroll = (event: Event): void => {
|
|
215
|
+
const messageBody = event.currentTarget as HTMLDivElement;
|
|
216
|
+
const distanceFromBottom =
|
|
217
|
+
messageBody.scrollHeight -
|
|
218
|
+
messageBody.clientHeight -
|
|
219
|
+
messageBody.scrollTop;
|
|
220
|
+
this.shouldAutoScroll =
|
|
221
|
+
distanceFromBottom <= PubnubChat.AUTO_SCROLL_THRESHOLD_PX;
|
|
222
|
+
};
|
|
223
|
+
|
|
208
224
|
async updated(): Promise<void> {
|
|
209
225
|
this.scrollToChatBottom();
|
|
210
226
|
try {
|
|
@@ -312,6 +328,7 @@ export class PubnubChat extends LitElement {
|
|
|
312
328
|
</div>
|
|
313
329
|
<div
|
|
314
330
|
id="conversation-body"
|
|
331
|
+
@scroll=${this.handleConversationScroll}
|
|
315
332
|
class=${classMap({
|
|
316
333
|
["hidden"]: this.needsConsent(),
|
|
317
334
|
["utilities-conversation-body-backdrop"]: true,
|
|
@@ -485,6 +502,7 @@ export class PubnubChat extends LitElement {
|
|
|
485
502
|
</div>
|
|
486
503
|
<div
|
|
487
504
|
id="conversation-body"
|
|
505
|
+
@scroll=${this.handleConversationScroll}
|
|
488
506
|
class=${classMap({
|
|
489
507
|
["hidden"]: this.needsConsent(),
|
|
490
508
|
["healthcare-conversation-body-backdrop"]: true,
|
|
@@ -667,6 +685,7 @@ export class PubnubChat extends LitElement {
|
|
|
667
685
|
</div>
|
|
668
686
|
<div
|
|
669
687
|
id="conversation-body"
|
|
688
|
+
@scroll=${this.handleConversationScroll}
|
|
670
689
|
style=${styleMap({
|
|
671
690
|
background:
|
|
672
691
|
this.building.orgId === 444
|
package/src/fetchFeatureFlag.ts
CHANGED
|
@@ -114,3 +114,25 @@ export async function fetchFeatureFlagUseApplicationsLinkReplacement(
|
|
|
114
114
|
return false;
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
+
|
|
118
|
+
const trackedRolloutEndpoint = (buildingSlug: string): string => {
|
|
119
|
+
const host = "https://app.meetelise.com";
|
|
120
|
+
return `${host}/platformApi/webchat/${buildingSlug}/tracked-rollout`;
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
export async function fetchTrackedRolloutBedroomPreferenceRequired(
|
|
124
|
+
buildingSlug: string
|
|
125
|
+
): Promise<boolean> {
|
|
126
|
+
if (!buildingSlug) return false;
|
|
127
|
+
try {
|
|
128
|
+
const response = await axios.get(trackedRolloutEndpoint(buildingSlug), {
|
|
129
|
+
params: {
|
|
130
|
+
rollout_name: "webchat_bedroom_preference_required",
|
|
131
|
+
default_value: false,
|
|
132
|
+
},
|
|
133
|
+
});
|
|
134
|
+
return response.data;
|
|
135
|
+
} catch (_) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
import { expect } from "@esm-bundle/chai";
|
|
2
|
+
import "../public/dist/index";
|
|
3
|
+
import { BuildingWebchatView } from "./fetchBuildingWebchatView";
|
|
4
|
+
import type { SimpleTextChatMessage } from "./MyPubnub";
|
|
5
|
+
import type { PubnubChat } from "./WebComponent/pubnub-chat";
|
|
6
|
+
|
|
7
|
+
declare const afterEach: (callback: () => void) => void;
|
|
8
|
+
declare const it: (name: string, callback: () => void | Promise<void>) => void;
|
|
9
|
+
|
|
10
|
+
interface ScrollMetrics {
|
|
11
|
+
clientHeight: number;
|
|
12
|
+
scrollHeight: number;
|
|
13
|
+
scrollTop: number;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const waitForRender = async (chat: PubnubChat): Promise<void> => {
|
|
17
|
+
await chat.updateComplete;
|
|
18
|
+
const messageUpdates = Array.from(
|
|
19
|
+
chat.shadowRoot?.querySelectorAll("pubnub-message") ?? []
|
|
20
|
+
).map(
|
|
21
|
+
(message) =>
|
|
22
|
+
(message as HTMLElement & { updateComplete: Promise<boolean> })
|
|
23
|
+
.updateComplete
|
|
24
|
+
);
|
|
25
|
+
await Promise.all(messageUpdates);
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const streamingMessage = (chunks: string[]): SimpleTextChatMessage => ({
|
|
29
|
+
timestamp: 100,
|
|
30
|
+
message: chunks.join(""),
|
|
31
|
+
isLeadMessage: false,
|
|
32
|
+
chunks: chunks.map((text, order) => ({ text, order, isDone: false })),
|
|
33
|
+
type: "text" as SimpleTextChatMessage["type"],
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
const completedMessage = (message: string): SimpleTextChatMessage => ({
|
|
37
|
+
timestamp: 100,
|
|
38
|
+
message,
|
|
39
|
+
isLeadMessage: false,
|
|
40
|
+
chunks: [{ text: message, order: 0, isDone: true }],
|
|
41
|
+
type: "text" as SimpleTextChatMessage["type"],
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const createScrollableChat = async (
|
|
45
|
+
initialMetrics: Partial<ScrollMetrics> = {}
|
|
46
|
+
): Promise<{
|
|
47
|
+
chat: PubnubChat;
|
|
48
|
+
messageBody: HTMLElement;
|
|
49
|
+
scrollCalls: ScrollToOptions[];
|
|
50
|
+
setMetrics: (metrics: Partial<ScrollMetrics>) => void;
|
|
51
|
+
}> => {
|
|
52
|
+
const chat = document.createElement("pubnub-chat") as PubnubChat;
|
|
53
|
+
chat.buildingSlug = "test-building";
|
|
54
|
+
chat.orgSlug = "test-org";
|
|
55
|
+
chat.building = {
|
|
56
|
+
id: 1,
|
|
57
|
+
orgId: 1,
|
|
58
|
+
name: "Test Building",
|
|
59
|
+
welcomeMessage: null,
|
|
60
|
+
state: "NY",
|
|
61
|
+
country: "US",
|
|
62
|
+
customAdditionalDisclaimer: null,
|
|
63
|
+
} as BuildingWebchatView;
|
|
64
|
+
document.body.appendChild(chat);
|
|
65
|
+
await waitForRender(chat);
|
|
66
|
+
|
|
67
|
+
const messageBody =
|
|
68
|
+
chat.shadowRoot?.querySelector<HTMLElement>("#conversation-body");
|
|
69
|
+
if (!messageBody) throw new Error("Expected the conversation body to render");
|
|
70
|
+
|
|
71
|
+
const metrics: ScrollMetrics = {
|
|
72
|
+
clientHeight: 400,
|
|
73
|
+
scrollHeight: 1_000,
|
|
74
|
+
scrollTop: 200,
|
|
75
|
+
...initialMetrics,
|
|
76
|
+
};
|
|
77
|
+
Object.defineProperties(messageBody, {
|
|
78
|
+
clientHeight: { configurable: true, get: () => metrics.clientHeight },
|
|
79
|
+
scrollHeight: { configurable: true, get: () => metrics.scrollHeight },
|
|
80
|
+
scrollTop: { configurable: true, get: () => metrics.scrollTop },
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
const scrollCalls: ScrollToOptions[] = [];
|
|
84
|
+
messageBody.scrollTo = ((options: ScrollToOptions) => {
|
|
85
|
+
scrollCalls.push(options);
|
|
86
|
+
}) as typeof messageBody.scrollTo;
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
chat,
|
|
90
|
+
messageBody,
|
|
91
|
+
scrollCalls,
|
|
92
|
+
setMetrics: (nextMetrics) => Object.assign(metrics, nextMetrics),
|
|
93
|
+
};
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const expectOnlyAutoScrollsTo = (
|
|
97
|
+
scrollCalls: ScrollToOptions[],
|
|
98
|
+
top: number
|
|
99
|
+
): void => {
|
|
100
|
+
expect(scrollCalls).not.to.be.empty;
|
|
101
|
+
expect(
|
|
102
|
+
scrollCalls.every(
|
|
103
|
+
(options) => options.top === top && options.behavior === "auto"
|
|
104
|
+
)
|
|
105
|
+
).to.equal(true);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
document.querySelectorAll("pubnub-chat").forEach((chat) => chat.remove());
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it("test_bug_preserves_scroll_position_during_streamed_updates", async () => {
|
|
113
|
+
const { chat, messageBody, scrollCalls } = await createScrollableChat();
|
|
114
|
+
|
|
115
|
+
messageBody.dispatchEvent(new Event("scroll"));
|
|
116
|
+
chat.messages = [streamingMessage(["First streamed chunk. "])];
|
|
117
|
+
await waitForRender(chat);
|
|
118
|
+
chat.messages = [
|
|
119
|
+
streamingMessage(["First streamed chunk. ", "Second streamed chunk."]),
|
|
120
|
+
];
|
|
121
|
+
await waitForRender(chat);
|
|
122
|
+
|
|
123
|
+
expect(scrollCalls).to.deep.equal([]);
|
|
124
|
+
expect(messageBody.scrollTop).to.equal(200);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("preserves scroll position when a completed message re-renders", async () => {
|
|
128
|
+
const { chat, messageBody, scrollCalls } = await createScrollableChat();
|
|
129
|
+
|
|
130
|
+
chat.messages = [
|
|
131
|
+
completedMessage(
|
|
132
|
+
"A completed response with enough content to exceed the visible pane."
|
|
133
|
+
),
|
|
134
|
+
];
|
|
135
|
+
await waitForRender(chat);
|
|
136
|
+
scrollCalls.length = 0;
|
|
137
|
+
|
|
138
|
+
messageBody.dispatchEvent(new Event("scroll"));
|
|
139
|
+
const renderedMessage = chat.shadowRoot?.querySelector("pubnub-message") as
|
|
140
|
+
| (HTMLElement & { updateComplete: Promise<boolean> })
|
|
141
|
+
| null;
|
|
142
|
+
if (!renderedMessage) throw new Error("Expected a rendered chat message");
|
|
143
|
+
|
|
144
|
+
// Completed messages can update later as links, previews, or media settle.
|
|
145
|
+
renderedMessage.requestUpdate();
|
|
146
|
+
await renderedMessage.updateComplete;
|
|
147
|
+
|
|
148
|
+
expect(scrollCalls).to.deep.equal([]);
|
|
149
|
+
expect(messageBody.scrollTop).to.equal(200);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("continues following streamed updates while already at the bottom", async () => {
|
|
153
|
+
const { chat, messageBody, scrollCalls, setMetrics } =
|
|
154
|
+
await createScrollableChat({ scrollTop: 600 });
|
|
155
|
+
|
|
156
|
+
messageBody.dispatchEvent(new Event("scroll"));
|
|
157
|
+
setMetrics({ scrollHeight: 1_200 });
|
|
158
|
+
chat.messages = [streamingMessage(["A newly streamed response."])];
|
|
159
|
+
await waitForRender(chat);
|
|
160
|
+
|
|
161
|
+
expectOnlyAutoScrollsTo(scrollCalls, 800);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("resumes following after the reader returns near the bottom", async () => {
|
|
165
|
+
const { chat, messageBody, scrollCalls, setMetrics } =
|
|
166
|
+
await createScrollableChat();
|
|
167
|
+
|
|
168
|
+
messageBody.dispatchEvent(new Event("scroll"));
|
|
169
|
+
chat.messages = [streamingMessage(["First streamed chunk. "])];
|
|
170
|
+
await waitForRender(chat);
|
|
171
|
+
expect(scrollCalls).to.deep.equal([]);
|
|
172
|
+
|
|
173
|
+
setMetrics({ scrollTop: 575 });
|
|
174
|
+
messageBody.dispatchEvent(new Event("scroll"));
|
|
175
|
+
setMetrics({ scrollHeight: 1_200 });
|
|
176
|
+
chat.messages = [
|
|
177
|
+
streamingMessage(["First streamed chunk. ", "Second streamed chunk."]),
|
|
178
|
+
];
|
|
179
|
+
await waitForRender(chat);
|
|
180
|
+
|
|
181
|
+
expectOnlyAutoScrollsTo(scrollCalls, 800);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("follows the conversation again when the reader sends a message", async () => {
|
|
185
|
+
const { chat, messageBody, scrollCalls } = await createScrollableChat();
|
|
186
|
+
|
|
187
|
+
messageBody.dispatchEvent(new Event("scroll"));
|
|
188
|
+
await chat.sendMessage("Show me another apartment");
|
|
189
|
+
await waitForRender(chat);
|
|
190
|
+
|
|
191
|
+
expectOnlyAutoScrollsTo(scrollCalls, 600);
|
|
192
|
+
});
|