@meetelise/chat 1.6.0 → 1.6.3

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/src/MEChat.tsx CHANGED
@@ -1,16 +1,19 @@
1
+ import * as LDClient from "launchdarkly-js-client-sdk";
2
+ import React from "react";
3
+ import ReactDOM from "react-dom";
1
4
  import Talk from "talkjs";
5
+
2
6
  import fetchBuildingInfo, { Building } from "./fetchBuildingInfo";
3
7
  import { getChatID, createChatID } from "./chatID";
4
8
  import createConversation from "./createConversation";
5
9
  import installTalkJSStyles from "./installTalkJSStyles";
6
10
  import resolveTheme, { Theme } from "./resolveTheme";
7
11
  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 { isMobile } from "./utils";
13
+ import InHouseLauncher from "./InHouseLauncher";
12
14
  import LaunchDarkly from "./launchDarklyManager";
13
15
  import styles from "./MEChat.module.scss";
16
+ import { defaultThemeId, themesById } from "./themes";
14
17
 
15
18
  /**
16
19
  * The interface to MeetElise chat.
@@ -30,7 +33,7 @@ export default class MEChat {
30
33
  id: "anonymous",
31
34
  name: "Me",
32
35
  email: null,
33
- role: "default",
36
+ role: "Default",
34
37
  });
35
38
  return new Talk.Session({
36
39
  appId: "ogKIvCor",
@@ -71,7 +74,8 @@ export default class MEChat {
71
74
  session,
72
75
  building,
73
76
  resolveTheme(building, this.theme),
74
- this.chatId
77
+ this.chatId,
78
+ this.isMobile
75
79
  )
76
80
  );
77
81
  }
@@ -97,7 +101,13 @@ export default class MEChat {
97
101
  ...theme,
98
102
  }));
99
103
  popup.select(
100
- createConversation(session, building, resolvedTheme, this.chatId)
104
+ createConversation(
105
+ session,
106
+ building,
107
+ resolvedTheme,
108
+ this.chatId,
109
+ this.isMobile
110
+ )
101
111
  );
102
112
  installTalkJSStyles(resolvedTheme);
103
113
  return new Promise(requestAnimationFrame);
@@ -120,6 +130,7 @@ export default class MEChat {
120
130
  }
121
131
 
122
132
  /** Show the chat button on the screen if it was previously hidden. */
133
+ // TODO: will this work with the new launcher? it needs to be display flex? will this just change the inline style and leave the stylesheet/style tag alone?
123
134
  show(): void {
124
135
  this.launcher.then((a) => (a.style.display = ""));
125
136
  }
@@ -129,73 +140,57 @@ export default class MEChat {
129
140
  this.launcher.then((a) => (a.style.display = "none"));
130
141
  }
131
142
 
132
- /** Show a speech bubble next to the chat button (launcher). Also adds some animations to the button. */
133
- private addChatBubble(popup: Talk.Popup, launcher: HTMLAnchorElement): void {
134
- const chatBubbleTarget = document.createElement("div");
135
- // set up scroll listener before mounting the chat bubble component so we don't miss any scroll events
136
- const closeChatBubble = (shouldFade = false) => {
137
- if (shouldFade) {
138
- chatBubbleTarget.classList.add(styles.fadeOut);
139
- setTimeout(() => {
140
- ReactDOM.unmountComponentAtNode(chatBubbleTarget);
141
- }, 500);
142
- } else {
143
- ReactDOM.unmountComponentAtNode(chatBubbleTarget);
144
- }
145
- };
146
- // wrap the launcher and chat bubble so we can position them together but also manipulate them independently
147
- const wrapper = document.createElement("div");
148
- // for us, the wrapper contains the chat bubble and launcher. for consumers, we'll just call the wrapper the launcher.
149
- wrapper.classList.add(styles.wrapper, "meetelise-chat", "launcher");
150
- launcher.parentNode?.appendChild(wrapper);
151
- wrapper.appendChild(launcher);
152
- wrapper.appendChild(chatBubbleTarget);
153
- // TalkJS positions the launcher, but we want to control its position ourselves
154
- launcher.style.position = "unset";
155
- launcher.style.top = "unset";
156
- launcher.style.right = "unset";
157
- // we initially hide the launcher in CSS so it doesn't visibly jump when we remove the native TalkJS positioning. Unhide it now.
158
- launcher.classList.add(styles.shouldBeVisible);
159
-
160
- popup.on("open", () => closeChatBubble());
161
- const triggerBounce = () => {
162
- launcher.classList.add(styles.bouncingLauncherButton);
163
- launcher.addEventListener("animationend", () => {
164
- launcher.classList.remove(styles.bouncingLauncherButton);
165
- });
143
+ /** Show a custom launcher designed in-house instead of the default TalkJS launcher. */
144
+ private async mountInHouseLauncher(
145
+ targetElement: HTMLElement,
146
+ firstMount: boolean
147
+ ): Promise<void> {
148
+ const chatTappedHandler = async () => {
149
+ ReactDOM.unmountComponentAtNode(targetElement);
150
+ (await this.popup).show();
166
151
  };
167
- const bounceInterval = 3;
152
+ const building = await this.building;
153
+ let theme = themesById[defaultThemeId];
154
+ if (Object.keys(themesById).includes(building.themeId)) {
155
+ theme = themesById[building.themeId as keyof typeof themesById];
156
+ }
168
157
  ReactDOM.render(
169
- <ChatBubble
170
- messages={[
171
- {
172
- title: "Ask us a question",
173
- text: "I can also help you schedule a tour.",
174
- },
175
- ]}
176
- triggerBounce={triggerBounce}
177
- bounceIntervalInSeconds={bounceInterval}
178
- onClick={() => {
179
- popup.show();
180
- closeChatBubble();
181
- }}
158
+ <InHouseLauncher
159
+ onChatTapped={chatTappedHandler}
160
+ mobile={this.isMobile}
161
+ firstMount={firstMount}
162
+ backgroundColor={theme.chatPaneBackgroundColor}
163
+ textColor={theme.chatHeader.textColor}
182
164
  />,
183
- chatBubbleTarget
165
+ targetElement
184
166
  );
185
- setTimeout(() => closeChatBubble(true), bounceInterval * 1000 + 3000);
186
- // TODO: remove? it seems to be triggered immediately on https://www.simpsonpropertygroup.com/apartments/houston-texas/skyhouse-river-oaks-galleria without scrolling
187
- // document.addEventListener("scroll", () => closeChatBubble(true));
167
+ }
168
+
169
+ private async getInHouseLauncher(): Promise<HTMLElement> {
170
+ const inHouseLauncherTarget = document.createElement("div");
171
+ inHouseLauncherTarget.classList.add(
172
+ styles.inHouseLauncherContainer,
173
+ this.isMobile ? styles.mobile : styles.desktop
174
+ );
175
+ document.body.appendChild(inHouseLauncherTarget);
176
+ this.mountInHouseLauncher(inHouseLauncherTarget, true);
177
+ (await this.popup).on("close", () => {
178
+ this.mountInHouseLauncher(inHouseLauncherTarget, false);
179
+ });
180
+ return inHouseLauncherTarget;
188
181
  }
189
182
 
190
183
  private buildingSlug: string;
191
184
  private orgSlug: string;
192
185
  private popup: Promise<Talk.Popup>;
193
- private launcher: Promise<HTMLAnchorElement>;
186
+ private launcher: Promise<HTMLElement>;
194
187
  private building: Promise<Building>;
195
188
  private theme: Partial<Theme>;
196
189
  private chatId: string;
197
190
  private analytics: Analytics;
198
191
  private launchDarklyClient: LDClient.LDClient;
192
+ private useInHouseLauncher: boolean;
193
+ private isMobile: boolean;
199
194
 
200
195
  private constructor({ organization, building, theme = {} }: Options) {
201
196
  this.orgSlug = organization;
@@ -206,6 +201,8 @@ export default class MEChat {
206
201
  this.analytics.ping("load");
207
202
  this.theme = theme;
208
203
  this.building = fetchBuildingInfo(organization, building);
204
+ this.useInHouseLauncher = true;
205
+ this.isMobile = isMobile();
209
206
 
210
207
  this.popup = Promise.all([
211
208
  this.building,
@@ -215,7 +212,18 @@ export default class MEChat {
215
212
  const resolvedTheme = (this.theme = resolveTheme(building, theme));
216
213
  installTalkJSStyles(resolvedTheme);
217
214
  const p = session.createPopup(
218
- createConversation(session, building, resolvedTheme, this.chatId)
215
+ createConversation(
216
+ session,
217
+ building,
218
+ resolvedTheme,
219
+ this.chatId,
220
+ this.isMobile
221
+ ),
222
+ {
223
+ launcher: this.useInHouseLauncher ? "never" : "always",
224
+ showCloseInHeader: true,
225
+ messageField: { placeholder: "Ask a question..." },
226
+ }
219
227
  );
220
228
  p.on("open", () => {
221
229
  this.analytics.ping("open");
@@ -233,38 +241,35 @@ export default class MEChat {
233
241
  const talkjsPopupElement = document.querySelector(".__talkjs_popup");
234
242
  if (!talkjsPopupElement) throw new Error("Failed to find chat window");
235
243
  talkjsPopupElement.classList.add("meetelise-chat", "pane");
244
+ if (!this.isMobile) {
245
+ talkjsPopupElement.classList.add(styles.desktop);
246
+ }
236
247
  return p;
237
248
  });
238
249
 
239
250
  this.launcher = Promise.all([this.popup, LaunchDarkly.isReady]).then(
240
- async ([popup]) => {
241
- const talkjsLauncherElement = document.querySelector<HTMLAnchorElement>(
242
- "a#__talkjs_launcher"
243
- );
244
- if (!talkjsLauncherElement)
245
- throw new Error("MeetElise Chat: Could not locate launcher.");
246
-
247
- const webchatBubbleFlag = this.launchDarklyClient.variation(
248
- "webchat-bubble",
249
- false
250
- );
251
- this.analytics.setFeatureFlags({
252
- webchatBubble: webchatBubbleFlag,
253
- });
254
- this.analytics.ping("receivedFeatureFlags");
251
+ async () => {
252
+ let launcherElement: HTMLElement;
255
253
 
256
- if (webchatBubbleFlag) {
257
- // TODO: The new icon hasn't been designed for color customization yet, so temporarily disable the background theme color
258
- talkjsLauncherElement.style.backgroundColor = "white";
259
- this.addChatBubble(popup, talkjsLauncherElement);
254
+ if (this.useInHouseLauncher) {
255
+ // TODO: there's a big delay between page load and the launcher getting added, maybe 2s. Maybe put it earlier even if it has to wait for TalkJS to load to be functional?
256
+ launcherElement = await this.getInHouseLauncher();
260
257
  } else {
261
- talkjsLauncherElement.classList.add(
262
- "meetelise-chat",
263
- "launcher",
264
- styles.shouldBeVisible
258
+ const talkjsLauncherElement = document.querySelector<HTMLElement>(
259
+ "a#__talkjs_launcher"
265
260
  );
261
+ if (!talkjsLauncherElement)
262
+ throw new Error("MeetElise Chat: Could not locate launcher.");
263
+ launcherElement = talkjsLauncherElement;
266
264
  }
267
- return talkjsLauncherElement;
265
+
266
+ launcherElement.classList.add(
267
+ "meetelise-chat",
268
+ "launcher",
269
+ styles.shouldBeVisible
270
+ );
271
+
272
+ return launcherElement;
268
273
  }
269
274
  );
270
275
  }
@@ -2,35 +2,58 @@ import Talk from "talkjs";
2
2
  import { Building } from "./fetchBuildingInfo";
3
3
  import getAvatarUrl from "./getAvatarUrl";
4
4
  import { Theme } from "./resolveTheme";
5
- //
5
+ import { defaultThemeId, themesById } from "./themes";
6
+
7
+ const defaultAvatarUrl =
8
+ "https://s3.us-west-2.amazonaws.com/meetelise.com/looping-gradient.gif";
9
+
6
10
  export default function createConversation(
7
11
  session: Talk.Session,
8
12
  building: Building,
9
13
  theme: Theme,
10
- chatID: string
14
+ chatID: string,
15
+ isMobile: boolean
11
16
  ): Talk.ConversationBuilder {
12
17
  const agent = new Talk.User({
13
18
  id: `building_${building.id}`,
14
19
  name: building.userFirstName,
15
20
  email: null,
16
21
  photoUrl: getAvatarUrl(building),
17
- role: "default",
22
+ role: "Default",
18
23
  welcomeMessage: building.welcomeMessage,
19
24
  });
20
25
  const conversation = session.getOrCreateConversation(chatID);
21
26
  conversation.subject = theme.chatTitle;
22
27
  conversation.setParticipant(session.me);
23
28
  conversation.setParticipant(agent);
29
+ // TODO: duplicate identifier theme
30
+ // TODO: typescript abuse
31
+ let themeId = defaultThemeId;
32
+ if (Object.keys(themesById).includes(building.themeId)) {
33
+ themeId = building.themeId as keyof typeof themesById;
34
+ }
35
+ const _theme = themesById[themeId];
24
36
  conversation.custom = {
25
37
  buildingId: building.id.toString(),
26
38
  userId: building.userId.toString(),
27
39
  orgId: building.orgId.toString(),
28
40
  subtitle: theme.chatSubtitle ?? null,
29
- bannerColor: theme.bannerColor,
30
- bannerTextColor: theme.bannerTextColor,
31
- messageColor: theme.messageColor,
32
- messageTextColor: theme.messageTextColor,
33
41
  url: location.href,
42
+ buildingName: building.name,
43
+ isMobile: isMobile.toString(),
44
+ chatHeaderBackgroundColor: _theme.chatHeader.backgroundColor,
45
+ chatHeaderTextColor: _theme.chatHeader.textColor,
46
+ chatPaneBackgroundColor: _theme.chatPaneBackgroundColor,
47
+ userMessageTextColor: _theme.message.user.textColor,
48
+ userMessageBackgroundColor: _theme.message.user.backgroundColor,
49
+ agentMessageTextColor: _theme.message.agent.textColor,
50
+ agentMessageBackgroundColor: _theme.message.agent.backgroundColor,
51
+ avatarUrl:
52
+ building.avatarType === "image" && building.avatarSrc
53
+ ? building.avatarSrc
54
+ : defaultAvatarUrl,
55
+ // uncomment this to test changes to the default avatar if your test building has its own avatar
56
+ // avatarUrl: defaultAvatarUrl,
34
57
  };
35
58
  return conversation;
36
59
  }
@@ -4,20 +4,13 @@
4
4
  export interface Building {
5
5
  id: number;
6
6
 
7
+ themeId: string;
7
8
  avatarInitials: string | null;
8
9
  avatarSrc: string | null;
9
10
  avatarType: "image" | "initials" | null;
10
- backgroundColor: string | null;
11
- bannerColor: string | null;
12
- bannerTextColor: string | null;
13
11
  chatSubtitle: string | null;
14
12
  chatTitle: string | null;
15
- launchButtonColor: string | null;
16
- launchButtonIconColor: string | null;
17
- launchButtonSize: string | null;
18
13
  logoSrc: string | null;
19
- messageColor: string | null;
20
- messageTextColor: string | null;
21
14
  name: string;
22
15
  primaryColor: string | null;
23
16
  userFirstName: string;
@@ -26,6 +19,15 @@ export interface Building {
26
19
  welcomeMessage: string | null;
27
20
  conversationMaintenanceMode: boolean;
28
21
  orgId: number;
22
+ // old: not sure if still present in API response, but we're not using (may have mised a few above)
23
+ backgroundColor: string | null;
24
+ bannerColor: string | null;
25
+ bannerTextColor: string | null;
26
+ launchButtonColor: string | null;
27
+ launchButtonIconColor: string | null;
28
+ launchButtonSize: string | null;
29
+ messageColor: string | null;
30
+ messageTextColor: string | null;
29
31
  }
30
32
 
31
33
  /**
package/src/themes.ts ADDED
@@ -0,0 +1,88 @@
1
+ export const white = "#FFFFFF";
2
+ export const darkGray = "#202020";
3
+ export const lightGray = "#83818E";
4
+
5
+ export const defaultThemeId = "Light" as keyof typeof themesById;
6
+
7
+ export const lightMessage = {
8
+ user: { textColor: white, backgroundColor: lightGray },
9
+ agent: { textColor: darkGray, backgroundColor: white },
10
+ };
11
+ export const darkMessage = {
12
+ user: { textColor: white, backgroundColor: lightGray },
13
+ agent: { textColor: white, backgroundColor: darkGray },
14
+ };
15
+ export const themesById = {
16
+ Light: {
17
+ chatHeader: {
18
+ backgroundColor: white,
19
+ textColor: darkGray,
20
+ },
21
+ chatPaneBackgroundColor: "rgba(255, 255, 255, 0.9)",
22
+ message: darkMessage,
23
+ },
24
+ Dark: {
25
+ chatHeader: {
26
+ backgroundColor: darkGray,
27
+ textColor: white,
28
+ },
29
+ chatPaneBackgroundColor: "rgba(32, 32, 32, 0.9)",
30
+ message: lightMessage,
31
+ },
32
+ Purple: {
33
+ chatHeader: {
34
+ backgroundColor: "#550098",
35
+ textColor: white,
36
+ },
37
+ chatPaneBackgroundColor: "rgba(85, 0, 152, 0.6)",
38
+ message: lightMessage,
39
+ },
40
+ Blue: {
41
+ chatHeader: {
42
+ backgroundColor: "#0814E5",
43
+ textColor: white,
44
+ },
45
+ chatPaneBackgroundColor: "rgba(4, 17, 245, 0.6)",
46
+ message: lightMessage,
47
+ },
48
+ Teal: {
49
+ chatHeader: {
50
+ backgroundColor: "#6EE7ED",
51
+ textColor: darkGray,
52
+ },
53
+ chatPaneBackgroundColor: "rgba(115, 247, 253, 0.8)",
54
+ message: darkMessage,
55
+ },
56
+ Green: {
57
+ chatHeader: {
58
+ backgroundColor: "#147B0E",
59
+ textColor: white,
60
+ },
61
+ chatPaneBackgroundColor: "rgba(13, 141, 5, 0.6)",
62
+ message: lightMessage,
63
+ },
64
+ Yellow: {
65
+ chatHeader: {
66
+ backgroundColor: "#F1E54F",
67
+ textColor: darkGray,
68
+ },
69
+ chatPaneBackgroundColor: "rgba(251, 239, 80, 0.9)",
70
+ message: darkMessage,
71
+ },
72
+ Orange: {
73
+ chatHeader: {
74
+ backgroundColor: "#C06C31",
75
+ textColor: white,
76
+ },
77
+ chatPaneBackgroundColor: "rgba(238, 126, 49, 0.7)",
78
+ message: lightMessage,
79
+ },
80
+ Pink: {
81
+ chatHeader: {
82
+ backgroundColor: "#A24599",
83
+ textColor: white,
84
+ },
85
+ chatPaneBackgroundColor: "rgba(167, 70, 157, 0.8)",
86
+ message: lightMessage,
87
+ },
88
+ };
package/src/utils.ts CHANGED
@@ -22,3 +22,6 @@ export function useInterval(callback: () => void, delay: number | null): void {
22
22
  return () => clearInterval(id);
23
23
  }, [delay]);
24
24
  }
25
+
26
+ export const isMobile = (): boolean =>
27
+ window.matchMedia("(max-width: 767px)").matches;
@@ -1,12 +0,0 @@
1
- import React from "react";
2
- interface ChatBubbleProps {
3
- messages: {
4
- title: string;
5
- text: string;
6
- }[];
7
- triggerBounce: () => void;
8
- bounceIntervalInSeconds: number;
9
- onClick: () => void;
10
- }
11
- declare const ChatBubble: React.FunctionComponent<ChatBubbleProps>;
12
- export default ChatBubble;