@meetelise/chat 1.3.0 → 1.4.2
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/.eslintrc.cjs +1 -0
- package/.github/workflows/release.yml +1 -0
- package/.vscode/settings.json +2 -1
- package/declarations.d.ts +4 -0
- package/package.json +31 -9
- package/public/demo/index.html +55 -0
- package/public/dist/bundle.js +2 -0
- package/public/dist/bundle.js.LICENSE.txt +32 -0
- package/{demo → public}/index.html +1 -1
- package/public/serve.json +13 -0
- package/public/ts-loader-build/ChatBubble.d.ts +12 -0
- package/{dist/src → public/ts-loader-build}/MEChat.d.ts +6 -0
- package/{dist/src → public/ts-loader-build}/analytics.d.ts +5 -0
- package/{dist/src → public/ts-loader-build}/chatID.d.ts +0 -0
- package/{dist/src → public/ts-loader-build}/createConversation.d.ts +0 -0
- package/{dist/src → public/ts-loader-build}/fetchBuildingInfo.d.ts +1 -0
- package/{dist/src → public/ts-loader-build}/getAvatarUrl.d.ts +0 -0
- package/{dist/src → public/ts-loader-build}/getIcons.d.ts +0 -0
- package/{dist/src → public/ts-loader-build}/installTalkJSStyles.d.ts +0 -0
- package/{dist/src → public/ts-loader-build}/resolveTheme.d.ts +0 -0
- package/public/ts-loader-build/utils.d.ts +1 -0
- package/src/ChatBubble.d.ts +11 -0
- package/src/ChatBubble.module.scss +58 -0
- package/src/ChatBubble.tsx +55 -0
- package/src/DemoApp.tsx +103 -0
- package/src/MEChat.d.ts +65 -0
- package/src/MEChat.module.scss +59 -0
- package/src/MEChat.test.ts +6 -10
- package/src/{MEChat.ts → MEChat.tsx} +118 -14
- package/src/analytics.ts +13 -0
- package/src/fetchBuildingInfo.ts +1 -0
- package/src/getAvatarUrl.ts +2 -1
- package/src/getIcons.ts +4 -12
- package/src/utils.ts +24 -0
- package/tsconfig.json +5 -4
- package/web-dev-server.config.js +12 -1
- package/web-test-runner.config.js +1 -1
- package/webpack.config.cjs +65 -0
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -2
- package/dist/index.js.map +0 -1
- package/dist/src/MEChat.js +0 -144
- package/dist/src/MEChat.js.map +0 -1
- package/dist/src/analytics.js +0 -30
- package/dist/src/analytics.js.map +0 -1
- package/dist/src/chatID.js +0 -28
- package/dist/src/chatID.js.map +0 -1
- package/dist/src/createConversation.js +0 -30
- package/dist/src/createConversation.js.map +0 -1
- package/dist/src/fetchBuildingInfo.js +0 -15
- package/dist/src/fetchBuildingInfo.js.map +0 -1
- package/dist/src/getAvatarUrl.js +0 -36
- package/dist/src/getAvatarUrl.js.map +0 -1
- package/dist/src/getIcons.js +0 -32
- package/dist/src/getIcons.js.map +0 -1
- package/dist/src/installTalkJSStyles.js +0 -26
- package/dist/src/installTalkJSStyles.js.map +0 -1
- package/dist/src/resolveTheme.js +0 -17
- package/dist/src/resolveTheme.js.map +0 -1
- package/index.ts +0 -1
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function useInterval(callback: () => void, delay: number | null): void;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
interface ChatBubbleProps {
|
|
3
|
+
messages: {
|
|
4
|
+
title: string;
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
triggerBounce: () => void;
|
|
8
|
+
onClick: () => void;
|
|
9
|
+
}
|
|
10
|
+
declare const ChatBubble: React.FunctionComponent<ChatBubbleProps>;
|
|
11
|
+
export default ChatBubble;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
@import url("https://fonts.googleapis.com/css2?family=Poppins:wght@600;900&display=swap");
|
|
2
|
+
|
|
3
|
+
$chatBubbleBackgroundColor: white;
|
|
4
|
+
|
|
5
|
+
.chatBubble,
|
|
6
|
+
.chatBubble * {
|
|
7
|
+
all: initial;
|
|
8
|
+
}
|
|
9
|
+
.chatBubble {
|
|
10
|
+
position: absolute;
|
|
11
|
+
right: 90px;
|
|
12
|
+
top: -55px;
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: column;
|
|
15
|
+
width: max-content;
|
|
16
|
+
padding: 20px;
|
|
17
|
+
margin: auto;
|
|
18
|
+
background-color: $chatBubbleBackgroundColor;
|
|
19
|
+
color: #28273e;
|
|
20
|
+
border-radius: 20px 20px 0px 20px;
|
|
21
|
+
filter: drop-shadow(0 6px 8px rgb(0 0 0 / 25%));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
$chatBubbleTailWidth: 23px;
|
|
25
|
+
|
|
26
|
+
.chatBubble::after {
|
|
27
|
+
content: "";
|
|
28
|
+
position: absolute;
|
|
29
|
+
background: radial-gradient(
|
|
30
|
+
ellipse 181% 42px at 90% 10%,
|
|
31
|
+
transparent 0,
|
|
32
|
+
transparent 50%,
|
|
33
|
+
$chatBubbleBackgroundColor 50%
|
|
34
|
+
);
|
|
35
|
+
width: $chatBubbleTailWidth;
|
|
36
|
+
height: $chatBubbleTailWidth;
|
|
37
|
+
right: -($chatBubbleTailWidth);
|
|
38
|
+
bottom: 0;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.title {
|
|
42
|
+
all: unset;
|
|
43
|
+
font-family: Poppins;
|
|
44
|
+
font-style: normal;
|
|
45
|
+
font-weight: 900;
|
|
46
|
+
font-size: 20px;
|
|
47
|
+
line-height: 22px;
|
|
48
|
+
margin: 0 0 0.05em 0;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.text {
|
|
52
|
+
all: unset;
|
|
53
|
+
font-family: Poppins;
|
|
54
|
+
font-style: normal;
|
|
55
|
+
font-weight: 600;
|
|
56
|
+
font-size: 12px;
|
|
57
|
+
line-height: 22px;
|
|
58
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import React, { useState } from "react";
|
|
2
|
+
import { useInterval } from "./utils";
|
|
3
|
+
import styles from "./ChatBubble.module.scss";
|
|
4
|
+
|
|
5
|
+
interface ChatBubbleProps {
|
|
6
|
+
messages: { title: string; text: string }[];
|
|
7
|
+
triggerBounce: () => void;
|
|
8
|
+
bounceIntervalInSeconds: number;
|
|
9
|
+
onClick: () => void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const ChatBubble: React.FunctionComponent<ChatBubbleProps> = ({
|
|
13
|
+
messages,
|
|
14
|
+
triggerBounce,
|
|
15
|
+
bounceIntervalInSeconds,
|
|
16
|
+
onClick,
|
|
17
|
+
}: ChatBubbleProps) => {
|
|
18
|
+
const [messageIndex, setMessageIndex] = useState(0);
|
|
19
|
+
const [message, setMessage] = useState(messages[messageIndex]);
|
|
20
|
+
const [weBouncedLauncher, setWeBouncedLauncher] = useState(false);
|
|
21
|
+
|
|
22
|
+
// if there are multiple messages, toggle between them and bounce the launcher
|
|
23
|
+
useInterval(
|
|
24
|
+
() => {
|
|
25
|
+
if (messageIndex + 1 < messages.length) {
|
|
26
|
+
const newIndex = messageIndex + 1;
|
|
27
|
+
const message = messages[newIndex];
|
|
28
|
+
triggerBounce();
|
|
29
|
+
setMessageIndex(newIndex);
|
|
30
|
+
setMessage(message);
|
|
31
|
+
}
|
|
32
|
+
},
|
|
33
|
+
messages.length > 1 ? bounceIntervalInSeconds * 1000 : null
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
// if there is only one message, bounce the launcher
|
|
37
|
+
useInterval(
|
|
38
|
+
() => {
|
|
39
|
+
triggerBounce();
|
|
40
|
+
setWeBouncedLauncher(true);
|
|
41
|
+
},
|
|
42
|
+
messages.length === 1 && !weBouncedLauncher
|
|
43
|
+
? bounceIntervalInSeconds * 1000
|
|
44
|
+
: null
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<div className={styles.chatBubble} onClick={onClick}>
|
|
49
|
+
<h1 className={styles.title}>{message.title}</h1>
|
|
50
|
+
<span className={styles.text}>{message.text}</span>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
export default ChatBubble;
|
package/src/DemoApp.tsx
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import React, { useEffect } from "react";
|
|
2
|
+
import ReactDOM from "react-dom";
|
|
3
|
+
import { debounce } from "lodash";
|
|
4
|
+
import MEChat from "./MEChat";
|
|
5
|
+
|
|
6
|
+
declare global {
|
|
7
|
+
interface Window {
|
|
8
|
+
chat: MEChat;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
const DemoApp = () => {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
const chat = MEChat.start({
|
|
15
|
+
organization: "test-company",
|
|
16
|
+
building: "e2e-test-yardi-building",
|
|
17
|
+
});
|
|
18
|
+
//////////////////////////////////////////////////
|
|
19
|
+
// The above code is all a client would need. //
|
|
20
|
+
// Below is some code to make local dev easier. //
|
|
21
|
+
//////////////////////////////////////////////////
|
|
22
|
+
|
|
23
|
+
// Put the chat instance on window so it's available in the console.
|
|
24
|
+
|
|
25
|
+
window.chat = chat;
|
|
26
|
+
|
|
27
|
+
// Reset on button click
|
|
28
|
+
document.getElementById("reset")?.addEventListener("click", () => {
|
|
29
|
+
chat.restartConversation();
|
|
30
|
+
});
|
|
31
|
+
const onChange = debounce((e) => {
|
|
32
|
+
chat.setTheme({ [e.target.name]: e.target.value });
|
|
33
|
+
}, 300);
|
|
34
|
+
document.querySelectorAll("#theme input").forEach((input) => {
|
|
35
|
+
input.addEventListener("input", onChange);
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Open the chat window shortly after the page loads/refreshes.
|
|
39
|
+
// setTimeout(() => chat.open(), 1000);
|
|
40
|
+
|
|
41
|
+
// Every x seconds, cycle through ui states. Close the window and
|
|
42
|
+
// remove the button from the screen, then show it and open the
|
|
43
|
+
// window. Set the DEBUG_cycle_ui_states boolean to false to disable.
|
|
44
|
+
const DEBUG_cycle_ui_states = false;
|
|
45
|
+
const period_seconds = 30;
|
|
46
|
+
if (DEBUG_cycle_ui_states) {
|
|
47
|
+
let seconds = 0;
|
|
48
|
+
setInterval(() => {
|
|
49
|
+
seconds = (seconds + 1) % period_seconds;
|
|
50
|
+
if (seconds === period_seconds - 4) chat.close();
|
|
51
|
+
if (seconds === period_seconds - 3) chat.hide();
|
|
52
|
+
if (seconds === period_seconds - 2) chat.show();
|
|
53
|
+
if (seconds === period_seconds - 1) chat.open();
|
|
54
|
+
}, 1000);
|
|
55
|
+
}
|
|
56
|
+
}, []);
|
|
57
|
+
|
|
58
|
+
return (
|
|
59
|
+
<>
|
|
60
|
+
<h1>Example Page</h1>
|
|
61
|
+
<p>This is an example page for serving the chat widget locally.</p>
|
|
62
|
+
<button id="reset" type="button">
|
|
63
|
+
Restart conversation
|
|
64
|
+
</button>
|
|
65
|
+
<form id="theme" style={{ display: "flex", flexDirection: "column" }}>
|
|
66
|
+
<label htmlFor="chatTitle">Title</label>
|
|
67
|
+
<input type="text" id="chatTitle" name="chatTitle" />
|
|
68
|
+
<label htmlFor="chatSubtitle">Subtitle</label>
|
|
69
|
+
<input type="text" id="chatSubtitle" name="chatSubtitle" />
|
|
70
|
+
<label htmlFor="bannerColor">Banner Color</label>
|
|
71
|
+
<input type="text" id="bannerColor" name="bannerColor" />
|
|
72
|
+
<label htmlFor="bannerTextColor">Banner Text Color</label>
|
|
73
|
+
<input type="text" id="bannerTextColor" name="bannerTextColor" />
|
|
74
|
+
<label htmlFor="launchButtonColor">Launch Button Color</label>
|
|
75
|
+
<input type="text" id="launchButtonColor" name="launchButtonColor" />
|
|
76
|
+
<label htmlFor="launchButtonIconColor">Launch Button Icon Color</label>
|
|
77
|
+
<input
|
|
78
|
+
type="text"
|
|
79
|
+
id="launchButtonIconColor"
|
|
80
|
+
name="launchButtonIconColor"
|
|
81
|
+
/>
|
|
82
|
+
</form>
|
|
83
|
+
<div
|
|
84
|
+
id="bottomBanner"
|
|
85
|
+
style={{
|
|
86
|
+
position: "absolute",
|
|
87
|
+
bottom: 0,
|
|
88
|
+
left: 0,
|
|
89
|
+
width: "100%",
|
|
90
|
+
height: "20vh",
|
|
91
|
+
backgroundColor: "lightpink",
|
|
92
|
+
textAlign: "center",
|
|
93
|
+
}}
|
|
94
|
+
>
|
|
95
|
+
<p style={{ marginTop: "5em" }}>
|
|
96
|
+
Hi, I'm a banner that appears at the bottom of the screen! I eat chat
|
|
97
|
+
widgets for breakfast and I'll eat yours if you aren't careful!
|
|
98
|
+
</p>
|
|
99
|
+
</div>
|
|
100
|
+
</>
|
|
101
|
+
);
|
|
102
|
+
};
|
|
103
|
+
ReactDOM.render(<DemoApp />, document.getElementById("root"));
|
package/src/MEChat.d.ts
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import Talk from "talkjs";
|
|
2
|
+
import { Theme } from "./resolveTheme";
|
|
3
|
+
/**
|
|
4
|
+
* The interface to MeetElise chat.
|
|
5
|
+
*
|
|
6
|
+
* To add meetelise chat to the screen, call its static method
|
|
7
|
+
* `start()` with your building and organization slug.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* MEChat.start({
|
|
11
|
+
* organization: 'the-jacobson-group',
|
|
12
|
+
* building: 'twin-rivers-pointe'
|
|
13
|
+
* });
|
|
14
|
+
*/
|
|
15
|
+
export default class MEChat {
|
|
16
|
+
static session: Promise<Talk.Session>;
|
|
17
|
+
/**
|
|
18
|
+
* Start an instance of MeetElise chat and add to the web page.
|
|
19
|
+
*
|
|
20
|
+
* @param opts The organization, building, and theme overrides.
|
|
21
|
+
* @returns An instance of MeetElise chat.
|
|
22
|
+
*/
|
|
23
|
+
static start(opts: Options): MEChat;
|
|
24
|
+
/**
|
|
25
|
+
* Remove the instance from the screen.
|
|
26
|
+
*
|
|
27
|
+
* Chat will be unusable after this. If you just need to hide the
|
|
28
|
+
* chat button, use {@link MEChat#hide} instead.
|
|
29
|
+
*/
|
|
30
|
+
remove(): void;
|
|
31
|
+
/**
|
|
32
|
+
* Clear all messages from the window and start a new conversation.
|
|
33
|
+
*/
|
|
34
|
+
restartConversation(): void;
|
|
35
|
+
/**
|
|
36
|
+
* Update the theme of the running chat instance.
|
|
37
|
+
*
|
|
38
|
+
* @param theme The updated theme
|
|
39
|
+
*/
|
|
40
|
+
setTheme(theme: Partial<Theme>): void;
|
|
41
|
+
/** Open the messages window */
|
|
42
|
+
open(): void;
|
|
43
|
+
/** Close the messages window */
|
|
44
|
+
close(): void;
|
|
45
|
+
/** Show the chat button on the screen if it was previously hidden. */
|
|
46
|
+
show(): void;
|
|
47
|
+
/** Hide the chat button from the screen (but don't remove from the DOM). */
|
|
48
|
+
hide(): void;
|
|
49
|
+
private buildingSlug;
|
|
50
|
+
private orgSlug;
|
|
51
|
+
private popup;
|
|
52
|
+
private launcher;
|
|
53
|
+
private building;
|
|
54
|
+
private theme;
|
|
55
|
+
private chatId;
|
|
56
|
+
private analytics;
|
|
57
|
+
private launchDarklyClient;
|
|
58
|
+
private launchDarklyIsReady;
|
|
59
|
+
private constructor();
|
|
60
|
+
}
|
|
61
|
+
export interface Options {
|
|
62
|
+
building: string;
|
|
63
|
+
organization: string;
|
|
64
|
+
theme?: Partial<Theme>;
|
|
65
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
.wrapper {
|
|
2
|
+
position: fixed;
|
|
3
|
+
display: flex;
|
|
4
|
+
bottom: 10vh;
|
|
5
|
+
right: 10vh;
|
|
6
|
+
z-index: 100000;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
:global(#__talkjs_launcher):not(.shouldBeVisible) {
|
|
10
|
+
display: none;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
:global(a#__talkjs_launcher) {
|
|
14
|
+
box-shadow: 0px 6px 8px rgba(0, 0, 0, 0.25);
|
|
15
|
+
background-position: 50% 50%;
|
|
16
|
+
background-size: 27px 27px;
|
|
17
|
+
background-color: white; // TODO: this makes the background color from the theme not apply. temporary because the icon looks bad with other colors right now.
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
:global(a#__talkjs_launcher).bouncingLauncherButton {
|
|
21
|
+
animation-name: bounce;
|
|
22
|
+
animation-duration: 1s;
|
|
23
|
+
animation-timing-function: cubic-bezier(0.28, 0.84, 0.42, 1);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/* https://css-tricks.com/making-css-animations-feel-natural/ */
|
|
27
|
+
@keyframes bounce {
|
|
28
|
+
0% {
|
|
29
|
+
transform: scale(1, 1) translateY(0);
|
|
30
|
+
}
|
|
31
|
+
10% {
|
|
32
|
+
transform: scale(1.1, 0.9) translateY(0);
|
|
33
|
+
}
|
|
34
|
+
30% {
|
|
35
|
+
transform: scale(0.9, 1.1) translateY(-100px);
|
|
36
|
+
}
|
|
37
|
+
50% {
|
|
38
|
+
transform: scale(1.05, 0.95) translateY(0);
|
|
39
|
+
}
|
|
40
|
+
57% {
|
|
41
|
+
transform: scale(1, 1) translateY(-7px);
|
|
42
|
+
}
|
|
43
|
+
64% {
|
|
44
|
+
transform: scale(1, 1) translateY(0);
|
|
45
|
+
}
|
|
46
|
+
100% {
|
|
47
|
+
transform: scale(1, 1) translateY(0);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.fadeOut {
|
|
52
|
+
animation: fadeOut 0.5s;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@keyframes fadeOut {
|
|
56
|
+
to {
|
|
57
|
+
opacity: 0;
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/MEChat.test.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { expect } from "@esm-bundle/chai";
|
|
2
2
|
import { stub, restore } from "sinon/pkg/sinon-esm";
|
|
3
|
-
import
|
|
4
|
-
import MEChat from "./MEChat";
|
|
3
|
+
import MEChat from "../public/dist/bundle";
|
|
5
4
|
|
|
6
5
|
const stubResponse = {
|
|
7
6
|
json: stub().resolves({
|
|
@@ -15,10 +14,6 @@ const stubResponse = {
|
|
|
15
14
|
}),
|
|
16
15
|
};
|
|
17
16
|
|
|
18
|
-
beforeEach(async () => {
|
|
19
|
-
await TalkJS.ready;
|
|
20
|
-
});
|
|
21
|
-
|
|
22
17
|
afterEach(() => {
|
|
23
18
|
restore();
|
|
24
19
|
});
|
|
@@ -41,9 +36,10 @@ it("shows the chat icon", async function () {
|
|
|
41
36
|
const launcher =
|
|
42
37
|
document.querySelector<HTMLAnchorElement>(".__talkjs_launcher");
|
|
43
38
|
expect(launcher).to.be.an.instanceof(HTMLAnchorElement);
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
39
|
+
// TODO: temporarily making the launcher always white because the new icon doesn't look good with all colors
|
|
40
|
+
// const launcherStyle = window.getComputedStyle(launcher);
|
|
41
|
+
// const launcherBG = launcherStyle.getPropertyValue("background-color");
|
|
42
|
+
// expect(launcherBG).to.equal("rgb(180, 190, 0)");
|
|
47
43
|
|
|
48
44
|
// And the popup should be hidden
|
|
49
45
|
const popup = document.querySelector<HTMLSpanElement>(".__talkjs_popup");
|
|
@@ -58,7 +54,7 @@ it("shows the chat icon", async function () {
|
|
|
58
54
|
// Then the popup should be visible
|
|
59
55
|
const popupStyle2 = window.getComputedStyle(popup);
|
|
60
56
|
const popupDisplay2 = popupStyle2.getPropertyValue("display");
|
|
61
|
-
expect(popupDisplay2).to.equal("
|
|
57
|
+
expect(popupDisplay2).not.to.equal("none");
|
|
62
58
|
|
|
63
59
|
// Ideally, expect welcome message, but we can't select inside the iframe
|
|
64
60
|
// Ideally, expect theme colors, but we can't select inside the iframe
|
|
@@ -5,6 +5,11 @@ import createConversation from "./createConversation";
|
|
|
5
5
|
import installTalkJSStyles from "./installTalkJSStyles";
|
|
6
6
|
import resolveTheme, { Theme } from "./resolveTheme";
|
|
7
7
|
import Analytics from "./analytics";
|
|
8
|
+
import ChatBubble from "./ChatBubble";
|
|
9
|
+
import ReactDOM from "react-dom";
|
|
10
|
+
import React from "react";
|
|
11
|
+
import * as LDClient from "launchdarkly-js-client-sdk";
|
|
12
|
+
import styles from "./MEChat.module.scss";
|
|
8
13
|
|
|
9
14
|
/**
|
|
10
15
|
* The interface to MeetElise chat.
|
|
@@ -118,6 +123,82 @@ export default class MEChat {
|
|
|
118
123
|
this.launcher.then((a) => (a.style.display = "none"));
|
|
119
124
|
}
|
|
120
125
|
|
|
126
|
+
/** Initialize the LaunchDarkly client so we can work with feature flags. */
|
|
127
|
+
private initializeLaunchDarkly(): [LDClient.LDClient, Promise<boolean>] {
|
|
128
|
+
const user = {
|
|
129
|
+
key: this.chatId,
|
|
130
|
+
custom: {
|
|
131
|
+
userType: "webchatUser",
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
const launchDarklyClient = LDClient.initialize(
|
|
135
|
+
process.env.LAUNCHDARKLY_CLIENT_ID || "",
|
|
136
|
+
user
|
|
137
|
+
);
|
|
138
|
+
const launchDarklyIsReady = new Promise((resolve) =>
|
|
139
|
+
launchDarklyClient.on("ready", resolve)
|
|
140
|
+
);
|
|
141
|
+
return [launchDarklyClient, launchDarklyIsReady as Promise<boolean>];
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/** Show a speech bubble next to the chat button (launcher). Also adds some animations to the button. */
|
|
145
|
+
private addChatBubble(popup: Talk.Popup, launcher: HTMLAnchorElement): void {
|
|
146
|
+
const chatBubbleTarget = document.createElement("div");
|
|
147
|
+
// set up scroll listener before mounting the chat bubble component so we don't miss any scroll events
|
|
148
|
+
const closeChatBubble = (shouldFade = false) => {
|
|
149
|
+
if (shouldFade) {
|
|
150
|
+
chatBubbleTarget.classList.add(styles.fadeOut);
|
|
151
|
+
setTimeout(() => {
|
|
152
|
+
ReactDOM.unmountComponentAtNode(chatBubbleTarget);
|
|
153
|
+
}, 500);
|
|
154
|
+
} else {
|
|
155
|
+
ReactDOM.unmountComponentAtNode(chatBubbleTarget);
|
|
156
|
+
}
|
|
157
|
+
};
|
|
158
|
+
// wrap the launcher and chat bubble so we can position them together but also manipulate them independently
|
|
159
|
+
const wrapper = document.createElement("div");
|
|
160
|
+
// for us, the wrapper contains the chat bubble and launcher. for consumers, we'll just call the wrapper the launcher.
|
|
161
|
+
wrapper.classList.add(styles.wrapper, "meetelise-chat", "launcher");
|
|
162
|
+
launcher.parentNode?.appendChild(wrapper);
|
|
163
|
+
wrapper.appendChild(launcher);
|
|
164
|
+
wrapper.appendChild(chatBubbleTarget);
|
|
165
|
+
// TalkJS positions the launcher, but we want to control its position ourselves
|
|
166
|
+
launcher.style.position = "unset";
|
|
167
|
+
launcher.style.top = "unset";
|
|
168
|
+
launcher.style.right = "unset";
|
|
169
|
+
// we initially hide the launcher in CSS so it doesn't visibly jump when we remove the native TalkJS positioning. Unhide it now.
|
|
170
|
+
launcher.classList.add(styles.shouldBeVisible);
|
|
171
|
+
|
|
172
|
+
popup.on("open", () => closeChatBubble());
|
|
173
|
+
const triggerBounce = () => {
|
|
174
|
+
launcher.classList.add(styles.bouncingLauncherButton);
|
|
175
|
+
launcher.addEventListener("animationend", () => {
|
|
176
|
+
launcher.classList.remove(styles.bouncingLauncherButton);
|
|
177
|
+
});
|
|
178
|
+
};
|
|
179
|
+
const bounceInterval = 3;
|
|
180
|
+
ReactDOM.render(
|
|
181
|
+
<ChatBubble
|
|
182
|
+
messages={[
|
|
183
|
+
{
|
|
184
|
+
title: "Ask us a question",
|
|
185
|
+
text: "I can also help you schedule a tour.",
|
|
186
|
+
},
|
|
187
|
+
]}
|
|
188
|
+
triggerBounce={triggerBounce}
|
|
189
|
+
bounceIntervalInSeconds={bounceInterval}
|
|
190
|
+
onClick={() => {
|
|
191
|
+
popup.show();
|
|
192
|
+
closeChatBubble();
|
|
193
|
+
}}
|
|
194
|
+
/>,
|
|
195
|
+
chatBubbleTarget
|
|
196
|
+
);
|
|
197
|
+
setTimeout(() => closeChatBubble(true), bounceInterval * 1000 + 3000);
|
|
198
|
+
// TODO: remove? it seems to be triggered immediately on https://www.simpsonpropertygroup.com/apartments/houston-texas/skyhouse-river-oaks-galleria without scrolling
|
|
199
|
+
// document.addEventListener("scroll", () => closeChatBubble(true));
|
|
200
|
+
}
|
|
201
|
+
|
|
121
202
|
private buildingSlug: string;
|
|
122
203
|
private orgSlug: string;
|
|
123
204
|
private popup: Promise<Talk.Popup>;
|
|
@@ -126,6 +207,8 @@ export default class MEChat {
|
|
|
126
207
|
private theme: Partial<Theme>;
|
|
127
208
|
private chatId: string;
|
|
128
209
|
private analytics: Analytics;
|
|
210
|
+
private launchDarklyClient: LDClient.LDClient;
|
|
211
|
+
private launchDarklyIsReady: Promise<boolean>;
|
|
129
212
|
|
|
130
213
|
private constructor({ organization, building, theme = {} }: Options) {
|
|
131
214
|
this.orgSlug = organization;
|
|
@@ -133,9 +216,11 @@ export default class MEChat {
|
|
|
133
216
|
this.chatId = getChatID(organization, building);
|
|
134
217
|
this.analytics = new Analytics(organization, building, this.chatId);
|
|
135
218
|
this.analytics.ping("load");
|
|
136
|
-
|
|
137
219
|
this.theme = theme;
|
|
138
220
|
this.building = fetchBuildingInfo(organization, building);
|
|
221
|
+
[this.launchDarklyClient, this.launchDarklyIsReady] =
|
|
222
|
+
this.initializeLaunchDarkly();
|
|
223
|
+
|
|
139
224
|
this.popup = Promise.all([this.building, MEChat.session]).then(
|
|
140
225
|
async ([building, session]) => {
|
|
141
226
|
const resolvedTheme = (this.theme = resolveTheme(building, theme));
|
|
@@ -156,23 +241,42 @@ export default class MEChat {
|
|
|
156
241
|
});
|
|
157
242
|
}
|
|
158
243
|
await p.mount({ show: false });
|
|
159
|
-
const
|
|
160
|
-
if (!
|
|
161
|
-
|
|
162
|
-
popupEl.classList.add("pane");
|
|
244
|
+
const talkjsPopupElement = document.querySelector(".__talkjs_popup");
|
|
245
|
+
if (!talkjsPopupElement) throw new Error("Failed to find chat window");
|
|
246
|
+
talkjsPopupElement.classList.add("meetelise-chat", "pane");
|
|
163
247
|
return p;
|
|
164
248
|
}
|
|
165
249
|
);
|
|
166
250
|
|
|
167
|
-
this.launcher = this.popup.then(
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
251
|
+
this.launcher = Promise.all([this.popup, this.launchDarklyIsReady]).then(
|
|
252
|
+
async ([popup]) => {
|
|
253
|
+
const talkjsLauncherElement = document.querySelector<HTMLAnchorElement>(
|
|
254
|
+
"a#__talkjs_launcher"
|
|
255
|
+
);
|
|
256
|
+
if (!talkjsLauncherElement)
|
|
257
|
+
throw new Error("MeetElise Chat: Could not locate launcher.");
|
|
258
|
+
|
|
259
|
+
const webchatBubbleFlag = this.launchDarklyClient.variation(
|
|
260
|
+
"webchat-bubble",
|
|
261
|
+
false
|
|
262
|
+
);
|
|
263
|
+
this.analytics.setFeatureFlags({
|
|
264
|
+
webchatBubble: webchatBubbleFlag,
|
|
265
|
+
});
|
|
266
|
+
this.analytics.ping("receivedFeatureFlags");
|
|
267
|
+
|
|
268
|
+
if (webchatBubbleFlag || process.env.IS_DEMO_APP) {
|
|
269
|
+
this.addChatBubble(popup, talkjsLauncherElement);
|
|
270
|
+
} else {
|
|
271
|
+
talkjsLauncherElement.classList.add(
|
|
272
|
+
"meetelise-chat",
|
|
273
|
+
"launcher",
|
|
274
|
+
styles.shouldBeVisible
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
return talkjsLauncherElement;
|
|
278
|
+
}
|
|
279
|
+
);
|
|
176
280
|
}
|
|
177
281
|
}
|
|
178
282
|
|
package/src/analytics.ts
CHANGED
|
@@ -4,12 +4,24 @@
|
|
|
4
4
|
export default class Analytics {
|
|
5
5
|
private org: string;
|
|
6
6
|
private building: string;
|
|
7
|
+
private featureFlags?: Record<string, boolean>;
|
|
7
8
|
public chatId: string;
|
|
8
9
|
|
|
9
10
|
constructor(org: string, building: string, chatId: string) {
|
|
10
11
|
this.org = org;
|
|
11
12
|
this.building = building;
|
|
12
13
|
this.chatId = chatId;
|
|
14
|
+
this.featureFlags = {};
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Store feature flag value(s) on the Analytics object to be sent with future pings. If `featureFlags` object is already present, the argument is merged into it.
|
|
19
|
+
*/
|
|
20
|
+
setFeatureFlags(featureFlags: Record<string, boolean>): void {
|
|
21
|
+
this.featureFlags = {
|
|
22
|
+
...this.featureFlags,
|
|
23
|
+
...featureFlags,
|
|
24
|
+
};
|
|
13
25
|
}
|
|
14
26
|
|
|
15
27
|
/**
|
|
@@ -28,6 +40,7 @@ export default class Analytics {
|
|
|
28
40
|
org: this.org,
|
|
29
41
|
building: this.building,
|
|
30
42
|
referrer: document.referrer,
|
|
43
|
+
featureFlags: this.featureFlags,
|
|
31
44
|
}),
|
|
32
45
|
});
|
|
33
46
|
}
|
package/src/fetchBuildingInfo.ts
CHANGED
package/src/getAvatarUrl.ts
CHANGED
|
@@ -12,7 +12,8 @@ import { Building } from "./fetchBuildingInfo";
|
|
|
12
12
|
* @returns a URL suitable for use as a CSS background-image or <img> src.
|
|
13
13
|
*/
|
|
14
14
|
export default function getAvatarURL(building: Building): string {
|
|
15
|
-
if (building.
|
|
15
|
+
if (building.avatarType === "image" && building.avatarSrc)
|
|
16
|
+
return building.avatarSrc;
|
|
16
17
|
|
|
17
18
|
const initials = building.avatarInitials;
|
|
18
19
|
const bgColor = building.launchButtonColor;
|