@meetelise/chat 1.20.88 → 1.20.90

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.
@@ -280,7 +280,8 @@ export class MEChat extends LitElement {
280
280
  this.analytics = new Analytics(
281
281
  this.orgSlug,
282
282
  this.buildingSlug,
283
- this.chatId
283
+ this.chatId,
284
+ this.currentLeadSource
284
285
  );
285
286
  this.analytics.ping("webchat_heartbeat");
286
287
  };
@@ -116,7 +116,19 @@ 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
 
129
+ #loading-message {
130
+ padding: 16px;
131
+ }
120
132
  .loading-dot {
121
133
  width: 6px;
122
134
  height: 6px;
@@ -188,4 +200,51 @@ export const pubnubChatStyles = css`
188
200
  border: none;
189
201
  cursor: pointer;
190
202
  }
203
+
204
+ .website-preview {
205
+ border: 1px solid rgba(0, 0, 0, 0.2);
206
+ border-radius: 8px;
207
+ overflow: hidden;
208
+ display: flex;
209
+ align-items: center;
210
+ flex-direction: column;
211
+ justify-content: center;
212
+
213
+ margin: 8px;
214
+ }
215
+ .website-preview-image {
216
+ max-width: 100%;
217
+ max-height: 200px;
218
+ width: auto;
219
+ height: auto;
220
+ }
221
+ .website-preview-body {
222
+ padding: 8px 0px 12px;
223
+ }
224
+ .website-preview-title {
225
+ font-size: 12px;
226
+ font-weight: bold;
227
+ margin: 0;
228
+ display: -webkit-box;
229
+ -webkit-box-orient: vertical;
230
+ -webkit-line-clamp: 2;
231
+ overflow: hidden;
232
+ text-overflow: ellipsis;
233
+ height: 24px;
234
+ }
235
+ .website-preview-description {
236
+ font-size: 10px;
237
+ margin: 0;
238
+
239
+ display: -webkit-box;
240
+ -webkit-box-orient: vertical;
241
+ -webkit-line-clamp: 3;
242
+ overflow: hidden;
243
+ text-overflow: ellipsis;
244
+ max-height: 34px;
245
+ }
246
+ .redirect-link-preview {
247
+ color: inherit;
248
+ text-decoration: none;
249
+ }
191
250
  `;
@@ -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);
@@ -62,7 +74,13 @@ export class PubnubChat extends LitElement {
62
74
  }
63
75
  );
64
76
  this.onMount();
77
+ // TODO (erol): scroll to bottom on all children load
78
+ // this is a hacky way to do it
79
+ setTimeout(() => {
80
+ this.scrollToChatBottom();
81
+ }, 1000);
65
82
  }
83
+
66
84
  scrollToChatBottom = (): void => {
67
85
  this.messageBody.scrollTo({
68
86
  top: this.messageBody.scrollHeight - this.messageBody.clientHeight,
@@ -70,7 +88,7 @@ export class PubnubChat extends LitElement {
70
88
  });
71
89
  };
72
90
 
73
- updated(): void {
91
+ async updated(): Promise<void> {
74
92
  this.scrollToChatBottom();
75
93
  }
76
94
 
@@ -133,11 +151,12 @@ export class PubnubChat extends LitElement {
133
151
  : undefined,
134
152
  })}
135
153
  >
136
- <p class="message-text">
137
- ${message.message.text
138
- .split("\n")
139
- .map((line) => html`${line}<br />`)}
140
- </p>
154
+ <pubnub-message
155
+ .message=${message}
156
+ .myPubnub=${this.myPubnub}
157
+ .onMount=${() => this.scrollToChatBottom()}
158
+ >
159
+ </pubnub-message>
141
160
  </li>
142
161
  `;
143
162
  })}
@@ -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
+ }
package/src/analytics.ts CHANGED
@@ -19,11 +19,18 @@ export default class Analytics {
19
19
  private building: string;
20
20
  private featureFlags?: Record<string, boolean>;
21
21
  public chatId: string;
22
+ private incomingProcessedLeadSource: string | null;
22
23
 
23
- constructor(org: string, building: string, chatId: string) {
24
+ constructor(
25
+ org: string,
26
+ building: string,
27
+ chatId: string,
28
+ incomingProcessedLeadSource: string | null
29
+ ) {
24
30
  this.org = org;
25
31
  this.building = building;
26
32
  this.chatId = chatId;
33
+ this.incomingProcessedLeadSource = incomingProcessedLeadSource;
27
34
  this.featureFlags = {};
28
35
  }
29
36
 
@@ -57,6 +64,7 @@ export default class Analytics {
57
64
  referrer: document.referrer,
58
65
  featureFlags: this.featureFlags,
59
66
  campaignSources: getCampaignSources(),
67
+ incomingProcessedLeadSource: this.incomingProcessedLeadSource, // if we already know the lead source, send it along
60
68
  }),
61
69
  }
62
70
  );