@ion299/sdk-react-native 0.1.0-beta.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.
Files changed (199) hide show
  1. package/CHANGELOG.md +17 -0
  2. package/ChatPlatformSdk.podspec +20 -0
  3. package/README.md +315 -0
  4. package/android/build.gradle +54 -0
  5. package/android/src/main/AndroidManifest.xml +18 -0
  6. package/android/src/main/java/com/chatplatform/sdk/ChatSdkDownloaderModule.kt +240 -0
  7. package/android/src/main/java/com/chatplatform/sdk/ChatSdkFilePickerModule.kt +165 -0
  8. package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +15 -0
  9. package/android/src/main/res/xml/chat_sdk_file_paths.xml +7 -0
  10. package/ios/ChatSdkDownloader.m +10 -0
  11. package/ios/ChatSdkDownloader.swift +141 -0
  12. package/ios/ChatSdkFilePicker.m +9 -0
  13. package/ios/ChatSdkFilePicker.swift +161 -0
  14. package/lib/commonjs/ChatSDK.js +193 -0
  15. package/lib/commonjs/ChatSDK.js.map +1 -0
  16. package/lib/commonjs/api.js +195 -0
  17. package/lib/commonjs/api.js.map +1 -0
  18. package/lib/commonjs/attachmentUtils.js +39 -0
  19. package/lib/commonjs/attachmentUtils.js.map +1 -0
  20. package/lib/commonjs/components/AttachmentGallery.js +367 -0
  21. package/lib/commonjs/components/AttachmentGallery.js.map +1 -0
  22. package/lib/commonjs/components/ChatScreen.js +286 -0
  23. package/lib/commonjs/components/ChatScreen.js.map +1 -0
  24. package/lib/commonjs/components/MessageBubble.js +227 -0
  25. package/lib/commonjs/components/MessageBubble.js.map +1 -0
  26. package/lib/commonjs/components/MessageInput.js +273 -0
  27. package/lib/commonjs/components/MessageInput.js.map +1 -0
  28. package/lib/commonjs/components/SurveyOverlay.js +499 -0
  29. package/lib/commonjs/components/SurveyOverlay.js.map +1 -0
  30. package/lib/commonjs/downloaders/defaultAttachmentDownloader.js +28 -0
  31. package/lib/commonjs/downloaders/defaultAttachmentDownloader.js.map +1 -0
  32. package/lib/commonjs/filePicker.js +25 -0
  33. package/lib/commonjs/filePicker.js.map +1 -0
  34. package/lib/commonjs/index.js +27 -0
  35. package/lib/commonjs/index.js.map +1 -0
  36. package/lib/commonjs/native/NativeChatSdkDownloader.js +28 -0
  37. package/lib/commonjs/native/NativeChatSdkDownloader.js.map +1 -0
  38. package/lib/commonjs/native/NativeChatSdkFilePicker.js +17 -0
  39. package/lib/commonjs/native/NativeChatSdkFilePicker.js.map +1 -0
  40. package/lib/commonjs/package.json +1 -0
  41. package/lib/commonjs/realtime.js +242 -0
  42. package/lib/commonjs/realtime.js.map +1 -0
  43. package/lib/commonjs/safeArea.js +18 -0
  44. package/lib/commonjs/safeArea.js.map +1 -0
  45. package/lib/commonjs/session.js +159 -0
  46. package/lib/commonjs/session.js.map +1 -0
  47. package/lib/commonjs/surveyCache.js +30 -0
  48. package/lib/commonjs/surveyCache.js.map +1 -0
  49. package/lib/commonjs/theme.js +29 -0
  50. package/lib/commonjs/theme.js.map +1 -0
  51. package/lib/commonjs/types.js +2 -0
  52. package/lib/commonjs/types.js.map +1 -0
  53. package/lib/commonjs/useChat.js +145 -0
  54. package/lib/commonjs/useChat.js.map +1 -0
  55. package/lib/module/ChatSDK.js +189 -0
  56. package/lib/module/ChatSDK.js.map +1 -0
  57. package/lib/module/api.js +190 -0
  58. package/lib/module/api.js.map +1 -0
  59. package/lib/module/attachmentUtils.js +33 -0
  60. package/lib/module/attachmentUtils.js.map +1 -0
  61. package/lib/module/components/AttachmentGallery.js +362 -0
  62. package/lib/module/components/AttachmentGallery.js.map +1 -0
  63. package/lib/module/components/ChatScreen.js +281 -0
  64. package/lib/module/components/ChatScreen.js.map +1 -0
  65. package/lib/module/components/MessageBubble.js +222 -0
  66. package/lib/module/components/MessageBubble.js.map +1 -0
  67. package/lib/module/components/MessageInput.js +268 -0
  68. package/lib/module/components/MessageInput.js.map +1 -0
  69. package/lib/module/components/SurveyOverlay.js +494 -0
  70. package/lib/module/components/SurveyOverlay.js.map +1 -0
  71. package/lib/module/downloaders/defaultAttachmentDownloader.js +22 -0
  72. package/lib/module/downloaders/defaultAttachmentDownloader.js.map +1 -0
  73. package/lib/module/filePicker.js +20 -0
  74. package/lib/module/filePicker.js.map +1 -0
  75. package/lib/module/index.js +6 -0
  76. package/lib/module/index.js.map +1 -0
  77. package/lib/module/native/NativeChatSdkDownloader.js +23 -0
  78. package/lib/module/native/NativeChatSdkDownloader.js.map +1 -0
  79. package/lib/module/native/NativeChatSdkFilePicker.js +13 -0
  80. package/lib/module/native/NativeChatSdkFilePicker.js.map +1 -0
  81. package/lib/module/package.json +1 -0
  82. package/lib/module/realtime.js +236 -0
  83. package/lib/module/realtime.js.map +1 -0
  84. package/lib/module/safeArea.js +14 -0
  85. package/lib/module/safeArea.js.map +1 -0
  86. package/lib/module/session.js +154 -0
  87. package/lib/module/session.js.map +1 -0
  88. package/lib/module/surveyCache.js +23 -0
  89. package/lib/module/surveyCache.js.map +1 -0
  90. package/lib/module/theme.js +25 -0
  91. package/lib/module/theme.js.map +1 -0
  92. package/lib/module/types.js +2 -0
  93. package/lib/module/types.js.map +1 -0
  94. package/lib/module/useChat.js +141 -0
  95. package/lib/module/useChat.js.map +1 -0
  96. package/lib/typescript/commonjs/ChatSDK.d.ts +49 -0
  97. package/lib/typescript/commonjs/ChatSDK.d.ts.map +1 -0
  98. package/lib/typescript/commonjs/api.d.ts +31 -0
  99. package/lib/typescript/commonjs/api.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/attachmentUtils.d.ts +12 -0
  101. package/lib/typescript/commonjs/attachmentUtils.d.ts.map +1 -0
  102. package/lib/typescript/commonjs/components/AttachmentGallery.d.ts +16 -0
  103. package/lib/typescript/commonjs/components/AttachmentGallery.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/components/ChatScreen.d.ts +16 -0
  105. package/lib/typescript/commonjs/components/ChatScreen.d.ts.map +1 -0
  106. package/lib/typescript/commonjs/components/MessageBubble.d.ts +12 -0
  107. package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -0
  108. package/lib/typescript/commonjs/components/MessageInput.d.ts +14 -0
  109. package/lib/typescript/commonjs/components/MessageInput.d.ts.map +1 -0
  110. package/lib/typescript/commonjs/components/SurveyOverlay.d.ts +13 -0
  111. package/lib/typescript/commonjs/components/SurveyOverlay.d.ts.map +1 -0
  112. package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts +3 -0
  113. package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
  114. package/lib/typescript/commonjs/filePicker.d.ts +4 -0
  115. package/lib/typescript/commonjs/filePicker.d.ts.map +1 -0
  116. package/lib/typescript/commonjs/index.d.ts +7 -0
  117. package/lib/typescript/commonjs/index.d.ts.map +1 -0
  118. package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts +24 -0
  119. package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts.map +1 -0
  120. package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts +17 -0
  121. package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts.map +1 -0
  122. package/lib/typescript/commonjs/package.json +1 -0
  123. package/lib/typescript/commonjs/realtime.d.ts +42 -0
  124. package/lib/typescript/commonjs/realtime.d.ts.map +1 -0
  125. package/lib/typescript/commonjs/safeArea.d.ts +4 -0
  126. package/lib/typescript/commonjs/safeArea.d.ts.map +1 -0
  127. package/lib/typescript/commonjs/session.d.ts +45 -0
  128. package/lib/typescript/commonjs/session.d.ts.map +1 -0
  129. package/lib/typescript/commonjs/surveyCache.d.ts +5 -0
  130. package/lib/typescript/commonjs/surveyCache.d.ts.map +1 -0
  131. package/lib/typescript/commonjs/theme.d.ts +21 -0
  132. package/lib/typescript/commonjs/theme.d.ts.map +1 -0
  133. package/lib/typescript/commonjs/types.d.ts +156 -0
  134. package/lib/typescript/commonjs/types.d.ts.map +1 -0
  135. package/lib/typescript/commonjs/useChat.d.ts +16 -0
  136. package/lib/typescript/commonjs/useChat.d.ts.map +1 -0
  137. package/lib/typescript/module/ChatSDK.d.ts +49 -0
  138. package/lib/typescript/module/ChatSDK.d.ts.map +1 -0
  139. package/lib/typescript/module/api.d.ts +31 -0
  140. package/lib/typescript/module/api.d.ts.map +1 -0
  141. package/lib/typescript/module/attachmentUtils.d.ts +12 -0
  142. package/lib/typescript/module/attachmentUtils.d.ts.map +1 -0
  143. package/lib/typescript/module/components/AttachmentGallery.d.ts +16 -0
  144. package/lib/typescript/module/components/AttachmentGallery.d.ts.map +1 -0
  145. package/lib/typescript/module/components/ChatScreen.d.ts +16 -0
  146. package/lib/typescript/module/components/ChatScreen.d.ts.map +1 -0
  147. package/lib/typescript/module/components/MessageBubble.d.ts +12 -0
  148. package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -0
  149. package/lib/typescript/module/components/MessageInput.d.ts +14 -0
  150. package/lib/typescript/module/components/MessageInput.d.ts.map +1 -0
  151. package/lib/typescript/module/components/SurveyOverlay.d.ts +13 -0
  152. package/lib/typescript/module/components/SurveyOverlay.d.ts.map +1 -0
  153. package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts +3 -0
  154. package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
  155. package/lib/typescript/module/filePicker.d.ts +4 -0
  156. package/lib/typescript/module/filePicker.d.ts.map +1 -0
  157. package/lib/typescript/module/index.d.ts +7 -0
  158. package/lib/typescript/module/index.d.ts.map +1 -0
  159. package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts +24 -0
  160. package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts.map +1 -0
  161. package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts +17 -0
  162. package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts.map +1 -0
  163. package/lib/typescript/module/package.json +1 -0
  164. package/lib/typescript/module/realtime.d.ts +42 -0
  165. package/lib/typescript/module/realtime.d.ts.map +1 -0
  166. package/lib/typescript/module/safeArea.d.ts +4 -0
  167. package/lib/typescript/module/safeArea.d.ts.map +1 -0
  168. package/lib/typescript/module/session.d.ts +45 -0
  169. package/lib/typescript/module/session.d.ts.map +1 -0
  170. package/lib/typescript/module/surveyCache.d.ts +5 -0
  171. package/lib/typescript/module/surveyCache.d.ts.map +1 -0
  172. package/lib/typescript/module/theme.d.ts +21 -0
  173. package/lib/typescript/module/theme.d.ts.map +1 -0
  174. package/lib/typescript/module/types.d.ts +156 -0
  175. package/lib/typescript/module/types.d.ts.map +1 -0
  176. package/lib/typescript/module/useChat.d.ts +16 -0
  177. package/lib/typescript/module/useChat.d.ts.map +1 -0
  178. package/package.json +75 -0
  179. package/react-native.config.js +10 -0
  180. package/src/ChatSDK.ts +237 -0
  181. package/src/api.ts +228 -0
  182. package/src/attachmentUtils.ts +49 -0
  183. package/src/components/AttachmentGallery.tsx +363 -0
  184. package/src/components/ChatScreen.tsx +267 -0
  185. package/src/components/MessageBubble.tsx +208 -0
  186. package/src/components/MessageInput.tsx +280 -0
  187. package/src/components/SurveyOverlay.tsx +469 -0
  188. package/src/downloaders/defaultAttachmentDownloader.ts +27 -0
  189. package/src/filePicker.ts +22 -0
  190. package/src/index.ts +30 -0
  191. package/src/native/NativeChatSdkDownloader.ts +49 -0
  192. package/src/native/NativeChatSdkFilePicker.ts +30 -0
  193. package/src/realtime.ts +278 -0
  194. package/src/safeArea.ts +8 -0
  195. package/src/session.ts +196 -0
  196. package/src/surveyCache.ts +24 -0
  197. package/src/theme.ts +49 -0
  198. package/src/types.ts +199 -0
  199. package/src/useChat.ts +190 -0
@@ -0,0 +1,278 @@
1
+ /**
2
+ * Минимальный Pusher-протокол поверх нативного WebSocket.
3
+ *
4
+ * Не требует pusher-js / @react-native-community/netinfo.
5
+ * Работает в Expo Go, bare RN, любом JS-окружении с WebSocket.
6
+ *
7
+ * Поддерживает:
8
+ * - presence-channels (подписка с auth)
9
+ * - WidgetMessagesUpdated
10
+ * - heartbeat (pong на pusher:ping)
11
+ * - exponential reconnect
12
+ */
13
+ import type { ReverbConfig } from './types'
14
+
15
+ export type RealtimeEvent =
16
+ | 'messages_updated'
17
+ | 'connected'
18
+ | 'disconnected'
19
+
20
+ export type RealtimeHandler = (data?: unknown) => void
21
+
22
+ const PUSHER_PROTOCOL = 7
23
+ const RECONNECT_DELAYS = [1000, 2000, 4000, 8000, 16000]
24
+
25
+ const MESSAGE_UPDATE_EVENTS = new Set([
26
+ 'ChatSdkMessagesUpdated',
27
+ '.ChatSdkMessagesUpdated',
28
+ 'App\\Events\\ChatSdk\\ChatSdkMessagesUpdated',
29
+ ])
30
+
31
+ /** На эмуляторе API часто на 10.0.2.2, а Reverb host с сервера — localhost. */
32
+ export function resolveReverbConfig(
33
+ reverb: ReverbConfig,
34
+ apiBaseUrl: string,
35
+ ): ReverbConfig {
36
+ const host = reverb.host?.trim() ?? ''
37
+ if (host === 'localhost' || host === '127.0.0.1') {
38
+ try {
39
+ const apiHost = new URL(apiBaseUrl).hostname
40
+ if (apiHost && apiHost !== 'localhost' && apiHost !== '127.0.0.1') {
41
+ return { ...reverb, host: apiHost }
42
+ }
43
+ } catch {
44
+ // keep original host
45
+ }
46
+ }
47
+ return reverb
48
+ }
49
+
50
+ export class RealtimeClient {
51
+ private ws: WebSocket | null = null
52
+ private socketId: string | null = null
53
+ private channelName: string | null = null
54
+ private handlers: Map<RealtimeEvent, Set<RealtimeHandler>> = new Map()
55
+ private reconnectAttempt = 0
56
+ private reconnectTimer: ReturnType<typeof setTimeout> | null = null
57
+ private destroyed = false
58
+
59
+ private authEndpoint = ''
60
+ private authHeaders: Record<string, string> = {}
61
+ private wsUrl = ''
62
+
63
+ // ─── Публичное API ─────────────────────────────────────────────────────────
64
+
65
+ connect(
66
+ config: ReverbConfig,
67
+ widgetToken: string,
68
+ contactId: string,
69
+ authEndpoint: string,
70
+ authHeaders: Record<string, string>,
71
+ apiBaseUrl?: string,
72
+ ): void {
73
+ this.destroy()
74
+ this.destroyed = false
75
+
76
+ const resolved = apiBaseUrl ? resolveReverbConfig(config, apiBaseUrl) : config
77
+ const secure = resolved.scheme === 'https'
78
+ const wsScheme = secure ? 'wss' : 'ws'
79
+ this.wsUrl = `${wsScheme}://${resolved.host}:${resolved.port}/app/${resolved.key}?protocol=${PUSHER_PROTOCOL}&client=js&version=8.4.0`
80
+ this.channelName = `presence-chat_sdk.${widgetToken}.contact.${contactId}`
81
+ this.authEndpoint = authEndpoint
82
+ this.authHeaders = authHeaders
83
+
84
+ this.openSocket()
85
+ }
86
+
87
+ disconnect(): void {
88
+ this.destroy()
89
+ }
90
+
91
+ isConnected(): boolean {
92
+ return this.ws?.readyState === WebSocket.OPEN && !!this.socketId
93
+ }
94
+
95
+ on(event: RealtimeEvent, handler: RealtimeHandler): () => void {
96
+ if (!this.handlers.has(event)) {
97
+ this.handlers.set(event, new Set())
98
+ }
99
+ this.handlers.get(event)!.add(handler)
100
+ return () => this.handlers.get(event)?.delete(handler)
101
+ }
102
+
103
+ // ─── Внутренняя логика ─────────────────────────────────────────────────────
104
+
105
+ private openSocket(): void {
106
+ try {
107
+ this.ws = new WebSocket(this.wsUrl)
108
+ } catch (e) {
109
+ this.scheduleReconnect()
110
+ return
111
+ }
112
+
113
+ this.ws.onopen = () => {
114
+ // Pusher пришлёт pusher:connection_established — ждём
115
+ }
116
+
117
+ this.ws.onmessage = (event) => {
118
+ this.handleMessage(String(event.data))
119
+ }
120
+
121
+ this.ws.onerror = () => {
122
+ // onclose вызовется следом
123
+ }
124
+
125
+ this.ws.onclose = () => {
126
+ this.socketId = null
127
+ this.emit('disconnected')
128
+ if (!this.destroyed) {
129
+ this.scheduleReconnect()
130
+ }
131
+ }
132
+ }
133
+
134
+ private handleMessage(raw: string): void {
135
+ let msg: { event: string; data?: unknown; channel?: string }
136
+ try {
137
+ msg = JSON.parse(raw)
138
+ } catch {
139
+ return
140
+ }
141
+
142
+ const eventName = msg.event ?? ''
143
+
144
+ switch (eventName) {
145
+ case 'pusher:connection_established': {
146
+ const data =
147
+ typeof msg.data === 'string' ? JSON.parse(msg.data) : msg.data
148
+ this.socketId = (data as { socket_id: string }).socket_id
149
+ this.reconnectAttempt = 0
150
+ void this.subscribe()
151
+ break
152
+ }
153
+
154
+ case 'pusher:ping':
155
+ this.send({ event: 'pusher:pong', data: {} })
156
+ break
157
+
158
+ case 'pusher:error': {
159
+ const errData = this.parseData(msg.data)
160
+ console.warn('ChatSDK realtime: pusher error', errData)
161
+ break
162
+ }
163
+
164
+ case 'pusher_internal:subscription_succeeded':
165
+ this.emit('connected')
166
+ break
167
+
168
+ case 'pusher:subscription_error': {
169
+ const errData = this.parseData(msg.data)
170
+ console.warn('ChatSDK realtime: subscription error', errData)
171
+ this.emit('disconnected')
172
+ break
173
+ }
174
+
175
+ default:
176
+ if (MESSAGE_UPDATE_EVENTS.has(eventName)) {
177
+ this.emit('messages_updated', this.parseData(msg.data))
178
+ }
179
+ break
180
+ }
181
+ }
182
+
183
+ private async subscribe(): Promise<void> {
184
+ if (!this.channelName || !this.socketId) return
185
+
186
+ let auth = ''
187
+ let channelData = ''
188
+ try {
189
+ const res = await fetch(this.authEndpoint, {
190
+ method: 'POST',
191
+ headers: {
192
+ 'Content-Type': 'application/json',
193
+ Accept: 'application/json',
194
+ ...this.authHeaders,
195
+ },
196
+ body: JSON.stringify({
197
+ socket_id: this.socketId,
198
+ channel_name: this.channelName,
199
+ }),
200
+ })
201
+ if (res.ok) {
202
+ const body = (await res.json()) as { auth?: string; channel_data?: string }
203
+ auth = body.auth ?? ''
204
+ channelData = body.channel_data ?? ''
205
+ } else {
206
+ const body = await res.text().catch(() => '')
207
+ console.warn('ChatSDK realtime: auth HTTP', res.status, body)
208
+ }
209
+ } catch (e) {
210
+ console.warn('ChatSDK realtime: auth request failed', e)
211
+ }
212
+
213
+ if (!auth) return
214
+ if (this.ws?.readyState !== WebSocket.OPEN) return
215
+
216
+ const payload: Record<string, string> = {
217
+ channel: this.channelName,
218
+ auth,
219
+ }
220
+ // Presence-канал требует channel_data — без него подписка не проходит
221
+ if (channelData) {
222
+ payload.channel_data = channelData
223
+ }
224
+
225
+ this.send({
226
+ event: 'pusher:subscribe',
227
+ data: payload,
228
+ })
229
+ }
230
+
231
+ private send(payload: unknown): void {
232
+ if (this.ws?.readyState === WebSocket.OPEN) {
233
+ this.ws.send(JSON.stringify(payload))
234
+ }
235
+ }
236
+
237
+ private scheduleReconnect(): void {
238
+ if (this.destroyed) return
239
+ const delay =
240
+ RECONNECT_DELAYS[Math.min(this.reconnectAttempt, RECONNECT_DELAYS.length - 1)]
241
+ this.reconnectAttempt++
242
+ this.reconnectTimer = setTimeout(() => {
243
+ if (!this.destroyed) this.openSocket()
244
+ }, delay)
245
+ }
246
+
247
+ private destroy(): void {
248
+ this.destroyed = true
249
+ if (this.reconnectTimer) {
250
+ clearTimeout(this.reconnectTimer)
251
+ this.reconnectTimer = null
252
+ }
253
+ if (this.ws) {
254
+ this.ws.onopen = null
255
+ this.ws.onmessage = null
256
+ this.ws.onerror = null
257
+ this.ws.onclose = null
258
+ this.ws.close()
259
+ this.ws = null
260
+ }
261
+ this.socketId = null
262
+ }
263
+
264
+ private parseData(data: unknown): unknown {
265
+ if (typeof data === 'string') {
266
+ try {
267
+ return JSON.parse(data)
268
+ } catch {
269
+ return data
270
+ }
271
+ }
272
+ return data
273
+ }
274
+
275
+ private emit(event: RealtimeEvent, data?: unknown): void {
276
+ this.handlers.get(event)?.forEach((h) => h(data))
277
+ }
278
+ }
@@ -0,0 +1,8 @@
1
+ import { Platform, StatusBar } from 'react-native'
2
+
3
+ /** Верхний отступ под status bar (Android). На iOS используйте SafeAreaView. */
4
+ export function getAndroidStatusBarHeight(): number {
5
+ return StatusBar.currentHeight ?? 24
6
+ }
7
+
8
+ export const INPUT_BOTTOM_PADDING = Platform.select({ ios: 0, android: 12, default: 12 }) ?? 12
package/src/session.ts ADDED
@@ -0,0 +1,196 @@
1
+ import { AppState, type NativeEventSubscription } from 'react-native'
2
+ import type { MobileApiClient } from './api'
3
+ import { RealtimeClient } from './realtime'
4
+ import type {
5
+ ChatMessage,
6
+ ChatOperator,
7
+ MobileConfig,
8
+ NewMessageEventPayload,
9
+ OperatorChangedEventPayload,
10
+ } from './types'
11
+
12
+ const POLL_INTERVAL_MS = 5000
13
+
14
+ export interface SessionEmitters {
15
+ emitMessagesUpdated: (messages: ChatMessage[], operator: ChatOperator | null) => void
16
+ emitConnectedChange: (connected: boolean) => void
17
+ emitOperatorChanged: (payload: OperatorChangedEventPayload) => void
18
+ emitNewMessage: (payload: NewMessageEventPayload) => void
19
+ }
20
+
21
+ /**
22
+ * Время жизни — от ChatSDK.login() до ChatSDK.logout().
23
+ * Один источник правды для realtime + polling + кэша сообщений/оператора.
24
+ * События проксируются в ChatSDK через emitters.
25
+ */
26
+ export class ChatSDKSession {
27
+ private realtime: RealtimeClient | null = null
28
+ private pollTimer: ReturnType<typeof setInterval> | null = null
29
+ private appStateSub: NativeEventSubscription | null = null
30
+ private destroyed = false
31
+ private refreshing = false
32
+
33
+ private messages: ChatMessage[] = []
34
+ private operator: ChatOperator | null = null
35
+ private knownMessageIds: Set<string> = new Set()
36
+ private initialLoaded = false
37
+ private connected = false
38
+
39
+ constructor(
40
+ private readonly api: MobileApiClient,
41
+ private readonly sdkConfig: MobileConfig,
42
+ private readonly widgetToken: string,
43
+ private readonly baseUrl: string,
44
+ private readonly emitters: SessionEmitters,
45
+ ) {}
46
+
47
+ start(): void {
48
+ this.openRealtime()
49
+ this.appStateSub = AppState.addEventListener('change', this.handleAppStateChange)
50
+ void this.refreshMessages()
51
+ }
52
+
53
+ destroy(): void {
54
+ this.destroyed = true
55
+ this.realtime?.disconnect()
56
+ this.realtime = null
57
+ this.stopPolling()
58
+ this.appStateSub?.remove()
59
+ this.appStateSub = null
60
+ this.messages = []
61
+ this.operator = null
62
+ this.knownMessageIds.clear()
63
+ this.initialLoaded = false
64
+ if (this.connected) {
65
+ this.connected = false
66
+ this.emitters.emitConnectedChange(false)
67
+ }
68
+ }
69
+
70
+ getMessages(): ChatMessage[] {
71
+ return this.messages
72
+ }
73
+
74
+ getOperator(): ChatOperator | null {
75
+ return this.operator
76
+ }
77
+
78
+ isConnected(): boolean {
79
+ return this.connected
80
+ }
81
+
82
+ async refreshMessages(): Promise<void> {
83
+ if (this.destroyed || this.refreshing) return
84
+ this.refreshing = true
85
+ try {
86
+ const data = await this.api.getMessages()
87
+ if (this.destroyed) return
88
+ const incoming = data.messages ?? []
89
+ const nextOperator = data.operator ?? null
90
+
91
+ if (this.initialLoaded) {
92
+ this.detectOperatorChange(nextOperator)
93
+ this.detectNewMessages(incoming, nextOperator)
94
+ }
95
+
96
+ this.messages = incoming
97
+ this.operator = nextOperator
98
+ this.knownMessageIds = new Set(incoming.map((m) => m.id))
99
+ this.initialLoaded = true
100
+
101
+ this.emitters.emitMessagesUpdated(incoming, nextOperator)
102
+ } catch (e) {
103
+ // Тихо — UI-слой (useChat) сам показывает ошибки своих явных действий.
104
+ // Фоновый refresh не должен ронять чат, если на короткое время пропала сеть.
105
+ if (this.initialLoaded) return
106
+ throw e
107
+ } finally {
108
+ this.refreshing = false
109
+ }
110
+ }
111
+
112
+ private detectOperatorChange(nextOperator: ChatOperator | null): void {
113
+ const prevId = this.operator?.id ?? null
114
+ const nextId = nextOperator?.id ?? null
115
+ if (prevId === nextId) return
116
+ const contactId = this.api.getContactId()
117
+ if (!contactId) return
118
+ this.emitters.emitOperatorChanged({
119
+ token: this.widgetToken,
120
+ contactId,
121
+ previousOperator: this.operator,
122
+ operator: nextOperator,
123
+ occurredAt: new Date().toISOString(),
124
+ })
125
+ }
126
+
127
+ private detectNewMessages(incoming: ChatMessage[], nextOperator: ChatOperator | null): void {
128
+ const contactId = this.api.getContactId()
129
+ if (!contactId) return
130
+ for (const message of incoming) {
131
+ if (this.knownMessageIds.has(message.id)) continue
132
+ if (message.type !== 'user') continue
133
+ this.emitters.emitNewMessage({
134
+ token: this.widgetToken,
135
+ contactId,
136
+ message,
137
+ operator: nextOperator,
138
+ occurredAt: new Date().toISOString(),
139
+ })
140
+ }
141
+ }
142
+
143
+ private openRealtime(): void {
144
+ const contactId = this.api.getContactId()
145
+ if (!contactId) return
146
+
147
+ const rt = new RealtimeClient()
148
+ this.realtime = rt
149
+
150
+ rt.on('connected', () => this.setConnected(true))
151
+ rt.on('disconnected', () => this.setConnected(false))
152
+ rt.on('messages_updated', () => void this.refreshMessages())
153
+
154
+ rt.connect(
155
+ this.sdkConfig.reverb,
156
+ this.widgetToken,
157
+ contactId,
158
+ this.api.getBroadcastAuthEndpoint(),
159
+ this.api.getAuthHeaders(),
160
+ this.baseUrl || this.sdkConfig.apiBaseUrl,
161
+ )
162
+ }
163
+
164
+ private setConnected(value: boolean): void {
165
+ if (this.connected === value) return
166
+ this.connected = value
167
+ this.emitters.emitConnectedChange(value)
168
+ if (value) {
169
+ this.stopPolling()
170
+ } else {
171
+ this.startPolling()
172
+ }
173
+ }
174
+
175
+ private startPolling(): void {
176
+ if (this.pollTimer || this.destroyed) return
177
+ if (AppState.currentState !== 'active') return
178
+ this.pollTimer = setInterval(() => void this.refreshMessages(), POLL_INTERVAL_MS)
179
+ }
180
+
181
+ private stopPolling(): void {
182
+ if (!this.pollTimer) return
183
+ clearInterval(this.pollTimer)
184
+ this.pollTimer = null
185
+ }
186
+
187
+ private handleAppStateChange = (next: string): void => {
188
+ if (this.destroyed) return
189
+ if (next === 'active') {
190
+ void this.refreshMessages()
191
+ if (!this.connected) this.startPolling()
192
+ } else {
193
+ this.stopPolling()
194
+ }
195
+ }
196
+ }
@@ -0,0 +1,24 @@
1
+ const cache = new Map<string, Set<string>>()
2
+
3
+ export function isSurveyEventSeen(contactId: string, messageId: string): boolean {
4
+ return cache.get(contactId)?.has(messageId) ?? false
5
+ }
6
+
7
+ export function markSurveyEventSeen(contactId: string, messageId: string): void {
8
+ if (!cache.has(contactId)) {
9
+ cache.set(contactId, new Set())
10
+ }
11
+ cache.get(contactId)!.add(messageId)
12
+ }
13
+
14
+ export function deleteSurveyEvent(contactId: string, messageId: string): void {
15
+ cache.get(contactId)?.delete(messageId)
16
+ }
17
+
18
+ export function clearSurveyCache(contactId?: string): void {
19
+ if (contactId) {
20
+ cache.delete(contactId)
21
+ } else {
22
+ cache.clear()
23
+ }
24
+ }
package/src/theme.ts ADDED
@@ -0,0 +1,49 @@
1
+ import type { MobileConfig } from './types'
2
+
3
+ export interface ChatTheme {
4
+ primaryColor: string
5
+ primaryColorStart: string | null
6
+ primaryColorEnd: string | null
7
+ isGradient: boolean
8
+ // Пузыри
9
+ outboundBg: string
10
+ inboundBg: string
11
+ outboundText: string
12
+ inboundText: string
13
+ // UI
14
+ background: string
15
+ inputBg: string
16
+ inputBorder: string
17
+ inputText: string
18
+ systemText: string
19
+ headerBg: string
20
+ headerText: string
21
+ sendButtonBg: string
22
+ }
23
+
24
+ export function buildTheme(config: MobileConfig | null): ChatTheme {
25
+ const primary = config?.themeColor ?? '#2d8ef0'
26
+ const isGradient =
27
+ config?.colorType === 'gradient' &&
28
+ !!config?.colorStart &&
29
+ !!config?.colorEnd
30
+
31
+ return {
32
+ primaryColor: primary,
33
+ primaryColorStart: config?.colorStart ?? null,
34
+ primaryColorEnd: config?.colorEnd ?? null,
35
+ isGradient,
36
+ outboundBg: primary,
37
+ inboundBg: '#f0f0f5',
38
+ outboundText: '#ffffff',
39
+ inboundText: '#1a1a1a',
40
+ background: '#ffffff',
41
+ inputBg: '#f7f7f9',
42
+ inputBorder: '#e0e0e8',
43
+ inputText: '#1a1a1a',
44
+ systemText: '#9999aa',
45
+ headerBg: primary,
46
+ headerText: '#ffffff',
47
+ sendButtonBg: primary,
48
+ }
49
+ }