@meetelise/chat 1.20.89 → 1.20.91

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/MyPubnub.ts CHANGED
@@ -269,7 +269,7 @@ class MyPubnub {
269
269
  buildingSlug: this.buildingSlug,
270
270
  };
271
271
  };
272
- getChatStorageKey = (): ChatInfo => {
272
+ getChatStorageKey = (createNewIfNotExist = true): ChatInfo => {
273
273
  const eliseaiLocalStorageValue = localStorage.getItem(
274
274
  "com.eliseai.webchat.slug=" + this.buildingSlug
275
275
  );
@@ -300,7 +300,12 @@ class MyPubnub {
300
300
  }
301
301
  }
302
302
 
303
- return this.createChatStorageKey();
303
+ if (createNewIfNotExist) return this.createChatStorageKey();
304
+ return {
305
+ leadId: null,
306
+ timestamp: null,
307
+ buildingSlug: null,
308
+ };
304
309
  };
305
310
  isChatKeyValid = (storageValueDeconstructed: ChatInfo): boolean => {
306
311
  if (
@@ -484,9 +484,16 @@ export class MEChat extends LitElement {
484
484
  initializePubnubVariables = async (): Promise<void> => {
485
485
  await this.setBuildingDerivedInfo();
486
486
  if (!this.building) return;
487
+ if (this.building.conversationMaintenanceMode) {
488
+ // eslint-disable-next-line no-console
489
+ console.warn(
490
+ "MeetElise Chat is in maintenance mode. Chat icon will not appear."
491
+ );
492
+ return;
493
+ }
487
494
 
488
495
  this.myPubnub = new MyPubnub(this.buildingSlug, this.building);
489
- if (this.myPubnub.isChatKeyValid(this.myPubnub.getChatStorageKey())) {
496
+ if (this.myPubnub.isChatKeyValid(this.myPubnub.getChatStorageKey(false))) {
490
497
  await this.myPubnub.initializePubnub();
491
498
  }
492
499
  this.attachOnClickToLauncher();
@@ -4,7 +4,7 @@ export const pubnubChatStyles = css`
4
4
  #pubnub-chat-container {
5
5
  position: fixed;
6
6
 
7
- z-index: 100000;
7
+ z-index: 100001;
8
8
  display: flex;
9
9
  align-items: center;
10
10
 
@@ -116,9 +116,18 @@ export const pubnubChatStyles = css`
116
116
  padding: 6px 12px;
117
117
  line-height: 130%;
118
118
  }
119
+ .displayed-message-image {
120
+ max-width: 100%;
121
+ max-height: 300px;
122
+ width: auto;
123
+ height: auto;
124
+ }
125
+ .redirect-link {
126
+ color: black;
127
+ }
119
128
 
120
129
  #loading-message {
121
- padding: 16px;
130
+ padding: 12px;
122
131
  }
123
132
  .loading-dot {
124
133
  width: 6px;
@@ -169,6 +178,7 @@ export const pubnubChatStyles = css`
169
178
  box-sizing: border-box;
170
179
  gap: 16px;
171
180
  padding: 24px;
181
+ z-index: 100001;
172
182
  }
173
183
  #message-input {
174
184
  height: 40px;
@@ -180,6 +190,7 @@ export const pubnubChatStyles = css`
180
190
  border: none;
181
191
  color: white;
182
192
  background: none;
193
+ z-index: 100001;
183
194
  }
184
195
  #message-input:focus {
185
196
  outline: none;
@@ -191,4 +202,51 @@ export const pubnubChatStyles = css`
191
202
  border: none;
192
203
  cursor: pointer;
193
204
  }
205
+
206
+ .website-preview {
207
+ border: 1px solid rgba(0, 0, 0, 0.2);
208
+ border-radius: 8px;
209
+ overflow: hidden;
210
+ display: flex;
211
+ align-items: center;
212
+ flex-direction: column;
213
+ justify-content: center;
214
+
215
+ margin: 8px;
216
+ }
217
+ .website-preview-image {
218
+ max-width: 100%;
219
+ max-height: 200px;
220
+ width: auto;
221
+ height: auto;
222
+ }
223
+ .website-preview-body {
224
+ padding: 8px 0px 12px;
225
+ }
226
+ .website-preview-title {
227
+ font-size: 12px;
228
+ font-weight: bold;
229
+ margin: 0;
230
+ display: -webkit-box;
231
+ -webkit-box-orient: vertical;
232
+ -webkit-line-clamp: 2;
233
+ overflow: hidden;
234
+ text-overflow: ellipsis;
235
+ height: 24px;
236
+ }
237
+ .website-preview-description {
238
+ font-size: 10px;
239
+ margin: 0;
240
+
241
+ display: -webkit-box;
242
+ -webkit-box-orient: vertical;
243
+ -webkit-line-clamp: 3;
244
+ overflow: hidden;
245
+ text-overflow: ellipsis;
246
+ max-height: 34px;
247
+ }
248
+ .redirect-link-preview {
249
+ color: inherit;
250
+ text-decoration: none;
251
+ }
194
252
  `;
@@ -8,6 +8,7 @@ import { SendMessageIconWhite, XBlackOutlineIcon } from "../svgIcons";
8
8
  import { defaultBrandColor } from "../themes";
9
9
  import { hexToAlmostWhite } from "../utils";
10
10
  import { pubnubChatStyles } from "./pubnub-chat-styles";
11
+ import "./pubnub-message";
11
12
 
12
13
  @customElement("pubnub-chat")
13
14
  export class PubnubChat extends LitElement {
@@ -49,6 +50,17 @@ export class PubnubChat extends LitElement {
49
50
  @state()
50
51
  isLoadingMessages = false;
51
52
 
53
+ @state()
54
+ websitePreviewMapping: {
55
+ [messageId: string]: {
56
+ title: string | null;
57
+ description: string | null;
58
+ image: string | null;
59
+ favicon: string | null;
60
+ isLoading: boolean;
61
+ }[];
62
+ } = {};
63
+
52
64
  sendMessage = async (message: string): Promise<void> => {
53
65
  this.messageInput.value = "";
54
66
  await this.myPubnub?.sendMessage(message);
@@ -63,6 +75,7 @@ export class PubnubChat extends LitElement {
63
75
  );
64
76
  this.onMount();
65
77
  }
78
+
66
79
  scrollToChatBottom = (): void => {
67
80
  this.messageBody.scrollTo({
68
81
  top: this.messageBody.scrollHeight - this.messageBody.clientHeight,
@@ -70,7 +83,7 @@ export class PubnubChat extends LitElement {
70
83
  });
71
84
  };
72
85
 
73
- updated(): void {
86
+ async updated(): Promise<void> {
74
87
  this.scrollToChatBottom();
75
88
  }
76
89
 
@@ -133,11 +146,12 @@ export class PubnubChat extends LitElement {
133
146
  : undefined,
134
147
  })}
135
148
  >
136
- <p class="message-text">
137
- ${message.message.text
138
- .split("\n")
139
- .map((line) => html`${line}<br />`)}
140
- </p>
149
+ <pubnub-message
150
+ .message=${message}
151
+ .myPubnub=${this.myPubnub}
152
+ .onMount=${() => this.scrollToChatBottom()}
153
+ >
154
+ </pubnub-message>
141
155
  </li>
142
156
  `;
143
157
  })}
@@ -0,0 +1,225 @@
1
+ import { html, LitElement, TemplateResult } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import MyPubnub, { ChatMessage } from "../MyPubnub";
4
+ import { pubnubChatStyles } from "./pubnub-chat-styles";
5
+ import { v4 as uuid } from "uuid";
6
+ import axios from "axios";
7
+
8
+ interface WebsitePreview {
9
+ title: string | null;
10
+ description: string | null;
11
+ image: string | null;
12
+ favicon: string | null;
13
+ link: string;
14
+ }
15
+ @customElement("pubnub-message")
16
+ export class PubnubMessage extends LitElement {
17
+ static styles = [pubnubChatStyles];
18
+
19
+ @property({ attribute: true })
20
+ message: ChatMessage | undefined;
21
+
22
+ @property({ attribute: true })
23
+ myPubnub: MyPubnub | undefined;
24
+
25
+ @property({ attribute: true })
26
+ onMount: () => void = () => ({});
27
+
28
+ @state()
29
+ loadingPreviews = true;
30
+
31
+ @state()
32
+ parsedMessage: TemplateResult | undefined;
33
+
34
+ @state()
35
+ websitePreviewInfo: WebsitePreview[] = [];
36
+
37
+ firstUpdated(): void {
38
+ if (!this.message) return;
39
+ const { hyperlinks, markedText } = this.mapAndMarkHyperLinks(
40
+ this.message.message.text
41
+ );
42
+ const urlRegex = /(https?:\/\/[^\s]+)/g; // Regular expression to match URLs
43
+ const imagesToDisplay: {
44
+ redirectTo: string;
45
+ source: string;
46
+ }[] = [];
47
+
48
+ const loadingWebsitePreviews: Promise<WebsitePreview>[] = [];
49
+ this.parsedMessage = html`${markedText.split("\n").map((line) => {
50
+ return html`${line.split(" ").map((word) => {
51
+ if (hyperlinks.find((obj) => obj.id === word)) {
52
+ // EliseAI made hyperlink
53
+ const link = hyperlinks.find((obj) => obj.id === word);
54
+ if (!link) return;
55
+ imagesToDisplay.push({
56
+ redirectTo: link?.link,
57
+ source: link?.link,
58
+ });
59
+ return html`<a
60
+ class="redirect-link"
61
+ href="${link?.link}"
62
+ target="_blank"
63
+ rel="noopener noreferrer"
64
+ >${link?.hyperlink}
65
+ </a>`;
66
+ }
67
+ if (urlRegex.test(word)) {
68
+ // general url
69
+ loadingWebsitePreviews.push(this.getLinkData(word));
70
+ return html`<a
71
+ class="redirect-link"
72
+ href="${word}"
73
+ target="_blank"
74
+ rel="noopener noreferrer"
75
+ >${word}
76
+ </a>`;
77
+ } else {
78
+ return html`${word} `;
79
+ }
80
+ })}<br />`;
81
+ })}
82
+ ${imagesToDisplay.map((image) => {
83
+ return html`<a
84
+ href="${image.redirectTo}"
85
+ target="_blank"
86
+ rel="noopener noreferrer"
87
+ ><img
88
+ class="displayed-message-image"
89
+ src=${image.source}
90
+ alt="displayed message image"
91
+ /></a>`;
92
+ })}`;
93
+ Promise.all(loadingWebsitePreviews).then((results) => {
94
+ this.websitePreviewInfo = results;
95
+ });
96
+ }
97
+
98
+ private mapAndMarkHyperLinks(text: string): {
99
+ hyperlinks: {
100
+ link: string;
101
+ hyperlink: string;
102
+ mark: string;
103
+ id: string;
104
+ }[];
105
+ markedText: string;
106
+ } {
107
+ const matches = Array.from(text.matchAll(/<https?:\/\/[^\s]+\|[^>]+>/g));
108
+ const listHyperlinks: {
109
+ link: string;
110
+ hyperlink: string;
111
+ mark: string;
112
+ id: string;
113
+ }[] = [];
114
+ matches.forEach((match) => {
115
+ try {
116
+ const link = match[0].split("|")[0].replace("<", "");
117
+ const hyperlink = match[0].split("|")[1].replace(">", "");
118
+ if (link && hyperlink) {
119
+ const uniqueKeyMark = uuid();
120
+ listHyperlinks.push({
121
+ link,
122
+ hyperlink,
123
+ mark: match[0],
124
+ id: uniqueKeyMark,
125
+ });
126
+ text = text.replace(match[0], uniqueKeyMark);
127
+ }
128
+ } catch (_) {
129
+ // pass
130
+ }
131
+ });
132
+
133
+ return { hyperlinks: listHyperlinks, markedText: text };
134
+ }
135
+
136
+ private async getLinkData(url: string): Promise<WebsitePreview> {
137
+ try {
138
+ // Getting link data is unlikely to work unless the site has CORS enabled and/or url is on the same domain
139
+ // For dev testing, can visit https://cors-anywhere.herokuapp.com/ and prefix url with it to bypass CORS
140
+ const response = await axios.get(url);
141
+ const html = response.data;
142
+ const parser = new DOMParser(); // creates a temp dom to parse HTML
143
+ const doc = parser.parseFromString(html, "text/html");
144
+ if (!doc)
145
+ return {
146
+ title: null,
147
+ description: null,
148
+ image: null,
149
+ favicon: null,
150
+ link: url,
151
+ };
152
+
153
+ return {
154
+ title: doc.querySelector("title")?.textContent ?? null,
155
+ description:
156
+ doc
157
+ .querySelector('meta[name="description"]')
158
+ ?.getAttribute("content") || null,
159
+ image:
160
+ doc
161
+ .querySelector('meta[property="og:image"]')
162
+ ?.getAttribute("content") || null,
163
+ favicon:
164
+ doc
165
+ .querySelector('link[rel="shortcut icon"]')
166
+ ?.getAttribute("href") ||
167
+ doc.querySelector('link[rel="icon"]')?.getAttribute("href") ||
168
+ null,
169
+ link: url,
170
+ };
171
+ } catch (error) {
172
+ // eslint-disable-next-line no-console
173
+ console.error("Error fetching website details:", error);
174
+ return {
175
+ title: null,
176
+ description: null,
177
+ image: null,
178
+ favicon: null,
179
+ link: url,
180
+ };
181
+ }
182
+ }
183
+
184
+ updated(): void {
185
+ this.onMount();
186
+ }
187
+
188
+ render(): TemplateResult {
189
+ if (!this.message) return html``;
190
+ return html`<div class="message-inner-body">
191
+ <p class="message-text">${this.parsedMessage}</p>
192
+ ${this.websitePreviewInfo.length > 0 &&
193
+ this.websitePreviewInfo.some(
194
+ (preview) => preview.title && (preview.image || preview.favicon)
195
+ )
196
+ ? html`<br />`
197
+ : ""}
198
+ ${this.websitePreviewInfo.map((preview) => {
199
+ if (!preview.title || !(preview.image || preview.favicon)) return;
200
+ return html` <a
201
+ class="redirect-link-preview"
202
+ href=${preview.link}
203
+ target="_blank"
204
+ rel="noopener noreferrer"
205
+ >
206
+ <div class="website-preview">
207
+ <img
208
+ src=${preview.image ?? preview.favicon}
209
+ class="website-preview-image"
210
+ alt="website preview image"
211
+ width="100%"
212
+ height="100%"
213
+ />
214
+ <div class="website-preview-body">
215
+ <p class="message-text website-preview-title">${preview.title}</p>
216
+ <p class="message-text website-preview-description">
217
+ ${preview.description}
218
+ </p>
219
+ </div>
220
+ </div>
221
+ </a></div> `;
222
+ })}
223
+ </div>`;
224
+ }
225
+ }