@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.
- package/CHANGELOG.md +17 -0
- package/ChatPlatformSdk.podspec +20 -0
- package/README.md +315 -0
- package/android/build.gradle +54 -0
- package/android/src/main/AndroidManifest.xml +18 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkDownloaderModule.kt +240 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkFilePickerModule.kt +165 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +15 -0
- package/android/src/main/res/xml/chat_sdk_file_paths.xml +7 -0
- package/ios/ChatSdkDownloader.m +10 -0
- package/ios/ChatSdkDownloader.swift +141 -0
- package/ios/ChatSdkFilePicker.m +9 -0
- package/ios/ChatSdkFilePicker.swift +161 -0
- package/lib/commonjs/ChatSDK.js +193 -0
- package/lib/commonjs/ChatSDK.js.map +1 -0
- package/lib/commonjs/api.js +195 -0
- package/lib/commonjs/api.js.map +1 -0
- package/lib/commonjs/attachmentUtils.js +39 -0
- package/lib/commonjs/attachmentUtils.js.map +1 -0
- package/lib/commonjs/components/AttachmentGallery.js +367 -0
- package/lib/commonjs/components/AttachmentGallery.js.map +1 -0
- package/lib/commonjs/components/ChatScreen.js +286 -0
- package/lib/commonjs/components/ChatScreen.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +227 -0
- package/lib/commonjs/components/MessageBubble.js.map +1 -0
- package/lib/commonjs/components/MessageInput.js +273 -0
- package/lib/commonjs/components/MessageInput.js.map +1 -0
- package/lib/commonjs/components/SurveyOverlay.js +499 -0
- package/lib/commonjs/components/SurveyOverlay.js.map +1 -0
- package/lib/commonjs/downloaders/defaultAttachmentDownloader.js +28 -0
- package/lib/commonjs/downloaders/defaultAttachmentDownloader.js.map +1 -0
- package/lib/commonjs/filePicker.js +25 -0
- package/lib/commonjs/filePicker.js.map +1 -0
- package/lib/commonjs/index.js +27 -0
- package/lib/commonjs/index.js.map +1 -0
- package/lib/commonjs/native/NativeChatSdkDownloader.js +28 -0
- package/lib/commonjs/native/NativeChatSdkDownloader.js.map +1 -0
- package/lib/commonjs/native/NativeChatSdkFilePicker.js +17 -0
- package/lib/commonjs/native/NativeChatSdkFilePicker.js.map +1 -0
- package/lib/commonjs/package.json +1 -0
- package/lib/commonjs/realtime.js +242 -0
- package/lib/commonjs/realtime.js.map +1 -0
- package/lib/commonjs/safeArea.js +18 -0
- package/lib/commonjs/safeArea.js.map +1 -0
- package/lib/commonjs/session.js +159 -0
- package/lib/commonjs/session.js.map +1 -0
- package/lib/commonjs/surveyCache.js +30 -0
- package/lib/commonjs/surveyCache.js.map +1 -0
- package/lib/commonjs/theme.js +29 -0
- package/lib/commonjs/theme.js.map +1 -0
- package/lib/commonjs/types.js +2 -0
- package/lib/commonjs/types.js.map +1 -0
- package/lib/commonjs/useChat.js +145 -0
- package/lib/commonjs/useChat.js.map +1 -0
- package/lib/module/ChatSDK.js +189 -0
- package/lib/module/ChatSDK.js.map +1 -0
- package/lib/module/api.js +190 -0
- package/lib/module/api.js.map +1 -0
- package/lib/module/attachmentUtils.js +33 -0
- package/lib/module/attachmentUtils.js.map +1 -0
- package/lib/module/components/AttachmentGallery.js +362 -0
- package/lib/module/components/AttachmentGallery.js.map +1 -0
- package/lib/module/components/ChatScreen.js +281 -0
- package/lib/module/components/ChatScreen.js.map +1 -0
- package/lib/module/components/MessageBubble.js +222 -0
- package/lib/module/components/MessageBubble.js.map +1 -0
- package/lib/module/components/MessageInput.js +268 -0
- package/lib/module/components/MessageInput.js.map +1 -0
- package/lib/module/components/SurveyOverlay.js +494 -0
- package/lib/module/components/SurveyOverlay.js.map +1 -0
- package/lib/module/downloaders/defaultAttachmentDownloader.js +22 -0
- package/lib/module/downloaders/defaultAttachmentDownloader.js.map +1 -0
- package/lib/module/filePicker.js +20 -0
- package/lib/module/filePicker.js.map +1 -0
- package/lib/module/index.js +6 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/native/NativeChatSdkDownloader.js +23 -0
- package/lib/module/native/NativeChatSdkDownloader.js.map +1 -0
- package/lib/module/native/NativeChatSdkFilePicker.js +13 -0
- package/lib/module/native/NativeChatSdkFilePicker.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/realtime.js +236 -0
- package/lib/module/realtime.js.map +1 -0
- package/lib/module/safeArea.js +14 -0
- package/lib/module/safeArea.js.map +1 -0
- package/lib/module/session.js +154 -0
- package/lib/module/session.js.map +1 -0
- package/lib/module/surveyCache.js +23 -0
- package/lib/module/surveyCache.js.map +1 -0
- package/lib/module/theme.js +25 -0
- package/lib/module/theme.js.map +1 -0
- package/lib/module/types.js +2 -0
- package/lib/module/types.js.map +1 -0
- package/lib/module/useChat.js +141 -0
- package/lib/module/useChat.js.map +1 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts +49 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts.map +1 -0
- package/lib/typescript/commonjs/api.d.ts +31 -0
- package/lib/typescript/commonjs/api.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachmentUtils.d.ts +12 -0
- package/lib/typescript/commonjs/attachmentUtils.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/AttachmentGallery.d.ts +16 -0
- package/lib/typescript/commonjs/components/AttachmentGallery.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/ChatScreen.d.ts +16 -0
- package/lib/typescript/commonjs/components/ChatScreen.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/MessageBubble.d.ts +12 -0
- package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/MessageInput.d.ts +14 -0
- package/lib/typescript/commonjs/components/MessageInput.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/SurveyOverlay.d.ts +13 -0
- package/lib/typescript/commonjs/components/SurveyOverlay.d.ts.map +1 -0
- package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts +3 -0
- package/lib/typescript/commonjs/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
- package/lib/typescript/commonjs/filePicker.d.ts +4 -0
- package/lib/typescript/commonjs/filePicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/index.d.ts +7 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts +24 -0
- package/lib/typescript/commonjs/native/NativeChatSdkDownloader.d.ts.map +1 -0
- package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts +17 -0
- package/lib/typescript/commonjs/native/NativeChatSdkFilePicker.d.ts.map +1 -0
- package/lib/typescript/commonjs/package.json +1 -0
- package/lib/typescript/commonjs/realtime.d.ts +42 -0
- package/lib/typescript/commonjs/realtime.d.ts.map +1 -0
- package/lib/typescript/commonjs/safeArea.d.ts +4 -0
- package/lib/typescript/commonjs/safeArea.d.ts.map +1 -0
- package/lib/typescript/commonjs/session.d.ts +45 -0
- package/lib/typescript/commonjs/session.d.ts.map +1 -0
- package/lib/typescript/commonjs/surveyCache.d.ts +5 -0
- package/lib/typescript/commonjs/surveyCache.d.ts.map +1 -0
- package/lib/typescript/commonjs/theme.d.ts +21 -0
- package/lib/typescript/commonjs/theme.d.ts.map +1 -0
- package/lib/typescript/commonjs/types.d.ts +156 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -0
- package/lib/typescript/commonjs/useChat.d.ts +16 -0
- package/lib/typescript/commonjs/useChat.d.ts.map +1 -0
- package/lib/typescript/module/ChatSDK.d.ts +49 -0
- package/lib/typescript/module/ChatSDK.d.ts.map +1 -0
- package/lib/typescript/module/api.d.ts +31 -0
- package/lib/typescript/module/api.d.ts.map +1 -0
- package/lib/typescript/module/attachmentUtils.d.ts +12 -0
- package/lib/typescript/module/attachmentUtils.d.ts.map +1 -0
- package/lib/typescript/module/components/AttachmentGallery.d.ts +16 -0
- package/lib/typescript/module/components/AttachmentGallery.d.ts.map +1 -0
- package/lib/typescript/module/components/ChatScreen.d.ts +16 -0
- package/lib/typescript/module/components/ChatScreen.d.ts.map +1 -0
- package/lib/typescript/module/components/MessageBubble.d.ts +12 -0
- package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -0
- package/lib/typescript/module/components/MessageInput.d.ts +14 -0
- package/lib/typescript/module/components/MessageInput.d.ts.map +1 -0
- package/lib/typescript/module/components/SurveyOverlay.d.ts +13 -0
- package/lib/typescript/module/components/SurveyOverlay.d.ts.map +1 -0
- package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts +3 -0
- package/lib/typescript/module/downloaders/defaultAttachmentDownloader.d.ts.map +1 -0
- package/lib/typescript/module/filePicker.d.ts +4 -0
- package/lib/typescript/module/filePicker.d.ts.map +1 -0
- package/lib/typescript/module/index.d.ts +7 -0
- package/lib/typescript/module/index.d.ts.map +1 -0
- package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts +24 -0
- package/lib/typescript/module/native/NativeChatSdkDownloader.d.ts.map +1 -0
- package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts +17 -0
- package/lib/typescript/module/native/NativeChatSdkFilePicker.d.ts.map +1 -0
- package/lib/typescript/module/package.json +1 -0
- package/lib/typescript/module/realtime.d.ts +42 -0
- package/lib/typescript/module/realtime.d.ts.map +1 -0
- package/lib/typescript/module/safeArea.d.ts +4 -0
- package/lib/typescript/module/safeArea.d.ts.map +1 -0
- package/lib/typescript/module/session.d.ts +45 -0
- package/lib/typescript/module/session.d.ts.map +1 -0
- package/lib/typescript/module/surveyCache.d.ts +5 -0
- package/lib/typescript/module/surveyCache.d.ts.map +1 -0
- package/lib/typescript/module/theme.d.ts +21 -0
- package/lib/typescript/module/theme.d.ts.map +1 -0
- package/lib/typescript/module/types.d.ts +156 -0
- package/lib/typescript/module/types.d.ts.map +1 -0
- package/lib/typescript/module/useChat.d.ts +16 -0
- package/lib/typescript/module/useChat.d.ts.map +1 -0
- package/package.json +75 -0
- package/react-native.config.js +10 -0
- package/src/ChatSDK.ts +237 -0
- package/src/api.ts +228 -0
- package/src/attachmentUtils.ts +49 -0
- package/src/components/AttachmentGallery.tsx +363 -0
- package/src/components/ChatScreen.tsx +267 -0
- package/src/components/MessageBubble.tsx +208 -0
- package/src/components/MessageInput.tsx +280 -0
- package/src/components/SurveyOverlay.tsx +469 -0
- package/src/downloaders/defaultAttachmentDownloader.ts +27 -0
- package/src/filePicker.ts +22 -0
- package/src/index.ts +30 -0
- package/src/native/NativeChatSdkDownloader.ts +49 -0
- package/src/native/NativeChatSdkFilePicker.ts +30 -0
- package/src/realtime.ts +278 -0
- package/src/safeArea.ts +8 -0
- package/src/session.ts +196 -0
- package/src/surveyCache.ts +24 -0
- package/src/theme.ts +49 -0
- package/src/types.ts +199 -0
- package/src/useChat.ts +190 -0
package/src/realtime.ts
ADDED
|
@@ -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
|
+
}
|
package/src/safeArea.ts
ADDED
|
@@ -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
|
+
}
|