@product7/feedback-sdk 1.4.8 → 1.5.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@product7/feedback-sdk",
3
- "version": "1.4.8",
3
+ "version": "1.5.0",
4
4
  "description": "JavaScript SDK for integrating Product7 feedback widgets into any website",
5
5
  "main": "dist/feedback-sdk.js",
6
6
  "module": "src/index.js",
@@ -348,7 +348,10 @@ export class MessengerWidget extends BaseWidget {
348
348
  },
349
349
  };
350
350
 
351
- this.messengerState.addMessage(conversation_id, localMessage);
351
+ this.messengerState.upsertMessage(conversation_id, localMessage, {
352
+ reconcileOwnOptimistic: true,
353
+ optimisticMatchWindowMs: 30000,
354
+ });
352
355
 
353
356
  if (
354
357
  !this.messengerState.isOpen ||
@@ -98,23 +98,136 @@ export class MessengerState {
98
98
  this._notify('messagesUpdate', { conversationId, messages });
99
99
  }
100
100
 
101
- addMessage(conversationId, message) {
101
+ _getMessageAttachmentsSignature(message) {
102
+ if (
103
+ !Array.isArray(message?.attachments) ||
104
+ message.attachments.length === 0
105
+ ) {
106
+ return '';
107
+ }
108
+
109
+ return message.attachments
110
+ .map((att) => `${att?.type || ''}:${att?.name || ''}`)
111
+ .join('|');
112
+ }
113
+
114
+ _findOptimisticMatchIndex(conversationId, incomingMessage, matchWindowMs) {
115
+ const messages = this.messages[conversationId] || [];
116
+ const incomingTimestamp = Date.parse(incomingMessage.timestamp);
117
+ const incomingSignature =
118
+ this._getMessageAttachmentsSignature(incomingMessage);
119
+
120
+ for (let i = messages.length - 1; i >= 0; i--) {
121
+ const candidate = messages[i];
122
+ if (!candidate?.isOptimistic || !candidate?.isOwn) {
123
+ continue;
124
+ }
125
+ if ((candidate.content || '') !== (incomingMessage.content || '')) {
126
+ continue;
127
+ }
128
+
129
+ const candidateSignature =
130
+ this._getMessageAttachmentsSignature(candidate);
131
+ if (candidateSignature !== incomingSignature) {
132
+ continue;
133
+ }
134
+
135
+ const candidateTimestamp = Date.parse(candidate.timestamp);
136
+ if (
137
+ Number.isNaN(incomingTimestamp) ||
138
+ Number.isNaN(candidateTimestamp) ||
139
+ Math.abs(incomingTimestamp - candidateTimestamp) <= matchWindowMs
140
+ ) {
141
+ return i;
142
+ }
143
+ }
144
+
145
+ return -1;
146
+ }
147
+
148
+ _updateConversationFromMessage(conversationId, message, countUnread) {
149
+ const conv = this.conversations.find((c) => c.id === conversationId);
150
+ if (!conv) {
151
+ return;
152
+ }
153
+
154
+ conv.lastMessage = message.content;
155
+ conv.lastMessageTime = message.timestamp;
156
+
157
+ if (countUnread && !message.isOwn) {
158
+ conv.unread = (conv.unread || 0) + 1;
159
+ this._updateUnreadCount();
160
+ }
161
+ }
162
+
163
+ upsertMessage(conversationId, message, options = {}) {
102
164
  if (!this.messages[conversationId]) {
103
165
  this.messages[conversationId] = [];
104
166
  }
105
- this.messages[conversationId].push(message);
106
167
 
107
- const conv = this.conversations.find((c) => c.id === conversationId);
108
- if (conv) {
109
- conv.lastMessage = message.content;
110
- conv.lastMessageTime = message.timestamp;
111
- if (!message.isOwn) {
112
- conv.unread = (conv.unread || 0) + 1;
113
- this._updateUnreadCount();
168
+ const reconcileOwnOptimistic = options.reconcileOwnOptimistic === true;
169
+ const optimisticMatchWindowMs = options.optimisticMatchWindowMs || 30000;
170
+ const messages = this.messages[conversationId];
171
+ const existingIndex =
172
+ message?.id != null
173
+ ? messages.findIndex((msg) => msg?.id === message.id)
174
+ : -1;
175
+
176
+ if (existingIndex !== -1) {
177
+ messages[existingIndex] = {
178
+ ...messages[existingIndex],
179
+ ...message,
180
+ isOptimistic: false,
181
+ };
182
+ this._updateConversationFromMessage(
183
+ conversationId,
184
+ messages[existingIndex],
185
+ false
186
+ );
187
+ this._notify('messagesUpdate', {
188
+ conversationId,
189
+ messages: [...messages],
190
+ });
191
+ return messages[existingIndex];
192
+ }
193
+
194
+ if (reconcileOwnOptimistic && message?.isOwn) {
195
+ const optimisticIndex = this._findOptimisticMatchIndex(
196
+ conversationId,
197
+ message,
198
+ optimisticMatchWindowMs
199
+ );
200
+ if (optimisticIndex !== -1) {
201
+ messages[optimisticIndex] = {
202
+ ...messages[optimisticIndex],
203
+ ...message,
204
+ isOptimistic: false,
205
+ };
206
+ this._updateConversationFromMessage(
207
+ conversationId,
208
+ messages[optimisticIndex],
209
+ false
210
+ );
211
+ this._notify('messagesUpdate', {
212
+ conversationId,
213
+ messages: [...messages],
214
+ });
215
+ return messages[optimisticIndex];
114
216
  }
115
217
  }
116
218
 
117
- this._notify('messageAdded', { conversationId, message });
219
+ const storedMessage = {
220
+ ...message,
221
+ isOptimistic: Boolean(message?.isOptimistic),
222
+ };
223
+ messages.push(storedMessage);
224
+ this._updateConversationFromMessage(conversationId, storedMessage, true);
225
+ this._notify('messageAdded', { conversationId, message: storedMessage });
226
+ return storedMessage;
227
+ }
228
+
229
+ addMessage(conversationId, message) {
230
+ return this.upsertMessage(conversationId, message);
118
231
  }
119
232
 
120
233
  updateConversation(conversationId, updates) {
@@ -117,7 +117,7 @@ export class ChatView {
117
117
 
118
118
  <div class="messenger-chat-compose">
119
119
  <button class="sdk-btn-icon messenger-compose-attach" aria-label="Attach file">
120
- <i class="ph ph-paperclip"></i>
120
+ <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 256 256"><rect width="16" height="16" fill="none"/><path d="M160,80,76.69,164.69a16,16,0,0,0,22.63,22.62L198.63,86.63a32,32,0,0,0-45.26-45.26L54.06,142.06a48,48,0,0,0,67.88,67.88L204,128" fill="none" stroke="currentColor" stroke-linecap="round" stroke-linejoin="round" stroke-width="16"/></svg>
121
121
  </button>
122
122
  <div class="messenger-compose-input-wrapper">
123
123
  <textarea class="messenger-compose-input" placeholder="${placeholder}" rows="1"></textarea>
@@ -485,6 +485,7 @@ export class ChatView {
485
485
  id: 'msg_' + Date.now(),
486
486
  content: content,
487
487
  isOwn: true,
488
+ isOptimistic: true,
488
489
  timestamp: new Date().toISOString(),
489
490
  attachments: attachmentsToSend.map((a) => ({
490
491
  url: a.preview,