@meetelise/chat 1.21.0 → 1.21.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.
Files changed (76) hide show
  1. package/.github/pull_request_template.md +61 -0
  2. package/.idea/codeStyles/Project.xml +57 -0
  3. package/.idea/codeStyles/codeStyleConfig.xml +5 -0
  4. package/.idea/inspectionProfiles/Project_Default.xml +6 -0
  5. package/.idea/vcs.xml +6 -0
  6. package/.idea/workspace.xml +67 -0
  7. package/README.md +29 -14
  8. package/declarations.d.ts +12 -0
  9. package/package.json +5 -1
  10. package/public/demo/index.html +62 -4
  11. package/public/demo/secret.html +63 -0
  12. package/public/dist/index.js +3184 -1105
  13. package/public/dist/index.js.LICENSE.txt +19 -9
  14. package/public/index.html +6 -4
  15. package/src/MEChat.ts +207 -52
  16. package/src/MyPubnub.ts +657 -0
  17. package/src/WebComponent/LeadSourceClient.ts +300 -0
  18. package/src/WebComponent/Scheduler/date-picker.ts +1 -1
  19. package/src/WebComponent/Scheduler/time-picker.ts +86 -76
  20. package/src/WebComponent/Scheduler/tour-scheduler.ts +694 -764
  21. package/src/WebComponent/Scheduler/tour-type-option.ts +17 -3
  22. package/src/WebComponent/Scheduler/tourSchedulerStyles.ts +418 -0
  23. package/src/WebComponent/actions/InputStyles.ts +32 -10
  24. package/src/WebComponent/actions/action-confirm-button.ts +16 -11
  25. package/src/WebComponent/actions/call-us-window.ts +341 -58
  26. package/src/WebComponent/actions/details-window.ts +30 -16
  27. package/src/WebComponent/actions/email-us-window.ts +89 -58
  28. package/src/WebComponent/actions/formatPhoneNumber.ts +15 -1
  29. package/src/WebComponent/actions/minimize-expand-button.ts +92 -0
  30. package/src/WebComponent/health-chat.ts +267 -0
  31. package/src/WebComponent/healthcare/healthcare-launcher-styles.ts +34 -0
  32. package/src/WebComponent/healthcare/healthcare-launcher.ts +100 -0
  33. package/src/WebComponent/healthchat-styles.ts +119 -0
  34. package/src/WebComponent/index.ts +1 -1
  35. package/src/WebComponent/launcher/Launcher.ts +919 -0
  36. package/src/WebComponent/{launcherStyles.ts → launcher/launcherStyles.ts} +172 -29
  37. package/src/WebComponent/launcher/mobile-launcher.ts +127 -0
  38. package/src/WebComponent/launcher/typeEmojiStyles.ts +161 -0
  39. package/src/WebComponent/launcher/typeMiniStyles.ts +60 -0
  40. package/src/WebComponent/launcher/typeMobileStyles.ts +50 -0
  41. package/src/WebComponent/leasing-chat-styles.ts +114 -0
  42. package/src/WebComponent/me-chat.ts +964 -351
  43. package/src/WebComponent/me-select.ts +48 -21
  44. package/src/WebComponent/mini-loader.ts +28 -0
  45. package/src/WebComponent/pubnub-chat-styles.ts +192 -0
  46. package/src/WebComponent/pubnub-chat.ts +707 -0
  47. package/src/WebComponent/pubnub-media.ts +208 -0
  48. package/src/WebComponent/pubnub-message-styles.ts +54 -0
  49. package/src/WebComponent/pubnub-message.ts +421 -0
  50. package/src/analytics.ts +114 -14
  51. package/src/assetUrls.ts +2 -0
  52. package/src/disclaimers.ts +56 -0
  53. package/src/fetchBuildingABTestType.ts +4 -0
  54. package/src/fetchBuildingInfo.ts +25 -17
  55. package/src/fetchFeatureFlag.ts +147 -0
  56. package/src/fetchLeadSources.ts +67 -1
  57. package/src/fetchPhoneNumberFromSource.ts +31 -0
  58. package/src/fetchWebchatPreferences.ts +55 -0
  59. package/src/getAvailabilities.ts +7 -3
  60. package/src/getBuildingPhoneNumber.ts +26 -0
  61. package/src/getShouldAllowScheduling.ts +16 -0
  62. package/src/getTimezoneString.ts +39 -0
  63. package/src/gtm.ts +17 -0
  64. package/src/handleChatId.ts +101 -0
  65. package/src/insertDNIIntoWebsite.ts +136 -0
  66. package/src/insertLeadSourceIntoSchedulerLinks.ts +50 -0
  67. package/src/postLeadSources.ts +39 -35
  68. package/src/svgIcons.ts +62 -53
  69. package/src/themes.ts +47 -121
  70. package/src/utils.ts +88 -1
  71. package/src/WebComponent/Launcher.ts +0 -559
  72. package/src/WebComponent/actions/text-us-window.ts +0 -279
  73. package/src/chatID.ts +0 -64
  74. package/src/createConversation.ts +0 -57
  75. package/src/fetchCurrentParsedLeadSource.ts +0 -24
  76. package/src/getRegisteredPhoneNumbers.ts +0 -56
@@ -0,0 +1,208 @@
1
+ import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
2
+ import { customElement, property, state } from "lit/decorators.js";
3
+ import MyPubnub, { SimpleMediaChatMessage } from "../MyPubnub";
4
+ import { classMap } from "lit/directives/class-map.js";
5
+ import { InputStyles } from "./actions/InputStyles";
6
+ import { pubnubChatStyles } from "./pubnub-chat-styles";
7
+
8
+ @customElement("pubnub-media")
9
+ export class PubnubMedia extends LitElement {
10
+ static styles = [
11
+ InputStyles,
12
+ pubnubChatStyles,
13
+ css`
14
+ .carousel {
15
+ position: relative;
16
+ max-width: 400px;
17
+ height: max-content;
18
+ min-height: 140px;
19
+ margin: auto;
20
+ overflow: hidden;
21
+ display: flex;
22
+ align-items: center;
23
+ justify-content: flex-start;
24
+ scroll-snap-type: x mandatory;
25
+ }
26
+ .carousel .image-container {
27
+ display: flex;
28
+ will-change: transform;
29
+ transition: transform 0.5s ease-in-out;
30
+ align-items: flex-start;
31
+ width: 100%;
32
+ }
33
+ .carousel img {
34
+ width: 100%;
35
+ max-width: none;
36
+ flex-shrink: 0;
37
+ opacity: 0.5;
38
+ transition: opacity 0.5s ease;
39
+ height: 100%;
40
+ object-fit: contain;
41
+ display: block;
42
+ }
43
+ .image-item {
44
+ width: 100%;
45
+ max-width: none;
46
+ flex-shrink: 0;
47
+ display: flex;
48
+ flex-direction: column;
49
+ gap: 10px;
50
+ }
51
+ .image-link {
52
+ width: 100%;
53
+ max-width: none;
54
+ flex-shrink: 0;
55
+ }
56
+ .carousel img.active {
57
+ opacity: 1;
58
+ }
59
+ .button {
60
+ position: absolute;
61
+ top: 50%;
62
+ transform: translateY(-50%);
63
+ background-color: rgba(0, 0, 0, 0.1);
64
+ color: white;
65
+ border: none;
66
+ border-radius: 2px;
67
+ cursor: pointer;
68
+ padding: 10px;
69
+ z-index: 100;
70
+ height: 64px;
71
+ width: 32px;
72
+ transition: background-color 0.3s ease;
73
+ }
74
+ .button:hover {
75
+ background-color: rgba(0, 0, 0, 0.5);
76
+ }
77
+ .prev {
78
+ left: 2px;
79
+ }
80
+ .next {
81
+ right: 2px;
82
+ }
83
+ .image-info {
84
+ display: flex;
85
+ flex-direction: column;
86
+ gap: 5px;
87
+ }
88
+ .image-info p {
89
+ margin: 0;
90
+ padding: 0;
91
+ white-space: wrap;
92
+ overflow: hidden;
93
+ word-wrap: break-word;
94
+ }
95
+ .image-info .img-header {
96
+ color: #333;
97
+ font-size: 14px;
98
+ }
99
+ .image-info .img-desc {
100
+ color: #666;
101
+ font-size: 12px;
102
+ max-height: 200px;
103
+ overflow-y: auto;
104
+ }
105
+ `,
106
+ ];
107
+
108
+ @property({
109
+ attribute: true,
110
+ })
111
+ message: SimpleMediaChatMessage | undefined;
112
+
113
+ @property({ attribute: true })
114
+ myPubnub: MyPubnub | undefined;
115
+
116
+ @property({ attribute: true })
117
+ onMount: () => void = () => ({});
118
+
119
+ updated(changedProperties: PropertyValues<this>): void {
120
+ this.onMount();
121
+ if (!changedProperties.has("message")) return;
122
+ }
123
+
124
+ @state()
125
+ showMediaAsCarousel = true;
126
+
127
+ @state()
128
+ activeIndex = 0;
129
+
130
+ private prevImage(): void {
131
+ if (!this.message) return;
132
+ if (this.activeIndex === 0) {
133
+ this.activeIndex = this.message.media.length - 1;
134
+ } else {
135
+ this.activeIndex -= 1;
136
+ }
137
+ }
138
+
139
+ private nextImage(): void {
140
+ if (!this.message) return;
141
+ if (this.activeIndex === this.message.media.length - 1) {
142
+ this.activeIndex = 0;
143
+ } else {
144
+ this.activeIndex += 1;
145
+ }
146
+ }
147
+
148
+ render(): TemplateResult {
149
+ if (!this.message) return html``;
150
+
151
+ if (this.showMediaAsCarousel) {
152
+ return html`<div class="message-inner-body">
153
+ <div class="carousel">
154
+ <div
155
+ class="image-container"
156
+ style="transform: translateX(-${this.activeIndex * 100}%);"
157
+ >
158
+ ${this.message.media.map(
159
+ (image, index) => html`
160
+ <div class="image-item">
161
+ <a
162
+ class="image-link"
163
+ href="${image.url}"
164
+ target="_blank"
165
+ rel="noopener noreferrer"
166
+ >
167
+ <img
168
+ class=${classMap({ active: index === this.activeIndex })}
169
+ src=${image.url}
170
+ alt=""
171
+ />
172
+ </a>
173
+ <div class="image-info">
174
+ ${image.title
175
+ ? html`<p class="img-header">${image.title}</p>`
176
+ : ""}
177
+ ${image.description
178
+ ? html`<p class="img-desc">${image.description}</p>`
179
+ : ""}
180
+ </div>
181
+ </div>
182
+ `
183
+ )}
184
+ </div>
185
+ ${this.message.media.length > 1
186
+ ? html` <button class="button prev" @click=${this.prevImage}>
187
+ &#10094;
188
+ </button>
189
+ <button class="button next" @click=${this.nextImage}>
190
+ &#10095;
191
+ </button>`
192
+ : ""}
193
+ </div>
194
+ </div>`;
195
+ } else {
196
+ return html`<div class="message-inner-body">
197
+ ${this.message.media.map((image) => {
198
+ return html`<a
199
+ href="${image.url}"
200
+ target="_blank"
201
+ rel="noopener noreferrer"
202
+ ><img class=${"displayed-message-image"} src=${image.url} alt=""
203
+ /></a>`;
204
+ })}
205
+ </div>`;
206
+ }
207
+ }
208
+ }
@@ -0,0 +1,54 @@
1
+ import { css } from "lit";
2
+
3
+ export const pubnubMessageStyles = css`
4
+ .website-preview {
5
+ border: 1px solid rgba(0, 0, 0, 0.2);
6
+ border-radius: 8px;
7
+ overflow: hidden;
8
+ display: flex;
9
+ align-items: center;
10
+ flex-direction: column;
11
+ justify-content: center;
12
+
13
+ margin: 0px 6px 6px;
14
+ }
15
+ .website-preview-image {
16
+ max-width: 100%;
17
+ max-height: 200px;
18
+ width: auto;
19
+ height: auto;
20
+ }
21
+ .website-preview-body {
22
+ padding: 8px 0px 12px;
23
+ }
24
+ .website-preview-title {
25
+ font-size: 12px;
26
+ font-weight: bold;
27
+ margin: 0;
28
+ display: -webkit-box;
29
+ -webkit-box-orient: vertical;
30
+ -webkit-line-clamp: 2;
31
+ overflow: hidden;
32
+ text-overflow: ellipsis;
33
+ max-height: 24px;
34
+ }
35
+ .website-preview-description {
36
+ font-size: 10px;
37
+ margin: 0;
38
+
39
+ display: -webkit-box;
40
+ -webkit-box-orient: vertical;
41
+ -webkit-line-clamp: 3;
42
+ overflow: hidden;
43
+ text-overflow: ellipsis;
44
+ max-height: 34px;
45
+ }
46
+ .redirect-link-preview {
47
+ color: inherit;
48
+ text-decoration: none;
49
+ }
50
+
51
+ .hidden {
52
+ display: none !important;
53
+ }
54
+ `;
@@ -0,0 +1,421 @@
1
+ import DOMPurify from "dompurify";
2
+ import { css, html, LitElement, PropertyValues, TemplateResult } from "lit";
3
+ import { customElement, property, state } from "lit/decorators.js";
4
+ import { unsafeHTML } from "lit/directives/unsafe-html.js";
5
+ import MyPubnub, { SimpleTextChatMessage } from "../MyPubnub";
6
+ import { v4 as uuid } from "uuid";
7
+ import axios from "axios";
8
+ import { classMap } from "lit/directives/class-map.js";
9
+ import { InputStyles } from "./actions/InputStyles";
10
+ import { isMobile } from "../utils";
11
+ import { LogType, sendLoggingEvent } from "../analytics";
12
+ import { pubnubChatStyles } from "./pubnub-chat-styles";
13
+ import "./mini-loader";
14
+
15
+ interface WebsitePreview {
16
+ title: string | null;
17
+ description: string | null;
18
+ image: string | null;
19
+ favicon: string | null;
20
+ link: string;
21
+ }
22
+
23
+ interface ImageToDisplay {
24
+ redirectTo: string;
25
+ source: string;
26
+ }
27
+ @customElement("pubnub-message")
28
+ export class PubnubMessage extends LitElement {
29
+ static styles = [
30
+ InputStyles,
31
+ pubnubChatStyles,
32
+ css`
33
+ .message-inner-body {
34
+ position: relative;
35
+ }
36
+ .message-loader {
37
+ position: absolute;
38
+ bottom: -2px;
39
+ right: -2px;
40
+ }
41
+ `,
42
+ ];
43
+
44
+ @property({ attribute: true })
45
+ buildingSlug: string | undefined;
46
+
47
+ @property({
48
+ attribute: true,
49
+ })
50
+ message: SimpleTextChatMessage | undefined;
51
+
52
+ @property({ attribute: true })
53
+ myPubnub: MyPubnub | undefined;
54
+
55
+ @property({ attribute: true })
56
+ onMount: () => void = () => ({});
57
+
58
+ @state()
59
+ loadingPreviews = true;
60
+
61
+ @state()
62
+ parsedMessage: TemplateResult | undefined;
63
+
64
+ @state()
65
+ websitePreviewInfo: WebsitePreview[] = [];
66
+
67
+ @state()
68
+ imagesToDisplay: ImageToDisplay[] = [];
69
+
70
+ @state()
71
+ imageUrlError: string[] = [];
72
+
73
+ @property({ type: Boolean })
74
+ isLeadMessage = false;
75
+
76
+ isMessageStillStreamingChunks(): boolean {
77
+ if (!this.message) return false;
78
+ const messageChunkMarkedAsDone = this.message.chunks.find(
79
+ (chunk) => chunk.isDone
80
+ );
81
+ if (!messageChunkMarkedAsDone) return true;
82
+
83
+ // We check to see if ALL the chunks have been received
84
+ return this.message.chunks.length === messageChunkMarkedAsDone.order;
85
+ }
86
+
87
+ updated(changedProperties: PropertyValues<this>): void {
88
+ this.onMount();
89
+ if (!changedProperties.has("message")) return;
90
+ if (!this.message) return;
91
+
92
+ try {
93
+ if (
94
+ !this.isMessageStillStreamingChunks() &&
95
+ !this.isLeadMessage &&
96
+ this.invalidMarkdownText(this.message.message)
97
+ ) {
98
+ sendLoggingEvent({
99
+ logType: LogType.error,
100
+ logTitle: "INVALID_MARKDOWN",
101
+ logData: {
102
+ message: this.message.message,
103
+ chatId: this.myPubnub?.channel,
104
+ },
105
+ buildingSlug: this.buildingSlug,
106
+ });
107
+ }
108
+ } catch (error) {
109
+ // eslint-disable-next-line no-console
110
+ console.error("Error parsing markdown:", error);
111
+ }
112
+
113
+ const { hyperlinks, markedText } = this.mapAndMarkHyperLinks(
114
+ this.message.message
115
+ );
116
+ const urlRegex = /(https?:\/\/[^\s]+)/g; // Regular expression to match URLs
117
+ const loadingWebsitePreviews: Promise<WebsitePreview>[] = [];
118
+ const imagesToDisplay: ImageToDisplay[] = [];
119
+ this.parsedMessage = html`${markedText
120
+ .trim()
121
+ .split("\n")
122
+ .map((line) => {
123
+ const sanitizedLine = this.sanitizeMarkdownText(line);
124
+ const allWords = this.preserveHTMLTags(sanitizedLine);
125
+ if (!allWords) return html``;
126
+ return html`${allWords.map((word) => {
127
+ if (hyperlinks.find((obj) => obj.id === word)) {
128
+ // EliseAI made hyperlink
129
+ const link = hyperlinks.find((obj) => obj.id === word);
130
+ if (!link) return;
131
+ imagesToDisplay.push({
132
+ redirectTo: link?.link,
133
+ source: link?.link,
134
+ });
135
+ return html`<a
136
+ class="redirect-link"
137
+ href="${link?.link}"
138
+ target="_blank"
139
+ rel="noopener noreferrer"
140
+ >${link?.hyperlink}
141
+ </a>`;
142
+ }
143
+ if (urlRegex.test(word)) {
144
+ // general url
145
+ const link = this.removeTrailingPunctuation(word);
146
+ loadingWebsitePreviews.push(this.getLinkData(link));
147
+ return html`<a
148
+ class="redirect-link"
149
+ href="${link}"
150
+ target="_blank"
151
+ rel="noopener noreferrer"
152
+ >${link}
153
+ </a>`;
154
+ } else {
155
+ return html`${unsafeHTML(DOMPurify.sanitize(word))} `;
156
+ }
157
+ })}<br />`;
158
+ })}`;
159
+ this.imagesToDisplay = imagesToDisplay;
160
+ Promise.all(loadingWebsitePreviews).then((results) => {
161
+ this.websitePreviewInfo = results;
162
+ });
163
+ }
164
+
165
+ private invalidMarkdownText(text: string): boolean {
166
+ try {
167
+ const markdownErrors = [
168
+ /\*\*[^*\n]*\*$/, // Unclosed bold (excluding newline)
169
+ /__[^_\n]*_$/, // Unclosed italic (excluding newline)
170
+ /`[^`\n]*`$/, // Unclosed code (excluding newline)
171
+ /\[[^\]\n]*\]$/, // Unclosed link text (excluding newline)
172
+ /\[[^\]]*\]\([^)\n]*\)$/, // Unclosed link URL (excluding newline)
173
+ /^(#+)\s*$/, // Header without text
174
+ /(^|[^!])\[[^\]]*\]\(([^)]+)\s*\)$/, // Improperly formatted link
175
+ /(?:^|\s)[*_]{1,2}(?:\s|$)/, // Improper use of bold or italic
176
+ ];
177
+
178
+ return markdownErrors.some((regex) => regex.test(text));
179
+ } catch (error) {
180
+ return false;
181
+ }
182
+ }
183
+ private preserveHTMLTags(text: string): string[] {
184
+ const tagRegex = /(<\/?[a-z][^>]*>[^<]*<\/?[a-z]*>|[^\s]+[.,?!]?|\S)/gi;
185
+ const splitTags = text.match(tagRegex);
186
+
187
+ return splitTags?.filter((str) => str.trim() !== "") ?? [];
188
+ }
189
+
190
+ private sanitizeMarkdownText(text: string): string {
191
+ const toHTML = text
192
+ .replace(/^### (.*$)/gim, "<h3>$1</h3>")
193
+ .replace(/^## (.*$)/gim, "<h2>$1</h2>")
194
+ .replace(/^# (.*$)/gim, "<h1>$1</h1>")
195
+ .replace(/\*\*(.*?)\*\*/gim, "<b>$1</b>")
196
+ .replace(/\*(.*?)\*/gim, "<i>$1</i>");
197
+
198
+ return toHTML.trim();
199
+ }
200
+
201
+ private removeTrailingPunctuation(text: string): string {
202
+ const punctuation = [".", ",", "!", "?", ":", ";"];
203
+ if (punctuation.includes(text[text.length - 1])) {
204
+ return this.removeTrailingPunctuation(text.slice(0, -1));
205
+ }
206
+ return text;
207
+ }
208
+ private mapAndMarkHyperLinks(text: string): {
209
+ hyperlinks: {
210
+ link: string;
211
+ hyperlink: string;
212
+ mark: string;
213
+ id: string;
214
+ }[];
215
+ markedText: string;
216
+ } {
217
+ // Updated regex to match all formats (EliseAI, embedded links, and markdown)
218
+ const regex =
219
+ /<https?:\/\/[^|]+\|[^>]+>|<a href="[^"]+"(?: target="[^"]*")?>[^<]+<\/a>|\[.*?\]\([^)]*\.[^)]*\)/g;
220
+
221
+ const matches = Array.from(text.matchAll(regex));
222
+ const listHyperlinks: {
223
+ link: string;
224
+ hyperlink: string;
225
+ mark: string;
226
+ id: string;
227
+ }[] = [];
228
+
229
+ const matchesParsed = matches.map((match) => {
230
+ const matchedLink = match[0];
231
+ if (match.index === undefined) return;
232
+ const charBefore = text[match.index - 1];
233
+ const charAfter = text[match.index + matchedLink.length];
234
+
235
+ // Handle for Elise Format
236
+ if (matchedLink.startsWith("<https") || matchedLink.startsWith("<http")) {
237
+ return {
238
+ matchedLink,
239
+ link: matchedLink.split("|")[0].replace("<", ""),
240
+ hyperlink: matchedLink.split("|")[1].replace(">", ""),
241
+ addSpaceBefore: !!(charBefore && charBefore !== " "),
242
+ addSpaceAfter: !!(charAfter && charAfter !== " "),
243
+ };
244
+ } else if (matchedLink.startsWith("<a href=")) {
245
+ // Handle for embedded links (<a>...</a>)
246
+ const linkMatch = matchedLink.match(/href="([^"]+)"/);
247
+ const hyperlinkMatch = matchedLink.match(/>[^<]+</);
248
+ return {
249
+ matchedLink,
250
+ link: linkMatch ? linkMatch[1] : "",
251
+ hyperlink: hyperlinkMatch ? hyperlinkMatch[0].slice(1, -1) : "",
252
+ addSpaceBefore: !!(charBefore && charBefore !== " "),
253
+ addSpaceAfter: !!(charAfter && charAfter !== " "),
254
+ };
255
+ } else {
256
+ // Handle for markdown links
257
+ const [hyperlinkMatch, linkMatch] = matchedLink.split("](");
258
+ return {
259
+ matchedLink,
260
+ link: linkMatch ? linkMatch.slice(0, -1) : "",
261
+ hyperlink: hyperlinkMatch ? hyperlinkMatch.slice(1) : "",
262
+ addSpaceBefore: !!(charBefore && charBefore !== " "),
263
+ addSpaceAfter: !!(charAfter && charAfter !== " "),
264
+ };
265
+ }
266
+ });
267
+
268
+ matchesParsed.forEach((match) => {
269
+ if (!match) return;
270
+
271
+ if (match.link && match.hyperlink) {
272
+ const uniqueKeyMark = uuid().replace(/-/g, "");
273
+ listHyperlinks.push({
274
+ link: this.removeTrailingPunctuation(match.link),
275
+ hyperlink: this.removeTrailingPunctuation(match.hyperlink),
276
+ mark: match.matchedLink,
277
+ id: uniqueKeyMark,
278
+ });
279
+
280
+ text = text.replace(
281
+ match.matchedLink,
282
+ `${match.addSpaceBefore ? " " : ""}${uniqueKeyMark}${
283
+ match.addSpaceAfter ? " " : ""
284
+ }`
285
+ );
286
+ }
287
+ });
288
+
289
+ return { hyperlinks: listHyperlinks, markedText: text };
290
+ }
291
+
292
+ private async getLinkData(url: string): Promise<WebsitePreview> {
293
+ try {
294
+ // Getting link data is unlikely to work unless the site has CORS enabled and/or url is on the same domain
295
+ // For dev testing, can visit https://cors-anywhere.herokuapp.com/ and prefix url with it to bypass CORS
296
+ const response = await axios.get(url);
297
+ const html = response.data;
298
+ const parser = new DOMParser(); // creates a temp dom to parse HTML
299
+ const doc = parser.parseFromString(html, "text/html");
300
+ if (!doc)
301
+ return {
302
+ title: null,
303
+ description: null,
304
+ image: null,
305
+ favicon: null,
306
+ link: url,
307
+ };
308
+
309
+ return {
310
+ title: doc.querySelector("title")?.textContent ?? null,
311
+ description:
312
+ doc
313
+ .querySelector('meta[name="description"]')
314
+ ?.getAttribute("content") || null,
315
+ image:
316
+ doc
317
+ .querySelector('meta[property="og:image"]')
318
+ ?.getAttribute("content") || null,
319
+ favicon: null, // https://meetelise.slack.com/archives/C03HAFYV2NN/p1707339577924899?thread_ts=1707317394.998999&cid=C03HAFYV2NN
320
+ link: url,
321
+ };
322
+ } catch (error) {
323
+ // eslint-disable-next-line no-console
324
+ console.error("Error fetching website details:", error);
325
+ return {
326
+ title: null,
327
+ description: null,
328
+ image: null,
329
+ favicon: null,
330
+ link: url,
331
+ };
332
+ }
333
+ }
334
+
335
+ render(): TemplateResult {
336
+ if (!this.message) return html``;
337
+ return html`<div class="message-inner-body">
338
+ ${this.isMessageStillStreamingChunks()
339
+ ? html`<div class="message-loader"><mini-loader></mini-loader></div>`
340
+ : ""}
341
+ <p
342
+ class=${classMap({
343
+ ["message-text"]: true,
344
+ ["webchat-font__desktop"]: !isMobile(),
345
+ ["webchat-font__mobile"]: isMobile(),
346
+ })}
347
+ >
348
+ ${this.parsedMessage}
349
+ </p>
350
+ ${this.websitePreviewInfo.length > 0 &&
351
+ this.websitePreviewInfo.some(
352
+ (preview) => preview.title && (preview.image || preview.favicon)
353
+ )
354
+ ? html`<br />`
355
+ : ""}
356
+ ${this.imagesToDisplay.map((image) => {
357
+ return html`<a
358
+ href="${image.redirectTo}"
359
+ target="_blank"
360
+ rel="noopener noreferrer"
361
+ ><img
362
+ class=${this.imageUrlError.includes(image.source)
363
+ ? "displayed-message-image-error"
364
+ : "displayed-message-image"}
365
+ src=${image.source}
366
+ alt="displayed message image"
367
+ @error=${() => {
368
+ // to trigger rerender
369
+ this.imageUrlError = [...this.imageUrlError, image.source];
370
+ }}
371
+ /></a>`;
372
+ })}
373
+ ${this.websitePreviewInfo.map((preview) => {
374
+ if (!preview.title || !(preview.image || preview.favicon)) return;
375
+ return html` <a
376
+ class="redirect-link-preview"
377
+ href=${preview.link}
378
+ target="_blank"
379
+ rel="noopener noreferrer"
380
+ >
381
+ <div class="website-preview">
382
+ ${
383
+ preview.image || preview.favicon
384
+ ? html` <img
385
+ src=${preview.image ?? preview.favicon}
386
+ class="website-preview-image"
387
+ alt="website preview image"
388
+ width="100%"
389
+ height="100%"
390
+ />`
391
+ : ""
392
+ }
393
+
394
+ <div class="website-preview-body">
395
+ <p class=${classMap({
396
+ ["message-text"]: true,
397
+ ["website-preview-title"]: true,
398
+ ["webchat-font__desktop"]: !isMobile(),
399
+ ["webchat-font__mobile"]: isMobile(),
400
+ })}>${preview.title}</p>
401
+ ${
402
+ preview.description
403
+ ? html`<p
404
+ class=${classMap({
405
+ ["message-text"]: true,
406
+ ["website-preview-description"]: true,
407
+ ["webchat-font__desktop"]: !isMobile(),
408
+ ["webchat-font__mobile"]: isMobile(),
409
+ })}
410
+ >
411
+ ${preview.description}
412
+ </p>`
413
+ : ""
414
+ }
415
+ </div>
416
+ </div>
417
+ </a></div> `;
418
+ })}
419
+ </div>`;
420
+ }
421
+ }