@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/CHANGELOG.md
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Все заметные изменения в пакете документируются в этом файле.
|
|
4
|
+
Формат основан на [Keep a Changelog](https://keepachangelog.com/ru/1.0.0/),
|
|
5
|
+
проект следует [семантическому версионированию](https://semver.org/lang/ru/).
|
|
6
|
+
|
|
7
|
+
## [Unreleased]
|
|
8
|
+
|
|
9
|
+
## [0.1.0-beta.1]
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Первая бета-версия React Native SDK для Chat Platform.
|
|
13
|
+
- `ChatSDK.init()` с авторизацией по токену.
|
|
14
|
+
- Компонент `ChatScreen` и хук `useChat`.
|
|
15
|
+
- Поддержка вложений, кнопок в сообщениях, CSI-опросов.
|
|
16
|
+
- Нативные модули file-picker и downloader (Android/iOS).
|
|
17
|
+
- Сборка пакета через `react-native-builder-bob` (commonjs + module + typescript).
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
package = JSON.parse(File.read(File.join(__dir__, "package.json")))
|
|
4
|
+
|
|
5
|
+
Pod::Spec.new do |s|
|
|
6
|
+
s.name = "ChatPlatformSdk"
|
|
7
|
+
s.version = package["version"]
|
|
8
|
+
s.summary = package["description"]
|
|
9
|
+
s.homepage = "https://github.com/robotmia/chat-platform"
|
|
10
|
+
s.license = "UNLICENSED"
|
|
11
|
+
s.author = "Chat Platform"
|
|
12
|
+
s.platforms = { :ios => "13.4" }
|
|
13
|
+
s.source = { :git => "https://github.com/robotmia/chat-platform.git", :tag => "#{s.version}" }
|
|
14
|
+
|
|
15
|
+
s.source_files = "ios/**/*.{h,m,mm,swift}"
|
|
16
|
+
s.swift_version = "5.0"
|
|
17
|
+
s.requires_arc = true
|
|
18
|
+
|
|
19
|
+
s.dependency "React-Core"
|
|
20
|
+
end
|
package/README.md
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
# @chat-platform/sdk-react-native
|
|
2
|
+
|
|
3
|
+
React Native SDK для встройки чата в мобильное приложение.
|
|
4
|
+
|
|
5
|
+
**Версия:** 0.1.0-beta.1
|
|
6
|
+
**JS-требования:** React Native ≥ 0.72, React ≥ 18.2
|
|
7
|
+
**Нативные минимумы:** iOS 13.4, Android API 24 (Android 7.0)
|
|
8
|
+
|
|
9
|
+
Пикер файлов и скачивание вложений работают «из коробки» — никаких дополнительных
|
|
10
|
+
нативных модулей (`expo-*`, `@react-native-documents/*` и т.п.) ставить **не нужно**.
|
|
11
|
+
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
## Установка
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
# npm
|
|
18
|
+
npm install @chat-platform/sdk-react-native
|
|
19
|
+
|
|
20
|
+
# yarn
|
|
21
|
+
yarn add @chat-platform/sdk-react-native
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
После установки:
|
|
25
|
+
|
|
26
|
+
**iOS**
|
|
27
|
+
```bash
|
|
28
|
+
cd ios && pod install
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
**Android** — пересборка через Android Studio или `./gradlew assembleDebug`.
|
|
32
|
+
Никаких правок в `MainApplication.kt` / `AppDelegate.mm` не требуется — модуль
|
|
33
|
+
подключается через autolinking (см. `react-native.config.js` в пакете).
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## Быстрый старт
|
|
38
|
+
|
|
39
|
+
### 1. Инициализация (один раз при старте приложения)
|
|
40
|
+
|
|
41
|
+
```tsx
|
|
42
|
+
// App.tsx
|
|
43
|
+
import { ChatSDK } from '@chat-platform/sdk-react-native'
|
|
44
|
+
|
|
45
|
+
ChatSDK.init({
|
|
46
|
+
token: 'ВАШ_ТОКЕН_ИЗ_ЧП', // универсальный токен (содержит widget token + baseUrl)
|
|
47
|
+
locale: 'ru', // 'ru' | 'en' (опционально)
|
|
48
|
+
})
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Токен из настроек виджета в ЧП — это base64-JSON вида `{"token":"...","baseUrl":"..."}`.
|
|
52
|
+
SDK сам распакует его и подставит нужный `baseUrl`. Передавать `baseUrl` отдельно
|
|
53
|
+
не нужно.
|
|
54
|
+
|
|
55
|
+
### 2. Логин после авторизации пользователя
|
|
56
|
+
|
|
57
|
+
```tsx
|
|
58
|
+
await ChatSDK.login(
|
|
59
|
+
{
|
|
60
|
+
userId: user.id, // обязательно — id из вашей системы
|
|
61
|
+
name: user.firstName,
|
|
62
|
+
surname: user.lastName,
|
|
63
|
+
email: user.email,
|
|
64
|
+
phone: user.phone,
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
platform: 'ios', // 'ios' | 'android' | 'other'
|
|
68
|
+
appVersion: '1.0.0',
|
|
69
|
+
bundleId: 'com.yourapp',
|
|
70
|
+
},
|
|
71
|
+
)
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### 3. Открыть чат
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { ChatScreen } from '@chat-platform/sdk-react-native'
|
|
78
|
+
|
|
79
|
+
// В стеке навигации
|
|
80
|
+
<Stack.Screen name="Chat" component={ChatScreen} />
|
|
81
|
+
|
|
82
|
+
// или как компонент с onClose
|
|
83
|
+
<ChatScreen onClose={() => navigation.goBack()} />
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
### 4. Логаут
|
|
87
|
+
|
|
88
|
+
```tsx
|
|
89
|
+
await ChatSDK.logout()
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
---
|
|
93
|
+
|
|
94
|
+
## Что включено по умолчанию
|
|
95
|
+
|
|
96
|
+
| Возможность | Android | iOS |
|
|
97
|
+
|------------------------------------------|------------------------------------------|--------------------------------------|
|
|
98
|
+
| Выбор файлов из системного пикера | SAF `OpenDocument` (множественный выбор) | `UIDocumentPickerViewController` |
|
|
99
|
+
| Скачивание вложений в системные «Файлы» | `MediaStore.Downloads` (API 29+) | `UIActivityViewController` (share-sheet) |
|
|
100
|
+
| Прогресс скачивания | Через `NativeEventEmitter` | Через `NativeEventEmitter` |
|
|
101
|
+
| Notification «Загрузка завершена» | Да, тап открывает файл | Native share-sheet после загрузки |
|
|
102
|
+
| Realtime (Laravel Reverb / Pusher) | Да | Да |
|
|
103
|
+
| Поддержка кнопок ботов в сообщениях | Да | Да |
|
|
104
|
+
| CSI-опрос после закрытия диалога | Да | Да |
|
|
105
|
+
| Галерея вложений (изображения + документы) | Да | Да |
|
|
106
|
+
|
|
107
|
+
Никаких runtime-permissions для скачивания на Android 10+ запрашивать не нужно.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Компонент `<ChatScreen />`
|
|
112
|
+
|
|
113
|
+
| Prop | Тип | Назначение |
|
|
114
|
+
|-------------------------|----------------------------------------------|---------------------------------------------------------|
|
|
115
|
+
| `onClose` | `() => void` | Колбэк для кнопки «назад» в хедере |
|
|
116
|
+
| `theme` | `Partial<ChatTheme>` | Переопределение цветов поверх темы из ЧП |
|
|
117
|
+
| `strings` | `ChatStrings` | Кастомные тексты (см. ниже) |
|
|
118
|
+
| `onPickFiles` | `() => Promise<AttachmentInput[] \| null>` | Escape-hatch: своя реализация пикера |
|
|
119
|
+
| `onDownloadAttachment` | `(a: GalleryAttachment) => Promise<void>` | Escape-hatch: своя реализация скачивания |
|
|
120
|
+
|
|
121
|
+
`onPickFiles` и `onDownloadAttachment` нужны **только** если вы хотите перебить
|
|
122
|
+
встроенное поведение (например, открывать кастомный пикер). Во всех остальных
|
|
123
|
+
случаях оставляйте их `undefined` — SDK использует свои нативные модули.
|
|
124
|
+
|
|
125
|
+
### Кастомные тексты
|
|
126
|
+
|
|
127
|
+
```tsx
|
|
128
|
+
<ChatScreen
|
|
129
|
+
strings={{
|
|
130
|
+
headerTitle: 'Поддержка',
|
|
131
|
+
emptyStateText: 'Опишите проблему — мы ответим в течение минуты',
|
|
132
|
+
inputPlaceholder: 'Ваше сообщение…',
|
|
133
|
+
errorRetry: 'Повторить',
|
|
134
|
+
galleryDownload: 'Сохранить',
|
|
135
|
+
surveyTitle: 'Оцените работу оператора',
|
|
136
|
+
surveySubmit: 'Отправить',
|
|
137
|
+
surveySkip: 'Пропустить',
|
|
138
|
+
surveyClose: 'Закрыть',
|
|
139
|
+
sendingText: 'Отправка…',
|
|
140
|
+
}}
|
|
141
|
+
/>
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Все поля опциональны — непереданные значения берутся из встроенных дефолтов на
|
|
145
|
+
русском.
|
|
146
|
+
|
|
147
|
+
### Тема
|
|
148
|
+
|
|
149
|
+
```tsx
|
|
150
|
+
<ChatScreen
|
|
151
|
+
theme={{
|
|
152
|
+
primaryColor: '#7c3aed',
|
|
153
|
+
headerBg: '#7c3aed',
|
|
154
|
+
outboundBg: '#7c3aed',
|
|
155
|
+
background: '#fafafa',
|
|
156
|
+
}}
|
|
157
|
+
/>
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Полный список полей — в `ChatTheme` (см. `src/theme.ts`).
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## События
|
|
165
|
+
|
|
166
|
+
После `ChatSDK.login()` SDK поднимает фоновую сессию (WebSocket + polling fallback)
|
|
167
|
+
и держит её живой до `logout()`. Поэтому события ниже **работают независимо от того,
|
|
168
|
+
открыт ли `<ChatScreen />`** — пока приложение в foreground, подписчик будет получать
|
|
169
|
+
их даже при закрытом чате.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
const unsubState = ChatSDK.on('stateChange', (state) => {
|
|
173
|
+
// 'idle' | 'initializing' | 'ready' | 'authenticated' | 'error'
|
|
174
|
+
})
|
|
175
|
+
|
|
176
|
+
const unsubOperator = ChatSDK.on('operatorChanged', (p) => {
|
|
177
|
+
// p: { token, contactId, previousOperator, operator, occurredAt }
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
const unsubMessage = ChatSDK.on('newMessage', (p) => {
|
|
181
|
+
// p: { token, contactId, message, operator, occurredAt }
|
|
182
|
+
// Срабатывает только для сообщений от операторов (type === 'user').
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
const unsubMessagesUpdated = ChatSDK.on('messagesUpdated', (p) => {
|
|
186
|
+
// p: { messages, operator } — полный текущий список после любого refresh
|
|
187
|
+
})
|
|
188
|
+
|
|
189
|
+
const unsubConnected = ChatSDK.on('connectedChange', (online) => {
|
|
190
|
+
// true когда WS подключён, false при отвале (тогда работает polling)
|
|
191
|
+
})
|
|
192
|
+
|
|
193
|
+
const unsubError = ChatSDK.on('error', (err) => {
|
|
194
|
+
// ошибка инициализации/сессии
|
|
195
|
+
})
|
|
196
|
+
|
|
197
|
+
// Снятие подписки
|
|
198
|
+
unsubState(); unsubOperator(); unsubMessage()
|
|
199
|
+
unsubMessagesUpdated(); unsubConnected(); unsubError()
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Подписка на `newMessage` подходит для собственных in-app уведомлений (тосты,
|
|
203
|
+
бейджи в табах) — событие приходит даже если пользователь не открывал чат, пока
|
|
204
|
+
приложение в foreground. Для push-уведомлений в background используйте webhook
|
|
205
|
+
(см. ниже).
|
|
206
|
+
|
|
207
|
+
### Поведение в фоне
|
|
208
|
+
|
|
209
|
+
- При уходе в background polling приостанавливается (батарея).
|
|
210
|
+
- WebSocket OS-зависим: некоторые системы дают ему ещё минуту-две, потом гасят.
|
|
211
|
+
- При возврате в foreground SDK делает one-shot `refreshMessages` и при
|
|
212
|
+
необходимости поднимает сокет.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Push-уведомления
|
|
217
|
+
|
|
218
|
+
ЧП **не** шлёт FCM/APNs напрямую. Вместо этого ЧП отправляет webhook на ваш
|
|
219
|
+
backend при новом сообщении от оператора. Ваш backend сам решает, кому и как
|
|
220
|
+
доставить push.
|
|
221
|
+
|
|
222
|
+
### Webhook payload
|
|
223
|
+
|
|
224
|
+
```json
|
|
225
|
+
{
|
|
226
|
+
"event": "message.created",
|
|
227
|
+
"version": 1,
|
|
228
|
+
"token": "widget_token",
|
|
229
|
+
"contactId": "user_123",
|
|
230
|
+
"conversationId": 456,
|
|
231
|
+
"messageId": 789,
|
|
232
|
+
"senderType": "user",
|
|
233
|
+
"preview": "Здравствуйте!",
|
|
234
|
+
"hasAttachments": false,
|
|
235
|
+
"createdAt": "2026-06-01T10:00:00Z"
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Подпись: заголовок `X-Chat-Platform-Signature: sha256=HMAC_SHA256(body, webhook_secret)`.
|
|
240
|
+
|
|
241
|
+
### Открытие чата из тапа по push
|
|
242
|
+
|
|
243
|
+
```tsx
|
|
244
|
+
import { ChatSDK } from '@chat-platform/sdk-react-native'
|
|
245
|
+
|
|
246
|
+
// В обработчике нотификации
|
|
247
|
+
ChatSDK.handleNotification({
|
|
248
|
+
token: data.cp_token,
|
|
249
|
+
contactId: data.cp_contact_id,
|
|
250
|
+
})
|
|
251
|
+
navigation.navigate('Chat')
|
|
252
|
+
```
|
|
253
|
+
|
|
254
|
+
Рекомендуемые data-ключи в payload FCM/APNs: `cp_token`, `cp_contact_id`.
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
## API
|
|
259
|
+
|
|
260
|
+
### `ChatSDK.init(config)`
|
|
261
|
+
|
|
262
|
+
| Поле | Тип | Обязательно |
|
|
263
|
+
|----------|--------------------|-------------|
|
|
264
|
+
| `token` | `string` | Да — универсальный токен из ЧП (base64-JSON с `token` + `baseUrl`) |
|
|
265
|
+
| `baseUrl`| `string` | Нет — указывается только при ручной разработке против локального инстанса ЧП |
|
|
266
|
+
| `locale` | `'ru' \| 'en'` | Нет |
|
|
267
|
+
|
|
268
|
+
### `ChatSDK.login(user, device?)`
|
|
269
|
+
|
|
270
|
+
| Поле | Тип | Обязательно |
|
|
271
|
+
|------------|-----------|-------------|
|
|
272
|
+
| `userId` | `string` | Да |
|
|
273
|
+
| `name` | `string` | Нет |
|
|
274
|
+
| `surname` | `string` | Нет |
|
|
275
|
+
| `email` | `string` | Нет |
|
|
276
|
+
| `phone` | `string` | Нет |
|
|
277
|
+
|
|
278
|
+
`device` (опционально): `{ platform, appVersion, bundleId }`.
|
|
279
|
+
|
|
280
|
+
### `ChatSDK.logout()`
|
|
281
|
+
|
|
282
|
+
Завершает сессию, отключает realtime.
|
|
283
|
+
|
|
284
|
+
### `ChatSDK.handleNotification(payload)`
|
|
285
|
+
|
|
286
|
+
Помечает push как относящийся к нашему виджету (по `token`). Навигацию на
|
|
287
|
+
`ChatScreen` запускает host-приложение.
|
|
288
|
+
|
|
289
|
+
### `ChatSDK.getState() / isAuthenticated() / getUser()`
|
|
290
|
+
|
|
291
|
+
Геттеры текущего состояния SDK.
|
|
292
|
+
|
|
293
|
+
### `ChatSDK.on(event, handler)`
|
|
294
|
+
|
|
295
|
+
Подписка на события — см. раздел «События» выше. Возвращает функцию отписки.
|
|
296
|
+
|
|
297
|
+
---
|
|
298
|
+
|
|
299
|
+
## Чеклист интеграции
|
|
300
|
+
|
|
301
|
+
- [ ] Получить `widget_token` и `baseUrl` от ЧП
|
|
302
|
+
- [ ] `npm install @chat-platform/sdk-react-native` + `pod install` для iOS
|
|
303
|
+
- [ ] Пересобрать нативную часть (Xcode / Android Studio)
|
|
304
|
+
- [ ] `ChatSDK.init(...)` в точке входа приложения
|
|
305
|
+
- [ ] `ChatSDK.login(...)` после авторизации пользователя
|
|
306
|
+
- [ ] `<ChatScreen />` в навигаторе
|
|
307
|
+
- [ ] Webhook URL в настройках виджета в ЧП
|
|
308
|
+
- [ ] FCM/APNs: данные `cp_token`, `cp_contact_id` в data payload
|
|
309
|
+
- [ ] `ChatSDK.handleNotification(...)` в обработчике push на стороне приложения
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
## Лицензия
|
|
314
|
+
|
|
315
|
+
UNLICENSED. Внутренний пакет — для использования только в рамках проектов ЧП.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
buildscript {
|
|
2
|
+
ext.getExtOrDefault = { name, defaultValue ->
|
|
3
|
+
rootProject.ext.has(name) ? rootProject.ext.get(name) : defaultValue
|
|
4
|
+
}
|
|
5
|
+
repositories {
|
|
6
|
+
google()
|
|
7
|
+
mavenCentral()
|
|
8
|
+
}
|
|
9
|
+
dependencies {
|
|
10
|
+
classpath "com.android.tools.build:gradle:8.1.1"
|
|
11
|
+
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.22"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
apply plugin: "com.android.library"
|
|
16
|
+
apply plugin: "kotlin-android"
|
|
17
|
+
|
|
18
|
+
android {
|
|
19
|
+
namespace "com.chatplatform.sdk"
|
|
20
|
+
compileSdkVersion getExtOrDefault("compileSdkVersion", 34)
|
|
21
|
+
|
|
22
|
+
defaultConfig {
|
|
23
|
+
minSdkVersion getExtOrDefault("minSdkVersion", 24)
|
|
24
|
+
targetSdkVersion getExtOrDefault("targetSdkVersion", 34)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
compileOptions {
|
|
28
|
+
sourceCompatibility JavaVersion.VERSION_17
|
|
29
|
+
targetCompatibility JavaVersion.VERSION_17
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
kotlinOptions {
|
|
33
|
+
jvmTarget = "17"
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
sourceSets {
|
|
37
|
+
main {
|
|
38
|
+
manifest.srcFile "src/main/AndroidManifest.xml"
|
|
39
|
+
java.srcDirs = ["src/main/java"]
|
|
40
|
+
res.srcDirs = ["src/main/res"]
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
repositories {
|
|
46
|
+
google()
|
|
47
|
+
mavenCentral()
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
dependencies {
|
|
51
|
+
implementation "com.facebook.react:react-android"
|
|
52
|
+
implementation "androidx.core:core-ktx:1.13.1"
|
|
53
|
+
implementation "com.squareup.okhttp3:okhttp:4.12.0"
|
|
54
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="utf-8"?>
|
|
2
|
+
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
3
|
+
|
|
4
|
+
<uses-permission android:name="android.permission.INTERNET" />
|
|
5
|
+
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
|
6
|
+
|
|
7
|
+
<application>
|
|
8
|
+
<provider
|
|
9
|
+
android:name="androidx.core.content.FileProvider"
|
|
10
|
+
android:authorities="${applicationId}.chatsdk.fileprovider"
|
|
11
|
+
android:exported="false"
|
|
12
|
+
android:grantUriPermissions="true">
|
|
13
|
+
<meta-data
|
|
14
|
+
android:name="android.support.FILE_PROVIDER_PATHS"
|
|
15
|
+
android:resource="@xml/chat_sdk_file_paths" />
|
|
16
|
+
</provider>
|
|
17
|
+
</application>
|
|
18
|
+
</manifest>
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
package com.chatplatform.sdk
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationChannel
|
|
4
|
+
import android.app.NotificationManager
|
|
5
|
+
import android.app.PendingIntent
|
|
6
|
+
import android.content.ContentValues
|
|
7
|
+
import android.content.Context
|
|
8
|
+
import android.content.Intent
|
|
9
|
+
import android.net.Uri
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import android.os.Environment
|
|
12
|
+
import android.provider.MediaStore
|
|
13
|
+
import androidx.core.app.NotificationCompat
|
|
14
|
+
import androidx.core.content.FileProvider
|
|
15
|
+
import com.facebook.react.bridge.Arguments
|
|
16
|
+
import com.facebook.react.bridge.Promise
|
|
17
|
+
import com.facebook.react.bridge.ReactApplicationContext
|
|
18
|
+
import com.facebook.react.bridge.ReactContextBaseJavaModule
|
|
19
|
+
import com.facebook.react.bridge.ReactMethod
|
|
20
|
+
import com.facebook.react.bridge.ReadableMap
|
|
21
|
+
import com.facebook.react.bridge.WritableMap
|
|
22
|
+
import com.facebook.react.modules.core.DeviceEventManagerModule
|
|
23
|
+
import okhttp3.OkHttpClient
|
|
24
|
+
import okhttp3.Request
|
|
25
|
+
import java.io.File
|
|
26
|
+
import java.io.FileOutputStream
|
|
27
|
+
import java.io.InputStream
|
|
28
|
+
import java.util.UUID
|
|
29
|
+
import java.util.concurrent.Executors
|
|
30
|
+
import java.util.concurrent.TimeUnit
|
|
31
|
+
|
|
32
|
+
class ChatSdkDownloaderModule(reactContext: ReactApplicationContext) :
|
|
33
|
+
ReactContextBaseJavaModule(reactContext) {
|
|
34
|
+
|
|
35
|
+
override fun getName(): String = NAME
|
|
36
|
+
|
|
37
|
+
private val executor = Executors.newCachedThreadPool()
|
|
38
|
+
private val client: OkHttpClient by lazy {
|
|
39
|
+
OkHttpClient.Builder()
|
|
40
|
+
.connectTimeout(30, TimeUnit.SECONDS)
|
|
41
|
+
.readTimeout(120, TimeUnit.SECONDS)
|
|
42
|
+
.build()
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
@ReactMethod
|
|
46
|
+
fun addListener(eventName: String) {
|
|
47
|
+
// RN требует наличия этих методов для NativeEventEmitter
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@ReactMethod
|
|
51
|
+
fun removeListeners(count: Int) {
|
|
52
|
+
// RN требует наличия этих методов для NativeEventEmitter
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
@ReactMethod
|
|
56
|
+
fun download(request: ReadableMap, promise: Promise) {
|
|
57
|
+
val url = request.getString("url")
|
|
58
|
+
val filename = sanitize(request.getString("filename") ?: "file")
|
|
59
|
+
val mime = request.getString("mime") ?: "application/octet-stream"
|
|
60
|
+
if (url.isNullOrEmpty()) {
|
|
61
|
+
promise.reject("INVALID_URL", "URL не задан")
|
|
62
|
+
return
|
|
63
|
+
}
|
|
64
|
+
val headers: ReadableMap? = if (request.hasKey("headers")) request.getMap("headers") else null
|
|
65
|
+
val id = UUID.randomUUID().toString()
|
|
66
|
+
|
|
67
|
+
executor.execute {
|
|
68
|
+
try {
|
|
69
|
+
val builder = Request.Builder().url(url).get()
|
|
70
|
+
headers?.toHashMap()?.forEach { (k, v) ->
|
|
71
|
+
if (v is String) builder.header(k, v)
|
|
72
|
+
}
|
|
73
|
+
val response = client.newCall(builder.build()).execute()
|
|
74
|
+
if (!response.isSuccessful) {
|
|
75
|
+
response.close()
|
|
76
|
+
promise.reject("HTTP_${response.code}", "Ошибка загрузки: HTTP ${response.code}")
|
|
77
|
+
return@execute
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
val body = response.body
|
|
81
|
+
if (body == null) {
|
|
82
|
+
response.close()
|
|
83
|
+
promise.reject("EMPTY_BODY", "Пустой ответ сервера")
|
|
84
|
+
return@execute
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
val total = body.contentLength()
|
|
88
|
+
val context = reactApplicationContext
|
|
89
|
+
val savedUri: Uri = body.byteStream().use { stream ->
|
|
90
|
+
saveToDownloads(context, stream, filename, mime, total, id)
|
|
91
|
+
}
|
|
92
|
+
response.close()
|
|
93
|
+
|
|
94
|
+
postCompletionNotification(context, filename, mime, savedUri)
|
|
95
|
+
|
|
96
|
+
val result: WritableMap = Arguments.createMap()
|
|
97
|
+
result.putString("id", id)
|
|
98
|
+
result.putString("uri", savedUri.toString())
|
|
99
|
+
promise.resolve(result)
|
|
100
|
+
} catch (e: Throwable) {
|
|
101
|
+
promise.reject("DOWNLOAD_FAILED", e.message ?: "Не удалось скачать файл", e)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
private fun saveToDownloads(
|
|
107
|
+
context: Context,
|
|
108
|
+
input: InputStream,
|
|
109
|
+
filename: String,
|
|
110
|
+
mime: String,
|
|
111
|
+
total: Long,
|
|
112
|
+
id: String,
|
|
113
|
+
): Uri {
|
|
114
|
+
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
|
115
|
+
saveViaMediaStore(context, input, filename, mime, total, id)
|
|
116
|
+
} else {
|
|
117
|
+
saveToExternalFiles(context, input, filename, total, id)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private fun saveViaMediaStore(
|
|
122
|
+
context: Context,
|
|
123
|
+
input: InputStream,
|
|
124
|
+
filename: String,
|
|
125
|
+
mime: String,
|
|
126
|
+
total: Long,
|
|
127
|
+
id: String,
|
|
128
|
+
): Uri {
|
|
129
|
+
val resolver = context.contentResolver
|
|
130
|
+
val collection = MediaStore.Downloads.getContentUri(MediaStore.VOLUME_EXTERNAL_PRIMARY)
|
|
131
|
+
val values = ContentValues().apply {
|
|
132
|
+
put(MediaStore.Downloads.DISPLAY_NAME, filename)
|
|
133
|
+
put(MediaStore.Downloads.MIME_TYPE, mime)
|
|
134
|
+
put(MediaStore.Downloads.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
|
|
135
|
+
put(MediaStore.Downloads.IS_PENDING, 1)
|
|
136
|
+
}
|
|
137
|
+
val uri = resolver.insert(collection, values)
|
|
138
|
+
?: throw IllegalStateException("Не удалось создать запись в MediaStore")
|
|
139
|
+
|
|
140
|
+
resolver.openOutputStream(uri).use { output ->
|
|
141
|
+
if (output == null) throw IllegalStateException("Не удалось открыть OutputStream")
|
|
142
|
+
copyWithProgress(input, output, total, id)
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
values.clear()
|
|
146
|
+
values.put(MediaStore.Downloads.IS_PENDING, 0)
|
|
147
|
+
resolver.update(uri, values, null, null)
|
|
148
|
+
return uri
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
private fun saveToExternalFiles(
|
|
152
|
+
context: Context,
|
|
153
|
+
input: InputStream,
|
|
154
|
+
filename: String,
|
|
155
|
+
total: Long,
|
|
156
|
+
id: String,
|
|
157
|
+
): Uri {
|
|
158
|
+
val dir = context.getExternalFilesDir(Environment.DIRECTORY_DOWNLOADS)
|
|
159
|
+
?: File(context.filesDir, "downloads").apply { mkdirs() }
|
|
160
|
+
if (!dir.exists()) dir.mkdirs()
|
|
161
|
+
val target = File(dir, filename)
|
|
162
|
+
FileOutputStream(target).use { output -> copyWithProgress(input, output, total, id) }
|
|
163
|
+
val authority = "${context.packageName}.chatsdk.fileprovider"
|
|
164
|
+
return FileProvider.getUriForFile(context, authority, target)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private fun copyWithProgress(input: InputStream, output: java.io.OutputStream, total: Long, id: String) {
|
|
168
|
+
val buffer = ByteArray(64 * 1024)
|
|
169
|
+
var written = 0L
|
|
170
|
+
var lastEmit = 0L
|
|
171
|
+
while (true) {
|
|
172
|
+
val read = input.read(buffer)
|
|
173
|
+
if (read <= 0) break
|
|
174
|
+
output.write(buffer, 0, read)
|
|
175
|
+
written += read
|
|
176
|
+
val now = System.currentTimeMillis()
|
|
177
|
+
if (now - lastEmit >= 100) {
|
|
178
|
+
emitProgress(id, written, total)
|
|
179
|
+
lastEmit = now
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
output.flush()
|
|
183
|
+
emitProgress(id, written, if (total > 0) total else written)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private fun emitProgress(id: String, written: Long, total: Long) {
|
|
187
|
+
val map: WritableMap = Arguments.createMap()
|
|
188
|
+
map.putString("id", id)
|
|
189
|
+
map.putDouble("bytesWritten", written.toDouble())
|
|
190
|
+
map.putDouble("totalBytes", total.toDouble())
|
|
191
|
+
reactApplicationContext
|
|
192
|
+
.getJSModule(DeviceEventManagerModule.RCTDeviceEventEmitter::class.java)
|
|
193
|
+
.emit("ChatSdkDownloadProgress", map)
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
private fun postCompletionNotification(context: Context, filename: String, mime: String, uri: Uri) {
|
|
197
|
+
ensureChannel(context)
|
|
198
|
+
val openIntent = Intent(Intent.ACTION_VIEW).apply {
|
|
199
|
+
setDataAndType(uri, mime)
|
|
200
|
+
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION or Intent.FLAG_ACTIVITY_NEW_TASK)
|
|
201
|
+
}
|
|
202
|
+
val pi = PendingIntent.getActivity(
|
|
203
|
+
context,
|
|
204
|
+
uri.hashCode(),
|
|
205
|
+
openIntent,
|
|
206
|
+
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
|
|
207
|
+
)
|
|
208
|
+
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
|
|
209
|
+
.setSmallIcon(android.R.drawable.stat_sys_download_done)
|
|
210
|
+
.setContentTitle(filename)
|
|
211
|
+
.setContentText("Загрузка завершена")
|
|
212
|
+
.setAutoCancel(true)
|
|
213
|
+
.setContentIntent(pi)
|
|
214
|
+
.build()
|
|
215
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
216
|
+
manager.notify(uri.hashCode(), notification)
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
private fun ensureChannel(context: Context) {
|
|
220
|
+
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
|
|
221
|
+
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
222
|
+
if (manager.getNotificationChannel(CHANNEL_ID) != null) return
|
|
223
|
+
val channel = NotificationChannel(
|
|
224
|
+
CHANNEL_ID,
|
|
225
|
+
"Загрузки",
|
|
226
|
+
NotificationManager.IMPORTANCE_LOW,
|
|
227
|
+
)
|
|
228
|
+
manager.createNotificationChannel(channel)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
private fun sanitize(name: String): String {
|
|
232
|
+
val cleaned = name.replace(Regex("[/\\\\?%*:|\"<>]"), "_").trim()
|
|
233
|
+
return if (cleaned.isEmpty()) "file" else cleaned
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
companion object {
|
|
237
|
+
const val NAME = "ChatSdkDownloader"
|
|
238
|
+
private const val CHANNEL_ID = "chat_sdk_downloads"
|
|
239
|
+
}
|
|
240
|
+
}
|