@ion299/sdk-react-native 0.1.0-beta.5 → 0.1.0-beta.7
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 +26 -0
- package/README.md +69 -11
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkAudioPlayerModule.kt +201 -0
- package/android/src/main/java/com/chatplatform/sdk/ChatSdkPackage.kt +1 -0
- package/ios/ChatSdkAudioPlayer.m +25 -0
- package/ios/ChatSdkAudioPlayer.swift +193 -0
- package/lib/commonjs/ChatSDK.js +16 -3
- package/lib/commonjs/ChatSDK.js.map +1 -1
- package/lib/commonjs/api.js +29 -0
- package/lib/commonjs/api.js.map +1 -1
- package/lib/commonjs/audio/audioController.js +145 -0
- package/lib/commonjs/audio/audioController.js.map +1 -0
- package/lib/commonjs/components/AudioMessage.js +198 -0
- package/lib/commonjs/components/AudioMessage.js.map +1 -0
- package/lib/commonjs/components/MessageBubble.js +9 -0
- package/lib/commonjs/components/MessageBubble.js.map +1 -1
- package/lib/commonjs/native/NativeChatSdkAudioPlayer.js +28 -0
- package/lib/commonjs/native/NativeChatSdkAudioPlayer.js.map +1 -0
- package/lib/module/ChatSDK.js +16 -3
- package/lib/module/ChatSDK.js.map +1 -1
- package/lib/module/api.js +29 -0
- package/lib/module/api.js.map +1 -1
- package/lib/module/audio/audioController.js +140 -0
- package/lib/module/audio/audioController.js.map +1 -0
- package/lib/module/components/AudioMessage.js +193 -0
- package/lib/module/components/AudioMessage.js.map +1 -0
- package/lib/module/components/MessageBubble.js +9 -0
- package/lib/module/components/MessageBubble.js.map +1 -1
- package/lib/module/native/NativeChatSdkAudioPlayer.js +23 -0
- package/lib/module/native/NativeChatSdkAudioPlayer.js.map +1 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts +3 -0
- package/lib/typescript/commonjs/ChatSDK.d.ts.map +1 -1
- package/lib/typescript/commonjs/api.d.ts +3 -0
- package/lib/typescript/commonjs/api.d.ts.map +1 -1
- package/lib/typescript/commonjs/audio/audioController.d.ts +25 -0
- package/lib/typescript/commonjs/audio/audioController.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/AudioMessage.d.ts +11 -0
- package/lib/typescript/commonjs/components/AudioMessage.d.ts.map +1 -0
- package/lib/typescript/commonjs/components/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/commonjs/native/NativeChatSdkAudioPlayer.d.ts +20 -0
- package/lib/typescript/commonjs/native/NativeChatSdkAudioPlayer.d.ts.map +1 -0
- package/lib/typescript/module/ChatSDK.d.ts +3 -0
- package/lib/typescript/module/ChatSDK.d.ts.map +1 -1
- package/lib/typescript/module/api.d.ts +3 -0
- package/lib/typescript/module/api.d.ts.map +1 -1
- package/lib/typescript/module/audio/audioController.d.ts +25 -0
- package/lib/typescript/module/audio/audioController.d.ts.map +1 -0
- package/lib/typescript/module/components/AudioMessage.d.ts +11 -0
- package/lib/typescript/module/components/AudioMessage.d.ts.map +1 -0
- package/lib/typescript/module/components/MessageBubble.d.ts.map +1 -1
- package/lib/typescript/module/native/NativeChatSdkAudioPlayer.d.ts +20 -0
- package/lib/typescript/module/native/NativeChatSdkAudioPlayer.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/ChatSDK.ts +18 -3
- package/src/api.ts +37 -0
- package/src/audio/audioController.ts +144 -0
- package/src/components/AudioMessage.tsx +198 -0
- package/src/components/MessageBubble.tsx +6 -0
- package/src/native/NativeChatSdkAudioPlayer.ts +54 -0
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,32 @@
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.1.0-beta.7]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Регистрация push-токена устройства для фоновых уведомлений:
|
|
13
|
+
`ChatSDK.registerPushToken(deviceToken, platform?)` и
|
|
14
|
+
`ChatSDK.unregisterPushToken(deviceToken?)`. Токен снимается автоматически
|
|
15
|
+
в `logout()`. WebSocket в фоне ОС гасит — события о новых сообщениях
|
|
16
|
+
оператора при свёрнутом/закрытом приложении доставляются через FCM/APNs:
|
|
17
|
+
ЧП шлёт push на зарегистрированные токены контакта (свой backend не нужен).
|
|
18
|
+
SDK провайдер-агностик: хост-приложение приносит нативный токен под свой стек.
|
|
19
|
+
|
|
20
|
+
## [0.1.0-beta.6]
|
|
21
|
+
|
|
22
|
+
### Added
|
|
23
|
+
- Аудио-вложения (голосовые сообщения) теперь проигрываются прямо в чате:
|
|
24
|
+
встроенный плеер с кнопкой play/pause, прогресс-баром и перемоткой по тапу.
|
|
25
|
+
Раньше аудио показывалось как обычный файл (`.webm`) только для скачивания.
|
|
26
|
+
- Собственный нативный модуль воспроизведения `ChatSdkAudioPlayer`
|
|
27
|
+
(Android `MediaPlayer`, iOS `AVPlayer`) — без сторонних JS-зависимостей.
|
|
28
|
+
Если модуль не собран, аудио откатывается на обычный файловый блок.
|
|
29
|
+
|
|
30
|
+
### Notes
|
|
31
|
+
- iOS (`AVPlayer`) не декодирует контейнер WebM/Opus: голосовые, записанные
|
|
32
|
+
в браузере как `audio/webm`, на iOS воспроизвести нельзя — их нужно
|
|
33
|
+
транскодировать на бэкенде (например, в `m4a`/`aac`). Android их играет.
|
|
34
|
+
|
|
9
35
|
## [0.1.0-beta.5]
|
|
10
36
|
|
|
11
37
|
### Changed
|
package/README.md
CHANGED
|
@@ -215,11 +215,56 @@ unsubMessagesUpdated(); unsubConnected(); unsubError()
|
|
|
215
215
|
|
|
216
216
|
## Push-уведомления
|
|
217
217
|
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
доставить
|
|
218
|
+
В фоне/закрытом приложении WebSocket гасит ОС — события туда не приходят
|
|
219
|
+
(это ограничение iOS и Android, см. «Поведение в фоне»). Единственный
|
|
220
|
+
кроссплатформенный способ доставить уведомление о сообщении оператора —
|
|
221
|
+
**push (FCM / APNs)**. Есть два варианта интеграции.
|
|
221
222
|
|
|
222
|
-
###
|
|
223
|
+
### Вариант A — `registerPushToken` (рекомендуется)
|
|
224
|
+
|
|
225
|
+
Хост-приложение получает **нативный** push-токен своим способом (под свой стек)
|
|
226
|
+
и отдаёт его SDK. ЧП сам шлёт FCM/APNs на зарегистрированные токены контакта —
|
|
227
|
+
свой backend не нужен. SDK **не зависит** от конкретного push-провайдера.
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
await ChatSDK.login(/* ... */)
|
|
231
|
+
await ChatSDK.registerPushToken(deviceToken, platform) // platform: 'fcm' | 'apns'
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`platform`:
|
|
235
|
+
- `'fcm'` — FCM registration token (Android всегда; iOS — если используете Firebase и на iOS);
|
|
236
|
+
- `'apns'` — «сырой» APNs device-token (iOS без Firebase).
|
|
237
|
+
|
|
238
|
+
Токен снимается автоматически в `logout()` (или вручную `unregisterPushToken()`).
|
|
239
|
+
|
|
240
|
+
**Где взять токен — примеры под разные стеки:**
|
|
241
|
+
|
|
242
|
+
```ts
|
|
243
|
+
// Expo — getDevicePushTokenAsync() отдаёт НАТИВНЫЙ FCM/APNs токен
|
|
244
|
+
import * as Notifications from 'expo-notifications'
|
|
245
|
+
import { Platform } from 'react-native'
|
|
246
|
+
|
|
247
|
+
await Notifications.requestPermissionsAsync()
|
|
248
|
+
const { data } = await Notifications.getDevicePushTokenAsync()
|
|
249
|
+
await ChatSDK.registerPushToken(String(data), Platform.OS === 'ios' ? 'apns' : 'fcm')
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
```ts
|
|
253
|
+
// Голый React Native — @react-native-firebase/messaging
|
|
254
|
+
import messaging from '@react-native-firebase/messaging'
|
|
255
|
+
|
|
256
|
+
await messaging().requestPermission()
|
|
257
|
+
const fcmToken = await messaging().getToken()
|
|
258
|
+
await ChatSDK.registerPushToken(fcmToken, 'fcm')
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
> Нативные SDK (iOS/Android без RN) регистрируют токен тем же эндпоинтом —
|
|
262
|
+
> `registerPushToken` лишь обёртка над `POST /api/mobile/{token}/contact/{contactId}/push-token`.
|
|
263
|
+
|
|
264
|
+
### Вариант B — webhook на свой backend
|
|
265
|
+
|
|
266
|
+
Если вы хотите управлять доставкой сами, ЧП может слать webhook
|
|
267
|
+
`message.created` на ваш backend, а push вы шлёте уже своей инфраструктурой.
|
|
223
268
|
|
|
224
269
|
```json
|
|
225
270
|
{
|
|
@@ -238,12 +283,15 @@ backend при новом сообщении от оператора. Ваш bac
|
|
|
238
283
|
|
|
239
284
|
Подпись: заголовок `X-Chat-Platform-Signature: sha256=HMAC_SHA256(body, webhook_secret)`.
|
|
240
285
|
|
|
241
|
-
###
|
|
286
|
+
### Приём push и открытие чата
|
|
287
|
+
|
|
288
|
+
Сам push принимает **хост-приложение** (SDK не перехватывает уведомления).
|
|
289
|
+
Распознать «наш» push и открыть чат на тап:
|
|
242
290
|
|
|
243
291
|
```tsx
|
|
244
292
|
import { ChatSDK } from '@chat-platform/sdk-react-native'
|
|
245
293
|
|
|
246
|
-
// В обработчике нотификации
|
|
294
|
+
// В обработчике тапа по нотификации
|
|
247
295
|
ChatSDK.handleNotification({
|
|
248
296
|
token: data.cp_token,
|
|
249
297
|
contactId: data.cp_contact_id,
|
|
@@ -251,7 +299,7 @@ ChatSDK.handleNotification({
|
|
|
251
299
|
navigation.navigate('Chat')
|
|
252
300
|
```
|
|
253
301
|
|
|
254
|
-
|
|
302
|
+
В data-payload push'а ЧП кладёт ключи `cp_token` и `cp_contact_id`.
|
|
255
303
|
|
|
256
304
|
---
|
|
257
305
|
|
|
@@ -279,7 +327,17 @@ navigation.navigate('Chat')
|
|
|
279
327
|
|
|
280
328
|
### `ChatSDK.logout()`
|
|
281
329
|
|
|
282
|
-
Завершает сессию, отключает realtime
|
|
330
|
+
Завершает сессию, отключает realtime, снимает зарегистрированный push-токен.
|
|
331
|
+
|
|
332
|
+
### `ChatSDK.registerPushToken(deviceToken, platform?)`
|
|
333
|
+
|
|
334
|
+
Регистрирует push-токен устройства для фоновых уведомлений. Требует `login()`.
|
|
335
|
+
`platform`: `'fcm'` (по умолчанию) или `'apns'`. Токен запоминается и снимается
|
|
336
|
+
в `logout()`.
|
|
337
|
+
|
|
338
|
+
### `ChatSDK.unregisterPushToken(deviceToken?)`
|
|
339
|
+
|
|
340
|
+
Снимает регистрацию push-токена (по умолчанию — последнего зарегистрированного).
|
|
283
341
|
|
|
284
342
|
### `ChatSDK.handleNotification(payload)`
|
|
285
343
|
|
|
@@ -304,9 +362,9 @@ navigation.navigate('Chat')
|
|
|
304
362
|
- [ ] `ChatSDK.init(...)` в точке входа приложения
|
|
305
363
|
- [ ] `ChatSDK.login(...)` после авторизации пользователя
|
|
306
364
|
- [ ] `<ChatScreen />` в навигаторе
|
|
307
|
-
- [ ]
|
|
308
|
-
|
|
309
|
-
- [ ] `ChatSDK.handleNotification(...)` в обработчике push
|
|
365
|
+
- [ ] Push: получить нативный токен и вызвать `ChatSDK.registerPushToken(token, platform)` после `login()`
|
|
366
|
+
(либо webhook на свой backend — вариант B)
|
|
367
|
+
- [ ] `ChatSDK.handleNotification(...)` в обработчике тапа по push (`cp_token`, `cp_contact_id` в data payload)
|
|
310
368
|
|
|
311
369
|
---
|
|
312
370
|
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
package com.chatplatform.sdk
|
|
2
|
+
|
|
3
|
+
import android.media.AudioAttributes
|
|
4
|
+
import android.media.MediaPlayer
|
|
5
|
+
import android.net.Uri
|
|
6
|
+
import android.os.Handler
|
|
7
|
+
import android.os.HandlerThread
|
|
8
|
+
import com.facebook.react.bridge.Arguments
|
|
9
|
+
import com.facebook.react.bridge.Promise
|
|
10
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
11
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
12
|
+
import com.facebook.react.bridge.ReactMethod
|
|
13
|
+
import com.facebook.react.bridge.ReadableMap
|
|
14
|
+
import com.facebook.react.bridge.WritableMap
|
|
15
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Проигрывает аудио-вложения (голосовые сообщения) прямо в чате.
|
|
19
|
+
* В каждый момент времени активен ровно один плеер — запуск нового
|
|
20
|
+
* освобождает предыдущий. Состояние отдаётся в JS событием
|
|
21
|
+
* "ChatSdkAudioState" { key, positionMillis, durationMillis, state }.
|
|
22
|
+
*/
|
|
23
|
+
class ChatSdkAudioPlayerModule(reactContext: ReactApplicationContext) :
|
|
24
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
25
|
+
|
|
26
|
+
override fun getName(): String = NAME
|
|
27
|
+
|
|
28
|
+
private val thread = HandlerThread("ChatSdkAudioPlayer").apply { start() }
|
|
29
|
+
private val handler = Handler(thread.looper)
|
|
30
|
+
|
|
31
|
+
private var player: MediaPlayer? = null
|
|
32
|
+
private var currentKey: String? = null
|
|
33
|
+
private var prepared = false
|
|
34
|
+
|
|
35
|
+
private val tick = object : Runnable {
|
|
36
|
+
override fun run() {
|
|
37
|
+
val p = player ?: return
|
|
38
|
+
try {
|
|
39
|
+
if (p.isPlaying) {
|
|
40
|
+
emit(currentKey, p.currentPosition.toLong(), safeDuration(p), "playing")
|
|
41
|
+
handler.postDelayed(this, 250)
|
|
42
|
+
}
|
|
43
|
+
} catch (_: Throwable) {
|
|
44
|
+
// плеер уже освобождён — просто перестаём тикать
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@ReactMethod
|
|
50
|
+
fun addListener(eventName: String) {
|
|
51
|
+
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
@ReactMethod
|
|
55
|
+
fun removeListeners(count: Int) {
|
|
56
|
+
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
@ReactMethod
|
|
60
|
+
fun play(key: String, url: String, headers: ReadableMap?, promise: Promise) {
|
|
61
|
+
val hdr = HashMap<String, String>()
|
|
62
|
+
headers?.toHashMap()?.forEach { (k, v) -> if (v is String) hdr[k] = v }
|
|
63
|
+
|
|
64
|
+
handler.post {
|
|
65
|
+
try {
|
|
66
|
+
val existing = player
|
|
67
|
+
if (currentKey == key && existing != null && prepared) {
|
|
68
|
+
existing.start()
|
|
69
|
+
emit(key, existing.currentPosition.toLong(), safeDuration(existing), "playing")
|
|
70
|
+
handler.removeCallbacks(tick)
|
|
71
|
+
handler.post(tick)
|
|
72
|
+
promise.resolve(null)
|
|
73
|
+
return@post
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
releaseInternal()
|
|
77
|
+
currentKey = key
|
|
78
|
+
prepared = false
|
|
79
|
+
emit(key, 0, 0, "loading")
|
|
80
|
+
|
|
81
|
+
val mp = MediaPlayer()
|
|
82
|
+
mp.setAudioAttributes(
|
|
83
|
+
AudioAttributes.Builder()
|
|
84
|
+
.setUsage(AudioAttributes.USAGE_MEDIA)
|
|
85
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
|
|
86
|
+
.build(),
|
|
87
|
+
)
|
|
88
|
+
if (hdr.isEmpty()) {
|
|
89
|
+
mp.setDataSource(reactApplicationContext, Uri.parse(url))
|
|
90
|
+
} else {
|
|
91
|
+
mp.setDataSource(reactApplicationContext, Uri.parse(url), hdr)
|
|
92
|
+
}
|
|
93
|
+
mp.setOnPreparedListener { p ->
|
|
94
|
+
if (currentKey != key) return@setOnPreparedListener
|
|
95
|
+
prepared = true
|
|
96
|
+
p.start()
|
|
97
|
+
emit(key, 0, safeDuration(p), "playing")
|
|
98
|
+
handler.removeCallbacks(tick)
|
|
99
|
+
handler.post(tick)
|
|
100
|
+
}
|
|
101
|
+
mp.setOnCompletionListener { p ->
|
|
102
|
+
handler.removeCallbacks(tick)
|
|
103
|
+
emit(key, safeDuration(p), safeDuration(p), "ended")
|
|
104
|
+
}
|
|
105
|
+
mp.setOnErrorListener { _, _, _ ->
|
|
106
|
+
handler.removeCallbacks(tick)
|
|
107
|
+
emit(key, 0, 0, "error")
|
|
108
|
+
releaseInternal()
|
|
109
|
+
true
|
|
110
|
+
}
|
|
111
|
+
player = mp
|
|
112
|
+
mp.prepareAsync()
|
|
113
|
+
promise.resolve(null)
|
|
114
|
+
} catch (e: Throwable) {
|
|
115
|
+
emit(key, 0, 0, "error")
|
|
116
|
+
releaseInternal()
|
|
117
|
+
promise.reject("AUDIO_PLAY_FAILED", e.message ?: "Не удалось воспроизвести аудио", e)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
@ReactMethod
|
|
123
|
+
fun pause(key: String, promise: Promise) {
|
|
124
|
+
handler.post {
|
|
125
|
+
try {
|
|
126
|
+
val p = player
|
|
127
|
+
if (currentKey == key && p != null && prepared && p.isPlaying) {
|
|
128
|
+
p.pause()
|
|
129
|
+
handler.removeCallbacks(tick)
|
|
130
|
+
emit(key, p.currentPosition.toLong(), safeDuration(p), "paused")
|
|
131
|
+
}
|
|
132
|
+
promise.resolve(null)
|
|
133
|
+
} catch (e: Throwable) {
|
|
134
|
+
promise.reject("AUDIO_PAUSE_FAILED", e.message ?: "Ошибка паузы", e)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
@ReactMethod
|
|
140
|
+
fun seek(key: String, positionMillis: Double, promise: Promise) {
|
|
141
|
+
handler.post {
|
|
142
|
+
try {
|
|
143
|
+
val p = player
|
|
144
|
+
if (currentKey == key && p != null && prepared) {
|
|
145
|
+
p.seekTo(positionMillis.toInt())
|
|
146
|
+
val state = if (p.isPlaying) "playing" else "paused"
|
|
147
|
+
emit(key, positionMillis.toLong(), safeDuration(p), state)
|
|
148
|
+
}
|
|
149
|
+
promise.resolve(null)
|
|
150
|
+
} catch (e: Throwable) {
|
|
151
|
+
promise.reject("AUDIO_SEEK_FAILED", e.message ?: "Ошибка перемотки", e)
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@ReactMethod
|
|
157
|
+
fun stop(key: String, promise: Promise) {
|
|
158
|
+
handler.post {
|
|
159
|
+
try {
|
|
160
|
+
if (currentKey == key) {
|
|
161
|
+
handler.removeCallbacks(tick)
|
|
162
|
+
releaseInternal()
|
|
163
|
+
emit(key, 0, 0, "stopped")
|
|
164
|
+
}
|
|
165
|
+
promise.resolve(null)
|
|
166
|
+
} catch (e: Throwable) {
|
|
167
|
+
promise.reject("AUDIO_STOP_FAILED", e.message ?: "Ошибка остановки", e)
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private fun releaseInternal() {
|
|
173
|
+
try { player?.reset() } catch (_: Throwable) {}
|
|
174
|
+
try { player?.release() } catch (_: Throwable) {}
|
|
175
|
+
player = null
|
|
176
|
+
prepared = false
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
private fun safeDuration(p: MediaPlayer): Long =
|
|
180
|
+
try {
|
|
181
|
+
val d = p.duration
|
|
182
|
+
if (d > 0) d.toLong() else 0L
|
|
183
|
+
} catch (_: Throwable) {
|
|
184
|
+
0L
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private fun emit(key: String?, position: Long, duration: Long, state: String) {
|
|
188
|
+
val map: WritableMap = Arguments.createMap()
|
|
189
|
+
map.putString("key", key ?: "")
|
|
190
|
+
map.putDouble("positionMillis", position.toDouble())
|
|
191
|
+
map.putDouble("durationMillis", duration.toDouble())
|
|
192
|
+
map.putString("state", state)
|
|
193
|
+
reactApplicationContext
|
|
194
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
195
|
+
.emit("ChatSdkAudioState", map)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
companion object {
|
|
199
|
+
const val NAME = "ChatSdkAudioPlayer"
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -9,6 +9,7 @@ class ChatSdkPackage : ReactPackage {
|
|
|
9
9
|
override fun createNativeModules(reactContext: ReactApplicationContext): List<NativeModule> = listOf(
|
|
10
10
|
ChatSdkFilePickerModule(reactContext),
|
|
11
11
|
ChatSdkDownloaderModule(reactContext),
|
|
12
|
+
ChatSdkAudioPlayerModule(reactContext),
|
|
12
13
|
)
|
|
13
14
|
|
|
14
15
|
override fun createViewManagers(reactContext: ReactApplicationContext): List<ViewManager<*, *>> = emptyList()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
#import <React/RCTBridgeModule.h>
|
|
2
|
+
#import <React/RCTEventEmitter.h>
|
|
3
|
+
|
|
4
|
+
@interface RCT_EXTERN_MODULE(ChatSdkAudioPlayer, RCTEventEmitter)
|
|
5
|
+
|
|
6
|
+
RCT_EXTERN_METHOD(play:(NSString *)key
|
|
7
|
+
url:(NSString *)url
|
|
8
|
+
headers:(NSDictionary *)headers
|
|
9
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
10
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
11
|
+
|
|
12
|
+
RCT_EXTERN_METHOD(pause:(NSString *)key
|
|
13
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
14
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
15
|
+
|
|
16
|
+
RCT_EXTERN_METHOD(seek:(NSString *)key
|
|
17
|
+
positionMillis:(nonnull NSNumber *)positionMillis
|
|
18
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
19
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
20
|
+
|
|
21
|
+
RCT_EXTERN_METHOD(stop:(NSString *)key
|
|
22
|
+
resolver:(RCTPromiseResolveBlock)resolve
|
|
23
|
+
rejecter:(RCTPromiseRejectBlock)reject)
|
|
24
|
+
|
|
25
|
+
@end
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import AVFoundation
|
|
3
|
+
import React
|
|
4
|
+
@objc(ChatSdkAudioPlayer)
|
|
5
|
+
class ChatSdkAudioPlayer: RCTEventEmitter {
|
|
6
|
+
|
|
7
|
+
private var player: AVPlayer?
|
|
8
|
+
private var observedItem: AVPlayerItem?
|
|
9
|
+
private var timeObserver: Any?
|
|
10
|
+
private var endObserver: NSObjectProtocol?
|
|
11
|
+
private var currentKey: String?
|
|
12
|
+
private var durationMillis: Double = 0
|
|
13
|
+
private var listenerCount = 0
|
|
14
|
+
|
|
15
|
+
@objc override static func requiresMainQueueSetup() -> Bool { return false }
|
|
16
|
+
override func supportedEvents() -> [String]! { return ["ChatSdkAudioState"] }
|
|
17
|
+
override func startObserving() { listenerCount += 1 }
|
|
18
|
+
override func stopObserving() { listenerCount = max(0, listenerCount - 1) }
|
|
19
|
+
|
|
20
|
+
@objc(play:url:headers:resolver:rejecter:)
|
|
21
|
+
func play(_ key: String,
|
|
22
|
+
url urlString: String,
|
|
23
|
+
headers: NSDictionary?,
|
|
24
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
25
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
26
|
+
DispatchQueue.main.async {
|
|
27
|
+
if self.currentKey == key, let p = self.player {
|
|
28
|
+
self.activateSession()
|
|
29
|
+
p.play()
|
|
30
|
+
self.emit(key: key, position: self.currentPositionMillis(), duration: self.durationMillis, state: "playing")
|
|
31
|
+
resolve(nil)
|
|
32
|
+
return
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
guard let url = URL(string: urlString) else {
|
|
36
|
+
reject("INVALID_URL", "URL не задан", nil)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
self.teardown()
|
|
41
|
+
self.currentKey = key
|
|
42
|
+
self.emit(key: key, position: 0, duration: 0, state: "loading")
|
|
43
|
+
|
|
44
|
+
var options: [String: Any] = [:]
|
|
45
|
+
if let h = headers as? [String: String], !h.isEmpty {
|
|
46
|
+
options["AVURLAssetHTTPHeaderFieldsKey"] = h
|
|
47
|
+
}
|
|
48
|
+
let asset = AVURLAsset(url: url, options: options)
|
|
49
|
+
let item = AVPlayerItem(asset: asset)
|
|
50
|
+
let p = AVPlayer(playerItem: item)
|
|
51
|
+
self.player = p
|
|
52
|
+
self.observedItem = item
|
|
53
|
+
|
|
54
|
+
self.activateSession()
|
|
55
|
+
|
|
56
|
+
item.addObserver(self, forKeyPath: "status", options: [.new], context: nil)
|
|
57
|
+
|
|
58
|
+
let interval = CMTime(seconds: 0.25, preferredTimescale: 600)
|
|
59
|
+
self.timeObserver = p.addPeriodicTimeObserver(forInterval: interval, queue: .main) { [weak self] time in
|
|
60
|
+
guard let self = self, self.currentKey == key else { return }
|
|
61
|
+
if p.timeControlStatus == .playing {
|
|
62
|
+
let pos = CMTimeGetSeconds(time) * 1000
|
|
63
|
+
self.emit(key: key, position: pos.isFinite ? pos : 0, duration: self.durationMillis, state: "playing")
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
self.endObserver = NotificationCenter.default.addObserver(
|
|
68
|
+
forName: .AVPlayerItemDidPlayToEndTime,
|
|
69
|
+
object: item,
|
|
70
|
+
queue: .main,
|
|
71
|
+
) { [weak self] _ in
|
|
72
|
+
guard let self = self, self.currentKey == key else { return }
|
|
73
|
+
self.emit(key: key, position: self.durationMillis, duration: self.durationMillis, state: "ended")
|
|
74
|
+
p.seek(to: .zero)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
p.play()
|
|
78
|
+
resolve(nil)
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
@objc(pause:resolver:rejecter:)
|
|
83
|
+
func pause(_ key: String,
|
|
84
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
85
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
86
|
+
DispatchQueue.main.async {
|
|
87
|
+
if self.currentKey == key, let p = self.player {
|
|
88
|
+
p.pause()
|
|
89
|
+
self.emit(key: key, position: self.currentPositionMillis(), duration: self.durationMillis, state: "paused")
|
|
90
|
+
}
|
|
91
|
+
resolve(nil)
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
@objc(seek:positionMillis:resolver:rejecter:)
|
|
96
|
+
func seek(_ key: String,
|
|
97
|
+
positionMillis: NSNumber,
|
|
98
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
99
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
100
|
+
DispatchQueue.main.async {
|
|
101
|
+
guard self.currentKey == key, let p = self.player else {
|
|
102
|
+
resolve(nil)
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
let seconds = positionMillis.doubleValue / 1000
|
|
106
|
+
let target = CMTime(seconds: seconds, preferredTimescale: 600)
|
|
107
|
+
p.seek(to: target) { [weak self] _ in
|
|
108
|
+
guard let self = self, self.currentKey == key else { return }
|
|
109
|
+
let state = p.timeControlStatus == .playing ? "playing" : "paused"
|
|
110
|
+
self.emit(key: key, position: positionMillis.doubleValue, duration: self.durationMillis, state: state)
|
|
111
|
+
}
|
|
112
|
+
resolve(nil)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
@objc(stop:resolver:rejecter:)
|
|
117
|
+
func stop(_ key: String,
|
|
118
|
+
resolver resolve: @escaping RCTPromiseResolveBlock,
|
|
119
|
+
rejecter reject: @escaping RCTPromiseRejectBlock) {
|
|
120
|
+
DispatchQueue.main.async {
|
|
121
|
+
if self.currentKey == key {
|
|
122
|
+
self.teardown()
|
|
123
|
+
self.deactivateSession()
|
|
124
|
+
self.emit(key: key, position: 0, duration: 0, state: "stopped")
|
|
125
|
+
}
|
|
126
|
+
resolve(nil)
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
override func observeValue(forKeyPath keyPath: String?,
|
|
131
|
+
of object: Any?,
|
|
132
|
+
change: [NSKeyValueChangeKey: Any]?,
|
|
133
|
+
context: UnsafeMutableRawPointer?) {
|
|
134
|
+
guard keyPath == "status", let item = object as? AVPlayerItem, item === observedItem else { return }
|
|
135
|
+
switch item.status {
|
|
136
|
+
case .readyToPlay:
|
|
137
|
+
let dur = CMTimeGetSeconds(item.duration) * 1000
|
|
138
|
+
durationMillis = (dur.isFinite && dur > 0) ? dur : 0
|
|
139
|
+
emit(key: currentKey, position: currentPositionMillis(), duration: durationMillis, state: "playing")
|
|
140
|
+
case .failed:
|
|
141
|
+
emit(key: currentKey, position: 0, duration: 0, state: "error")
|
|
142
|
+
teardown()
|
|
143
|
+
default:
|
|
144
|
+
break
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
private func currentPositionMillis() -> Double {
|
|
149
|
+
guard let p = player else { return 0 }
|
|
150
|
+
let s = CMTimeGetSeconds(p.currentTime())
|
|
151
|
+
return s.isFinite ? s * 1000 : 0
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
private func teardown() {
|
|
155
|
+
if let obs = timeObserver { player?.removeTimeObserver(obs); timeObserver = nil }
|
|
156
|
+
if let end = endObserver { NotificationCenter.default.removeObserver(end); endObserver = nil }
|
|
157
|
+
if let item = observedItem { item.removeObserver(self, forKeyPath: "status"); observedItem = nil }
|
|
158
|
+
player?.pause()
|
|
159
|
+
player = nil
|
|
160
|
+
durationMillis = 0
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
private func activateSession() {
|
|
164
|
+
do {
|
|
165
|
+
try AVAudioSession.sharedInstance().setCategory(.playback, mode: .spokenAudio, options: [])
|
|
166
|
+
try AVAudioSession.sharedInstance().setActive(true)
|
|
167
|
+
} catch {
|
|
168
|
+
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
private func deactivateSession() {
|
|
173
|
+
try? AVAudioSession.sharedInstance().setActive(false, options: .notifyOthersOnDeactivation)
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private func emit(key: String?, position: Double, duration: Double, state: String) {
|
|
177
|
+
guard listenerCount > 0 else { return }
|
|
178
|
+
sendEvent(withName: "ChatSdkAudioState", body: [
|
|
179
|
+
"key": key ?? "",
|
|
180
|
+
"positionMillis": NSNumber(value: position),
|
|
181
|
+
"durationMillis": NSNumber(value: duration),
|
|
182
|
+
"state": state,
|
|
183
|
+
])
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
override func invalidate() {
|
|
187
|
+
DispatchQueue.main.async {
|
|
188
|
+
self.teardown()
|
|
189
|
+
self.deactivateSession()
|
|
190
|
+
}
|
|
191
|
+
super.invalidate()
|
|
192
|
+
}
|
|
193
|
+
}
|
package/lib/commonjs/ChatSDK.js
CHANGED
|
@@ -15,6 +15,7 @@ class ChatSDKSingleton {
|
|
|
15
15
|
lastError = null;
|
|
16
16
|
currentUser = null;
|
|
17
17
|
listeners = new Map();
|
|
18
|
+
pushDeviceToken = null;
|
|
18
19
|
|
|
19
20
|
// ─── Lifecycle ────────────────────────────────────────────────────────────
|
|
20
21
|
|
|
@@ -75,6 +76,7 @@ class ChatSDKSingleton {
|
|
|
75
76
|
}
|
|
76
77
|
}
|
|
77
78
|
async logout() {
|
|
79
|
+
await this.unregisterPushToken().catch(() => {});
|
|
78
80
|
this.currentUser = null;
|
|
79
81
|
this.session?.destroy();
|
|
80
82
|
this.session = null;
|
|
@@ -84,11 +86,22 @@ class ChatSDKSingleton {
|
|
|
84
86
|
|
|
85
87
|
// ─── Push ─────────────────────────────────────────────────────────────────
|
|
86
88
|
|
|
89
|
+
async registerPushToken(deviceToken, platform = 'fcm') {
|
|
90
|
+
this.assertInitialized();
|
|
91
|
+
await this.api.registerPushToken(deviceToken, platform);
|
|
92
|
+
this.pushDeviceToken = deviceToken;
|
|
93
|
+
}
|
|
94
|
+
async unregisterPushToken(deviceToken) {
|
|
95
|
+
const target = deviceToken ?? this.pushDeviceToken;
|
|
96
|
+
if (!target || !this.api) return;
|
|
97
|
+
try {
|
|
98
|
+
await this.api.deletePushToken(target);
|
|
99
|
+
} finally {
|
|
100
|
+
if (target === this.pushDeviceToken) this.pushDeviceToken = null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
87
103
|
handleNotification(_payload) {
|
|
88
|
-
// Навигация обрабатывается в host app через onNotification callback.
|
|
89
|
-
// SDK только проверяет, что payload относится к нашему токену.
|
|
90
104
|
if (_payload.token !== this.config?.token) return;
|
|
91
|
-
// В будущем: открыть ChatScreen если приложение foreground
|
|
92
105
|
}
|
|
93
106
|
|
|
94
107
|
// ─── Getters ──────────────────────────────────────────────────────────────
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"names":["_api","require","_session","ChatSDKSingleton","config","api","sdkConfig","session","state","lastError","currentUser","listeners","Map","init","raw","console","warn","resolveConfig","MobileApiClient","baseUrl","token","setState","getConfig","then","cfg","emit","catch","err","msg","Error","message","String","getLastError","login","user","device","assertInitialized","result","createSession","userId","name","surname","email","phone","setSession","sessionToken","contactId","setUserProfile","startSession","logout","destroy","clearSession","handleNotification","_payload","getApi","getSDKConfig","getBaseUrl","getState","isAuthenticated","getUser","getMessages","getOperator","isRealtimeConnected","isConnected","refreshMessages","Promise","resolve","on","event","handler","has","set","Set","handlers","get","add","delete","ChatSDKSession","apiBaseUrl","emitMessagesUpdated","messages","operator","emitConnectedChange","connected","emitOperatorChanged","payload","emitNewMessage","start","next","data","forEach","h","decoded","JSON","parse","atob","ChatSDK","exports"],"sourceRoot":"..\\..\\src","sources":["ChatSDK.ts"],"mappings":";;;;;;AAAA,IAAAA,IAAA,GAAAC,OAAA;AACA,IAAAC,QAAA,GAAAD,OAAA;AA2BA,MAAME,gBAAgB,CAAC;EACbC,MAAM,GAAyB,IAAI;EACnCC,GAAG,GAA2B,IAAI;EAClCC,SAAS,GAAwB,IAAI;EACrCC,OAAO,GAA0B,IAAI;EACrCC,KAAK,GAAa,MAAM;EACxBC,SAAS,GAAkB,IAAI;EAC/BC,WAAW,GAAuB,IAAI;EACtCC,SAAS,GAAsC,IAAIC,GAAG,CAAC,CAAC;;
|
|
1
|
+
{"version":3,"names":["_api","require","_session","ChatSDKSingleton","config","api","sdkConfig","session","state","lastError","currentUser","listeners","Map","pushDeviceToken","init","raw","console","warn","resolveConfig","MobileApiClient","baseUrl","token","setState","getConfig","then","cfg","emit","catch","err","msg","Error","message","String","getLastError","login","user","device","assertInitialized","result","createSession","userId","name","surname","email","phone","setSession","sessionToken","contactId","setUserProfile","startSession","logout","unregisterPushToken","destroy","clearSession","registerPushToken","deviceToken","platform","target","deletePushToken","handleNotification","_payload","getApi","getSDKConfig","getBaseUrl","getState","isAuthenticated","getUser","getMessages","getOperator","isRealtimeConnected","isConnected","refreshMessages","Promise","resolve","on","event","handler","has","set","Set","handlers","get","add","delete","ChatSDKSession","apiBaseUrl","emitMessagesUpdated","messages","operator","emitConnectedChange","connected","emitOperatorChanged","payload","emitNewMessage","start","next","data","forEach","h","decoded","JSON","parse","atob","ChatSDK","exports"],"sourceRoot":"..\\..\\src","sources":["ChatSDK.ts"],"mappings":"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","ignoreList":[]}
|