@product7/feedback-sdk 1.2.8 → 1.3.1

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.
@@ -1,7 +1,6 @@
1
1
  /**
2
2
  * MessengerWidget - Full-featured Messenger/Chat widget
3
3
  */
4
- import { WebSocketService } from '../core/WebSocketService.js';
5
4
  import { BaseWidget } from './BaseWidget.js';
6
5
  import { MessengerState } from './messenger/MessengerState.js';
7
6
  import { MessengerLauncher } from './messenger/components/MessengerLauncher.js';
@@ -11,6 +10,8 @@ import { ChatView } from './messenger/views/ChatView.js';
11
10
  import { ConversationsView } from './messenger/views/ConversationsView.js';
12
11
  import { HelpView } from './messenger/views/HelpView.js';
13
12
  import { HomeView } from './messenger/views/HomeView.js';
13
+ import { PreChatFormView } from './messenger/views/PreChatFormView.js';
14
+ import { WebSocketService } from '../core/WebSocketService.js';
14
15
 
15
16
  export class MessengerWidget extends BaseWidget {
16
17
  constructor(options) {
@@ -24,7 +25,7 @@ export class MessengerWidget extends BaseWidget {
24
25
  welcomeMessage: options.welcomeMessage || 'How can we help?',
25
26
  enableHelp: options.enableHelp !== false,
26
27
  enableChangelog: options.enableChangelog !== false,
27
- logoUrl: options.logoUrl || 'https://feedback-sdk.product7.io/p7.png',
28
+ logoUrl: options.logoUrl || 'https://product7.io/p7logo.svg',
28
29
  featuredContent: options.featuredContent || null,
29
30
  primaryColor: options.primaryColor || '#1c1c1e',
30
31
  // Callbacks
@@ -53,6 +54,7 @@ export class MessengerWidget extends BaseWidget {
53
54
  this._handleWebSocketMessage = this._handleWebSocketMessage.bind(this);
54
55
  this._handleTypingStarted = this._handleTypingStarted.bind(this);
55
56
  this._handleTypingStopped = this._handleTypingStopped.bind(this);
57
+ this._handleConversationClosed = this._handleConversationClosed.bind(this);
56
58
  }
57
59
 
58
60
  _render() {
@@ -84,6 +86,8 @@ export class MessengerWidget extends BaseWidget {
84
86
  // Conversation list callbacks
85
87
  onSelectConversation: this._handleSelectConversation.bind(this),
86
88
  onStartNewConversation: this._handleNewConversationClick.bind(this),
89
+ // Pre-chat form callbacks
90
+ onIdentifyContact: this._handleIdentifyContact.bind(this),
87
91
  // Article/changelog callbacks
88
92
  onArticleClick: this.messengerOptions.onArticleClick,
89
93
  onChangelogClick: this.messengerOptions.onChangelogClick,
@@ -93,6 +97,7 @@ export class MessengerWidget extends BaseWidget {
93
97
  this.panel.registerView('home', HomeView);
94
98
  this.panel.registerView('messages', ConversationsView);
95
99
  this.panel.registerView('chat', ChatView);
100
+ this.panel.registerView('prechat', PreChatFormView);
96
101
  this.panel.registerView('help', HelpView);
97
102
  this.panel.registerView('changelog', ChangelogView);
98
103
 
@@ -108,6 +113,9 @@ export class MessengerWidget extends BaseWidget {
108
113
  if (type === 'openChange') {
109
114
  this._handleOpenChange(data.isOpen);
110
115
  }
116
+ if (type === 'conversationChange') {
117
+ this._handleActiveConversationChange(data.conversationId, data.previousConversationId);
118
+ }
111
119
  });
112
120
  }
113
121
 
@@ -122,14 +130,44 @@ export class MessengerWidget extends BaseWidget {
122
130
  }
123
131
  }
124
132
 
133
+ /**
134
+ * Subscribe/unsubscribe to conversation WebSocket channel
135
+ */
136
+ _handleActiveConversationChange(conversationId, previousConversationId) {
137
+ if (previousConversationId && this.wsService) {
138
+ this.wsService.send('conversation:unsubscribe', { conversation_id: previousConversationId });
139
+ }
140
+ if (conversationId && this.wsService) {
141
+ this.wsService.send('conversation:subscribe', { conversation_id: conversationId });
142
+ }
143
+ }
144
+
125
145
  /**
126
146
  * Handle starting a new conversation
147
+ * If there's an existing open conversation, send the message there instead
127
148
  */
128
- async _handleStartConversation(messageContent) {
149
+ async _handleStartConversation(messageContent, pendingAttachments) {
129
150
  try {
130
- await this.startNewConversation(messageContent);
151
+ // Check for existing open conversation first
152
+ const openConversation = this.messengerState.conversations.find(
153
+ (c) => c.status === 'open'
154
+ );
155
+
156
+ if (openConversation) {
157
+ // Route message to existing open conversation
158
+ this.messengerState.setActiveConversation(openConversation.id);
159
+ await this._handleSendMessage(
160
+ openConversation.id,
161
+ { content: messageContent },
162
+ pendingAttachments
163
+ );
164
+ return openConversation;
165
+ }
166
+
167
+ return await this.startNewConversation(messageContent, '', pendingAttachments);
131
168
  } catch (error) {
132
169
  console.error('[MessengerWidget] Failed to start conversation:', error);
170
+ return null;
133
171
  }
134
172
  }
135
173
 
@@ -146,13 +184,88 @@ export class MessengerWidget extends BaseWidget {
146
184
 
147
185
  /**
148
186
  * Handle clicking "new conversation" button
187
+ * Reuses the most recent open conversation if one exists
149
188
  */
150
189
  _handleNewConversationClick() {
151
- // View is already changed by ConversationsView
152
- // This is for any additional setup needed
190
+ // Check for an existing open conversation to reuse
191
+ const openConversation = this.messengerState.conversations.find(
192
+ (c) => c.status === 'open'
193
+ );
194
+
195
+ if (openConversation) {
196
+ // Reuse existing open conversation
197
+ this.messengerState.setActiveConversation(openConversation.id);
198
+ this.messengerState.setView('chat');
199
+ this._handleSelectConversation(openConversation.id);
200
+ } else {
201
+ // No open conversation — start a new one
202
+ this.messengerState.setActiveConversation(null);
203
+ this.messengerState.setView('chat');
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Handle identifying contact from pre-chat form
209
+ */
210
+ async _handleIdentifyContact(contactData) {
211
+ try {
212
+ // Call API to identify/update contact
213
+ const response = await this.apiService.identifyContact({
214
+ name: contactData.name,
215
+ email: contactData.email,
216
+ });
217
+
218
+ if (response.status) {
219
+ console.log('[MessengerWidget] Contact identified:', contactData.email);
220
+
221
+ // Update local user context
222
+ if (!this.messengerState.userContext) {
223
+ this.messengerState.userContext = {};
224
+ }
225
+ this.messengerState.userContext.name = contactData.name;
226
+ this.messengerState.userContext.email = contactData.email;
227
+ }
228
+
229
+ return response;
230
+ } catch (error) {
231
+ console.error('[MessengerWidget] Failed to identify contact:', error);
232
+ throw error;
233
+ }
234
+ }
235
+
236
+ async _handleUploadFile(base64Data, filename) {
237
+ try {
238
+ const response = await this.apiService.uploadFile(base64Data, filename);
239
+ if (response.status && response.url) {
240
+ return response.url;
241
+ }
242
+ throw new Error('Upload failed');
243
+ } catch (error) {
244
+ console.error('[MessengerWidget] Failed to upload file:', error);
245
+ throw error;
246
+ }
153
247
  }
154
248
 
155
- async _handleSendMessage(conversationId, message) {
249
+ async _uploadPendingAttachments(pendingAttachments) {
250
+ if (!pendingAttachments || pendingAttachments.length === 0) return [];
251
+
252
+ const uploaded = [];
253
+ for (const att of pendingAttachments) {
254
+ try {
255
+ const cdnUrl = await this._handleUploadFile(att.preview, att.file.name);
256
+ uploaded.push({
257
+ url: cdnUrl,
258
+ type: att.type.startsWith('image') ? 'image' : 'file',
259
+ name: att.file.name,
260
+ });
261
+ } catch (err) {
262
+ console.error('[MessengerWidget] Skipping failed attachment upload:', att.file.name, err);
263
+ }
264
+ }
265
+ return uploaded;
266
+ }
267
+
268
+ async _handleSendMessage(conversationId, message, pendingAttachments) {
156
269
  // Emit event for external listeners
157
270
  this.sdk.eventBus.emit('messenger:messageSent', {
158
271
  widget: this,
@@ -161,9 +274,13 @@ export class MessengerWidget extends BaseWidget {
161
274
  });
162
275
 
163
276
  try {
277
+ // Upload attachments to CDN first
278
+ const uploadedAttachments = await this._uploadPendingAttachments(pendingAttachments);
279
+
164
280
  // Send message through API
165
281
  const response = await this.apiService.sendMessage(conversationId, {
166
282
  content: message.content,
283
+ attachments: uploadedAttachments,
167
284
  });
168
285
 
169
286
  if (response.status && response.data) {
@@ -200,12 +317,25 @@ export class MessengerWidget extends BaseWidget {
200
317
  _handleWebSocketMessage(data) {
201
318
  const { conversation_id, message } = data;
202
319
 
320
+ // Parse attachments from server message
321
+ let attachments = [];
322
+ if (message.attachments) {
323
+ try {
324
+ attachments = typeof message.attachments === 'string'
325
+ ? JSON.parse(message.attachments)
326
+ : message.attachments;
327
+ } catch (e) {
328
+ // ignore parse errors
329
+ }
330
+ }
331
+
203
332
  // Transform message to local format
204
333
  const localMessage = {
205
334
  id: message.id,
206
335
  content: message.content,
207
336
  isOwn: message.sender_type === 'customer',
208
337
  timestamp: message.created_at,
338
+ attachments: attachments.length > 0 ? attachments : undefined,
209
339
  sender: {
210
340
  name: message.sender_name || 'Support',
211
341
  avatarUrl: message.sender_avatar || null,
@@ -245,6 +375,19 @@ export class MessengerWidget extends BaseWidget {
245
375
  });
246
376
  }
247
377
 
378
+ /**
379
+ * Handle conversation closed event
380
+ */
381
+ _handleConversationClosed(data) {
382
+ const conversationId =
383
+ data?.conversation_id ||
384
+ data?.id ||
385
+ data?.conversation?.id;
386
+ if (!conversationId) return;
387
+
388
+ this.messengerState.updateConversation(conversationId, { status: 'closed' });
389
+ }
390
+
248
391
  /**
249
392
  * Update unread count from API
250
393
  */
@@ -287,9 +430,18 @@ export class MessengerWidget extends BaseWidget {
287
430
  this._wsUnsubscribers.push(
288
431
  this.wsService.on('typing_stopped', this._handleTypingStopped)
289
432
  );
433
+ this._wsUnsubscribers.push(
434
+ this.wsService.on('conversation_closed', this._handleConversationClosed)
435
+ );
290
436
  this._wsUnsubscribers.push(
291
437
  this.wsService.on('connected', () => {
292
438
  console.log('[MessengerWidget] WebSocket connected');
439
+ // Re-subscribe to active conversation on reconnect
440
+ if (this.messengerState.activeConversationId) {
441
+ this.wsService.send('conversation:subscribe', {
442
+ conversation_id: this.messengerState.activeConversationId,
443
+ });
444
+ }
293
445
  })
294
446
  );
295
447
  this._wsUnsubscribers.push(
@@ -482,18 +634,29 @@ export class MessengerWidget extends BaseWidget {
482
634
  try {
483
635
  const response = await this.apiService.getConversation(conversationId);
484
636
  if (response.status && response.data) {
485
- const messages = (response.data.messages || []).map((msg) => ({
486
- id: msg.id,
487
- content: msg.content,
488
- isOwn: msg.sender_type === 'customer',
489
- timestamp: msg.created_at,
490
- sender: {
491
- name:
492
- msg.sender_name ||
493
- (msg.sender_type === 'customer' ? 'You' : 'Support'),
494
- avatarUrl: msg.sender_avatar || null,
495
- },
496
- }));
637
+ const messages = (response.data.messages || []).map((msg) => {
638
+ let attachments;
639
+ if (msg.attachments) {
640
+ try {
641
+ attachments = typeof msg.attachments === 'string'
642
+ ? JSON.parse(msg.attachments)
643
+ : msg.attachments;
644
+ } catch (e) {
645
+ // ignore parse errors
646
+ }
647
+ }
648
+ return {
649
+ id: msg.id,
650
+ content: msg.content,
651
+ isOwn: msg.sender_type === 'customer',
652
+ timestamp: msg.created_at,
653
+ attachments: attachments && attachments.length > 0 ? attachments : undefined,
654
+ sender: {
655
+ name: msg.sender_name || (msg.sender_type === 'customer' ? 'You' : 'Support'),
656
+ avatarUrl: msg.sender_avatar || null,
657
+ },
658
+ };
659
+ });
497
660
  this.messengerState.setMessages(conversationId, messages);
498
661
 
499
662
  // Mark as read
@@ -512,13 +675,28 @@ export class MessengerWidget extends BaseWidget {
512
675
  /**
513
676
  * Start a new conversation
514
677
  */
515
- async startNewConversation(message, subject = '') {
678
+ async startNewConversation(message, subject = '', pendingAttachments = []) {
516
679
  try {
680
+ // Upload attachments to CDN first
681
+ const uploadedAttachments = await this._uploadPendingAttachments(pendingAttachments);
682
+
683
+ console.log('[MessengerWidget] Starting conversation...', {
684
+ message,
685
+ attachmentCount: uploadedAttachments.length,
686
+ hasSession: this.apiService.isSessionValid(),
687
+ sessionToken: this.apiService.sessionToken ? this.apiService.sessionToken.substring(0, 10) + '...' : null,
688
+ baseURL: this.apiService.baseURL,
689
+ mock: this.apiService.mock,
690
+ });
691
+
517
692
  const response = await this.apiService.startConversation({
518
693
  message,
519
694
  subject,
695
+ attachments: uploadedAttachments,
520
696
  });
521
697
 
698
+ console.log('[MessengerWidget] Conversation response:', response);
699
+
522
700
  if (response.status && response.data) {
523
701
  const conv = response.data;
524
702
  const newConversation = {
@@ -578,6 +756,12 @@ export class MessengerWidget extends BaseWidget {
578
756
  this.messengerState.agentsOnline = response.data.agents_online;
579
757
  this.messengerState.onlineCount = response.data.online_count || 0;
580
758
  this.messengerState.responseTime = response.data.response_time || '';
759
+
760
+ // Update team avatars from online agents
761
+ if (response.data.available_agents) {
762
+ this.messengerState.setTeamAvatarsFromAgents(response.data.available_agents);
763
+ }
764
+
581
765
  this.messengerState._notify('availabilityUpdate', response.data);
582
766
  return response.data;
583
767
  }
@@ -589,7 +773,6 @@ export class MessengerWidget extends BaseWidget {
589
773
  }
590
774
 
591
775
  async _fetchChangelog() {
592
- // Mock data for now - simulating changelog API response
593
776
  if (this.apiService?.mock) {
594
777
  return {
595
778
  homeItems: [
@@ -84,8 +84,9 @@ export class MessengerState {
84
84
  * Set active conversation for chat view
85
85
  */
86
86
  setActiveConversation(conversationId) {
87
+ const previousConversationId = this.activeConversationId;
87
88
  this.activeConversationId = conversationId;
88
- this._notify('conversationChange', { conversationId });
89
+ this._notify('conversationChange', { conversationId, previousConversationId });
89
90
  }
90
91
 
91
92
  /**
@@ -137,6 +138,20 @@ export class MessengerState {
137
138
  this._notify('messageAdded', { conversationId, message });
138
139
  }
139
140
 
141
+ /**
142
+ * Update a conversation by id
143
+ */
144
+ updateConversation(conversationId, updates) {
145
+ const conv = this.conversations.find((c) => c.id === conversationId);
146
+ if (!conv) {
147
+ return null;
148
+ }
149
+
150
+ Object.assign(conv, updates);
151
+ this._notify('conversationUpdated', { conversationId, conversation: conv });
152
+ return conv;
153
+ }
154
+
140
155
  /**
141
156
  * Mark conversation as read
142
157
  */
@@ -206,6 +221,21 @@ export class MessengerState {
206
221
  return this.messages[this.activeConversationId] || [];
207
222
  }
208
223
 
224
+ /**
225
+ * Update team avatars from backend agent data.
226
+ * Converts available_agents ({full_name, picture}) into avatar strings
227
+ * the views already support (URL strings or initial strings).
228
+ */
229
+ setTeamAvatarsFromAgents(agents) {
230
+ if (!agents || agents.length === 0) return;
231
+
232
+ this.teamAvatars = agents.map((agent) => {
233
+ if (agent.picture) return agent.picture;
234
+ return agent.full_name || '?';
235
+ });
236
+ this._notify('teamAvatarsUpdate', { teamAvatars: this.teamAvatars });
237
+ }
238
+
209
239
  /**
210
240
  * Get filtered help articles
211
241
  */
@@ -1,12 +1,9 @@
1
- /**
2
- * MessengerLauncher - Floating trigger button for messenger
3
- */
4
1
  export class MessengerLauncher {
5
2
  constructor(state, options = {}) {
6
3
  this.state = state;
7
4
  this.options = {
8
5
  position: options.position || 'bottom-right',
9
- primaryColor: options.primaryColor || '#1c1c1e',
6
+ primaryColor: options.primaryColor || '#155eff',
10
7
  ...options,
11
8
  };
12
9
  this.element = null;
@@ -20,7 +17,6 @@ export class MessengerLauncher {
20
17
  this._updateContent();
21
18
  this._attachEvents();
22
19
 
23
- // Subscribe to state changes
24
20
  this._unsubscribe = this.state.subscribe((type, data) => {
25
21
  if (type === 'openChange') {
26
22
  this._updateIcon();
@@ -42,10 +38,14 @@ export class MessengerLauncher {
42
38
  this.element.innerHTML = `
43
39
  <button class="messenger-launcher-btn" aria-label="Open messenger" style="background: ${this.options.primaryColor};">
44
40
  <span class="messenger-launcher-icon messenger-launcher-icon-chat">
45
- <i class="ph ph-chat-circle-dots" style="font-size: 24px;"></i>
41
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#ffffff" viewBox="0 0 256 256">
42
+ <path d="M144,140a12,12,0,1,1-12-12A12,12,0,0,1,144,140Zm44-12a12,12,0,1,0,12,12A12,12,0,0,0,188,128Zm51.34,83.47a16,16,0,0,1-19.87,19.87l-24.71-7.27A80,80,0,0,1,86.43,183.42a79,79,0,0,1-25.19-7.35l-24.71,7.27a16,16,0,0,1-19.87-19.87l7.27-24.71A80,80,0,1,1,169.58,72.59a80,80,0,0,1,62.49,114.17ZM81.3,166.3a79.94,79.94,0,0,1,70.38-93.87A64,64,0,0,0,39.55,134.19a8,8,0,0,1,.63,6L32,168l27.76-8.17a8,8,0,0,1,6,.63A63.45,63.45,0,0,0,81.3,166.3Zm135.15,15.89a64,64,0,1,0-26.26,26.26,8,8,0,0,1,6-.63L224,216l-8.17-27.76A8,8,0,0,1,216.45,182.19Z"></path>
43
+ </svg>
46
44
  </span>
47
45
  <span class="messenger-launcher-icon messenger-launcher-icon-close" style="display: none;">
48
- <i class="ph ph-x" style="font-size: 24px;"></i>
46
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#ffffff" viewBox="0 0 256 256">
47
+ <path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path>
48
+ </svg>
49
49
  </span>
50
50
  ${badgeHtml}
51
51
  </button>
@@ -108,19 +108,31 @@ export class NavigationTabs {
108
108
  }
109
109
 
110
110
  _getHomeIcon() {
111
- return `<i class="ph-duotone ph-house" style="font-size: 24px;"></i>`;
111
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#000000" viewBox="0 0 256 256">
112
+ <path d="M216,115.54V208a8,8,0,0,1-8,8H160a8,8,0,0,1-8-8V160a8,8,0,0,0-8-8H112a8,8,0,0,0-8,8v48a8,8,0,0,1-8,8H48a8,8,0,0,1-8-8V115.54a8,8,0,0,1,2.62-5.92l80-75.54a8,8,0,0,1,10.77,0l80,75.54A8,8,0,0,1,216,115.54Z" opacity="0.2"></path>
113
+ <path d="M218.83,103.77l-80-75.48a1.14,1.14,0,0,1-.11-.11,16,16,0,0,0-21.53,0l-.11.11L37.17,103.77A16,16,0,0,0,32,115.55V208a16,16,0,0,0,16,16H96a16,16,0,0,0,16-16V160h32v48a16,16,0,0,0,16,16h48a16,16,0,0,0,16-16V115.55A16,16,0,0,0,218.83,103.77ZM208,208H160V160a16,16,0,0,0-16-16H112a16,16,0,0,0-16,16v48H48V115.55l.11-.1L128,40l79.9,75.43.11.1Z"></path>
114
+ </svg>`;
112
115
  }
113
116
 
114
117
  _getMessagesIcon() {
115
- return `<i class="ph-duotone ph-chat" style="font-size: 24px;"></i>`;
118
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#000000" viewBox="0 0 256 256">
119
+ <path d="M224,64V192a8,8,0,0,1-8,8H82.5a8,8,0,0,0-5.15,1.88l-32.2,28.23A8,8,0,0,1,32,224V64a8,8,0,0,1,8-8H216A8,8,0,0,1,224,64Z" opacity="0.2"></path>
120
+ <path d="M216,48H40A16,16,0,0,0,24,64V224a15.85,15.85,0,0,0,9.24,14.5A16.13,16.13,0,0,0,40,240a15.89,15.89,0,0,0,10.25-3.78.69.69,0,0,0,.13-.11L82.5,208H216a16,16,0,0,0,16-16V64A16,16,0,0,0,216,48ZM40,224h0ZM216,192H82.5a16,16,0,0,0-10.3,3.75l-.12.11L40,224V64H216Z"></path>
121
+ </svg>`;
116
122
  }
117
123
 
118
124
  _getHelpIcon() {
119
- return `<i class="ph-duotone ph-question" style="font-size: 24px;"></i>`;
125
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#000000" viewBox="0 0 256 256">
126
+ <path d="M224,128a96,96,0,1,1-96-96A96,96,0,0,1,224,128Z" opacity="0.2"></path>
127
+ <path d="M128,24A104,104,0,1,0,232,128,104.11,104.11,0,0,0,128,24Zm0,192a88,88,0,1,1,88-88A88.1,88.1,0,0,1,128,216Zm16-40a8,8,0,0,1-8,8,16,16,0,0,1-16-16V128a8,8,0,0,1,0-16,16,16,0,0,1,16,16v40A8,8,0,0,1,144,176ZM112,84a12,12,0,1,1,12,12A12,12,0,0,1,112,84Z"></path>
128
+ </svg>`;
120
129
  }
121
130
 
122
131
  _getChangelogIcon() {
123
- return `<i class="ph-duotone ph-megaphone" style="font-size: 24px;"></i>`;
132
+ return `<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" fill="#000000" viewBox="0 0 256 256">
133
+ <path d="M200,144a32,32,0,0,1-63.5,4.5L85.83,121.25a32.07,32.07,0,0,1-41.54-34l-24.15-21a8,8,0,0,1,10.25-12.29L54.55,75a32,32,0,0,1,59.16,2.5l50.63,27.25a31.88,31.88,0,0,1,35.66,7l26.46-23A8,8,0,1,1,236.71,101l-26.46,23A32,32,0,0,1,200,144Z" opacity="0.2"></path>
134
+ <path d="M228.54,86.66l-26.46,23.07A40,40,0,0,0,168,72.13L120.89,46.5a40,40,0,0,0-75.44-4l-22-19.2a8,8,0,0,0-10.5,12L35.44,54.77a40,40,0,0,0,50,61.07l47.1,25.64a40,40,0,0,0,75.41,4.07l26.46-23.07a8,8,0,0,0-10.5-12ZM56,96A24,24,0,1,1,77.25,82.75,24,24,0,0,1,56,96Zm144,64a24,24,0,1,1,24-24A24,24,0,0,1,200,160Z"></path>
135
+ </svg>`;
124
136
  }
125
137
 
126
138
  destroy() {
@@ -1,6 +1,3 @@
1
- /**
2
- * ChangelogView - Changelog and announcements
3
- */
4
1
  export class ChangelogView {
5
2
  constructor(state, options = {}) {
6
3
  this.state = state;
@@ -15,7 +12,6 @@ export class ChangelogView {
15
12
 
16
13
  this._updateContent();
17
14
 
18
- // Subscribe to state changes
19
15
  this._unsubscribe = this.state.subscribe((type) => {
20
16
  if (type === 'changelogUpdate') {
21
17
  this._updateChangelogList();
@@ -32,7 +28,9 @@ export class ChangelogView {
32
28
  <div class="messenger-changelog-header">
33
29
  <h2>Changelog</h2>
34
30
  <button class="messenger-close-btn" aria-label="Close">
35
- <i class="ph ph-x" style="font-size: 20px;"></i>
31
+ <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="#000000" viewBox="0 0 256 256">
32
+ <path d="M205.66,194.34a8,8,0,0,1-11.32,11.32L128,139.31,61.66,205.66a8,8,0,0,1-11.32-11.32L116.69,128,50.34,61.66A8,8,0,0,1,61.66,50.34L128,116.69l66.34-66.35a8,8,0,0,1,11.32,11.32L139.31,128Z"></path>
33
+ </svg>
36
34
  </button>
37
35
  </div>
38
36
 
@@ -68,7 +66,6 @@ export class ChangelogView {
68
66
  .map((item) => this._renderChangelogCard(item))
69
67
  .join('');
70
68
 
71
- // Attach click events
72
69
  this._attachChangelogEvents();
73
70
  }
74
71
 
@@ -99,7 +96,9 @@ export class ChangelogView {
99
96
  ${item.description ? `<p class="messenger-changelog-description">${this._truncateText(item.description, 100)}</p>` : ''}
100
97
  <div class="messenger-changelog-meta">
101
98
  <span class="messenger-changelog-date">${dateStr}</span>
102
- <i class="ph ph-caret-right messenger-changelog-arrow" style="font-size: 16px;"></i>
99
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="#000000" viewBox="0 0 256 256" class="messenger-changelog-arrow">
100
+ <path d="M181.66,133.66l-80,80a8,8,0,0,1-11.32-11.32L164.69,128,90.34,53.66a8,8,0,0,1,11.32-11.32l80,80A8,8,0,0,1,181.66,133.66Z"></path>
101
+ </svg>
103
102
  </div>
104
103
  </div>
105
104
  </div>
@@ -134,7 +133,9 @@ export class ChangelogView {
134
133
  return `
135
134
  <div class="messenger-changelog-empty">
136
135
  <div class="messenger-changelog-empty-icon">
137
- <i class="ph ph-megaphone" style="font-size: 48px;"></i>
136
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" fill="#000000" viewBox="0 0 256 256">
137
+ <path d="M228.54,86.66l-26.46,23.07A40,40,0,0,0,168,72.13L120.89,46.5a40,40,0,0,0-75.44-4l-22-19.2a8,8,0,0,0-10.5,12L35.44,54.77a40,40,0,0,0,50,61.07l47.1,25.64a40,40,0,0,0,75.41,4.07l26.46-23.07a8,8,0,0,0-10.5-12ZM56,96A24,24,0,1,1,77.25,82.75,24,24,0,0,1,56,96Zm144,64a24,24,0,1,1,24-24A24,24,0,0,1,200,160Z"></path>
138
+ </svg>
138
139
  </div>
139
140
  <h3>No changelog yet</h3>
140
141
  <p>Check back later for updates</p>
@@ -159,7 +160,6 @@ export class ChangelogView {
159
160
  }
160
161
 
161
162
  _attachEvents() {
162
- // Close button
163
163
  this.element
164
164
  .querySelector('.messenger-close-btn')
165
165
  .addEventListener('click', () => {