@regantis-sdk/react-native-chat 0.1.3 → 0.1.5
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/README.md +8 -7
- package/RegantisReactNativeChat.podspec +1 -1
- package/android/src/main/AndroidManifest.xml +9 -0
- package/android/src/main/java/technology/regantis/chat/RegantisChatNativeModule.java +59 -6
- package/assets/regantis_logo_black.png +0 -0
- package/assets/regantis_logo_white.png +0 -0
- package/assets/welcome_background_dark.png +0 -0
- package/assets/welcome_background_light.png +0 -0
- package/ios/RegantisChatNative.m +46 -15
- package/package.json +2 -1
- package/src/RegantisChat.js +406 -81
- package/src/client.js +16 -1
- package/src/defaults.js +1 -1
package/README.md
CHANGED
|
@@ -34,7 +34,7 @@ To use the built-in camera action, add a camera usage description to the host ap
|
|
|
34
34
|
<string>Take photos to attach to support chat messages.</string>
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
-
Android
|
|
37
|
+
Android declares `CAMERA` in the SDK manifest and requests the permission only when the visitor chooses **Take photo**. If permission was denied permanently, the SDK offers a shortcut to the app settings.
|
|
38
38
|
|
|
39
39
|
## Basic usage
|
|
40
40
|
|
|
@@ -123,19 +123,20 @@ Server configuration remains the default. Per-app overrides are optional:
|
|
|
123
123
|
- WebSocket realtime updates with HTTP polling fallback
|
|
124
124
|
- typing state
|
|
125
125
|
- customer-side translation selector
|
|
126
|
-
- image gallery, file picker and direct camera capture
|
|
127
|
-
- in-chat image lightbox with pinch zoom and
|
|
128
|
-
- native incoming-message sound
|
|
126
|
+
- image gallery, file picker and direct camera capture with runtime permission handling
|
|
127
|
+
- in-chat image lightbox with pinch zoom, pan, reset and +/- zoom controls
|
|
128
|
+
- native incoming-message sound with persisted user preference
|
|
129
129
|
- close/reopen conversation
|
|
130
130
|
- rating and rating comment
|
|
131
131
|
- transcript email flow
|
|
132
132
|
- read state and unread callback
|
|
133
133
|
- light/dark mode
|
|
134
|
-
- basic/advanced color configuration
|
|
135
|
-
- logo, agent photo toggle and white-label behavior
|
|
134
|
+
- basic/advanced color configuration including background opacity
|
|
135
|
+
- custom logo with fallback, agent photo toggle, profile-name display and white-label behavior
|
|
136
|
+
- mobile welcome screen with the same soft blurred-glow treatment as the web widget
|
|
136
137
|
- CRM-managed localized UI texts
|
|
137
138
|
|
|
138
|
-
The web-only launcher settings such as desktop/mobile corner placement and minimized bubble/bar mode are still returned in the settings object for configuration parity, but they do not change a full-page React Native component because there is no browser launcher to position.
|
|
139
|
+
The web-only launcher settings such as desktop/mobile corner placement, launcher visibility and minimized bubble/bar mode are still returned in the settings object for configuration parity, but they do not change a full-page React Native component because there is no browser launcher to position. `before_you_go_enabled` is also intentionally not used in native because that feature is a desktop mouse-exit prompt in the web loader.
|
|
139
140
|
|
|
140
141
|
## Headless client
|
|
141
142
|
|
|
@@ -12,6 +12,6 @@ Pod::Spec.new do |s|
|
|
|
12
12
|
s.platforms = { :ios => "15.1" }
|
|
13
13
|
s.source = { :git => "https://github.com/regantis/react-native-chat.git", :tag => "#{s.version}" }
|
|
14
14
|
s.source_files = "ios/**/*.{h,m,mm}"
|
|
15
|
-
s.frameworks = "AudioToolbox", "UniformTypeIdentifiers"
|
|
15
|
+
s.frameworks = "AudioToolbox", "AVFoundation", "UniformTypeIdentifiers"
|
|
16
16
|
s.dependency "React-Core"
|
|
17
17
|
end
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
|
2
|
+
<uses-permission android:name="android.permission.CAMERA" />
|
|
3
|
+
<uses-feature android:name="android.hardware.camera" android:required="false" />
|
|
4
|
+
|
|
5
|
+
<queries>
|
|
6
|
+
<intent>
|
|
7
|
+
<action android:name="android.media.action.IMAGE_CAPTURE" />
|
|
8
|
+
</intent>
|
|
9
|
+
</queries>
|
|
10
|
+
|
|
2
11
|
<application>
|
|
3
12
|
<provider
|
|
4
13
|
android:name="androidx.core.content.FileProvider"
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
package technology.regantis.chat;
|
|
2
2
|
|
|
3
|
+
import android.Manifest;
|
|
3
4
|
import android.app.Activity;
|
|
4
5
|
import android.content.ClipData;
|
|
5
6
|
import android.content.Context;
|
|
6
7
|
import android.content.Intent;
|
|
7
8
|
import android.content.SharedPreferences;
|
|
9
|
+
import android.content.pm.PackageManager;
|
|
8
10
|
import android.content.pm.ResolveInfo;
|
|
9
11
|
import android.database.Cursor;
|
|
10
12
|
import android.media.Ringtone;
|
|
@@ -16,6 +18,7 @@ import android.webkit.MimeTypeMap;
|
|
|
16
18
|
|
|
17
19
|
import androidx.annotation.NonNull;
|
|
18
20
|
import androidx.annotation.Nullable;
|
|
21
|
+
import androidx.core.content.ContextCompat;
|
|
19
22
|
import androidx.core.content.FileProvider;
|
|
20
23
|
|
|
21
24
|
import com.facebook.react.bridge.ActivityEventListener;
|
|
@@ -26,6 +29,8 @@ import com.facebook.react.bridge.ReactApplicationContext;
|
|
|
26
29
|
import com.facebook.react.bridge.ReactContextBaseJavaModule;
|
|
27
30
|
import com.facebook.react.bridge.ReactMethod;
|
|
28
31
|
import com.facebook.react.bridge.WritableMap;
|
|
32
|
+
import com.facebook.react.modules.core.PermissionAwareActivity;
|
|
33
|
+
import com.facebook.react.modules.core.PermissionListener;
|
|
29
34
|
|
|
30
35
|
import java.io.File;
|
|
31
36
|
import java.util.List;
|
|
@@ -35,10 +40,12 @@ public class RegantisChatNativeModule extends ReactContextBaseJavaModule {
|
|
|
35
40
|
private static final String STORAGE_NAME = "regantis_react_native_chat";
|
|
36
41
|
private static final int PICK_ATTACHMENT_REQUEST = 17841;
|
|
37
42
|
private static final int TAKE_PHOTO_REQUEST = 17842;
|
|
43
|
+
private static final int CAMERA_PERMISSION_REQUEST = 17843;
|
|
38
44
|
|
|
39
45
|
private final ReactApplicationContext reactContext;
|
|
40
46
|
private Promise pickerPromise;
|
|
41
47
|
private Promise cameraPromise;
|
|
48
|
+
private Promise cameraPermissionPromise;
|
|
42
49
|
private File cameraFile;
|
|
43
50
|
private Uri cameraOutputUri;
|
|
44
51
|
|
|
@@ -178,18 +185,58 @@ public class RegantisChatNativeModule extends ReactContextBaseJavaModule {
|
|
|
178
185
|
promise.reject("NO_ACTIVITY", "No active Android Activity");
|
|
179
186
|
return;
|
|
180
187
|
}
|
|
181
|
-
if (cameraPromise != null || pickerPromise != null) {
|
|
188
|
+
if (cameraPromise != null || cameraPermissionPromise != null || pickerPromise != null) {
|
|
182
189
|
promise.reject("PICKER_BUSY", "An attachment picker is already open");
|
|
183
190
|
return;
|
|
184
191
|
}
|
|
185
192
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
promise.reject("CAMERA_UNAVAILABLE", "No camera application is available");
|
|
193
|
+
if (ContextCompat.checkSelfPermission(reactContext, Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
|
|
194
|
+
if (!(activity instanceof PermissionAwareActivity)) {
|
|
195
|
+
promise.reject("CAMERA_PERMISSION_UNAVAILABLE", "The current Activity cannot request camera permission");
|
|
190
196
|
return;
|
|
191
197
|
}
|
|
192
198
|
|
|
199
|
+
cameraPermissionPromise = promise;
|
|
200
|
+
PermissionAwareActivity permissionActivity = (PermissionAwareActivity) activity;
|
|
201
|
+
permissionActivity.requestPermissions(
|
|
202
|
+
new String[]{Manifest.permission.CAMERA},
|
|
203
|
+
CAMERA_PERMISSION_REQUEST,
|
|
204
|
+
new PermissionListener() {
|
|
205
|
+
@Override
|
|
206
|
+
public boolean onRequestPermissionsResult(int requestCode, String[] permissions, int[] grantResults) {
|
|
207
|
+
if (requestCode != CAMERA_PERMISSION_REQUEST) return false;
|
|
208
|
+
|
|
209
|
+
Promise pendingPromise = cameraPermissionPromise;
|
|
210
|
+
cameraPermissionPromise = null;
|
|
211
|
+
if (pendingPromise == null) return true;
|
|
212
|
+
|
|
213
|
+
boolean granted = grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED;
|
|
214
|
+
if (!granted) {
|
|
215
|
+
pendingPromise.reject("CAMERA_PERMISSION_DENIED", "Camera permission is required to take a photo");
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
Activity currentActivity = getCurrentActivity();
|
|
220
|
+
if (currentActivity == null) {
|
|
221
|
+
pendingPromise.reject("NO_ACTIVITY", "No active Android Activity");
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
launchCamera(currentActivity, pendingPromise);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
launchCamera(activity, promise);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
private void launchCamera(Activity activity, Promise promise) {
|
|
237
|
+
try {
|
|
238
|
+
Intent intent = new Intent(MediaStore.ACTION_IMAGE_CAPTURE);
|
|
239
|
+
|
|
193
240
|
File directory = new File(reactContext.getCacheDir(), "regantis-chat/camera");
|
|
194
241
|
if (!directory.exists() && !directory.mkdirs()) {
|
|
195
242
|
promise.reject("CAMERA_FAILED", "Unable to create camera directory");
|
|
@@ -217,6 +264,12 @@ public class RegantisChatNativeModule extends ReactContextBaseJavaModule {
|
|
|
217
264
|
cameraFile = file;
|
|
218
265
|
cameraOutputUri = outputUri;
|
|
219
266
|
activity.startActivityForResult(intent, TAKE_PHOTO_REQUEST);
|
|
267
|
+
} catch (android.content.ActivityNotFoundException error) {
|
|
268
|
+
cameraPromise = null;
|
|
269
|
+
if (cameraFile != null) cameraFile.delete();
|
|
270
|
+
cameraFile = null;
|
|
271
|
+
cameraOutputUri = null;
|
|
272
|
+
promise.reject("CAMERA_UNAVAILABLE", "No camera application is available", error);
|
|
220
273
|
} catch (Exception error) {
|
|
221
274
|
cameraPromise = null;
|
|
222
275
|
if (cameraFile != null) cameraFile.delete();
|
|
@@ -233,7 +286,7 @@ public class RegantisChatNativeModule extends ReactContextBaseJavaModule {
|
|
|
233
286
|
promise.reject("NO_ACTIVITY", "No active Android Activity");
|
|
234
287
|
return;
|
|
235
288
|
}
|
|
236
|
-
if (pickerPromise != null || cameraPromise != null) {
|
|
289
|
+
if (pickerPromise != null || cameraPromise != null || cameraPermissionPromise != null) {
|
|
237
290
|
promise.reject("PICKER_BUSY", "An attachment picker is already open");
|
|
238
291
|
return;
|
|
239
292
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/ios/RegantisChatNative.m
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
#import "RegantisChatNative.h"
|
|
2
2
|
#import <AudioToolbox/AudioToolbox.h>
|
|
3
|
+
#import <AVFoundation/AVFoundation.h>
|
|
3
4
|
#import <React/RCTUtils.h>
|
|
4
5
|
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>
|
|
5
6
|
|
|
@@ -66,35 +67,65 @@ RCT_EXPORT_METHOD(playIncomingSound)
|
|
|
66
67
|
AudioServicesPlaySystemSound(1007);
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
- (void)presentCameraWithResolver:(RCTPromiseResolveBlock)resolve rejecter:(RCTPromiseRejectBlock)reject
|
|
71
|
+
{
|
|
72
|
+
if (self.pickerResolve != nil) {
|
|
73
|
+
reject(@"PICKER_BUSY", @"An attachment picker is already open", nil);
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (![UIImagePickerController isSourceTypeAvailable:UIImagePickerControllerSourceTypeCamera]) {
|
|
78
|
+
reject(@"CAMERA_UNAVAILABLE", @"Camera is not available on this device", nil);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
UIViewController *controller = RCTPresentedViewController();
|
|
83
|
+
if (controller == nil) {
|
|
84
|
+
reject(@"NO_VIEW_CONTROLLER", @"No active iOS view controller", nil);
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
UIImagePickerController *picker = [[UIImagePickerController alloc] init];
|
|
89
|
+
picker.sourceType = UIImagePickerControllerSourceTypeCamera;
|
|
90
|
+
picker.delegate = self;
|
|
91
|
+
picker.allowsEditing = NO;
|
|
92
|
+
|
|
93
|
+
self.pickerResolve = resolve;
|
|
94
|
+
self.pickerReject = reject;
|
|
95
|
+
[controller presentViewController:picker animated:YES completion:nil];
|
|
96
|
+
}
|
|
97
|
+
|
|
69
98
|
RCT_REMAP_METHOD(takePhoto,
|
|
70
99
|
takePhotoWithResolver:(RCTPromiseResolveBlock)resolve
|
|
71
100
|
rejecter:(RCTPromiseRejectBlock)reject)
|
|
72
101
|
{
|
|
73
102
|
dispatch_async(dispatch_get_main_queue(), ^{
|
|
74
|
-
|
|
75
|
-
|
|
103
|
+
NSString *usageDescription = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"NSCameraUsageDescription"];
|
|
104
|
+
if (![usageDescription isKindOfClass:[NSString class]] || usageDescription.length == 0) {
|
|
105
|
+
reject(@"CAMERA_USAGE_DESCRIPTION_MISSING", @"NSCameraUsageDescription is required in the host app Info.plist", nil);
|
|
76
106
|
return;
|
|
77
107
|
}
|
|
78
108
|
|
|
79
|
-
|
|
80
|
-
|
|
109
|
+
AVAuthorizationStatus status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo];
|
|
110
|
+
if (status == AVAuthorizationStatusAuthorized) {
|
|
111
|
+
[self presentCameraWithResolver:resolve rejecter:reject];
|
|
81
112
|
return;
|
|
82
113
|
}
|
|
83
114
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
reject(@"NO_VIEW_CONTROLLER", @"No active iOS view controller", nil);
|
|
115
|
+
if (status == AVAuthorizationStatusDenied || status == AVAuthorizationStatusRestricted) {
|
|
116
|
+
reject(@"CAMERA_PERMISSION_DENIED", @"Camera permission is required to take a photo", nil);
|
|
87
117
|
return;
|
|
88
118
|
}
|
|
89
119
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
120
|
+
[AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) {
|
|
121
|
+
dispatch_async(dispatch_get_main_queue(), ^{
|
|
122
|
+
if (!granted) {
|
|
123
|
+
reject(@"CAMERA_PERMISSION_DENIED", @"Camera permission is required to take a photo", nil);
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
[self presentCameraWithResolver:resolve rejecter:reject];
|
|
127
|
+
});
|
|
128
|
+
}];
|
|
98
129
|
});
|
|
99
130
|
}
|
|
100
131
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@regantis-sdk/react-native-chat",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.5",
|
|
4
4
|
"description": "Regantis customer support chat SDK for React Native Android and iOS",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"src",
|
|
12
12
|
"android",
|
|
13
13
|
"ios",
|
|
14
|
+
"assets",
|
|
14
15
|
"RegantisReactNativeChat.podspec",
|
|
15
16
|
"README.md"
|
|
16
17
|
],
|
package/src/RegantisChat.js
CHANGED
|
@@ -13,7 +13,9 @@ const {
|
|
|
13
13
|
PanResponder,
|
|
14
14
|
Platform,
|
|
15
15
|
Pressable,
|
|
16
|
+
SafeAreaView,
|
|
16
17
|
ScrollView,
|
|
18
|
+
StatusBar,
|
|
17
19
|
StyleSheet,
|
|
18
20
|
Text,
|
|
19
21
|
TextInput,
|
|
@@ -21,16 +23,21 @@ const {
|
|
|
21
23
|
} = require('react-native');
|
|
22
24
|
const { RegantisChatClient } = require('./client');
|
|
23
25
|
|
|
26
|
+
const REGANTIS_LOGO_BLACK = require('../assets/regantis_logo_black.png');
|
|
27
|
+
const REGANTIS_LOGO_WHITE = require('../assets/regantis_logo_white.png');
|
|
28
|
+
const WELCOME_BACKGROUND_LIGHT = require('../assets/welcome_background_light.png');
|
|
29
|
+
const WELCOME_BACKGROUND_DARK = require('../assets/welcome_background_dark.png');
|
|
30
|
+
|
|
24
31
|
const EMOJIS = ['🙂', '😁', '😂', '😊', '😍', '😐', '🤔', '😞', '😢', '😭', '🎉', '❤️', '👌', '👍', '👎', '🙏'];
|
|
25
32
|
|
|
26
33
|
|
|
27
34
|
const LOCAL_TEXTS = {
|
|
28
|
-
en: { attach_camera: 'Take photo', lightbox_close: 'Close', lightbox_reset: 'Reset zoom', back: 'Back' },
|
|
29
|
-
ro: { attach_camera: 'Fă o fotografie', lightbox_close: 'Închide', lightbox_reset: 'Resetează zoom', back: 'Înapoi' },
|
|
30
|
-
de: { attach_camera: 'Foto aufnehmen', lightbox_close: 'Schließen', lightbox_reset: 'Zoom zurücksetzen', back: 'Zurück' },
|
|
31
|
-
fr: { attach_camera: 'Prendre une photo', lightbox_close: 'Fermer', lightbox_reset: 'Réinitialiser le zoom', back: 'Retour' },
|
|
32
|
-
es: { attach_camera: 'Tomar una foto', lightbox_close: 'Cerrar', lightbox_reset: 'Restablecer zoom', back: 'Atrás' },
|
|
33
|
-
it: { attach_camera: 'Scatta una foto', lightbox_close: 'Chiudi', lightbox_reset: 'Reimposta zoom', back: 'Indietro' },
|
|
35
|
+
en: { attach_camera: 'Take photo', lightbox_close: 'Close', lightbox_reset: 'Reset zoom', lightbox_zoom_in: 'Zoom in', lightbox_zoom_out: 'Zoom out', back: 'Back', camera_permission_denied: 'Camera permission is required to take a photo.', camera_unavailable: 'Camera is not available on this device.', camera_setup_required: 'Camera access is not configured for this app.', open_settings: 'Open settings' },
|
|
36
|
+
ro: { attach_camera: 'Fă o fotografie', lightbox_close: 'Închide', lightbox_reset: 'Resetează zoom', lightbox_zoom_in: 'Mărește', lightbox_zoom_out: 'Micșorează', back: 'Înapoi', camera_permission_denied: 'Permisiunea pentru cameră este necesară pentru a face o fotografie.', camera_unavailable: 'Camera nu este disponibilă pe acest dispozitiv.', camera_setup_required: 'Accesul la cameră nu este configurat pentru această aplicație.', open_settings: 'Deschide setările' },
|
|
37
|
+
de: { attach_camera: 'Foto aufnehmen', lightbox_close: 'Schließen', lightbox_reset: 'Zoom zurücksetzen', lightbox_zoom_in: 'Vergrößern', lightbox_zoom_out: 'Verkleinern', back: 'Zurück', camera_permission_denied: 'Die Kameraberechtigung ist erforderlich, um ein Foto aufzunehmen.', camera_unavailable: 'Die Kamera ist auf diesem Gerät nicht verfügbar.', camera_setup_required: 'Der Kamerazugriff ist für diese App nicht konfiguriert.', open_settings: 'Einstellungen öffnen' },
|
|
38
|
+
fr: { attach_camera: 'Prendre une photo', lightbox_close: 'Fermer', lightbox_reset: 'Réinitialiser le zoom', lightbox_zoom_in: 'Agrandir', lightbox_zoom_out: 'Réduire', back: 'Retour', camera_permission_denied: 'L’autorisation de la caméra est nécessaire pour prendre une photo.', camera_unavailable: 'La caméra n’est pas disponible sur cet appareil.', camera_setup_required: 'L’accès à la caméra n’est pas configuré pour cette application.', open_settings: 'Ouvrir les réglages' },
|
|
39
|
+
es: { attach_camera: 'Tomar una foto', lightbox_close: 'Cerrar', lightbox_reset: 'Restablecer zoom', lightbox_zoom_in: 'Acercar', lightbox_zoom_out: 'Alejar', back: 'Atrás', camera_permission_denied: 'Se requiere permiso de cámara para tomar una foto.', camera_unavailable: 'La cámara no está disponible en este dispositivo.', camera_setup_required: 'El acceso a la cámara no está configurado para esta aplicación.', open_settings: 'Abrir ajustes' },
|
|
40
|
+
it: { attach_camera: 'Scatta una foto', lightbox_close: 'Chiudi', lightbox_reset: 'Reimposta zoom', lightbox_zoom_in: 'Ingrandisci', lightbox_zoom_out: 'Riduci', back: 'Indietro', camera_permission_denied: 'È necessaria l’autorizzazione della fotocamera per scattare una foto.', camera_unavailable: 'La fotocamera non è disponibile su questo dispositivo.', camera_setup_required: 'L’accesso alla fotocamera non è configurato per questa app.', open_settings: 'Apri impostazioni' },
|
|
34
41
|
};
|
|
35
42
|
|
|
36
43
|
function localText(language, key) {
|
|
@@ -45,29 +52,54 @@ function touchDistance(touches) {
|
|
|
45
52
|
return Math.sqrt(dx * dx + dy * dy);
|
|
46
53
|
}
|
|
47
54
|
|
|
48
|
-
function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel }) {
|
|
55
|
+
function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel, zoomInLabel, zoomOutLabel }) {
|
|
49
56
|
const scale = React.useRef(new Animated.Value(1)).current;
|
|
50
57
|
const translateX = React.useRef(new Animated.Value(0)).current;
|
|
51
58
|
const translateY = React.useRef(new Animated.Value(0)).current;
|
|
52
59
|
const current = React.useRef({ scale: 1, x: 0, y: 0 });
|
|
53
60
|
const gesture = React.useRef({ mode: '', distance: 0, scale: 1, x: 0, y: 0, startX: 0, startY: 0 });
|
|
54
61
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
+
const applyZoom = React.useCallback((value, animated = true) => {
|
|
63
|
+
const next = Math.max(1, Math.min(5, Number(value) || 1));
|
|
64
|
+
current.current.scale = next;
|
|
65
|
+
|
|
66
|
+
if (next <= 1.01) {
|
|
67
|
+
current.current.x = 0;
|
|
68
|
+
current.current.y = 0;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!animated) {
|
|
72
|
+
scale.setValue(next);
|
|
73
|
+
if (next <= 1.01) {
|
|
74
|
+
translateX.setValue(0);
|
|
75
|
+
translateY.setValue(0);
|
|
76
|
+
}
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const animations = [
|
|
81
|
+
Animated.spring(scale, { toValue: next, useNativeDriver: true, friction: 8, tension: 70 }),
|
|
62
82
|
];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
83
|
+
|
|
84
|
+
if (next <= 1.01) {
|
|
85
|
+
animations.push(
|
|
86
|
+
Animated.spring(translateX, { toValue: 0, useNativeDriver: true, friction: 8, tension: 70 }),
|
|
87
|
+
Animated.spring(translateY, { toValue: 0, useNativeDriver: true, friction: 8, tension: 70 })
|
|
88
|
+
);
|
|
68
89
|
}
|
|
90
|
+
|
|
91
|
+
Animated.parallel(animations).start();
|
|
69
92
|
}, [scale, translateX, translateY]);
|
|
70
93
|
|
|
94
|
+
const resetZoom = React.useCallback((animated = true) => {
|
|
95
|
+
gesture.current.mode = '';
|
|
96
|
+
applyZoom(1, animated);
|
|
97
|
+
}, [applyZoom]);
|
|
98
|
+
|
|
99
|
+
const changeZoom = React.useCallback(delta => {
|
|
100
|
+
applyZoom(current.current.scale + delta, true);
|
|
101
|
+
}, [applyZoom]);
|
|
102
|
+
|
|
71
103
|
React.useEffect(() => {
|
|
72
104
|
if (visible) resetZoom(false);
|
|
73
105
|
}, [visible, uri, resetZoom]);
|
|
@@ -151,14 +183,30 @@ function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel }) {
|
|
|
151
183
|
return (
|
|
152
184
|
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose} statusBarTranslucent>
|
|
153
185
|
<View style={lightboxStyles.root}>
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
186
|
+
<SafeAreaView
|
|
187
|
+
pointerEvents="box-none"
|
|
188
|
+
style={[
|
|
189
|
+
lightboxStyles.safeArea,
|
|
190
|
+
Platform.OS === 'android' && { paddingTop: Math.max(10, Number(StatusBar.currentHeight || 24)) },
|
|
191
|
+
]}
|
|
192
|
+
>
|
|
193
|
+
<View style={lightboxStyles.toolbar}>
|
|
194
|
+
<Pressable accessibilityRole="button" accessibilityLabel={closeLabel} style={lightboxStyles.toolbarButton} onPress={onClose}>
|
|
195
|
+
<Text style={lightboxStyles.closeIcon}>‹</Text>
|
|
196
|
+
</Pressable>
|
|
197
|
+
<View style={lightboxStyles.zoomControls}>
|
|
198
|
+
<Pressable accessibilityRole="button" accessibilityLabel={zoomOutLabel} style={lightboxStyles.zoomButton} onPress={() => changeZoom(-0.5)}>
|
|
199
|
+
<Text style={lightboxStyles.zoomText}>−</Text>
|
|
200
|
+
</Pressable>
|
|
201
|
+
<Pressable accessibilityRole="button" accessibilityLabel={resetLabel} style={lightboxStyles.resetButton} onPress={() => resetZoom(true)}>
|
|
202
|
+
<Text style={lightboxStyles.resetText}>1:1</Text>
|
|
203
|
+
</Pressable>
|
|
204
|
+
<Pressable accessibilityRole="button" accessibilityLabel={zoomInLabel} style={lightboxStyles.zoomButton} onPress={() => changeZoom(0.5)}>
|
|
205
|
+
<Text style={lightboxStyles.zoomText}>+</Text>
|
|
206
|
+
</Pressable>
|
|
207
|
+
</View>
|
|
208
|
+
</View>
|
|
209
|
+
</SafeAreaView>
|
|
162
210
|
<View style={lightboxStyles.stage} {...panResponder.panHandlers}>
|
|
163
211
|
{uri ? (
|
|
164
212
|
<Animated.Image
|
|
@@ -178,10 +226,14 @@ function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel }) {
|
|
|
178
226
|
|
|
179
227
|
const lightboxStyles = StyleSheet.create({
|
|
180
228
|
root: { flex: 1, backgroundColor: 'rgba(0,0,0,0.96)' },
|
|
181
|
-
|
|
229
|
+
safeArea: { position: 'absolute', zIndex: 20, top: 0, left: 0, right: 0 },
|
|
230
|
+
toolbar: { minHeight: 64, paddingHorizontal: 14, paddingTop: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
182
231
|
toolbarButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: 'rgba(255,255,255,0.14)', alignItems: 'center', justifyContent: 'center' },
|
|
183
|
-
closeIcon: { color: '#FFFFFF', fontSize: 39, lineHeight: 40,
|
|
184
|
-
|
|
232
|
+
closeIcon: { color: '#FFFFFF', fontSize: 39, lineHeight: 40, transform: [{ translateY: -2 }] },
|
|
233
|
+
zoomControls: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
234
|
+
zoomButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: 'rgba(255,255,255,0.14)', alignItems: 'center', justifyContent: 'center' },
|
|
235
|
+
zoomText: { color: '#FFFFFF', fontSize: 27, lineHeight: 30, fontWeight: '500' },
|
|
236
|
+
resetButton: { minWidth: 52, height: 44, paddingHorizontal: 12, borderRadius: 22, backgroundColor: 'rgba(255,255,255,0.14)', alignItems: 'center', justifyContent: 'center' },
|
|
185
237
|
resetText: { color: '#FFFFFF', fontSize: 13, fontWeight: '700' },
|
|
186
238
|
stage: { flex: 1, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' },
|
|
187
239
|
image: { width: '100%', height: '100%' },
|
|
@@ -199,26 +251,26 @@ function colorWithOpacity(hex, opacity) {
|
|
|
199
251
|
|
|
200
252
|
function makePalette(settings) {
|
|
201
253
|
const dark = settings.theme_mode === 'dark';
|
|
202
|
-
const
|
|
254
|
+
const advanced = settings.color_mode === 'advanced';
|
|
255
|
+
const primary = advanced ? settings.primary_color : settings.theme_color;
|
|
256
|
+
const backgroundHex = advanced ? settings.chat_background : (dark ? '#111316' : '#F5F7FB');
|
|
257
|
+
const backgroundOpacity = advanced ? settings.chat_background_opacity : 100;
|
|
203
258
|
return {
|
|
204
259
|
dark,
|
|
205
260
|
primary,
|
|
206
|
-
background: colorWithOpacity(
|
|
261
|
+
background: colorWithOpacity(backgroundHex, backgroundOpacity),
|
|
207
262
|
surface: dark ? '#1D2127' : '#FFFFFF',
|
|
208
263
|
surfaceAlt: dark ? '#252A31' : '#F5F7FB',
|
|
209
264
|
header: dark ? '#181B20' : '#F8F9FC',
|
|
210
265
|
border: dark ? 'rgba(255,255,255,0.10)' : 'rgba(20,32,54,0.10)',
|
|
211
266
|
text: dark ? '#F7F8FA' : '#17181B',
|
|
212
|
-
muted: settings.system_text
|
|
213
|
-
customerBubble:
|
|
214
|
-
customerText:
|
|
215
|
-
agentBubble:
|
|
216
|
-
agentText:
|
|
267
|
+
muted: advanced ? settings.system_text : (dark ? '#AEB4BE' : '#6F7480'),
|
|
268
|
+
customerBubble: advanced ? settings.customer_bubble : primary,
|
|
269
|
+
customerText: advanced ? settings.customer_text : '#FFFFFF',
|
|
270
|
+
agentBubble: advanced ? settings.agent_bubble : (dark ? '#22262C' : '#FFFFFF'),
|
|
271
|
+
agentText: advanced ? settings.agent_text : (dark ? '#F5F6F7' : '#17181B'),
|
|
217
272
|
danger: '#DF2917',
|
|
218
|
-
heroBase: dark ? '#151B21' : '#
|
|
219
|
-
heroBlobA: dark ? 'rgba(73,83,152,0.25)' : 'rgba(118,137,255,0.30)',
|
|
220
|
-
heroBlobB: dark ? 'rgba(34,126,168,0.22)' : 'rgba(96,222,215,0.28)',
|
|
221
|
-
heroBlobC: dark ? 'rgba(255,255,255,0.035)' : 'rgba(255,255,255,0.52)',
|
|
273
|
+
heroBase: dark ? '#151B21' : '#EEF5FC',
|
|
222
274
|
};
|
|
223
275
|
}
|
|
224
276
|
|
|
@@ -234,6 +286,140 @@ function isSupportMessage(message) {
|
|
|
234
286
|
return sender === 'agent' || sender === 'admin' || sender === 'chatbot';
|
|
235
287
|
}
|
|
236
288
|
|
|
289
|
+
|
|
290
|
+
function humanSize(bytes) {
|
|
291
|
+
const size = Number(bytes || 0);
|
|
292
|
+
if (!size) return '0 B';
|
|
293
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
294
|
+
const index = Math.min(units.length - 1, Math.max(0, Math.floor(Math.log(size) / Math.log(1024))));
|
|
295
|
+
const value = size / Math.pow(1024, index);
|
|
296
|
+
return `${value.toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function normalizeMarkdownSource(source) {
|
|
300
|
+
return String(source == null ? '' : source)
|
|
301
|
+
.replace(/\\([*_`#>\[\]()~+-])/g, '$1')
|
|
302
|
+
.replace(/\*{3,}([^*\n]+?)\*{3,}/g, '**$1**')
|
|
303
|
+
.replace(/^(\s*[-+*]\s+)\*([^*\n].*?)\*\*\s*$/gm, '$1**$2**');
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function renderInlineMarkdown(value, textStyle, styles, keyPrefix) {
|
|
307
|
+
const text = String(value || '');
|
|
308
|
+
const pattern = /(\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|\[[^\]\n]+\]\((?:https?:\/\/|mailto:|tel:)[^)\s]+\)|\*[^*\n]+\*|_[^_\n]+_)/g;
|
|
309
|
+
const nodes = [];
|
|
310
|
+
let last = 0;
|
|
311
|
+
let match;
|
|
312
|
+
let index = 0;
|
|
313
|
+
|
|
314
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
315
|
+
if (match.index > last) nodes.push(text.slice(last, match.index));
|
|
316
|
+
const token = match[0];
|
|
317
|
+
const key = `${keyPrefix}-${index++}`;
|
|
318
|
+
|
|
319
|
+
if ((token.startsWith('**') && token.endsWith('**')) || (token.startsWith('__') && token.endsWith('__'))) {
|
|
320
|
+
nodes.push(<Text key={key} style={[textStyle, styles.markdownStrong]}>{token.slice(2, -2)}</Text>);
|
|
321
|
+
} else if (token.startsWith('`')) {
|
|
322
|
+
nodes.push(<Text key={key} style={[textStyle, styles.markdownInlineCode]}>{token.slice(1, -1)}</Text>);
|
|
323
|
+
} else if (token.startsWith('[')) {
|
|
324
|
+
const linkMatch = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
325
|
+
if (linkMatch) {
|
|
326
|
+
const url = linkMatch[2];
|
|
327
|
+
nodes.push(
|
|
328
|
+
<Text key={key} style={[textStyle, styles.markdownLink]} onPress={() => Linking.openURL(url).catch(() => {})}>
|
|
329
|
+
{linkMatch[1]}
|
|
330
|
+
</Text>
|
|
331
|
+
);
|
|
332
|
+
} else {
|
|
333
|
+
nodes.push(token);
|
|
334
|
+
}
|
|
335
|
+
} else {
|
|
336
|
+
nodes.push(<Text key={key} style={[textStyle, styles.markdownEm]}>{token.slice(1, -1)}</Text>);
|
|
337
|
+
}
|
|
338
|
+
last = pattern.lastIndex;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
if (last < text.length) nodes.push(text.slice(last));
|
|
342
|
+
return nodes;
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
function SafeMarkdown({ source, textStyle, styles }) {
|
|
346
|
+
const lines = normalizeMarkdownSource(source).replace(/\r\n?/g, '\n').split('\n');
|
|
347
|
+
const blocks = [];
|
|
348
|
+
let codeLines = [];
|
|
349
|
+
let inCode = false;
|
|
350
|
+
|
|
351
|
+
const flushCode = key => {
|
|
352
|
+
if (!codeLines.length) return;
|
|
353
|
+
blocks.push(
|
|
354
|
+
<View key={key} style={styles.markdownCodeBlock}>
|
|
355
|
+
<Text style={[textStyle, styles.markdownCodeText]}>{codeLines.join('\n')}</Text>
|
|
356
|
+
</View>
|
|
357
|
+
);
|
|
358
|
+
codeLines = [];
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
lines.forEach((line, index) => {
|
|
362
|
+
if (/^```/.test(line.trim())) {
|
|
363
|
+
if (inCode) flushCode(`code-${index}`);
|
|
364
|
+
inCode = !inCode;
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
if (inCode) {
|
|
369
|
+
codeLines.push(line);
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (!line.trim()) {
|
|
374
|
+
blocks.push(<View key={`space-${index}`} style={styles.markdownSpacer} />);
|
|
375
|
+
return;
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
const heading = line.match(/^\s*(#{1,6})\s+(.+)$/);
|
|
379
|
+
if (heading) {
|
|
380
|
+
blocks.push(
|
|
381
|
+
<Text key={`h-${index}`} style={[textStyle, styles.markdownHeading]}>
|
|
382
|
+
{renderInlineMarkdown(heading[2], textStyle, styles, `h-${index}`)}
|
|
383
|
+
</Text>
|
|
384
|
+
);
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
const quote = line.match(/^\s*>\s?(.*)$/);
|
|
389
|
+
if (quote) {
|
|
390
|
+
blocks.push(
|
|
391
|
+
<View key={`q-${index}`} style={styles.markdownQuote}>
|
|
392
|
+
<Text style={textStyle}>{renderInlineMarkdown(quote[1], textStyle, styles, `q-${index}`)}</Text>
|
|
393
|
+
</View>
|
|
394
|
+
);
|
|
395
|
+
return;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
const unordered = line.match(/^\s*[-+*]\s+(.+)$/);
|
|
399
|
+
const ordered = line.match(/^\s*(\d+)[.)]\s+(.+)$/);
|
|
400
|
+
if (unordered || ordered) {
|
|
401
|
+
const prefix = ordered ? `${ordered[1]}.` : '•';
|
|
402
|
+
const content = ordered ? ordered[2] : unordered[1];
|
|
403
|
+
blocks.push(
|
|
404
|
+
<View key={`li-${index}`} style={styles.markdownListRow}>
|
|
405
|
+
<Text style={[textStyle, styles.markdownListPrefix]}>{prefix}</Text>
|
|
406
|
+
<Text style={[textStyle, styles.markdownListText]}>{renderInlineMarkdown(content, textStyle, styles, `li-${index}`)}</Text>
|
|
407
|
+
</View>
|
|
408
|
+
);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
blocks.push(
|
|
413
|
+
<Text key={`p-${index}`} style={[textStyle, styles.markdownParagraph]}>
|
|
414
|
+
{renderInlineMarkdown(line, textStyle, styles, `p-${index}`)}
|
|
415
|
+
</Text>
|
|
416
|
+
);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
if (codeLines.length) flushCode('code-last');
|
|
420
|
+
return <View style={styles.markdownRoot}>{blocks}</View>;
|
|
421
|
+
}
|
|
422
|
+
|
|
237
423
|
function RegantisChat(props) {
|
|
238
424
|
const {
|
|
239
425
|
apiKey,
|
|
@@ -286,13 +472,18 @@ function RegantisChat(props) {
|
|
|
286
472
|
const [ratingComment, setRatingComment] = React.useState('');
|
|
287
473
|
const [ratingStatus, setRatingStatus] = React.useState('');
|
|
288
474
|
const [busyAction, setBusyAction] = React.useState('');
|
|
475
|
+
const [attachmentError, setAttachmentError] = React.useState('');
|
|
476
|
+
const [cameraSettingsNeeded, setCameraSettingsNeeded] = React.useState(false);
|
|
289
477
|
const [lightboxUri, setLightboxUri] = React.useState('');
|
|
478
|
+
const [logoFailed, setLogoFailed] = React.useState(false);
|
|
290
479
|
const listRef = React.useRef(null);
|
|
480
|
+
const previousStatusRef = React.useRef(null);
|
|
481
|
+
const activeViewRef = React.useRef(initialView === 'chat' ? 'chat' : 'home');
|
|
291
482
|
|
|
292
483
|
React.useEffect(() => {
|
|
293
484
|
const unsubscribe = client.subscribe(next => setState(next));
|
|
294
485
|
client.start().catch(() => {});
|
|
295
|
-
const sub = AppState.addEventListener('change', value => client.setVisible(value === 'active'));
|
|
486
|
+
const sub = AppState.addEventListener('change', value => client.setVisible(value === 'active' && activeViewRef.current === 'chat'));
|
|
296
487
|
return () => {
|
|
297
488
|
if (sub && sub.remove) sub.remove();
|
|
298
489
|
unsubscribe();
|
|
@@ -300,6 +491,15 @@ function RegantisChat(props) {
|
|
|
300
491
|
};
|
|
301
492
|
}, [client]);
|
|
302
493
|
|
|
494
|
+
React.useEffect(() => {
|
|
495
|
+
activeViewRef.current = activeView;
|
|
496
|
+
client.setVisible(AppState.currentState === 'active' && activeView === 'chat');
|
|
497
|
+
}, [activeView, client]);
|
|
498
|
+
|
|
499
|
+
React.useEffect(() => {
|
|
500
|
+
setLogoFailed(false);
|
|
501
|
+
}, [state.settings.logo_url]);
|
|
502
|
+
|
|
303
503
|
React.useEffect(() => {
|
|
304
504
|
const c = state.conversation || {};
|
|
305
505
|
if (c.contact_name && !contactName) setContactName(c.contact_name);
|
|
@@ -341,9 +541,36 @@ function RegantisChat(props) {
|
|
|
341
541
|
const homePreview = lastSupport
|
|
342
542
|
? String(lastSupport.message || '').trim() || (lastSupport.attachment ? `[${String(lastSupport.message_type || 'file')}]` : t('empty'))
|
|
343
543
|
: t('empty');
|
|
544
|
+
const latestNamedAgent = Number(state.settings.use_profile_names || 0) === 1
|
|
545
|
+
? [...state.messages].reverse().find(message => {
|
|
546
|
+
const sender = String(message && message.sender_type || '').toLowerCase();
|
|
547
|
+
return (sender === 'agent' || sender === 'admin') && String(message && message.sender_name || '').trim();
|
|
548
|
+
})
|
|
549
|
+
: null;
|
|
550
|
+
const operatorDisplayName = latestNamedAgent
|
|
551
|
+
? String(latestNamedAgent.sender_name || '').trim()
|
|
552
|
+
: (state.settings.operator_name || t('support_name'));
|
|
553
|
+
|
|
554
|
+
React.useEffect(() => {
|
|
555
|
+
const nextStatus = Number(c.status || 0);
|
|
556
|
+
const previousStatus = previousStatusRef.current;
|
|
557
|
+
previousStatusRef.current = nextStatus;
|
|
558
|
+
|
|
559
|
+
if (
|
|
560
|
+
previousStatus === 1 &&
|
|
561
|
+
nextStatus === 0 &&
|
|
562
|
+
Number(state.settings.rating_enabled || 0) === 1 &&
|
|
563
|
+
contactConfirmed &&
|
|
564
|
+
!c.feedback_submitted
|
|
565
|
+
) {
|
|
566
|
+
const timer = setTimeout(() => setRatingOpen(true), 120);
|
|
567
|
+
return () => clearTimeout(timer);
|
|
568
|
+
}
|
|
569
|
+
}, [c.status, c.feedback_submitted, contactConfirmed, state.settings.rating_enabled]);
|
|
344
570
|
|
|
345
571
|
function showView(next) {
|
|
346
572
|
const view = next === 'chat' ? 'chat' : 'home';
|
|
573
|
+
activeViewRef.current = view;
|
|
347
574
|
setActiveView(view);
|
|
348
575
|
setOptionsOpen(false);
|
|
349
576
|
setLanguageOpen(false);
|
|
@@ -382,10 +609,23 @@ function RegantisChat(props) {
|
|
|
382
609
|
|
|
383
610
|
async function pick(kind) {
|
|
384
611
|
setAttachOpen(false);
|
|
612
|
+
setAttachmentError('');
|
|
613
|
+
setCameraSettingsNeeded(false);
|
|
385
614
|
setBusyAction('upload');
|
|
386
615
|
try {
|
|
387
616
|
await client.pickAndUpload(kind);
|
|
388
|
-
|
|
617
|
+
setAttachmentError('');
|
|
618
|
+
setCameraSettingsNeeded(false);
|
|
619
|
+
} catch (error) {
|
|
620
|
+
const code = String(error && (error.code || error.message) || '');
|
|
621
|
+
if (code.includes('CAMERA_PERMISSION_DENIED')) {
|
|
622
|
+
setAttachmentError(t('camera_permission_denied'));
|
|
623
|
+
setCameraSettingsNeeded(true);
|
|
624
|
+
} else if (code.includes('CAMERA_USAGE_DESCRIPTION_MISSING') || code.includes('CAMERA_PERMISSION_UNAVAILABLE')) {
|
|
625
|
+
setAttachmentError(t('camera_setup_required'));
|
|
626
|
+
} else if (code.includes('CAMERA_UNAVAILABLE')) setAttachmentError(t('camera_unavailable'));
|
|
627
|
+
else setAttachmentError(error && error.message ? error.message : t('send_error'));
|
|
628
|
+
if (typeof onError === 'function') onError(error);
|
|
389
629
|
} finally {
|
|
390
630
|
setBusyAction('');
|
|
391
631
|
}
|
|
@@ -463,8 +703,8 @@ function RegantisChat(props) {
|
|
|
463
703
|
|
|
464
704
|
function renderLogo(size, marginRight) {
|
|
465
705
|
if (!state.settings.show_logo) return null;
|
|
466
|
-
if (state.settings.logo_url) {
|
|
467
|
-
return <Image source={{ uri: state.settings.logo_url }} style={[styles.logoImage, { width: size, height: size, borderRadius: size / 2, marginRight: marginRight || 0 }]} resizeMode="cover" />;
|
|
706
|
+
if (state.settings.logo_url && !logoFailed) {
|
|
707
|
+
return <Image source={{ uri: state.settings.logo_url }} onError={() => setLogoFailed(true)} style={[styles.logoImage, { width: size, height: size, borderRadius: size / 2, marginRight: marginRight || 0 }]} resizeMode="cover" />;
|
|
468
708
|
}
|
|
469
709
|
return (
|
|
470
710
|
<View style={[styles.logoFallback, { width: size, height: size, borderRadius: size / 2, marginRight: marginRight || 0 }]}>
|
|
@@ -498,7 +738,11 @@ function RegantisChat(props) {
|
|
|
498
738
|
const textStyle = visitor ? styles.customerText : styles.agentText;
|
|
499
739
|
const attachment = item.attachment || null;
|
|
500
740
|
const isImage = attachment && String(attachment.mime || '').startsWith('image/');
|
|
501
|
-
const
|
|
741
|
+
const configuredName = state.settings.operator_name || t('support_name');
|
|
742
|
+
const senderName = sender === 'chatbot'
|
|
743
|
+
? 'Chatbot'
|
|
744
|
+
: (Number(state.settings.use_profile_names || 0) === 1 ? (item.sender_name || configuredName) : configuredName);
|
|
745
|
+
const fileExt = String(attachment && attachment.ext || '').replace(/^\./, '').slice(0, 4).toUpperCase() || 'FILE';
|
|
502
746
|
|
|
503
747
|
return (
|
|
504
748
|
<View style={[styles.messageRow, visitor ? styles.messageRowRight : styles.messageRowLeft]}>
|
|
@@ -508,11 +752,21 @@ function RegantisChat(props) {
|
|
|
508
752
|
<Image source={{ uri: attachment.url }} style={styles.attachmentImage} resizeMode="cover" />
|
|
509
753
|
</Pressable>
|
|
510
754
|
) : attachment ? (
|
|
511
|
-
<Pressable style={styles.fileAttachment} onPress={() => Linking.openURL(attachment.url)}>
|
|
512
|
-
<
|
|
755
|
+
<Pressable style={styles.fileAttachment} onPress={() => Linking.openURL(attachment.url).catch(() => {})}>
|
|
756
|
+
<View style={[styles.fileAttachmentIcon, { borderColor: textStyle.color }]}>
|
|
757
|
+
<Text style={[styles.fileAttachmentIconText, textStyle]}>{fileExt}</Text>
|
|
758
|
+
</View>
|
|
759
|
+
<View style={styles.fileAttachmentCopy}>
|
|
760
|
+
<Text numberOfLines={1} style={[styles.fileAttachmentText, textStyle]}>{attachment.name || t('attach_file')}</Text>
|
|
761
|
+
<Text numberOfLines={1} style={[styles.fileAttachmentMeta, textStyle]}>{humanSize(attachment.size)}{attachment.ext ? ` · ${String(attachment.ext).toUpperCase()}` : ''}</Text>
|
|
762
|
+
</View>
|
|
513
763
|
</Pressable>
|
|
514
764
|
) : null}
|
|
515
|
-
{item.message ?
|
|
765
|
+
{item.message ? (
|
|
766
|
+
sender === 'chatbot'
|
|
767
|
+
? <SafeMarkdown source={item.message} textStyle={[styles.messageText, textStyle]} styles={styles} />
|
|
768
|
+
: <Text style={[styles.messageText, textStyle]}>{item.message}</Text>
|
|
769
|
+
) : null}
|
|
516
770
|
<View style={styles.messageMeta}>
|
|
517
771
|
{!visitor ? <Text numberOfLines={1} style={[styles.messageSender, textStyle]}>{senderName}</Text> : <View />}
|
|
518
772
|
<Text style={[styles.timeText, textStyle]}>{formatTime(item.date_added)}</Text>
|
|
@@ -524,16 +778,24 @@ function RegantisChat(props) {
|
|
|
524
778
|
|
|
525
779
|
function renderPoweredBy() {
|
|
526
780
|
if (state.settings.white_label) return null;
|
|
527
|
-
return
|
|
781
|
+
return (
|
|
782
|
+
<View style={styles.poweredBy}>
|
|
783
|
+
<Text style={styles.poweredByText}>{t('powered_by')}</Text>
|
|
784
|
+
<Image source={palette.dark ? REGANTIS_LOGO_WHITE : REGANTIS_LOGO_BLACK} style={styles.poweredByLogo} resizeMode="contain" />
|
|
785
|
+
</View>
|
|
786
|
+
);
|
|
528
787
|
}
|
|
529
788
|
|
|
530
789
|
function renderHome() {
|
|
531
790
|
return (
|
|
532
791
|
<View style={styles.screen}>
|
|
533
792
|
<View style={styles.homeHero}>
|
|
534
|
-
<
|
|
535
|
-
|
|
536
|
-
|
|
793
|
+
<Image
|
|
794
|
+
pointerEvents="none"
|
|
795
|
+
source={palette.dark ? WELCOME_BACKGROUND_DARK : WELCOME_BACKGROUND_LIGHT}
|
|
796
|
+
style={styles.homeHeroBackground}
|
|
797
|
+
resizeMode="cover"
|
|
798
|
+
/>
|
|
537
799
|
|
|
538
800
|
<View style={styles.homeTop}>
|
|
539
801
|
<View style={styles.homeTopLeft}>
|
|
@@ -560,7 +822,7 @@ function RegantisChat(props) {
|
|
|
560
822
|
<View style={styles.homeCardTop}>
|
|
561
823
|
{state.settings.show_agent_photo ? <View style={styles.homeAvatar}><Text style={styles.homeAvatarText}>R</Text></View> : null}
|
|
562
824
|
<View style={styles.homeCardCopy}>
|
|
563
|
-
<Text numberOfLines={1} style={styles.homeCardAgent}>{
|
|
825
|
+
<Text numberOfLines={1} style={styles.homeCardAgent}>{operatorDisplayName}</Text>
|
|
564
826
|
<Text numberOfLines={1} style={styles.homeCardPreview}>{homePreview}</Text>
|
|
565
827
|
</View>
|
|
566
828
|
</View>
|
|
@@ -595,20 +857,22 @@ function RegantisChat(props) {
|
|
|
595
857
|
<Text style={styles.backIcon}>‹</Text>
|
|
596
858
|
</Pressable>
|
|
597
859
|
<Pressable accessibilityRole="button" accessibilityLabel={t('options')} style={styles.headerIconButton} onPress={() => setOptionsOpen(value => !value)}>
|
|
598
|
-
<
|
|
860
|
+
<View style={styles.menuDots}><View style={styles.menuDot} /><View style={styles.menuDot} /><View style={styles.menuDot} /></View>
|
|
599
861
|
</Pressable>
|
|
600
862
|
</View>
|
|
601
863
|
|
|
602
|
-
<
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
864
|
+
<View pointerEvents="box-none" style={styles.chatHeaderCenter}>
|
|
865
|
+
<Pressable
|
|
866
|
+
accessibilityRole="button"
|
|
867
|
+
accessibilityLabel={operatorDisplayName}
|
|
868
|
+
style={styles.agentTrigger}
|
|
869
|
+
onPress={() => {
|
|
870
|
+
if (state.settings.rating_enabled && contactConfirmed) setRatingOpen(true);
|
|
871
|
+
}}
|
|
872
|
+
>
|
|
873
|
+
{renderAgentVisual()}
|
|
874
|
+
</Pressable>
|
|
875
|
+
</View>
|
|
612
876
|
|
|
613
877
|
<View style={styles.chatHeaderRight}>
|
|
614
878
|
{typeof onClose === 'function' ? (
|
|
@@ -640,7 +904,7 @@ function RegantisChat(props) {
|
|
|
640
904
|
<Text style={styles.popoverChevron}>›</Text>
|
|
641
905
|
</Pressable>
|
|
642
906
|
) : null}
|
|
643
|
-
{state.
|
|
907
|
+
{state.soundAvailable ? (
|
|
644
908
|
<Pressable style={styles.popoverRow} onPress={() => client.setSoundEnabled(!state.settings.sound_enabled)}>
|
|
645
909
|
<Text style={styles.popoverIcon}>◖</Text>
|
|
646
910
|
<Text style={styles.popoverText}>{t('sounds')}</Text>
|
|
@@ -674,6 +938,20 @@ function RegantisChat(props) {
|
|
|
674
938
|
);
|
|
675
939
|
}
|
|
676
940
|
|
|
941
|
+
function renderContactInfoCard() {
|
|
942
|
+
if (!contactConfirmed || (!c.contact_name && !c.contact_email)) return null;
|
|
943
|
+
return (
|
|
944
|
+
<View style={styles.contactInfoRow}>
|
|
945
|
+
<View style={styles.contactInfoAvatar}><Text style={styles.contactInfoAvatarText}>i</Text></View>
|
|
946
|
+
<View style={styles.contactInfoBubble}>
|
|
947
|
+
<Text style={styles.contactInfoTitle}>{t('personal_details')}</Text>
|
|
948
|
+
{c.contact_name ? <Text style={styles.contactInfoLine}><Text style={styles.contactInfoLabel}>{t('name')}: </Text>{c.contact_name}</Text> : null}
|
|
949
|
+
{c.contact_email ? <Text style={styles.contactInfoLine}><Text style={styles.contactInfoLabel}>{t('email')}: </Text>{c.contact_email}</Text> : null}
|
|
950
|
+
</View>
|
|
951
|
+
</View>
|
|
952
|
+
);
|
|
953
|
+
}
|
|
954
|
+
|
|
677
955
|
function renderComposer() {
|
|
678
956
|
if (!active) {
|
|
679
957
|
return (
|
|
@@ -710,6 +988,17 @@ function RegantisChat(props) {
|
|
|
710
988
|
</View>
|
|
711
989
|
) : null}
|
|
712
990
|
|
|
991
|
+
{attachmentError ? (
|
|
992
|
+
<View style={styles.composerErrorRow}>
|
|
993
|
+
<Text style={styles.composerError}>{attachmentError}</Text>
|
|
994
|
+
{cameraSettingsNeeded ? (
|
|
995
|
+
<Pressable style={styles.composerErrorAction} onPress={() => Linking.openSettings().catch(() => {})}>
|
|
996
|
+
<Text style={styles.composerErrorActionText}>{t('open_settings')}</Text>
|
|
997
|
+
</Pressable>
|
|
998
|
+
) : null}
|
|
999
|
+
</View>
|
|
1000
|
+
) : null}
|
|
1001
|
+
|
|
713
1002
|
<View style={styles.composer}>
|
|
714
1003
|
<Pressable disabled={!canUpload || state.uploading} style={[styles.composerButton, (!canUpload || state.uploading) && styles.disabled]} onPress={() => { setEmojiOpen(false); setAttachOpen(value => !value); }}>
|
|
715
1004
|
<Text style={styles.composerPlus}>+</Text>
|
|
@@ -749,7 +1038,12 @@ function RegantisChat(props) {
|
|
|
749
1038
|
style={styles.messageList}
|
|
750
1039
|
contentContainerStyle={styles.messageListContent}
|
|
751
1040
|
onScrollBeginDrag={() => state.hasMore && client.loadOlder().catch(() => {})}
|
|
752
|
-
ListHeaderComponent={
|
|
1041
|
+
ListHeaderComponent={(
|
|
1042
|
+
<>
|
|
1043
|
+
{state.hasMore ? <View style={styles.historyLoader}><ActivityIndicator size="small" color={palette.primary} /></View> : null}
|
|
1044
|
+
{renderContactInfoCard()}
|
|
1045
|
+
</>
|
|
1046
|
+
)}
|
|
753
1047
|
ListEmptyComponent={<View style={styles.emptyState}><Text style={styles.emptyText}>{t('empty')}</Text></View>}
|
|
754
1048
|
/>
|
|
755
1049
|
{state.remoteTyping ? <View style={styles.typingRow}><Text style={styles.typingText}>{state.remoteTyping === 'chatbot' ? t('chatbot_typing') : t('typing')}</Text></View> : null}
|
|
@@ -825,7 +1119,7 @@ function RegantisChat(props) {
|
|
|
825
1119
|
<Modal transparent visible={ratingOpen} animationType="fade" onRequestClose={() => setRatingOpen(false)}>
|
|
826
1120
|
<View style={styles.modalBackdropTop}>
|
|
827
1121
|
<View style={styles.modalCard}>
|
|
828
|
-
<Text style={styles.modalTitle}>{
|
|
1122
|
+
<Text style={styles.modalTitle}>{operatorDisplayName}</Text>
|
|
829
1123
|
<Text style={styles.ratingLabel}>{t('rating_agent_label')}</Text>
|
|
830
1124
|
<View style={styles.ratingButtons}>
|
|
831
1125
|
<Pressable style={styles.ratingButton} onPress={() => rate(1)}><Text style={styles.ratingIcon}>👍</Text></Pressable>
|
|
@@ -847,6 +1141,8 @@ function RegantisChat(props) {
|
|
|
847
1141
|
onClose={() => setLightboxUri('')}
|
|
848
1142
|
closeLabel={t('lightbox_close')}
|
|
849
1143
|
resetLabel={t('lightbox_reset')}
|
|
1144
|
+
zoomInLabel={t('lightbox_zoom_in')}
|
|
1145
|
+
zoomOutLabel={t('lightbox_zoom_out')}
|
|
850
1146
|
/>
|
|
851
1147
|
</KeyboardAvoidingView>
|
|
852
1148
|
);
|
|
@@ -869,9 +1165,7 @@ function buildStyles(p) {
|
|
|
869
1165
|
errorText: { color: p.text, fontSize: 14, textAlign: 'center', marginBottom: 12 },
|
|
870
1166
|
|
|
871
1167
|
homeHero: { position: 'relative', overflow: 'hidden', paddingHorizontal: 18, paddingTop: 24, paddingBottom: 28, backgroundColor: p.heroBase },
|
|
872
|
-
|
|
873
|
-
heroDecorationB: { position: 'absolute', width: 230, height: 230, borderRadius: 115, top: 80, right: -92, backgroundColor: p.heroBlobB },
|
|
874
|
-
heroDecorationC: { position: 'absolute', width: 210, height: 210, borderRadius: 105, right: -82, bottom: -120, backgroundColor: p.heroBlobC },
|
|
1168
|
+
homeHeroBackground: { ...StyleSheet.absoluteFillObject, width: '100%', height: '100%' },
|
|
875
1169
|
homeTop: { minHeight: 42, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', zIndex: 2 },
|
|
876
1170
|
homeTopLeft: { flexDirection: 'row', alignItems: 'center', gap: 9 },
|
|
877
1171
|
homeBackIcon: { color: p.text, fontSize: 37, lineHeight: 38, marginTop: -5 },
|
|
@@ -901,14 +1195,16 @@ function buildStyles(p) {
|
|
|
901
1195
|
bottomNavText: { marginTop: 2, color: p.muted, fontSize: 10, lineHeight: 12 },
|
|
902
1196
|
bottomNavTextActive: { color: p.text },
|
|
903
1197
|
|
|
904
|
-
chatHeader: { position: 'relative', zIndex: 20, minHeight: 64, flexDirection: 'row', alignItems: '
|
|
905
|
-
chatHeaderLeft: {
|
|
906
|
-
|
|
1198
|
+
chatHeader: { position: 'relative', zIndex: 20, minHeight: 64, flexDirection: 'row', alignItems: 'flex-start', justifyContent: 'space-between', paddingHorizontal: 12, borderBottomWidth: 1, borderBottomColor: p.border, backgroundColor: p.header, overflow: 'visible' },
|
|
1199
|
+
chatHeaderLeft: { zIndex: 30, width: 76, height: 64, flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', gap: 5 },
|
|
1200
|
+
chatHeaderCenter: { position: 'absolute', zIndex: 18, top: 0, left: 0, right: 0, height: 64, alignItems: 'center', justifyContent: 'flex-start' },
|
|
1201
|
+
chatHeaderRight: { zIndex: 30, width: 76, height: 64, flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 5 },
|
|
907
1202
|
headerIconButton: { width: 32, height: 32, borderRadius: 16, backgroundColor: p.dark ? 'rgba(255,255,255,0.08)' : '#FFFFFF', alignItems: 'center', justifyContent: 'center' },
|
|
908
|
-
backIcon: { color: p.text, fontSize: 31, lineHeight: 31,
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
1203
|
+
backIcon: { color: p.text, fontSize: 31, lineHeight: 31, transform: [{ translateY: -1 }] },
|
|
1204
|
+
menuDots: { width: 24, height: 24, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 3 },
|
|
1205
|
+
menuDot: { width: 3.5, height: 3.5, borderRadius: 2, backgroundColor: p.text },
|
|
1206
|
+
closeIcon: { color: p.text, fontSize: 23, lineHeight: 24, transform: [{ translateY: -1 }] },
|
|
1207
|
+
agentTrigger: { zIndex: 18, width: '100%', maxWidth: 218, height: 48, borderWidth: 1, borderTopWidth: 0, borderColor: p.border, borderBottomLeftRadius: 24, borderBottomRightRadius: 24, backgroundColor: p.surface, alignItems: 'center', justifyContent: 'center', shadowColor: '#172236', shadowOffset: { width: 0, height: 5 }, shadowOpacity: p.dark ? 0.18 : 0.07, shadowRadius: 8, elevation: 2 },
|
|
912
1208
|
agentVisual: { height: 30, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
|
|
913
1209
|
agentAvatar: { width: 28, height: 28, borderRadius: 14, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
|
|
914
1210
|
agentAvatarOverlap: { marginLeft: -8, borderWidth: 2, borderColor: p.surface },
|
|
@@ -942,6 +1238,13 @@ function buildStyles(p) {
|
|
|
942
1238
|
dangerButton: { flex: 1, minHeight: 40, borderRadius: 9, backgroundColor: p.danger, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center' },
|
|
943
1239
|
disabled: { opacity: 0.55 },
|
|
944
1240
|
|
|
1241
|
+
contactInfoRow: { width: '100%', flexDirection: 'row', alignItems: 'flex-start', marginBottom: 12, paddingRight: 44 },
|
|
1242
|
+
contactInfoAvatar: { width: 28, height: 28, borderRadius: 14, marginRight: 8, marginTop: 2, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
|
|
1243
|
+
contactInfoAvatarText: { color: p.muted, fontSize: 13, fontWeight: '800' },
|
|
1244
|
+
contactInfoBubble: { flex: 1, minWidth: 0, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 14, borderBottomLeftRadius: 5, backgroundColor: p.agentBubble },
|
|
1245
|
+
contactInfoTitle: { color: p.agentText, fontSize: 12, lineHeight: 16, fontWeight: '800', marginBottom: 5 },
|
|
1246
|
+
contactInfoLine: { color: p.agentText, fontSize: 11, lineHeight: 16 },
|
|
1247
|
+
contactInfoLabel: { fontWeight: '700' },
|
|
945
1248
|
messageList: { flex: 1, minHeight: 0 },
|
|
946
1249
|
messageListContent: { paddingHorizontal: 16, paddingTop: 18, paddingBottom: 12, flexGrow: 1 },
|
|
947
1250
|
historyLoader: { minHeight: 34, alignItems: 'center', justifyContent: 'center', paddingBottom: 10 },
|
|
@@ -962,8 +1265,26 @@ function buildStyles(p) {
|
|
|
962
1265
|
systemRow: { alignSelf: 'center', paddingHorizontal: 16, paddingVertical: 7, marginVertical: 3 },
|
|
963
1266
|
systemText: { color: p.muted, fontSize: 11, lineHeight: 15, textAlign: 'center' },
|
|
964
1267
|
attachmentImage: { width: 220, maxWidth: '100%', height: 180, borderRadius: 10, marginBottom: 4, backgroundColor: p.surfaceAlt },
|
|
965
|
-
fileAttachment: { paddingVertical: 4 },
|
|
1268
|
+
fileAttachment: { minWidth: 180, maxWidth: 260, flexDirection: 'row', alignItems: 'center', gap: 10, paddingVertical: 4 },
|
|
1269
|
+
fileAttachmentIcon: { width: 34, height: 34, borderWidth: 1, borderRadius: 9, alignItems: 'center', justifyContent: 'center', opacity: 0.85 },
|
|
1270
|
+
fileAttachmentIconText: { fontSize: 10, fontWeight: '800' },
|
|
1271
|
+
fileAttachmentCopy: { flex: 1, minWidth: 0 },
|
|
966
1272
|
fileAttachmentText: { fontSize: 13, fontWeight: '600' },
|
|
1273
|
+
fileAttachmentMeta: { marginTop: 2, fontSize: 10, opacity: 0.72 },
|
|
1274
|
+
markdownRoot: { flexShrink: 1 },
|
|
1275
|
+
markdownParagraph: { marginBottom: 2 },
|
|
1276
|
+
markdownSpacer: { height: 7 },
|
|
1277
|
+
markdownStrong: { fontWeight: '800' },
|
|
1278
|
+
markdownEm: { fontStyle: 'italic' },
|
|
1279
|
+
markdownInlineCode: { fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontSize: 12, backgroundColor: p.dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)' },
|
|
1280
|
+
markdownLink: { textDecorationLine: 'underline' },
|
|
1281
|
+
markdownHeading: { marginTop: 2, marginBottom: 4, fontSize: 15, lineHeight: 20, fontWeight: '800' },
|
|
1282
|
+
markdownQuote: { marginVertical: 3, paddingLeft: 9, borderLeftWidth: 3, borderLeftColor: p.muted },
|
|
1283
|
+
markdownListRow: { flexDirection: 'row', alignItems: 'flex-start', marginVertical: 1 },
|
|
1284
|
+
markdownListPrefix: { width: 20, fontWeight: '700' },
|
|
1285
|
+
markdownListText: { flex: 1, minWidth: 0 },
|
|
1286
|
+
markdownCodeBlock: { marginVertical: 4, paddingHorizontal: 9, paddingVertical: 8, borderRadius: 8, backgroundColor: p.dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)' },
|
|
1287
|
+
markdownCodeText: { fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontSize: 12, lineHeight: 17 },
|
|
967
1288
|
typingRow: { flex: 0, paddingHorizontal: 18, paddingTop: 8, paddingBottom: 7 },
|
|
968
1289
|
typingText: { color: p.muted, fontSize: 11, lineHeight: 15 },
|
|
969
1290
|
|
|
@@ -978,6 +1299,10 @@ function buildStyles(p) {
|
|
|
978
1299
|
sendButton: { width: 38, height: 38, borderRadius: 19, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
|
|
979
1300
|
sendIcon: { color: '#FFFFFF', fontSize: 17, lineHeight: 18, transform: [{ rotate: '-3deg' }] },
|
|
980
1301
|
aiCompliance: { color: p.muted, fontSize: 9, lineHeight: 13, textAlign: 'center', paddingHorizontal: 5, paddingBottom: 8 },
|
|
1302
|
+
composerErrorRow: { alignItems: 'center', justifyContent: 'center', paddingHorizontal: 8, paddingBottom: 7 },
|
|
1303
|
+
composerError: { color: '#B42318', fontSize: 11, lineHeight: 15, textAlign: 'center' },
|
|
1304
|
+
composerErrorAction: { marginTop: 5, minHeight: 28, paddingHorizontal: 10, borderRadius: 14, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
|
|
1305
|
+
composerErrorActionText: { color: p.text, fontSize: 10, lineHeight: 13, fontWeight: '700' },
|
|
981
1306
|
attachmentMenu: { position: 'absolute', zIndex: 12, left: 12, bottom: 72, width: 210, padding: 7, borderWidth: 1, borderColor: p.border, borderRadius: 14, backgroundColor: p.surface, ...shadow },
|
|
982
1307
|
attachmentAction: { minHeight: 40, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, borderRadius: 9 },
|
|
983
1308
|
attachmentActionIcon: { width: 26, color: p.text, fontSize: 15 },
|
|
@@ -986,9 +1311,9 @@ function buildStyles(p) {
|
|
|
986
1311
|
emojiButton: { width: '11%', aspectRatio: 1, minHeight: 34, borderRadius: 8, alignItems: 'center', justifyContent: 'center' },
|
|
987
1312
|
emojiText: { fontSize: 20, lineHeight: 22 },
|
|
988
1313
|
|
|
989
|
-
poweredBy: { flex: 0, minHeight:
|
|
1314
|
+
poweredBy: { flex: 0, minHeight: 22, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 4, paddingBottom: 5, backgroundColor: p.background },
|
|
990
1315
|
poweredByText: { color: '#9398A2', fontSize: 9, lineHeight: 11, textAlign: 'center' },
|
|
991
|
-
|
|
1316
|
+
poweredByLogo: { width: 48, height: 15 },
|
|
992
1317
|
|
|
993
1318
|
modalBackdrop: { flex: 1, backgroundColor: 'rgba(16,20,27,0.55)', justifyContent: 'flex-end', padding: 16 },
|
|
994
1319
|
modalBackdropTop: { flex: 1, backgroundColor: 'rgba(16,20,27,0.55)', justifyContent: 'flex-start', padding: 16 },
|
package/src/client.js
CHANGED
|
@@ -119,6 +119,7 @@ class RegantisChatClient {
|
|
|
119
119
|
upload: null,
|
|
120
120
|
realtime: null,
|
|
121
121
|
realtimeConnected: false,
|
|
122
|
+
soundAvailable: false,
|
|
122
123
|
remoteTyping: '',
|
|
123
124
|
sending: false,
|
|
124
125
|
uploading: false,
|
|
@@ -153,6 +154,10 @@ class RegantisChatClient {
|
|
|
153
154
|
return `regantis_chat_visitor_${this.options.apiKey}`;
|
|
154
155
|
}
|
|
155
156
|
|
|
157
|
+
soundStorageKey() {
|
|
158
|
+
return `regantis_chat_sound_${this.options.apiKey}`;
|
|
159
|
+
}
|
|
160
|
+
|
|
156
161
|
async start() {
|
|
157
162
|
this.destroyed = false;
|
|
158
163
|
this.setState({ loading: true, error: '' });
|
|
@@ -190,7 +195,9 @@ class RegantisChatClient {
|
|
|
190
195
|
}
|
|
191
196
|
|
|
192
197
|
setSoundEnabled(enabled) {
|
|
193
|
-
|
|
198
|
+
const value = this.state.soundAvailable && enabled ? 1 : 0;
|
|
199
|
+
this.setState({ settings: { ...this.state.settings, sound_enabled: value } });
|
|
200
|
+
if (this.state.soundAvailable) native.setItem(this.soundStorageKey(), value ? '1' : '0').catch(() => {});
|
|
194
201
|
}
|
|
195
202
|
|
|
196
203
|
async loadConfig() {
|
|
@@ -201,12 +208,20 @@ class RegantisChatClient {
|
|
|
201
208
|
|
|
202
209
|
const settings = normalizeSettings({ ...(data.settings || {}), ...(this.options.theme || {}) });
|
|
203
210
|
const texts = { ...(data.texts || {}), ...(this.options.texts || {}) };
|
|
211
|
+
const soundAvailable = Number(settings.sound_enabled || 0) === 1;
|
|
212
|
+
if (soundAvailable) {
|
|
213
|
+
const storedSound = await native.getItem(this.soundStorageKey());
|
|
214
|
+
if (storedSound !== null && storedSound !== undefined) settings.sound_enabled = String(storedSound) === '0' ? 0 : 1;
|
|
215
|
+
} else {
|
|
216
|
+
settings.sound_enabled = 0;
|
|
217
|
+
}
|
|
204
218
|
this.setState({
|
|
205
219
|
config: data,
|
|
206
220
|
settings,
|
|
207
221
|
texts,
|
|
208
222
|
languages: Array.isArray(data.languages) ? data.languages : [],
|
|
209
223
|
translationCustomerEnabled: Number(data.translation_customer_enabled || 0) === 1,
|
|
224
|
+
soundAvailable,
|
|
210
225
|
});
|
|
211
226
|
return data;
|
|
212
227
|
}
|