@product7/feedback-sdk 1.4.7 → 1.4.9

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.7",
3
+ "version": "1.4.9",
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",
@@ -221,44 +221,30 @@ export const feedbackStyles = `
221
221
  }
222
222
 
223
223
  .feedback-panel {
224
- width: 100%;
224
+ width: min(420px, calc(100vw - (var(--spacing-4) * 2)));
225
+ max-height: min(500px, calc(100vh - 88px));
225
226
  top: auto;
226
- bottom: 0;
227
- right: 0;
228
- left: 0;
229
- height: 85vh;
230
- max-height: 85vh;
231
- transform: translateY(100%);
232
- border-radius: var(--radius-3xl) var(--radius-3xl) 0 0;
227
+ bottom: 72px;
228
+ right: var(--spacing-4);
229
+ left: auto;
230
+ transform: translateX(calc(100% + 24px));
231
+ border-radius: var(--radius-2xl);
233
232
  }
234
233
 
235
234
  .feedback-panel.open {
236
- transform: translateY(0);
235
+ transform: translateX(0);
237
236
  }
238
237
 
239
238
  .feedback-panel-header {
240
- padding: var(--spacing-5);
241
- position: relative;
242
- }
243
-
244
- .feedback-panel-header::before {
245
- content: '';
246
- position: absolute;
247
- top: var(--spacing-2);
248
- left: 50%;
249
- transform: translateX(-50%);
250
- width: 40px;
251
- height: 4px;
252
- background: var(--color-neutral-300);
253
- border-radius: 2px;
239
+ padding: var(--spacing-4) var(--spacing-6);
254
240
  }
255
241
 
256
242
  .feedback-panel-body {
257
- padding: var(--spacing-5);
243
+ padding: var(--spacing-6);
258
244
  }
259
245
 
260
246
  .feedback-form-group textarea {
261
- min-height: 150px;
247
+ min-height: 120px;
262
248
  }
263
249
  }
264
250
  `;
@@ -4,6 +4,7 @@ export class ButtonWidget extends BaseWidget {
4
4
  constructor(options) {
5
5
  super({ ...options, type: 'button' });
6
6
  this.isMinimized = false;
7
+ this._hiddenForOpenPanel = false;
7
8
  }
8
9
 
9
10
  _render() {
@@ -90,6 +91,28 @@ export class ButtonWidget extends BaseWidget {
90
91
  this.element.classList.remove('minimized');
91
92
  }
92
93
 
94
+ openPanel() {
95
+ if (!this.state.isOpen) {
96
+ this._hiddenForOpenPanel = true;
97
+ this.hide();
98
+ }
99
+ super.openPanel();
100
+ }
101
+
102
+ closePanel() {
103
+ const shouldRestoreButton = this._hiddenForOpenPanel;
104
+ super.closePanel();
105
+
106
+ if (shouldRestoreButton) {
107
+ setTimeout(() => {
108
+ if (!this.destroyed) {
109
+ this.show();
110
+ }
111
+ this._hiddenForOpenPanel = false;
112
+ }, 320);
113
+ }
114
+ }
115
+
93
116
  mount(container) {
94
117
  super.mount(container);
95
118
  }
@@ -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,133 @@ export class MessengerState {
98
98
  this._notify('messagesUpdate', { conversationId, messages });
99
99
  }
100
100
 
101
- addMessage(conversationId, message) {
101
+ _getMessageAttachmentsSignature(message) {
102
+ if (!Array.isArray(message?.attachments) || message.attachments.length === 0) {
103
+ return '';
104
+ }
105
+
106
+ return message.attachments
107
+ .map((att) => `${att?.type || ''}:${att?.name || ''}`)
108
+ .join('|');
109
+ }
110
+
111
+ _findOptimisticMatchIndex(conversationId, incomingMessage, matchWindowMs) {
112
+ const messages = this.messages[conversationId] || [];
113
+ const incomingTimestamp = Date.parse(incomingMessage.timestamp);
114
+ const incomingSignature =
115
+ this._getMessageAttachmentsSignature(incomingMessage);
116
+
117
+ for (let i = messages.length - 1; i >= 0; i--) {
118
+ const candidate = messages[i];
119
+ if (!candidate?.isOptimistic || !candidate?.isOwn) {
120
+ continue;
121
+ }
122
+ if ((candidate.content || '') !== (incomingMessage.content || '')) {
123
+ continue;
124
+ }
125
+
126
+ const candidateSignature =
127
+ this._getMessageAttachmentsSignature(candidate);
128
+ if (candidateSignature !== incomingSignature) {
129
+ continue;
130
+ }
131
+
132
+ const candidateTimestamp = Date.parse(candidate.timestamp);
133
+ if (
134
+ Number.isNaN(incomingTimestamp) ||
135
+ Number.isNaN(candidateTimestamp) ||
136
+ Math.abs(incomingTimestamp - candidateTimestamp) <= matchWindowMs
137
+ ) {
138
+ return i;
139
+ }
140
+ }
141
+
142
+ return -1;
143
+ }
144
+
145
+ _updateConversationFromMessage(conversationId, message, countUnread) {
146
+ const conv = this.conversations.find((c) => c.id === conversationId);
147
+ if (!conv) {
148
+ return;
149
+ }
150
+
151
+ conv.lastMessage = message.content;
152
+ conv.lastMessageTime = message.timestamp;
153
+
154
+ if (countUnread && !message.isOwn) {
155
+ conv.unread = (conv.unread || 0) + 1;
156
+ this._updateUnreadCount();
157
+ }
158
+ }
159
+
160
+ upsertMessage(conversationId, message, options = {}) {
102
161
  if (!this.messages[conversationId]) {
103
162
  this.messages[conversationId] = [];
104
163
  }
105
- this.messages[conversationId].push(message);
106
164
 
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();
165
+ const reconcileOwnOptimistic = options.reconcileOwnOptimistic === true;
166
+ const optimisticMatchWindowMs = options.optimisticMatchWindowMs || 30000;
167
+ const messages = this.messages[conversationId];
168
+ const existingIndex =
169
+ message?.id != null
170
+ ? messages.findIndex((msg) => msg?.id === message.id)
171
+ : -1;
172
+
173
+ if (existingIndex !== -1) {
174
+ messages[existingIndex] = {
175
+ ...messages[existingIndex],
176
+ ...message,
177
+ isOptimistic: false,
178
+ };
179
+ this._updateConversationFromMessage(
180
+ conversationId,
181
+ messages[existingIndex],
182
+ false
183
+ );
184
+ this._notify('messagesUpdate', {
185
+ conversationId,
186
+ messages: [...messages],
187
+ });
188
+ return messages[existingIndex];
189
+ }
190
+
191
+ if (reconcileOwnOptimistic && message?.isOwn) {
192
+ const optimisticIndex = this._findOptimisticMatchIndex(
193
+ conversationId,
194
+ message,
195
+ optimisticMatchWindowMs
196
+ );
197
+ if (optimisticIndex !== -1) {
198
+ messages[optimisticIndex] = {
199
+ ...messages[optimisticIndex],
200
+ ...message,
201
+ isOptimistic: false,
202
+ };
203
+ this._updateConversationFromMessage(
204
+ conversationId,
205
+ messages[optimisticIndex],
206
+ false
207
+ );
208
+ this._notify('messagesUpdate', {
209
+ conversationId,
210
+ messages: [...messages],
211
+ });
212
+ return messages[optimisticIndex];
114
213
  }
115
214
  }
116
215
 
117
- this._notify('messageAdded', { conversationId, message });
216
+ const storedMessage = {
217
+ ...message,
218
+ isOptimistic: Boolean(message?.isOptimistic),
219
+ };
220
+ messages.push(storedMessage);
221
+ this._updateConversationFromMessage(conversationId, storedMessage, true);
222
+ this._notify('messageAdded', { conversationId, message: storedMessage });
223
+ return storedMessage;
224
+ }
225
+
226
+ addMessage(conversationId, message) {
227
+ return this.upsertMessage(conversationId, message);
118
228
  }
119
229
 
120
230
  updateConversation(conversationId, updates) {
@@ -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,