@product7/feedback-sdk 1.2.4 → 1.2.6
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/README.md +176 -7
- package/dist/README.md +176 -7
- package/dist/feedback-sdk.js +8156 -5140
- package/dist/feedback-sdk.js.map +1 -1
- package/dist/feedback-sdk.min.js +1 -1
- package/dist/feedback-sdk.min.js.map +1 -1
- package/package.json +1 -1
- package/src/core/APIService.js +643 -0
- package/src/core/FeedbackSDK.js +115 -5
- package/src/core/WebSocketService.js +273 -0
- package/src/index.js +3 -0
- package/src/styles/messengerStyles.js +100 -0
- package/src/styles/styles.js +747 -1
- package/src/widgets/BaseWidget.js +96 -0
- package/src/widgets/ButtonWidget.js +93 -91
- package/src/widgets/ChangelogWidget.js +619 -0
- package/src/widgets/MessengerWidget.js +374 -89
- package/src/widgets/WidgetFactory.js +2 -0
- package/src/widgets/messenger/MessengerState.js +12 -0
- package/src/widgets/messenger/components/NavigationTabs.js +1 -1
- package/src/widgets/messenger/views/ChatView.js +121 -16
- package/src/widgets/messenger/views/ConversationsView.js +12 -11
- package/src/widgets/messenger/views/HomeView.js +23 -1
- package/types/index.d.ts +152 -123
package/src/core/FeedbackSDK.js
CHANGED
|
@@ -192,6 +192,68 @@ export class FeedbackSDK {
|
|
|
192
192
|
return surveyWidget;
|
|
193
193
|
}
|
|
194
194
|
|
|
195
|
+
/**
|
|
196
|
+
* Show a changelog widget with sidebar
|
|
197
|
+
* @param {Object} options - Changelog widget options
|
|
198
|
+
* @param {string} options.position - Position: 'bottom-right', 'bottom-left', 'top-right', 'top-left'
|
|
199
|
+
* @param {string} options.theme - Theme: 'light', 'dark'
|
|
200
|
+
* @param {string} options.title - Sidebar title
|
|
201
|
+
* @param {string} options.triggerText - Text on the trigger button
|
|
202
|
+
* @param {boolean} options.showBadge - Show notification badge
|
|
203
|
+
* @param {string} options.viewButtonText - Text for the view update button
|
|
204
|
+
* @param {string} options.changelogBaseUrl - Base URL for changelog links
|
|
205
|
+
* @param {boolean} options.openInNewTab - Open changelog links in new tab (default: true)
|
|
206
|
+
* @param {Function} options.onViewUpdate - Callback when user clicks view update
|
|
207
|
+
* @returns {ChangelogWidget} The changelog widget instance
|
|
208
|
+
*/
|
|
209
|
+
showChangelog(options = {}) {
|
|
210
|
+
if (!this.initialized) {
|
|
211
|
+
throw new SDKError(
|
|
212
|
+
'SDK must be initialized before showing changelog. Call init() first.'
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const changelogWidget = this.createWidget('changelog', {
|
|
217
|
+
position: options.position || 'bottom-right',
|
|
218
|
+
theme: options.theme || this.config.theme || 'light',
|
|
219
|
+
title: options.title || "What's New",
|
|
220
|
+
triggerText: options.triggerText || "What's New",
|
|
221
|
+
showBadge: options.showBadge !== false,
|
|
222
|
+
viewButtonText: options.viewButtonText || 'View Update',
|
|
223
|
+
changelogBaseUrl: options.changelogBaseUrl,
|
|
224
|
+
openInNewTab: options.openInNewTab,
|
|
225
|
+
onViewUpdate: options.onViewUpdate,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
changelogWidget.mount();
|
|
229
|
+
changelogWidget.show();
|
|
230
|
+
|
|
231
|
+
return changelogWidget;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Get changelogs from the backend
|
|
236
|
+
* @param {Object} options - Optional query parameters
|
|
237
|
+
* @param {number} options.limit - Number of changelogs to fetch
|
|
238
|
+
* @param {number} options.offset - Offset for pagination
|
|
239
|
+
* @returns {Promise<Array>} Array of changelog entries
|
|
240
|
+
*/
|
|
241
|
+
async getChangelogs(options = {}) {
|
|
242
|
+
if (!this.initialized) {
|
|
243
|
+
throw new SDKError(
|
|
244
|
+
'SDK must be initialized before fetching changelogs. Call init() first.'
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
try {
|
|
249
|
+
const result = await this.apiService.getChangelogs(options);
|
|
250
|
+
return result.data || [];
|
|
251
|
+
} catch (error) {
|
|
252
|
+
this.eventBus.emit('sdk:error', { error });
|
|
253
|
+
throw new SDKError(`Failed to fetch changelogs: ${error.message}`, error);
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
195
257
|
getAllWidgets() {
|
|
196
258
|
return Array.from(this.widgets.values());
|
|
197
259
|
}
|
|
@@ -283,16 +345,64 @@ export class FeedbackSDK {
|
|
|
283
345
|
this.eventBus.emit('sdk:destroyed');
|
|
284
346
|
}
|
|
285
347
|
|
|
348
|
+
/**
|
|
349
|
+
* Check if feedback was recently submitted for this workspace
|
|
350
|
+
* Uses backend tracking (preferred) with localStorage as fallback
|
|
351
|
+
* @param {number} cooldownDays - Days to consider as "recently" (default: 30)
|
|
352
|
+
* @returns {boolean} true if feedback was submitted within the cooldown period
|
|
353
|
+
*/
|
|
354
|
+
hasFeedbackBeenSubmitted(cooldownDays = 30) {
|
|
355
|
+
const cooldownMs = cooldownDays * 24 * 60 * 60 * 1000;
|
|
356
|
+
const now = Date.now();
|
|
357
|
+
|
|
358
|
+
// Check backend tracking first (from init response)
|
|
359
|
+
if (this.config.last_feedback_at) {
|
|
360
|
+
try {
|
|
361
|
+
const backendTimestamp = new Date(this.config.last_feedback_at).getTime();
|
|
362
|
+
if ((now - backendTimestamp) < cooldownMs) {
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
} catch (e) {
|
|
366
|
+
// Invalid date format, continue to localStorage check
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Fallback to localStorage
|
|
371
|
+
try {
|
|
372
|
+
const storageKey = `feedback_submitted_${this.config.workspace}`;
|
|
373
|
+
const stored = localStorage.getItem(storageKey);
|
|
374
|
+
if (!stored) return false;
|
|
375
|
+
|
|
376
|
+
const data = JSON.parse(stored);
|
|
377
|
+
return (now - data.submittedAt) < cooldownMs;
|
|
378
|
+
} catch (e) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Clear the feedback submission tracking (allow showing widgets again)
|
|
385
|
+
*/
|
|
386
|
+
clearFeedbackSubmissionTracking() {
|
|
387
|
+
try {
|
|
388
|
+
const storageKey = `feedback_submitted_${this.config.workspace}`;
|
|
389
|
+
localStorage.removeItem(storageKey);
|
|
390
|
+
this.eventBus.emit('feedback:trackingCleared');
|
|
391
|
+
} catch (e) {
|
|
392
|
+
console.warn('Failed to clear feedback tracking:', e);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
286
396
|
_detectEnvironment() {
|
|
287
397
|
if (typeof window === 'undefined') {
|
|
288
398
|
return 'production';
|
|
289
399
|
}
|
|
290
400
|
|
|
291
401
|
const hostname = window.location.hostname.toLowerCase();
|
|
292
|
-
|
|
402
|
+
|
|
293
403
|
if (
|
|
294
|
-
hostname.includes('staging') ||
|
|
295
|
-
hostname.includes('localhost') ||
|
|
404
|
+
hostname.includes('staging') ||
|
|
405
|
+
hostname.includes('localhost') ||
|
|
296
406
|
hostname.includes('127.0.0.1') ||
|
|
297
407
|
hostname.includes('.local')
|
|
298
408
|
) {
|
|
@@ -313,7 +423,7 @@ export class FeedbackSDK {
|
|
|
313
423
|
autoShow: true,
|
|
314
424
|
debug: false,
|
|
315
425
|
mock: false,
|
|
316
|
-
env: this._detectEnvironment(),
|
|
426
|
+
env: this._detectEnvironment(),
|
|
317
427
|
};
|
|
318
428
|
|
|
319
429
|
const mergedConfig = deepMerge(
|
|
@@ -398,4 +508,4 @@ export class FeedbackSDK {
|
|
|
398
508
|
: undefined,
|
|
399
509
|
};
|
|
400
510
|
}
|
|
401
|
-
}
|
|
511
|
+
}
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* WebSocketService - Real-time communication for messenger widget
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export class WebSocketService {
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.baseURL = config.baseURL || '';
|
|
8
|
+
this.workspace = config.workspace || '';
|
|
9
|
+
this.sessionToken = config.sessionToken || null;
|
|
10
|
+
this.mock = config.mock || false;
|
|
11
|
+
|
|
12
|
+
this.ws = null;
|
|
13
|
+
this.reconnectAttempts = 0;
|
|
14
|
+
this.maxReconnectAttempts = 5;
|
|
15
|
+
this.reconnectDelay = 1000;
|
|
16
|
+
this.pingInterval = null;
|
|
17
|
+
this.isConnected = false;
|
|
18
|
+
|
|
19
|
+
// Event listeners
|
|
20
|
+
this._listeners = new Map();
|
|
21
|
+
|
|
22
|
+
// Bind methods
|
|
23
|
+
this._onOpen = this._onOpen.bind(this);
|
|
24
|
+
this._onMessage = this._onMessage.bind(this);
|
|
25
|
+
this._onClose = this._onClose.bind(this);
|
|
26
|
+
this._onError = this._onError.bind(this);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Connect to WebSocket server
|
|
31
|
+
*/
|
|
32
|
+
connect(sessionToken = null) {
|
|
33
|
+
if (sessionToken) {
|
|
34
|
+
this.sessionToken = sessionToken;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (!this.sessionToken) {
|
|
38
|
+
console.warn('[WebSocket] No session token provided');
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Mock mode - simulate connection
|
|
43
|
+
if (this.mock) {
|
|
44
|
+
this.isConnected = true;
|
|
45
|
+
this._emit('connected', {});
|
|
46
|
+
this._startMockResponses();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Build WebSocket URL
|
|
51
|
+
const wsProtocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
52
|
+
let wsURL = this.baseURL.replace(/^https?:/, wsProtocol);
|
|
53
|
+
wsURL = wsURL.replace('/api/v1', '');
|
|
54
|
+
wsURL = `${wsURL}/api/v1/widget/messenger/ws?token=${encodeURIComponent(this.sessionToken)}`;
|
|
55
|
+
|
|
56
|
+
try {
|
|
57
|
+
this.ws = new WebSocket(wsURL);
|
|
58
|
+
this.ws.onopen = this._onOpen;
|
|
59
|
+
this.ws.onmessage = this._onMessage;
|
|
60
|
+
this.ws.onclose = this._onClose;
|
|
61
|
+
this.ws.onerror = this._onError;
|
|
62
|
+
} catch (error) {
|
|
63
|
+
console.error('[WebSocket] Connection error:', error);
|
|
64
|
+
this._scheduleReconnect();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Disconnect from WebSocket server
|
|
70
|
+
*/
|
|
71
|
+
disconnect() {
|
|
72
|
+
this.isConnected = false;
|
|
73
|
+
this.reconnectAttempts = this.maxReconnectAttempts; // Prevent reconnection
|
|
74
|
+
|
|
75
|
+
if (this.pingInterval) {
|
|
76
|
+
clearInterval(this.pingInterval);
|
|
77
|
+
this.pingInterval = null;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (this.ws) {
|
|
81
|
+
this.ws.close();
|
|
82
|
+
this.ws = null;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (this._mockInterval) {
|
|
86
|
+
clearInterval(this._mockInterval);
|
|
87
|
+
this._mockInterval = null;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Subscribe to events
|
|
93
|
+
* @param {string} event - Event name
|
|
94
|
+
* @param {Function} callback - Event handler
|
|
95
|
+
* @returns {Function} Unsubscribe function
|
|
96
|
+
*/
|
|
97
|
+
on(event, callback) {
|
|
98
|
+
if (!this._listeners.has(event)) {
|
|
99
|
+
this._listeners.set(event, new Set());
|
|
100
|
+
}
|
|
101
|
+
this._listeners.get(event).add(callback);
|
|
102
|
+
return () => this._listeners.get(event).delete(callback);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Remove event listener
|
|
107
|
+
*/
|
|
108
|
+
off(event, callback) {
|
|
109
|
+
if (this._listeners.has(event)) {
|
|
110
|
+
this._listeners.get(event).delete(callback);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Send message through WebSocket
|
|
116
|
+
*/
|
|
117
|
+
send(type, payload = {}) {
|
|
118
|
+
if (!this.isConnected) {
|
|
119
|
+
console.warn('[WebSocket] Not connected, cannot send message');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (this.mock) {
|
|
124
|
+
// Mock mode - just log
|
|
125
|
+
console.log('[WebSocket Mock] Sending:', type, payload);
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (this.ws && this.ws.readyState === WebSocket.OPEN) {
|
|
130
|
+
this.ws.send(JSON.stringify({ type, payload }));
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Private methods
|
|
135
|
+
|
|
136
|
+
_onOpen() {
|
|
137
|
+
console.log('[WebSocket] Connected');
|
|
138
|
+
this.isConnected = true;
|
|
139
|
+
this.reconnectAttempts = 0;
|
|
140
|
+
this._emit('connected', {});
|
|
141
|
+
|
|
142
|
+
// Start ping interval to keep connection alive
|
|
143
|
+
this.pingInterval = setInterval(() => {
|
|
144
|
+
this.send('ping', {});
|
|
145
|
+
}, 30000);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
_onMessage(event) {
|
|
149
|
+
try {
|
|
150
|
+
const data = JSON.parse(event.data);
|
|
151
|
+
const { type, payload } = data;
|
|
152
|
+
|
|
153
|
+
// Handle different event types
|
|
154
|
+
switch (type) {
|
|
155
|
+
case 'message:new':
|
|
156
|
+
this._emit('message', payload);
|
|
157
|
+
break;
|
|
158
|
+
case 'typing:started':
|
|
159
|
+
this._emit('typing_started', payload);
|
|
160
|
+
break;
|
|
161
|
+
case 'typing:stopped':
|
|
162
|
+
this._emit('typing_stopped', payload);
|
|
163
|
+
break;
|
|
164
|
+
case 'conversation:updated':
|
|
165
|
+
this._emit('conversation_updated', payload);
|
|
166
|
+
break;
|
|
167
|
+
case 'conversation:closed':
|
|
168
|
+
this._emit('conversation_closed', payload);
|
|
169
|
+
break;
|
|
170
|
+
case 'availability:changed':
|
|
171
|
+
this._emit('availability_changed', payload);
|
|
172
|
+
break;
|
|
173
|
+
case 'pong':
|
|
174
|
+
// Ping response, ignore
|
|
175
|
+
break;
|
|
176
|
+
default:
|
|
177
|
+
console.log('[WebSocket] Unknown event:', type, payload);
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
console.error('[WebSocket] Failed to parse message:', error);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
_onClose(event) {
|
|
185
|
+
console.log('[WebSocket] Disconnected:', event.code, event.reason);
|
|
186
|
+
this.isConnected = false;
|
|
187
|
+
|
|
188
|
+
if (this.pingInterval) {
|
|
189
|
+
clearInterval(this.pingInterval);
|
|
190
|
+
this.pingInterval = null;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
this._emit('disconnected', { code: event.code, reason: event.reason });
|
|
194
|
+
this._scheduleReconnect();
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
_onError(error) {
|
|
198
|
+
console.error('[WebSocket] Error:', error);
|
|
199
|
+
this._emit('error', { error });
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
_scheduleReconnect() {
|
|
203
|
+
if (this.reconnectAttempts >= this.maxReconnectAttempts) {
|
|
204
|
+
console.log('[WebSocket] Max reconnect attempts reached');
|
|
205
|
+
this._emit('reconnect_failed', {});
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
this.reconnectAttempts++;
|
|
210
|
+
const delay = this.reconnectDelay * Math.pow(2, this.reconnectAttempts - 1);
|
|
211
|
+
console.log(`[WebSocket] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempts})`);
|
|
212
|
+
|
|
213
|
+
setTimeout(() => {
|
|
214
|
+
this.connect();
|
|
215
|
+
}, delay);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
_emit(event, data) {
|
|
219
|
+
if (this._listeners.has(event)) {
|
|
220
|
+
this._listeners.get(event).forEach((callback) => {
|
|
221
|
+
try {
|
|
222
|
+
callback(data);
|
|
223
|
+
} catch (error) {
|
|
224
|
+
console.error(`[WebSocket] Error in ${event} handler:`, error);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// Mock support for development
|
|
231
|
+
_startMockResponses() {
|
|
232
|
+
// Simulate agent typing and responses
|
|
233
|
+
this._mockInterval = setInterval(() => {
|
|
234
|
+
// Randomly emit typing or message events for demo
|
|
235
|
+
const random = Math.random();
|
|
236
|
+
if (random < 0.1) {
|
|
237
|
+
this._emit('typing_started', {
|
|
238
|
+
conversation_id: 'conv_1',
|
|
239
|
+
user_id: 'agent_1',
|
|
240
|
+
user_name: 'Sarah',
|
|
241
|
+
is_agent: true,
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
// Stop typing after 2 seconds
|
|
245
|
+
setTimeout(() => {
|
|
246
|
+
this._emit('typing_stopped', {
|
|
247
|
+
conversation_id: 'conv_1',
|
|
248
|
+
user_id: 'agent_1',
|
|
249
|
+
});
|
|
250
|
+
}, 2000);
|
|
251
|
+
}
|
|
252
|
+
}, 10000);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Simulate receiving a message (for mock mode)
|
|
257
|
+
*/
|
|
258
|
+
simulateMessage(conversationId, message) {
|
|
259
|
+
if (this.mock) {
|
|
260
|
+
this._emit('message', {
|
|
261
|
+
conversation_id: conversationId,
|
|
262
|
+
message: {
|
|
263
|
+
id: 'msg_' + Date.now(),
|
|
264
|
+
content: message.content,
|
|
265
|
+
sender_type: message.sender_type || 'agent',
|
|
266
|
+
sender_name: message.sender_name || 'Support',
|
|
267
|
+
sender_avatar: message.sender_avatar || null,
|
|
268
|
+
created_at: new Date().toISOString(),
|
|
269
|
+
},
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
}
|
package/src/index.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import * as helpers from './utils/helpers.js';
|
|
14
14
|
import { BaseWidget } from './widgets/BaseWidget.js';
|
|
15
15
|
import { ButtonWidget } from './widgets/ButtonWidget.js';
|
|
16
|
+
import { ChangelogWidget } from './widgets/ChangelogWidget.js';
|
|
16
17
|
import { InlineWidget } from './widgets/InlineWidget.js';
|
|
17
18
|
import { MessengerWidget } from './widgets/MessengerWidget.js';
|
|
18
19
|
import { SurveyWidget } from './widgets/SurveyWidget.js';
|
|
@@ -95,6 +96,7 @@ const FeedbackSDKExport = {
|
|
|
95
96
|
FeedbackSDK,
|
|
96
97
|
BaseWidget,
|
|
97
98
|
ButtonWidget,
|
|
99
|
+
ChangelogWidget,
|
|
98
100
|
TabWidget,
|
|
99
101
|
InlineWidget,
|
|
100
102
|
SurveyWidget,
|
|
@@ -180,6 +182,7 @@ export {
|
|
|
180
182
|
APIService,
|
|
181
183
|
BaseWidget,
|
|
182
184
|
ButtonWidget,
|
|
185
|
+
ChangelogWidget,
|
|
183
186
|
ConfigError,
|
|
184
187
|
EventBus,
|
|
185
188
|
FeedbackSDK,
|
|
@@ -1623,6 +1623,106 @@ export const MESSENGER_STYLES = `
|
|
|
1623
1623
|
}
|
|
1624
1624
|
}
|
|
1625
1625
|
|
|
1626
|
+
/* ========================================
|
|
1627
|
+
Availability Status
|
|
1628
|
+
======================================== */
|
|
1629
|
+
|
|
1630
|
+
.messenger-home-availability,
|
|
1631
|
+
.messenger-chat-availability {
|
|
1632
|
+
display: flex;
|
|
1633
|
+
align-items: center;
|
|
1634
|
+
gap: 6px;
|
|
1635
|
+
margin-top: 8px;
|
|
1636
|
+
font-size: 13px;
|
|
1637
|
+
color: rgba(255, 255, 255, 0.7);
|
|
1638
|
+
}
|
|
1639
|
+
|
|
1640
|
+
.theme-light .messenger-home-availability,
|
|
1641
|
+
.theme-light .messenger-chat-availability {
|
|
1642
|
+
color: #6b7280;
|
|
1643
|
+
}
|
|
1644
|
+
|
|
1645
|
+
.messenger-availability-dot {
|
|
1646
|
+
width: 8px;
|
|
1647
|
+
height: 8px;
|
|
1648
|
+
border-radius: 50%;
|
|
1649
|
+
flex-shrink: 0;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
.messenger-availability-online {
|
|
1653
|
+
background: #22c55e;
|
|
1654
|
+
box-shadow: 0 0 0 2px rgba(34, 197, 94, 0.2);
|
|
1655
|
+
}
|
|
1656
|
+
|
|
1657
|
+
.messenger-availability-away {
|
|
1658
|
+
background: #9ca3af;
|
|
1659
|
+
}
|
|
1660
|
+
|
|
1661
|
+
.messenger-availability-text {
|
|
1662
|
+
opacity: 0.9;
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/* ========================================
|
|
1666
|
+
Typing Indicator
|
|
1667
|
+
======================================== */
|
|
1668
|
+
|
|
1669
|
+
.messenger-typing-indicator {
|
|
1670
|
+
display: flex;
|
|
1671
|
+
align-items: center;
|
|
1672
|
+
gap: 8px;
|
|
1673
|
+
padding: 8px 12px;
|
|
1674
|
+
margin: 4px 0;
|
|
1675
|
+
}
|
|
1676
|
+
|
|
1677
|
+
.messenger-typing-dots {
|
|
1678
|
+
display: flex;
|
|
1679
|
+
align-items: center;
|
|
1680
|
+
gap: 4px;
|
|
1681
|
+
background: #374151;
|
|
1682
|
+
padding: 8px 12px;
|
|
1683
|
+
border-radius: 16px;
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
.theme-light .messenger-typing-dots {
|
|
1687
|
+
background: #e5e7eb;
|
|
1688
|
+
}
|
|
1689
|
+
|
|
1690
|
+
.messenger-typing-dots span {
|
|
1691
|
+
width: 6px;
|
|
1692
|
+
height: 6px;
|
|
1693
|
+
background: #9ca3af;
|
|
1694
|
+
border-radius: 50%;
|
|
1695
|
+
animation: messenger-typing-bounce 1.4s infinite ease-in-out;
|
|
1696
|
+
}
|
|
1697
|
+
|
|
1698
|
+
.messenger-typing-dots span:nth-child(1) {
|
|
1699
|
+
animation-delay: -0.32s;
|
|
1700
|
+
}
|
|
1701
|
+
|
|
1702
|
+
.messenger-typing-dots span:nth-child(2) {
|
|
1703
|
+
animation-delay: -0.16s;
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
.messenger-typing-dots span:nth-child(3) {
|
|
1707
|
+
animation-delay: 0s;
|
|
1708
|
+
}
|
|
1709
|
+
|
|
1710
|
+
.messenger-typing-text {
|
|
1711
|
+
font-size: 12px;
|
|
1712
|
+
color: #9ca3af;
|
|
1713
|
+
}
|
|
1714
|
+
|
|
1715
|
+
@keyframes messenger-typing-bounce {
|
|
1716
|
+
0%, 80%, 100% {
|
|
1717
|
+
transform: scale(0.8);
|
|
1718
|
+
opacity: 0.5;
|
|
1719
|
+
}
|
|
1720
|
+
40% {
|
|
1721
|
+
transform: scale(1);
|
|
1722
|
+
opacity: 1;
|
|
1723
|
+
}
|
|
1724
|
+
}
|
|
1725
|
+
|
|
1626
1726
|
/* ========================================
|
|
1627
1727
|
Animations
|
|
1628
1728
|
======================================== */
|