turbodesk-livechat-react-native 0.1.0-alpha.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.
Files changed (171) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/README.md +91 -0
  3. package/dist/api/conversation-api.d.ts +16 -0
  4. package/dist/api/conversation-api.d.ts.map +1 -0
  5. package/dist/api/conversation-api.js +44 -0
  6. package/dist/api/conversation-api.js.map +1 -0
  7. package/dist/api/file-api.d.ts +5 -0
  8. package/dist/api/file-api.d.ts.map +1 -0
  9. package/dist/api/file-api.js +15 -0
  10. package/dist/api/file-api.js.map +1 -0
  11. package/dist/api/widget-api.d.ts +4 -0
  12. package/dist/api/widget-api.d.ts.map +1 -0
  13. package/dist/api/widget-api.js +15 -0
  14. package/dist/api/widget-api.js.map +1 -0
  15. package/dist/axios/axios.d.ts +32 -0
  16. package/dist/axios/axios.d.ts.map +1 -0
  17. package/dist/axios/axios.js +120 -0
  18. package/dist/axios/axios.js.map +1 -0
  19. package/dist/core/config.d.ts +17 -0
  20. package/dist/core/config.d.ts.map +1 -0
  21. package/dist/core/config.js +42 -0
  22. package/dist/core/config.js.map +1 -0
  23. package/dist/core/http-client.d.ts +33 -0
  24. package/dist/core/http-client.d.ts.map +1 -0
  25. package/dist/core/http-client.js +104 -0
  26. package/dist/core/http-client.js.map +1 -0
  27. package/dist/core/identity.d.ts +7 -0
  28. package/dist/core/identity.d.ts.map +1 -0
  29. package/dist/core/identity.js +62 -0
  30. package/dist/core/identity.js.map +1 -0
  31. package/dist/core/visitor-params.d.ts +15 -0
  32. package/dist/core/visitor-params.d.ts.map +1 -0
  33. package/dist/core/visitor-params.js +45 -0
  34. package/dist/core/visitor-params.js.map +1 -0
  35. package/dist/hooks/use-conversations.d.ts +12 -0
  36. package/dist/hooks/use-conversations.d.ts.map +1 -0
  37. package/dist/hooks/use-conversations.js +177 -0
  38. package/dist/hooks/use-conversations.js.map +1 -0
  39. package/dist/hooks/use-live-chat.d.ts +30 -0
  40. package/dist/hooks/use-live-chat.d.ts.map +1 -0
  41. package/dist/hooks/use-live-chat.js +52 -0
  42. package/dist/hooks/use-live-chat.js.map +1 -0
  43. package/dist/hooks/use-messages.d.ts +11 -0
  44. package/dist/hooks/use-messages.d.ts.map +1 -0
  45. package/dist/hooks/use-messages.js +185 -0
  46. package/dist/hooks/use-messages.js.map +1 -0
  47. package/dist/hooks/use-send-message.d.ts +22 -0
  48. package/dist/hooks/use-send-message.d.ts.map +1 -0
  49. package/dist/hooks/use-send-message.js +125 -0
  50. package/dist/hooks/use-send-message.js.map +1 -0
  51. package/dist/index.d.ts +49 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +97 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/navigation/LiveChatPanel.d.ts +5 -0
  56. package/dist/navigation/LiveChatPanel.d.ts.map +1 -0
  57. package/dist/navigation/LiveChatPanel.js +81 -0
  58. package/dist/navigation/LiveChatPanel.js.map +1 -0
  59. package/dist/navigation/panel-router-context.d.ts +22 -0
  60. package/dist/navigation/panel-router-context.d.ts.map +1 -0
  61. package/dist/navigation/panel-router-context.js +42 -0
  62. package/dist/navigation/panel-router-context.js.map +1 -0
  63. package/dist/navigation/router-types.d.ts +2 -0
  64. package/dist/navigation/router-types.d.ts.map +1 -0
  65. package/dist/navigation/router-types.js +3 -0
  66. package/dist/navigation/router-types.js.map +1 -0
  67. package/dist/provider/LiveChatContext.d.ts +4 -0
  68. package/dist/provider/LiveChatContext.d.ts.map +1 -0
  69. package/dist/provider/LiveChatContext.js +35 -0
  70. package/dist/provider/LiveChatContext.js.map +1 -0
  71. package/dist/provider/LiveChatProvider.d.ts +3 -0
  72. package/dist/provider/LiveChatProvider.d.ts.map +1 -0
  73. package/dist/provider/LiveChatProvider.js +308 -0
  74. package/dist/provider/LiveChatProvider.js.map +1 -0
  75. package/dist/provider/types.d.ts +42 -0
  76. package/dist/provider/types.d.ts.map +1 -0
  77. package/dist/provider/types.js +3 -0
  78. package/dist/provider/types.js.map +1 -0
  79. package/dist/realtime/ws-client.d.ts +51 -0
  80. package/dist/realtime/ws-client.d.ts.map +1 -0
  81. package/dist/realtime/ws-client.js +322 -0
  82. package/dist/realtime/ws-client.js.map +1 -0
  83. package/dist/ui/components/AssigneeAvatar.d.ts +12 -0
  84. package/dist/ui/components/AssigneeAvatar.d.ts.map +1 -0
  85. package/dist/ui/components/AssigneeAvatar.js +58 -0
  86. package/dist/ui/components/AssigneeAvatar.js.map +1 -0
  87. package/dist/ui/components/Avatar.d.ts +10 -0
  88. package/dist/ui/components/Avatar.d.ts.map +1 -0
  89. package/dist/ui/components/Avatar.js +76 -0
  90. package/dist/ui/components/Avatar.js.map +1 -0
  91. package/dist/ui/components/ConversationHeader.d.ts +10 -0
  92. package/dist/ui/components/ConversationHeader.d.ts.map +1 -0
  93. package/dist/ui/components/ConversationHeader.js +90 -0
  94. package/dist/ui/components/ConversationHeader.js.map +1 -0
  95. package/dist/ui/components/ConversationListScreen.d.ts +9 -0
  96. package/dist/ui/components/ConversationListScreen.d.ts.map +1 -0
  97. package/dist/ui/components/ConversationListScreen.js +350 -0
  98. package/dist/ui/components/ConversationListScreen.js.map +1 -0
  99. package/dist/ui/components/ConversationScreen.d.ts +8 -0
  100. package/dist/ui/components/ConversationScreen.d.ts.map +1 -0
  101. package/dist/ui/components/ConversationScreen.js +235 -0
  102. package/dist/ui/components/ConversationScreen.js.map +1 -0
  103. package/dist/ui/components/HomeScreen.d.ts +6 -0
  104. package/dist/ui/components/HomeScreen.d.ts.map +1 -0
  105. package/dist/ui/components/HomeScreen.js +133 -0
  106. package/dist/ui/components/HomeScreen.js.map +1 -0
  107. package/dist/ui/components/LivechatMessageRenderer.d.ts +17 -0
  108. package/dist/ui/components/LivechatMessageRenderer.d.ts.map +1 -0
  109. package/dist/ui/components/LivechatMessageRenderer.js +122 -0
  110. package/dist/ui/components/LivechatMessageRenderer.js.map +1 -0
  111. package/dist/ui/components/LogMessage.d.ts +5 -0
  112. package/dist/ui/components/LogMessage.d.ts.map +1 -0
  113. package/dist/ui/components/LogMessage.js +83 -0
  114. package/dist/ui/components/LogMessage.js.map +1 -0
  115. package/dist/ui/components/MessageBubble.d.ts +15 -0
  116. package/dist/ui/components/MessageBubble.d.ts.map +1 -0
  117. package/dist/ui/components/MessageBubble.js +84 -0
  118. package/dist/ui/components/MessageBubble.js.map +1 -0
  119. package/dist/ui/components/MessageComposer.d.ts +31 -0
  120. package/dist/ui/components/MessageComposer.d.ts.map +1 -0
  121. package/dist/ui/components/MessageComposer.js +295 -0
  122. package/dist/ui/components/MessageComposer.js.map +1 -0
  123. package/dist/ui/components/WsStatusStrip.d.ts +2 -0
  124. package/dist/ui/components/WsStatusStrip.d.ts.map +1 -0
  125. package/dist/ui/components/WsStatusStrip.js +103 -0
  126. package/dist/ui/components/WsStatusStrip.js.map +1 -0
  127. package/dist/ui/icons.d.ts +22 -0
  128. package/dist/ui/icons.d.ts.map +1 -0
  129. package/dist/ui/icons.js +71 -0
  130. package/dist/ui/icons.js.map +1 -0
  131. package/dist/ui/theme.d.ts +72 -0
  132. package/dist/ui/theme.d.ts.map +1 -0
  133. package/dist/ui/theme.js +170 -0
  134. package/dist/ui/theme.js.map +1 -0
  135. package/docs/backend-contract.md +392 -0
  136. package/docs/migration-notes.md +32 -0
  137. package/package.json +60 -0
  138. package/src/api/conversation-api.ts +71 -0
  139. package/src/api/file-api.ts +14 -0
  140. package/src/api/widget-api.ts +12 -0
  141. package/src/axios/axios.ts +159 -0
  142. package/src/core/config.ts +54 -0
  143. package/src/core/http-client.ts +136 -0
  144. package/src/core/identity.ts +68 -0
  145. package/src/core/visitor-params.ts +48 -0
  146. package/src/hooks/use-conversations.ts +181 -0
  147. package/src/hooks/use-live-chat.ts +84 -0
  148. package/src/hooks/use-messages.ts +188 -0
  149. package/src/hooks/use-send-message.ts +159 -0
  150. package/src/index.ts +114 -0
  151. package/src/navigation/LiveChatPanel.tsx +118 -0
  152. package/src/navigation/panel-router-context.tsx +89 -0
  153. package/src/navigation/router-types.ts +1 -0
  154. package/src/provider/LiveChatContext.ts +33 -0
  155. package/src/provider/LiveChatProvider.tsx +380 -0
  156. package/src/provider/types.ts +57 -0
  157. package/src/realtime/ws-client.ts +369 -0
  158. package/src/types/react-native-svg.d.ts +10 -0
  159. package/src/ui/components/AssigneeAvatar.tsx +102 -0
  160. package/src/ui/components/Avatar.tsx +110 -0
  161. package/src/ui/components/ConversationHeader.tsx +202 -0
  162. package/src/ui/components/ConversationListScreen.tsx +454 -0
  163. package/src/ui/components/ConversationScreen.tsx +362 -0
  164. package/src/ui/components/HomeScreen.tsx +278 -0
  165. package/src/ui/components/LivechatMessageRenderer.tsx +268 -0
  166. package/src/ui/components/LogMessage.tsx +88 -0
  167. package/src/ui/components/MessageBubble.tsx +148 -0
  168. package/src/ui/components/MessageComposer.tsx +461 -0
  169. package/src/ui/components/WsStatusStrip.tsx +123 -0
  170. package/src/ui/icons.tsx +111 -0
  171. package/src/ui/theme.ts +237 -0
@@ -0,0 +1,369 @@
1
+ import { AppState, type AppStateStatus } from "react-native";
2
+ import { getApiBaseUrl } from "../axios/axios";
3
+
4
+ function buildWsBaseUrl(apiBaseUrl: string): string {
5
+ return apiBaseUrl
6
+ .replace(/\/+$/, "")
7
+ .replace(/^https:\/\//i, "wss://")
8
+ .replace(/^http:\/\//i, "ws://");
9
+ }
10
+
11
+ // ─── constants ────────────────────────────────────────────────────────────────
12
+ const INITIAL_RETRY_DELAY_MS = 1_000;
13
+ const MAX_RETRY_DELAY_MS = 30_000;
14
+ const BACKOFF_MULTIPLIER = 2;
15
+ const CONNECTING_TIMEOUT_MS = 20_000;
16
+ const IDLE_SAFETY_MS = 30 * 60 * 1_000;
17
+ const STATUS_POLL_INTERVAL_MS = 3_000;
18
+ const MIN_RECONNECT_INTERVAL_MS = 2_000;
19
+
20
+ type WsEventCallback = (data: unknown) => void;
21
+
22
+ export type WsConnectionState = {
23
+ isConnected: boolean;
24
+ isConnecting: boolean;
25
+ isAwaitingRetry: boolean;
26
+ lastError: Error | null;
27
+ };
28
+
29
+ type StateChangeListener = (state: WsConnectionState) => void;
30
+
31
+ // Optional NetInfo — loaded lazily so the package works without it
32
+ let NetInfo: {
33
+ addEventListener(
34
+ cb: (s: { isConnected: boolean | null }) => void
35
+ ): () => void;
36
+ } | null = null;
37
+ try {
38
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
39
+ NetInfo = require("@react-native-community/netinfo").default;
40
+ } catch {
41
+ NetInfo = null;
42
+ }
43
+
44
+ function buildWsUrl(
45
+ widgetId: string,
46
+ userId?: string,
47
+ userToken?: string
48
+ ): string {
49
+ const base = buildWsBaseUrl(getApiBaseUrl());
50
+ let query = "widgetId=" + encodeURIComponent(widgetId);
51
+ if (userToken) query += "&userToken=" + encodeURIComponent(userToken);
52
+ else if (userId) query += "&userId=" + encodeURIComponent(userId);
53
+ return base + "/api/v1/ws/livechat?" + query;
54
+ }
55
+
56
+ // ─── WsClient class ───────────────────────────────────────────────────────────
57
+ export class WsClient {
58
+ private widgetId: string;
59
+ private userId: string | undefined;
60
+ private userToken: string | undefined;
61
+
62
+ private socket: WebSocket | null = null;
63
+ private retryCount = 0;
64
+ private retryTimeout: ReturnType<typeof setTimeout> | null = null;
65
+ private statusInterval: ReturnType<typeof setInterval> | null = null;
66
+ private isConnectingFlag = false;
67
+ private connectionStartedAt = 0;
68
+ private lastMessageTime = Date.now();
69
+ private lastReconnectAt = 0;
70
+ private appStateSubscription: { remove(): void } | null = null;
71
+ private netInfoUnsubscribe: (() => void) | null = null;
72
+
73
+ private state: WsConnectionState = {
74
+ isConnected: false,
75
+ isConnecting: false,
76
+ isAwaitingRetry: false,
77
+ lastError: null,
78
+ };
79
+ private stateListeners: Set<StateChangeListener> = new Set();
80
+ private eventListeners: Map<string, WsEventCallback[]> = new Map();
81
+
82
+ constructor(opts: {
83
+ widgetId: string;
84
+ userId?: string;
85
+ userToken?: string;
86
+ }) {
87
+ this.widgetId = opts.widgetId;
88
+ this.userId = opts.userId;
89
+ this.userToken = opts.userToken;
90
+ }
91
+
92
+ // ── identity update ─────────────────────────────────────────────────────────
93
+ updateIdentity(opts: { userId?: string; userToken?: string }): void {
94
+ const changed =
95
+ opts.userId !== this.userId || opts.userToken !== this.userToken;
96
+ this.userId = opts.userId;
97
+ this.userToken = opts.userToken;
98
+ if (changed && this.widgetId) {
99
+ this.reconnect("identity-changed", true);
100
+ }
101
+ }
102
+
103
+ // ── public API ──────────────────────────────────────────────────────────────
104
+ start(): void {
105
+ this.connect();
106
+ this.startStatusPoller();
107
+ this.bindAppState();
108
+ this.bindNetInfo();
109
+ }
110
+
111
+ destroy(): void {
112
+ this.clearRetry();
113
+ this.stopStatusPoller();
114
+ this.appStateSubscription?.remove();
115
+ this.appStateSubscription = null;
116
+ this.netInfoUnsubscribe?.();
117
+ this.netInfoUnsubscribe = null;
118
+ if (this.socket) {
119
+ const s = this.socket;
120
+ this.socket = null;
121
+ s.close();
122
+ }
123
+ this.setState({
124
+ isConnected: false,
125
+ isConnecting: false,
126
+ isAwaitingRetry: false,
127
+ lastError: null,
128
+ });
129
+ }
130
+
131
+ reconnect(reason = "manual", force = false): void {
132
+ if (!this.widgetId) return;
133
+ const now = Date.now();
134
+ if (!force && now - this.lastReconnectAt < MIN_RECONNECT_INTERVAL_MS)
135
+ return;
136
+ this.lastReconnectAt = now;
137
+
138
+ this.clearRetry();
139
+ if (this.socket) {
140
+ const s = this.socket;
141
+ this.socket = null;
142
+ s.close();
143
+ }
144
+ this.retryCount = 0;
145
+ this.isConnectingFlag = false;
146
+ this.connectionStartedAt = 0;
147
+ this.setState({
148
+ isConnected: false,
149
+ isConnecting: false,
150
+ isAwaitingRetry: false,
151
+ lastError: null,
152
+ });
153
+ this.connect();
154
+ }
155
+
156
+ subscribe(eventType: string, cb: WsEventCallback): () => void {
157
+ if (!this.eventListeners.has(eventType))
158
+ this.eventListeners.set(eventType, []);
159
+ const list = this.eventListeners.get(eventType)!;
160
+ if (!list.includes(cb)) list.push(cb);
161
+ return () => {
162
+ const l = this.eventListeners.get(eventType);
163
+ if (!l) return;
164
+ const idx = l.indexOf(cb);
165
+ if (idx > -1) l.splice(idx, 1);
166
+ if (l.length === 0) this.eventListeners.delete(eventType);
167
+ };
168
+ }
169
+
170
+ onStateChange(listener: StateChangeListener): () => void {
171
+ this.stateListeners.add(listener);
172
+ // Immediately emit current state
173
+ listener(this.state);
174
+ return () => {
175
+ this.stateListeners.delete(listener);
176
+ };
177
+ }
178
+
179
+ getState(): WsConnectionState {
180
+ return this.state;
181
+ }
182
+
183
+ // ── internals ───────────────────────────────────────────────────────────────
184
+ private setState(next: Partial<WsConnectionState>): void {
185
+ this.state = { ...this.state, ...next };
186
+ this.stateListeners.forEach((l) => {
187
+ try {
188
+ l(this.state);
189
+ } catch {
190
+ /* ignore listener errors */
191
+ }
192
+ });
193
+ }
194
+
195
+ private connect(): void {
196
+ if (!this.widgetId || this.isConnectingFlag) return;
197
+
198
+ this.isConnectingFlag = true;
199
+ this.clearRetry();
200
+ this.setState({ isConnecting: true, isAwaitingRetry: false });
201
+ this.connectionStartedAt = Date.now();
202
+
203
+ if (this.socket) {
204
+ const s = this.socket;
205
+ this.socket = null;
206
+ s.close();
207
+ }
208
+
209
+ const url = buildWsUrl(
210
+ this.widgetId,
211
+ this.userId,
212
+ this.userToken
213
+ );
214
+
215
+ const socket = new (global as any).WebSocket(url) as WebSocket;
216
+ this.socket = socket;
217
+ this.lastMessageTime = Date.now();
218
+
219
+ const isActive = () => this.socket === socket;
220
+
221
+ const scheduleReconnect = () => {
222
+ const delay = Math.min(
223
+ INITIAL_RETRY_DELAY_MS * Math.pow(BACKOFF_MULTIPLIER, this.retryCount),
224
+ MAX_RETRY_DELAY_MS
225
+ );
226
+ this.retryCount += 1;
227
+ this.isConnectingFlag = false;
228
+ this.connectionStartedAt = 0;
229
+ this.setState({ isConnecting: false, isAwaitingRetry: true });
230
+ this.retryTimeout = setTimeout(() => {
231
+ this.retryTimeout = null;
232
+ if (this.widgetId) this.connect();
233
+ }, delay);
234
+ };
235
+
236
+ socket.onopen = () => {
237
+ if (!isActive()) return;
238
+ this.retryCount = 0;
239
+ this.isConnectingFlag = false;
240
+ this.connectionStartedAt = 0;
241
+ this.lastMessageTime = Date.now();
242
+ this.setState({
243
+ isConnected: true,
244
+ isConnecting: false,
245
+ isAwaitingRetry: false,
246
+ lastError: null,
247
+ });
248
+ };
249
+
250
+ socket.onerror = (ev: Event) => {
251
+ if (!isActive()) return;
252
+ this.setState({ lastError: new Error("WebSocket error") });
253
+ };
254
+
255
+ socket.onclose = () => {
256
+ if (!isActive()) return;
257
+ this.socket = null;
258
+ this.isConnectingFlag = false;
259
+ this.connectionStartedAt = 0;
260
+ this.setState({ isConnected: false, isConnecting: false });
261
+ if (this.widgetId) scheduleReconnect();
262
+ };
263
+
264
+ socket.onmessage = (ev) => {
265
+ if (!isActive()) return;
266
+ this.lastMessageTime = Date.now();
267
+ try {
268
+ const raw = typeof ev.data === "string" ? ev.data : String(ev.data);
269
+ const data = JSON.parse(raw) as { type?: string };
270
+ const type = data.type ?? "";
271
+ const dispatch = (list: WsEventCallback[]) => {
272
+ for (const cb of list) {
273
+ try {
274
+ cb(data);
275
+ } catch (e) {
276
+ console.error(
277
+ "[WS] Listener error for " + JSON.stringify(type) + ":",
278
+ e
279
+ );
280
+ }
281
+ }
282
+ };
283
+ dispatch([...(this.eventListeners.get(type) ?? [])]);
284
+ dispatch([...(this.eventListeners.get("*") ?? [])]);
285
+ } catch {
286
+ // ignore invalid JSON
287
+ }
288
+ };
289
+ }
290
+
291
+ private clearRetry(): void {
292
+ if (this.retryTimeout) {
293
+ clearTimeout(this.retryTimeout);
294
+ this.retryTimeout = null;
295
+ }
296
+ }
297
+
298
+ private startStatusPoller(): void {
299
+ this.statusInterval = setInterval(() => {
300
+ const ws = this.socket;
301
+ if (!ws) {
302
+ this.setState({ isConnected: false, isConnecting: false });
303
+ if (this.widgetId && !this.isConnectingFlag && !this.retryTimeout) {
304
+ this.reconnect("no-socket");
305
+ }
306
+ return;
307
+ }
308
+
309
+ const OPEN = 1;
310
+ const CONNECTING = 0;
311
+ const CLOSED = 3;
312
+
313
+ if (ws.readyState === OPEN) {
314
+ this.setState({ isConnected: true, isConnecting: false });
315
+ if (Date.now() - this.lastMessageTime > IDLE_SAFETY_MS) {
316
+ this.reconnect("idle-safety", true);
317
+ }
318
+ } else if (ws.readyState === CONNECTING) {
319
+ this.setState({ isConnected: false, isConnecting: true });
320
+ if (
321
+ this.connectionStartedAt &&
322
+ Date.now() - this.connectionStartedAt > CONNECTING_TIMEOUT_MS
323
+ ) {
324
+ this.reconnect("connecting-timeout", true);
325
+ }
326
+ } else if (ws.readyState === CLOSED) {
327
+ this.setState({ isConnected: false, isConnecting: false });
328
+ if (this.widgetId && !this.isConnectingFlag && !this.retryTimeout) {
329
+ this.reconnect("closed");
330
+ }
331
+ }
332
+ }, STATUS_POLL_INTERVAL_MS);
333
+ }
334
+
335
+ private stopStatusPoller(): void {
336
+ if (this.statusInterval) {
337
+ clearInterval(this.statusInterval);
338
+ this.statusInterval = null;
339
+ }
340
+ }
341
+
342
+ private recover(reason: string, force = false): void {
343
+ if (!this.widgetId) return;
344
+ const ws = this.socket;
345
+ const stale = Date.now() - this.lastMessageTime > IDLE_SAFETY_MS;
346
+ if (!ws || ws.readyState === 3 || stale) this.reconnect(reason, force);
347
+ }
348
+
349
+ private bindAppState(): void {
350
+ this.appStateSubscription = AppState.addEventListener(
351
+ "change",
352
+ (nextState: AppStateStatus) => {
353
+ if (nextState === "active") this.recover("app-active");
354
+ }
355
+ );
356
+ }
357
+
358
+ private bindNetInfo(): void {
359
+ if (!NetInfo) return;
360
+ let prevConnected: boolean | null = null;
361
+ this.netInfoUnsubscribe = NetInfo.addEventListener((s) => {
362
+ const isConn = s.isConnected === true;
363
+ if (isConn && prevConnected === false) {
364
+ this.reconnect("network-restored", true);
365
+ }
366
+ prevConnected = isConn;
367
+ });
368
+ }
369
+ }
@@ -0,0 +1,10 @@
1
+ declare module "react-native-svg" {
2
+ import React from "react";
3
+ export const Svg: React.ComponentType<any>;
4
+ export const Path: React.ComponentType<any>;
5
+ export const Circle: React.ComponentType<any>;
6
+ export const G: React.ComponentType<any>;
7
+ export const Rect: React.ComponentType<any>;
8
+ export const Line: React.ComponentType<any>;
9
+ export default Svg;
10
+ }
@@ -0,0 +1,102 @@
1
+ import React from "react";
2
+ import { View, Text, StyleSheet } from "react-native";
3
+ import { Avatar } from "./Avatar";
4
+ import { useLiveChatContext } from "../../provider/LiveChatContext";
5
+
6
+ export type AssigneeType = "ai" | "user" | "bot" | "unassigned" | null;
7
+
8
+ export type AssigneeAvatarProps = {
9
+ assigneeType: AssigneeType;
10
+ name?: string | null;
11
+ avatarUrl?: string | null;
12
+ brandLogoUrl?: string | null;
13
+ size?: number;
14
+ showPresenceDot?: boolean;
15
+ presenceStatus?: "active" | "away" | null;
16
+ };
17
+
18
+ /** Small "AI" mark used for ai assignee type */
19
+ function AiMark({ size }: { size: number }) {
20
+ return (
21
+ <View
22
+ style={[
23
+ styles.aiContainer,
24
+ { width: size, height: size, borderRadius: size * 0.22 },
25
+ ]}
26
+ >
27
+ <Text style={[styles.aiText, { fontSize: size * 0.28 }]}>AI</Text>
28
+ </View>
29
+ );
30
+ }
31
+
32
+ export function AssigneeAvatar({
33
+ assigneeType,
34
+ name,
35
+ avatarUrl,
36
+ brandLogoUrl: brandLogoUrlProp,
37
+ size = 32,
38
+ showPresenceDot = false,
39
+ presenceStatus,
40
+ }: AssigneeAvatarProps) {
41
+ const { theme: t, widgetName, widgetConfig } = useLiveChatContext();
42
+ const brandLogoUrl = brandLogoUrlProp ?? widgetConfig?.widgetSettings?.brandLogoUrl ?? null;
43
+
44
+ if (assigneeType === "ai") {
45
+ return <AiMark size={size} />;
46
+ }
47
+
48
+ if (assigneeType === "user" || assigneeType === "bot") {
49
+ const dotColor =
50
+ presenceStatus === "active"
51
+ ? t.colors.presenceActive
52
+ : t.colors.presenceAway;
53
+
54
+ return (
55
+ <View style={{ width: size, height: size }}>
56
+ <Avatar uri={avatarUrl} name={name ?? widgetName} size={size} />
57
+ {showPresenceDot && presenceStatus ? (
58
+ <View
59
+ style={[
60
+ styles.presenceDot,
61
+ {
62
+ width: size * 0.3,
63
+ height: size * 0.3,
64
+ borderRadius: size * 0.15,
65
+ backgroundColor: dotColor,
66
+ borderColor: t.colors.surface,
67
+ bottom: 0,
68
+ right: 0,
69
+ },
70
+ ]}
71
+ />
72
+ ) : null}
73
+ </View>
74
+ );
75
+ }
76
+
77
+ // null / "unassigned" → brand logo or widget name initials
78
+ return (
79
+ <Avatar
80
+ uri={brandLogoUrl}
81
+ name={name ?? widgetName}
82
+ size={size}
83
+ />
84
+ );
85
+ }
86
+
87
+ const styles = StyleSheet.create({
88
+ aiContainer: {
89
+ backgroundColor: "#18181b",
90
+ alignItems: "center",
91
+ justifyContent: "center",
92
+ },
93
+ aiText: {
94
+ color: "#ffffff",
95
+ fontWeight: "700",
96
+ letterSpacing: 0.5,
97
+ },
98
+ presenceDot: {
99
+ position: "absolute",
100
+ borderWidth: 2,
101
+ },
102
+ });
@@ -0,0 +1,110 @@
1
+ import React, { memo } from "react";
2
+ import { Image, StyleSheet, Text, View } from "react-native";
3
+
4
+ // ── color helpers (mirrors web app-avatar.tsx) ────────────────────────────────
5
+
6
+ function getHashOfString(str: string): number {
7
+ let hash = 0;
8
+ for (let i = 0; i < str.length; i++) {
9
+ hash = str.charCodeAt(i) + ((hash << 5) - hash);
10
+ }
11
+ return Math.abs(hash);
12
+ }
13
+
14
+ function normalizeHash(hash: number, min: number, max: number): number {
15
+ if (max <= min) return min;
16
+ return Math.floor((hash % (max - min)) + min);
17
+ }
18
+
19
+ function getRange(value: number, range: number): [number, number] {
20
+ return [Math.max(0, value - range), Math.min(value + range, 100)];
21
+ }
22
+
23
+ function generateHSL(
24
+ name: string,
25
+ saturationRange: [number, number],
26
+ lightnessRange: [number, number]
27
+ ): [number, number, number] {
28
+ const hash = getHashOfString(name);
29
+ const h = normalizeHash(hash, 0, 360);
30
+ const s = normalizeHash(hash, saturationRange[0], saturationRange[1]);
31
+ const l = normalizeHash(hash, lightnessRange[0], lightnessRange[1]);
32
+ return [h, s, l];
33
+ }
34
+
35
+ function hslToHex(h: number, s: number, l: number): string {
36
+ const sl = s / 100;
37
+ const ll = l / 100;
38
+ const a = sl * Math.min(ll, 1 - ll);
39
+ const f = (n: number) => {
40
+ const k = (n + h / 30) % 12;
41
+ const color = ll - a * Math.max(Math.min(k - 3, 9 - k, 1), -1);
42
+ return Math.round(255 * color).toString(16).padStart(2, "0");
43
+ };
44
+ return `#${f(0)}${f(8)}${f(4)}`;
45
+ }
46
+
47
+ function getAvatarBg(name: string): string {
48
+ const [h, s, l] = generateHSL(name, getRange(50, 10), getRange(50, 10));
49
+ return hslToHex(h, s, l);
50
+ }
51
+
52
+ function getInitials(name: string): string {
53
+ return name
54
+ .split(" ")
55
+ .map((n) => n?.charAt(0))
56
+ .filter(Boolean)
57
+ .slice(0, 2)
58
+ .join("")
59
+ .toUpperCase();
60
+ }
61
+
62
+ // ── component ─────────────────────────────────────────────────────────────────
63
+
64
+ export type AvatarProps = {
65
+ uri?: string | null;
66
+ name?: string | null;
67
+ size?: number;
68
+ };
69
+
70
+ function AvatarComponent({ uri, name, size = 36 }: AvatarProps) {
71
+ const label = (name && name.trim()) || "Support";
72
+ const bg = getAvatarBg(label);
73
+ const initials = getInitials(label) || "?";
74
+
75
+ return (
76
+ <View
77
+ style={[
78
+ styles.container,
79
+ { width: size, height: size, borderRadius: size / 2, backgroundColor: bg },
80
+ ]}
81
+ >
82
+ {uri ? (
83
+ <Image
84
+ source={{ uri }}
85
+ style={{ width: size, height: size }}
86
+ resizeMode="cover"
87
+ />
88
+ ) : (
89
+ <Text style={[styles.initials, { fontSize: size * 0.38 }]}>
90
+ {initials}
91
+ </Text>
92
+ )}
93
+ </View>
94
+ );
95
+ }
96
+
97
+ export const Avatar = memo(AvatarComponent);
98
+
99
+ const styles = StyleSheet.create({
100
+ container: {
101
+ overflow: "hidden",
102
+ alignItems: "center",
103
+ justifyContent: "center",
104
+ },
105
+ initials: {
106
+ color: "#ffffff",
107
+ fontWeight: "600",
108
+ textTransform: "uppercase",
109
+ },
110
+ });