@regantis-sdk/react-native-chat 0.1.3 → 0.1.4
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/soft_glow.png +0 -0
- package/ios/RegantisChatNative.m +46 -15
- package/package.json +2 -1
- package/src/RegantisChat.js +411 -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
|
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.4",
|
|
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,20 @@ 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 SOFT_GLOW = require('../assets/soft_glow.png');
|
|
29
|
+
|
|
24
30
|
const EMOJIS = ['🙂', '😁', '😂', '😊', '😍', '😐', '🤔', '😞', '😢', '😭', '🎉', '❤️', '👌', '👍', '👎', '🙏'];
|
|
25
31
|
|
|
26
32
|
|
|
27
33
|
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' },
|
|
34
|
+
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' },
|
|
35
|
+
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' },
|
|
36
|
+
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' },
|
|
37
|
+
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' },
|
|
38
|
+
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' },
|
|
39
|
+
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
40
|
};
|
|
35
41
|
|
|
36
42
|
function localText(language, key) {
|
|
@@ -45,29 +51,54 @@ function touchDistance(touches) {
|
|
|
45
51
|
return Math.sqrt(dx * dx + dy * dy);
|
|
46
52
|
}
|
|
47
53
|
|
|
48
|
-
function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel }) {
|
|
54
|
+
function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel, zoomInLabel, zoomOutLabel }) {
|
|
49
55
|
const scale = React.useRef(new Animated.Value(1)).current;
|
|
50
56
|
const translateX = React.useRef(new Animated.Value(0)).current;
|
|
51
57
|
const translateY = React.useRef(new Animated.Value(0)).current;
|
|
52
58
|
const current = React.useRef({ scale: 1, x: 0, y: 0 });
|
|
53
59
|
const gesture = React.useRef({ mode: '', distance: 0, scale: 1, x: 0, y: 0, startX: 0, startY: 0 });
|
|
54
60
|
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
61
|
+
const applyZoom = React.useCallback((value, animated = true) => {
|
|
62
|
+
const next = Math.max(1, Math.min(5, Number(value) || 1));
|
|
63
|
+
current.current.scale = next;
|
|
64
|
+
|
|
65
|
+
if (next <= 1.01) {
|
|
66
|
+
current.current.x = 0;
|
|
67
|
+
current.current.y = 0;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (!animated) {
|
|
71
|
+
scale.setValue(next);
|
|
72
|
+
if (next <= 1.01) {
|
|
73
|
+
translateX.setValue(0);
|
|
74
|
+
translateY.setValue(0);
|
|
75
|
+
}
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const animations = [
|
|
80
|
+
Animated.spring(scale, { toValue: next, useNativeDriver: true, friction: 8, tension: 70 }),
|
|
62
81
|
];
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
82
|
+
|
|
83
|
+
if (next <= 1.01) {
|
|
84
|
+
animations.push(
|
|
85
|
+
Animated.spring(translateX, { toValue: 0, useNativeDriver: true, friction: 8, tension: 70 }),
|
|
86
|
+
Animated.spring(translateY, { toValue: 0, useNativeDriver: true, friction: 8, tension: 70 })
|
|
87
|
+
);
|
|
68
88
|
}
|
|
89
|
+
|
|
90
|
+
Animated.parallel(animations).start();
|
|
69
91
|
}, [scale, translateX, translateY]);
|
|
70
92
|
|
|
93
|
+
const resetZoom = React.useCallback((animated = true) => {
|
|
94
|
+
gesture.current.mode = '';
|
|
95
|
+
applyZoom(1, animated);
|
|
96
|
+
}, [applyZoom]);
|
|
97
|
+
|
|
98
|
+
const changeZoom = React.useCallback(delta => {
|
|
99
|
+
applyZoom(current.current.scale + delta, true);
|
|
100
|
+
}, [applyZoom]);
|
|
101
|
+
|
|
71
102
|
React.useEffect(() => {
|
|
72
103
|
if (visible) resetZoom(false);
|
|
73
104
|
}, [visible, uri, resetZoom]);
|
|
@@ -151,14 +182,30 @@ function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel }) {
|
|
|
151
182
|
return (
|
|
152
183
|
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose} statusBarTranslucent>
|
|
153
184
|
<View style={lightboxStyles.root}>
|
|
154
|
-
<
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
185
|
+
<SafeAreaView
|
|
186
|
+
pointerEvents="box-none"
|
|
187
|
+
style={[
|
|
188
|
+
lightboxStyles.safeArea,
|
|
189
|
+
Platform.OS === 'android' && { paddingTop: Math.max(10, Number(StatusBar.currentHeight || 24)) },
|
|
190
|
+
]}
|
|
191
|
+
>
|
|
192
|
+
<View style={lightboxStyles.toolbar}>
|
|
193
|
+
<Pressable accessibilityRole="button" accessibilityLabel={closeLabel} style={lightboxStyles.toolbarButton} onPress={onClose}>
|
|
194
|
+
<Text style={lightboxStyles.closeIcon}>‹</Text>
|
|
195
|
+
</Pressable>
|
|
196
|
+
<View style={lightboxStyles.zoomControls}>
|
|
197
|
+
<Pressable accessibilityRole="button" accessibilityLabel={zoomOutLabel} style={lightboxStyles.zoomButton} onPress={() => changeZoom(-0.5)}>
|
|
198
|
+
<Text style={lightboxStyles.zoomText}>−</Text>
|
|
199
|
+
</Pressable>
|
|
200
|
+
<Pressable accessibilityRole="button" accessibilityLabel={resetLabel} style={lightboxStyles.resetButton} onPress={() => resetZoom(true)}>
|
|
201
|
+
<Text style={lightboxStyles.resetText}>1:1</Text>
|
|
202
|
+
</Pressable>
|
|
203
|
+
<Pressable accessibilityRole="button" accessibilityLabel={zoomInLabel} style={lightboxStyles.zoomButton} onPress={() => changeZoom(0.5)}>
|
|
204
|
+
<Text style={lightboxStyles.zoomText}>+</Text>
|
|
205
|
+
</Pressable>
|
|
206
|
+
</View>
|
|
207
|
+
</View>
|
|
208
|
+
</SafeAreaView>
|
|
162
209
|
<View style={lightboxStyles.stage} {...panResponder.panHandlers}>
|
|
163
210
|
{uri ? (
|
|
164
211
|
<Animated.Image
|
|
@@ -178,10 +225,14 @@ function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel }) {
|
|
|
178
225
|
|
|
179
226
|
const lightboxStyles = StyleSheet.create({
|
|
180
227
|
root: { flex: 1, backgroundColor: 'rgba(0,0,0,0.96)' },
|
|
181
|
-
|
|
228
|
+
safeArea: { position: 'absolute', zIndex: 20, top: 0, left: 0, right: 0 },
|
|
229
|
+
toolbar: { minHeight: 64, paddingHorizontal: 14, paddingTop: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
|
|
182
230
|
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
|
-
|
|
231
|
+
closeIcon: { color: '#FFFFFF', fontSize: 39, lineHeight: 40, transform: [{ translateY: -2 }] },
|
|
232
|
+
zoomControls: { flexDirection: 'row', alignItems: 'center', gap: 8 },
|
|
233
|
+
zoomButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: 'rgba(255,255,255,0.14)', alignItems: 'center', justifyContent: 'center' },
|
|
234
|
+
zoomText: { color: '#FFFFFF', fontSize: 27, lineHeight: 30, fontWeight: '500' },
|
|
235
|
+
resetButton: { minWidth: 52, height: 44, paddingHorizontal: 12, borderRadius: 22, backgroundColor: 'rgba(255,255,255,0.14)', alignItems: 'center', justifyContent: 'center' },
|
|
185
236
|
resetText: { color: '#FFFFFF', fontSize: 13, fontWeight: '700' },
|
|
186
237
|
stage: { flex: 1, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' },
|
|
187
238
|
image: { width: '100%', height: '100%' },
|
|
@@ -199,26 +250,29 @@ function colorWithOpacity(hex, opacity) {
|
|
|
199
250
|
|
|
200
251
|
function makePalette(settings) {
|
|
201
252
|
const dark = settings.theme_mode === 'dark';
|
|
202
|
-
const
|
|
253
|
+
const advanced = settings.color_mode === 'advanced';
|
|
254
|
+
const primary = advanced ? settings.primary_color : settings.theme_color;
|
|
255
|
+
const backgroundHex = advanced ? settings.chat_background : (dark ? '#111316' : '#F5F7FB');
|
|
256
|
+
const backgroundOpacity = advanced ? settings.chat_background_opacity : 100;
|
|
203
257
|
return {
|
|
204
258
|
dark,
|
|
205
259
|
primary,
|
|
206
|
-
background: colorWithOpacity(
|
|
260
|
+
background: colorWithOpacity(backgroundHex, backgroundOpacity),
|
|
207
261
|
surface: dark ? '#1D2127' : '#FFFFFF',
|
|
208
262
|
surfaceAlt: dark ? '#252A31' : '#F5F7FB',
|
|
209
263
|
header: dark ? '#181B20' : '#F8F9FC',
|
|
210
264
|
border: dark ? 'rgba(255,255,255,0.10)' : 'rgba(20,32,54,0.10)',
|
|
211
265
|
text: dark ? '#F7F8FA' : '#17181B',
|
|
212
|
-
muted: settings.system_text
|
|
213
|
-
customerBubble:
|
|
214
|
-
customerText:
|
|
215
|
-
agentBubble:
|
|
216
|
-
agentText:
|
|
266
|
+
muted: advanced ? settings.system_text : (dark ? '#AEB4BE' : '#6F7480'),
|
|
267
|
+
customerBubble: advanced ? settings.customer_bubble : primary,
|
|
268
|
+
customerText: advanced ? settings.customer_text : '#FFFFFF',
|
|
269
|
+
agentBubble: advanced ? settings.agent_bubble : (dark ? '#22262C' : '#FFFFFF'),
|
|
270
|
+
agentText: advanced ? settings.agent_text : (dark ? '#F5F6F7' : '#17181B'),
|
|
217
271
|
danger: '#DF2917',
|
|
218
|
-
heroBase: dark ? '#151B21' : '#
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
272
|
+
heroBase: dark ? '#151B21' : '#EEF5FC',
|
|
273
|
+
heroGlowA: dark ? '#495398' : '#7689FF',
|
|
274
|
+
heroGlowB: dark ? '#227EA8' : '#60DED7',
|
|
275
|
+
heroGlowC: dark ? '#293038' : '#FFFFFF',
|
|
222
276
|
};
|
|
223
277
|
}
|
|
224
278
|
|
|
@@ -234,6 +288,140 @@ function isSupportMessage(message) {
|
|
|
234
288
|
return sender === 'agent' || sender === 'admin' || sender === 'chatbot';
|
|
235
289
|
}
|
|
236
290
|
|
|
291
|
+
|
|
292
|
+
function humanSize(bytes) {
|
|
293
|
+
const size = Number(bytes || 0);
|
|
294
|
+
if (!size) return '0 B';
|
|
295
|
+
const units = ['B', 'KB', 'MB', 'GB'];
|
|
296
|
+
const index = Math.min(units.length - 1, Math.max(0, Math.floor(Math.log(size) / Math.log(1024))));
|
|
297
|
+
const value = size / Math.pow(1024, index);
|
|
298
|
+
return `${value.toFixed(index === 0 ? 0 : 1)} ${units[index]}`;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
function normalizeMarkdownSource(source) {
|
|
302
|
+
return String(source == null ? '' : source)
|
|
303
|
+
.replace(/\\([*_`#>\[\]()~+-])/g, '$1')
|
|
304
|
+
.replace(/\*{3,}([^*\n]+?)\*{3,}/g, '**$1**')
|
|
305
|
+
.replace(/^(\s*[-+*]\s+)\*([^*\n].*?)\*\*\s*$/gm, '$1**$2**');
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
function renderInlineMarkdown(value, textStyle, styles, keyPrefix) {
|
|
309
|
+
const text = String(value || '');
|
|
310
|
+
const pattern = /(\*\*[^*\n]+\*\*|__[^_\n]+__|`[^`\n]+`|\[[^\]\n]+\]\((?:https?:\/\/|mailto:|tel:)[^)\s]+\)|\*[^*\n]+\*|_[^_\n]+_)/g;
|
|
311
|
+
const nodes = [];
|
|
312
|
+
let last = 0;
|
|
313
|
+
let match;
|
|
314
|
+
let index = 0;
|
|
315
|
+
|
|
316
|
+
while ((match = pattern.exec(text)) !== null) {
|
|
317
|
+
if (match.index > last) nodes.push(text.slice(last, match.index));
|
|
318
|
+
const token = match[0];
|
|
319
|
+
const key = `${keyPrefix}-${index++}`;
|
|
320
|
+
|
|
321
|
+
if ((token.startsWith('**') && token.endsWith('**')) || (token.startsWith('__') && token.endsWith('__'))) {
|
|
322
|
+
nodes.push(<Text key={key} style={[textStyle, styles.markdownStrong]}>{token.slice(2, -2)}</Text>);
|
|
323
|
+
} else if (token.startsWith('`')) {
|
|
324
|
+
nodes.push(<Text key={key} style={[textStyle, styles.markdownInlineCode]}>{token.slice(1, -1)}</Text>);
|
|
325
|
+
} else if (token.startsWith('[')) {
|
|
326
|
+
const linkMatch = token.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
|
|
327
|
+
if (linkMatch) {
|
|
328
|
+
const url = linkMatch[2];
|
|
329
|
+
nodes.push(
|
|
330
|
+
<Text key={key} style={[textStyle, styles.markdownLink]} onPress={() => Linking.openURL(url).catch(() => {})}>
|
|
331
|
+
{linkMatch[1]}
|
|
332
|
+
</Text>
|
|
333
|
+
);
|
|
334
|
+
} else {
|
|
335
|
+
nodes.push(token);
|
|
336
|
+
}
|
|
337
|
+
} else {
|
|
338
|
+
nodes.push(<Text key={key} style={[textStyle, styles.markdownEm]}>{token.slice(1, -1)}</Text>);
|
|
339
|
+
}
|
|
340
|
+
last = pattern.lastIndex;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
if (last < text.length) nodes.push(text.slice(last));
|
|
344
|
+
return nodes;
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
function SafeMarkdown({ source, textStyle, styles }) {
|
|
348
|
+
const lines = normalizeMarkdownSource(source).replace(/\r\n?/g, '\n').split('\n');
|
|
349
|
+
const blocks = [];
|
|
350
|
+
let codeLines = [];
|
|
351
|
+
let inCode = false;
|
|
352
|
+
|
|
353
|
+
const flushCode = key => {
|
|
354
|
+
if (!codeLines.length) return;
|
|
355
|
+
blocks.push(
|
|
356
|
+
<View key={key} style={styles.markdownCodeBlock}>
|
|
357
|
+
<Text style={[textStyle, styles.markdownCodeText]}>{codeLines.join('\n')}</Text>
|
|
358
|
+
</View>
|
|
359
|
+
);
|
|
360
|
+
codeLines = [];
|
|
361
|
+
};
|
|
362
|
+
|
|
363
|
+
lines.forEach((line, index) => {
|
|
364
|
+
if (/^```/.test(line.trim())) {
|
|
365
|
+
if (inCode) flushCode(`code-${index}`);
|
|
366
|
+
inCode = !inCode;
|
|
367
|
+
return;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
if (inCode) {
|
|
371
|
+
codeLines.push(line);
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (!line.trim()) {
|
|
376
|
+
blocks.push(<View key={`space-${index}`} style={styles.markdownSpacer} />);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
const heading = line.match(/^\s*(#{1,6})\s+(.+)$/);
|
|
381
|
+
if (heading) {
|
|
382
|
+
blocks.push(
|
|
383
|
+
<Text key={`h-${index}`} style={[textStyle, styles.markdownHeading]}>
|
|
384
|
+
{renderInlineMarkdown(heading[2], textStyle, styles, `h-${index}`)}
|
|
385
|
+
</Text>
|
|
386
|
+
);
|
|
387
|
+
return;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
const quote = line.match(/^\s*>\s?(.*)$/);
|
|
391
|
+
if (quote) {
|
|
392
|
+
blocks.push(
|
|
393
|
+
<View key={`q-${index}`} style={styles.markdownQuote}>
|
|
394
|
+
<Text style={textStyle}>{renderInlineMarkdown(quote[1], textStyle, styles, `q-${index}`)}</Text>
|
|
395
|
+
</View>
|
|
396
|
+
);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
const unordered = line.match(/^\s*[-+*]\s+(.+)$/);
|
|
401
|
+
const ordered = line.match(/^\s*(\d+)[.)]\s+(.+)$/);
|
|
402
|
+
if (unordered || ordered) {
|
|
403
|
+
const prefix = ordered ? `${ordered[1]}.` : '•';
|
|
404
|
+
const content = ordered ? ordered[2] : unordered[1];
|
|
405
|
+
blocks.push(
|
|
406
|
+
<View key={`li-${index}`} style={styles.markdownListRow}>
|
|
407
|
+
<Text style={[textStyle, styles.markdownListPrefix]}>{prefix}</Text>
|
|
408
|
+
<Text style={[textStyle, styles.markdownListText]}>{renderInlineMarkdown(content, textStyle, styles, `li-${index}`)}</Text>
|
|
409
|
+
</View>
|
|
410
|
+
);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
blocks.push(
|
|
415
|
+
<Text key={`p-${index}`} style={[textStyle, styles.markdownParagraph]}>
|
|
416
|
+
{renderInlineMarkdown(line, textStyle, styles, `p-${index}`)}
|
|
417
|
+
</Text>
|
|
418
|
+
);
|
|
419
|
+
});
|
|
420
|
+
|
|
421
|
+
if (codeLines.length) flushCode('code-last');
|
|
422
|
+
return <View style={styles.markdownRoot}>{blocks}</View>;
|
|
423
|
+
}
|
|
424
|
+
|
|
237
425
|
function RegantisChat(props) {
|
|
238
426
|
const {
|
|
239
427
|
apiKey,
|
|
@@ -286,13 +474,18 @@ function RegantisChat(props) {
|
|
|
286
474
|
const [ratingComment, setRatingComment] = React.useState('');
|
|
287
475
|
const [ratingStatus, setRatingStatus] = React.useState('');
|
|
288
476
|
const [busyAction, setBusyAction] = React.useState('');
|
|
477
|
+
const [attachmentError, setAttachmentError] = React.useState('');
|
|
478
|
+
const [cameraSettingsNeeded, setCameraSettingsNeeded] = React.useState(false);
|
|
289
479
|
const [lightboxUri, setLightboxUri] = React.useState('');
|
|
480
|
+
const [logoFailed, setLogoFailed] = React.useState(false);
|
|
290
481
|
const listRef = React.useRef(null);
|
|
482
|
+
const previousStatusRef = React.useRef(null);
|
|
483
|
+
const activeViewRef = React.useRef(initialView === 'chat' ? 'chat' : 'home');
|
|
291
484
|
|
|
292
485
|
React.useEffect(() => {
|
|
293
486
|
const unsubscribe = client.subscribe(next => setState(next));
|
|
294
487
|
client.start().catch(() => {});
|
|
295
|
-
const sub = AppState.addEventListener('change', value => client.setVisible(value === 'active'));
|
|
488
|
+
const sub = AppState.addEventListener('change', value => client.setVisible(value === 'active' && activeViewRef.current === 'chat'));
|
|
296
489
|
return () => {
|
|
297
490
|
if (sub && sub.remove) sub.remove();
|
|
298
491
|
unsubscribe();
|
|
@@ -300,6 +493,15 @@ function RegantisChat(props) {
|
|
|
300
493
|
};
|
|
301
494
|
}, [client]);
|
|
302
495
|
|
|
496
|
+
React.useEffect(() => {
|
|
497
|
+
activeViewRef.current = activeView;
|
|
498
|
+
client.setVisible(AppState.currentState === 'active' && activeView === 'chat');
|
|
499
|
+
}, [activeView, client]);
|
|
500
|
+
|
|
501
|
+
React.useEffect(() => {
|
|
502
|
+
setLogoFailed(false);
|
|
503
|
+
}, [state.settings.logo_url]);
|
|
504
|
+
|
|
303
505
|
React.useEffect(() => {
|
|
304
506
|
const c = state.conversation || {};
|
|
305
507
|
if (c.contact_name && !contactName) setContactName(c.contact_name);
|
|
@@ -330,6 +532,9 @@ function RegantisChat(props) {
|
|
|
330
532
|
palette.agentBubble,
|
|
331
533
|
palette.agentText,
|
|
332
534
|
palette.heroBase,
|
|
535
|
+
palette.heroGlowA,
|
|
536
|
+
palette.heroGlowB,
|
|
537
|
+
palette.heroGlowC,
|
|
333
538
|
]);
|
|
334
539
|
|
|
335
540
|
const c = state.conversation || {};
|
|
@@ -341,9 +546,36 @@ function RegantisChat(props) {
|
|
|
341
546
|
const homePreview = lastSupport
|
|
342
547
|
? String(lastSupport.message || '').trim() || (lastSupport.attachment ? `[${String(lastSupport.message_type || 'file')}]` : t('empty'))
|
|
343
548
|
: t('empty');
|
|
549
|
+
const latestNamedAgent = Number(state.settings.use_profile_names || 0) === 1
|
|
550
|
+
? [...state.messages].reverse().find(message => {
|
|
551
|
+
const sender = String(message && message.sender_type || '').toLowerCase();
|
|
552
|
+
return (sender === 'agent' || sender === 'admin') && String(message && message.sender_name || '').trim();
|
|
553
|
+
})
|
|
554
|
+
: null;
|
|
555
|
+
const operatorDisplayName = latestNamedAgent
|
|
556
|
+
? String(latestNamedAgent.sender_name || '').trim()
|
|
557
|
+
: (state.settings.operator_name || t('support_name'));
|
|
558
|
+
|
|
559
|
+
React.useEffect(() => {
|
|
560
|
+
const nextStatus = Number(c.status || 0);
|
|
561
|
+
const previousStatus = previousStatusRef.current;
|
|
562
|
+
previousStatusRef.current = nextStatus;
|
|
563
|
+
|
|
564
|
+
if (
|
|
565
|
+
previousStatus === 1 &&
|
|
566
|
+
nextStatus === 0 &&
|
|
567
|
+
Number(state.settings.rating_enabled || 0) === 1 &&
|
|
568
|
+
contactConfirmed &&
|
|
569
|
+
!c.feedback_submitted
|
|
570
|
+
) {
|
|
571
|
+
const timer = setTimeout(() => setRatingOpen(true), 120);
|
|
572
|
+
return () => clearTimeout(timer);
|
|
573
|
+
}
|
|
574
|
+
}, [c.status, c.feedback_submitted, contactConfirmed, state.settings.rating_enabled]);
|
|
344
575
|
|
|
345
576
|
function showView(next) {
|
|
346
577
|
const view = next === 'chat' ? 'chat' : 'home';
|
|
578
|
+
activeViewRef.current = view;
|
|
347
579
|
setActiveView(view);
|
|
348
580
|
setOptionsOpen(false);
|
|
349
581
|
setLanguageOpen(false);
|
|
@@ -382,10 +614,23 @@ function RegantisChat(props) {
|
|
|
382
614
|
|
|
383
615
|
async function pick(kind) {
|
|
384
616
|
setAttachOpen(false);
|
|
617
|
+
setAttachmentError('');
|
|
618
|
+
setCameraSettingsNeeded(false);
|
|
385
619
|
setBusyAction('upload');
|
|
386
620
|
try {
|
|
387
621
|
await client.pickAndUpload(kind);
|
|
388
|
-
|
|
622
|
+
setAttachmentError('');
|
|
623
|
+
setCameraSettingsNeeded(false);
|
|
624
|
+
} catch (error) {
|
|
625
|
+
const code = String(error && (error.code || error.message) || '');
|
|
626
|
+
if (code.includes('CAMERA_PERMISSION_DENIED')) {
|
|
627
|
+
setAttachmentError(t('camera_permission_denied'));
|
|
628
|
+
setCameraSettingsNeeded(true);
|
|
629
|
+
} else if (code.includes('CAMERA_USAGE_DESCRIPTION_MISSING') || code.includes('CAMERA_PERMISSION_UNAVAILABLE')) {
|
|
630
|
+
setAttachmentError(t('camera_setup_required'));
|
|
631
|
+
} else if (code.includes('CAMERA_UNAVAILABLE')) setAttachmentError(t('camera_unavailable'));
|
|
632
|
+
else setAttachmentError(error && error.message ? error.message : t('send_error'));
|
|
633
|
+
if (typeof onError === 'function') onError(error);
|
|
389
634
|
} finally {
|
|
390
635
|
setBusyAction('');
|
|
391
636
|
}
|
|
@@ -463,8 +708,8 @@ function RegantisChat(props) {
|
|
|
463
708
|
|
|
464
709
|
function renderLogo(size, marginRight) {
|
|
465
710
|
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" />;
|
|
711
|
+
if (state.settings.logo_url && !logoFailed) {
|
|
712
|
+
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
713
|
}
|
|
469
714
|
return (
|
|
470
715
|
<View style={[styles.logoFallback, { width: size, height: size, borderRadius: size / 2, marginRight: marginRight || 0 }]}>
|
|
@@ -498,7 +743,11 @@ function RegantisChat(props) {
|
|
|
498
743
|
const textStyle = visitor ? styles.customerText : styles.agentText;
|
|
499
744
|
const attachment = item.attachment || null;
|
|
500
745
|
const isImage = attachment && String(attachment.mime || '').startsWith('image/');
|
|
501
|
-
const
|
|
746
|
+
const configuredName = state.settings.operator_name || t('support_name');
|
|
747
|
+
const senderName = sender === 'chatbot'
|
|
748
|
+
? 'Chatbot'
|
|
749
|
+
: (Number(state.settings.use_profile_names || 0) === 1 ? (item.sender_name || configuredName) : configuredName);
|
|
750
|
+
const fileExt = String(attachment && attachment.ext || '').replace(/^\./, '').slice(0, 4).toUpperCase() || 'FILE';
|
|
502
751
|
|
|
503
752
|
return (
|
|
504
753
|
<View style={[styles.messageRow, visitor ? styles.messageRowRight : styles.messageRowLeft]}>
|
|
@@ -508,11 +757,21 @@ function RegantisChat(props) {
|
|
|
508
757
|
<Image source={{ uri: attachment.url }} style={styles.attachmentImage} resizeMode="cover" />
|
|
509
758
|
</Pressable>
|
|
510
759
|
) : attachment ? (
|
|
511
|
-
<Pressable style={styles.fileAttachment} onPress={() => Linking.openURL(attachment.url)}>
|
|
512
|
-
<
|
|
760
|
+
<Pressable style={styles.fileAttachment} onPress={() => Linking.openURL(attachment.url).catch(() => {})}>
|
|
761
|
+
<View style={[styles.fileAttachmentIcon, { borderColor: textStyle.color }]}>
|
|
762
|
+
<Text style={[styles.fileAttachmentIconText, textStyle]}>{fileExt}</Text>
|
|
763
|
+
</View>
|
|
764
|
+
<View style={styles.fileAttachmentCopy}>
|
|
765
|
+
<Text numberOfLines={1} style={[styles.fileAttachmentText, textStyle]}>{attachment.name || t('attach_file')}</Text>
|
|
766
|
+
<Text numberOfLines={1} style={[styles.fileAttachmentMeta, textStyle]}>{humanSize(attachment.size)}{attachment.ext ? ` · ${String(attachment.ext).toUpperCase()}` : ''}</Text>
|
|
767
|
+
</View>
|
|
513
768
|
</Pressable>
|
|
514
769
|
) : null}
|
|
515
|
-
{item.message ?
|
|
770
|
+
{item.message ? (
|
|
771
|
+
sender === 'chatbot'
|
|
772
|
+
? <SafeMarkdown source={item.message} textStyle={[styles.messageText, textStyle]} styles={styles} />
|
|
773
|
+
: <Text style={[styles.messageText, textStyle]}>{item.message}</Text>
|
|
774
|
+
) : null}
|
|
516
775
|
<View style={styles.messageMeta}>
|
|
517
776
|
{!visitor ? <Text numberOfLines={1} style={[styles.messageSender, textStyle]}>{senderName}</Text> : <View />}
|
|
518
777
|
<Text style={[styles.timeText, textStyle]}>{formatTime(item.date_added)}</Text>
|
|
@@ -524,16 +783,21 @@ function RegantisChat(props) {
|
|
|
524
783
|
|
|
525
784
|
function renderPoweredBy() {
|
|
526
785
|
if (state.settings.white_label) return null;
|
|
527
|
-
return
|
|
786
|
+
return (
|
|
787
|
+
<View style={styles.poweredBy}>
|
|
788
|
+
<Text style={styles.poweredByText}>{t('powered_by')}</Text>
|
|
789
|
+
<Image source={palette.dark ? REGANTIS_LOGO_WHITE : REGANTIS_LOGO_BLACK} style={styles.poweredByLogo} resizeMode="contain" />
|
|
790
|
+
</View>
|
|
791
|
+
);
|
|
528
792
|
}
|
|
529
793
|
|
|
530
794
|
function renderHome() {
|
|
531
795
|
return (
|
|
532
796
|
<View style={styles.screen}>
|
|
533
797
|
<View style={styles.homeHero}>
|
|
534
|
-
<
|
|
535
|
-
<
|
|
536
|
-
<
|
|
798
|
+
<Image pointerEvents="none" source={SOFT_GLOW} style={[styles.heroGlow, styles.heroGlowA, { tintColor: palette.heroGlowA }]} resizeMode="stretch" />
|
|
799
|
+
<Image pointerEvents="none" source={SOFT_GLOW} style={[styles.heroGlow, styles.heroGlowB, { tintColor: palette.heroGlowB }]} resizeMode="stretch" />
|
|
800
|
+
<Image pointerEvents="none" source={SOFT_GLOW} style={[styles.heroGlow, styles.heroGlowC, { tintColor: palette.heroGlowC }]} resizeMode="stretch" />
|
|
537
801
|
|
|
538
802
|
<View style={styles.homeTop}>
|
|
539
803
|
<View style={styles.homeTopLeft}>
|
|
@@ -560,7 +824,7 @@ function RegantisChat(props) {
|
|
|
560
824
|
<View style={styles.homeCardTop}>
|
|
561
825
|
{state.settings.show_agent_photo ? <View style={styles.homeAvatar}><Text style={styles.homeAvatarText}>R</Text></View> : null}
|
|
562
826
|
<View style={styles.homeCardCopy}>
|
|
563
|
-
<Text numberOfLines={1} style={styles.homeCardAgent}>{
|
|
827
|
+
<Text numberOfLines={1} style={styles.homeCardAgent}>{operatorDisplayName}</Text>
|
|
564
828
|
<Text numberOfLines={1} style={styles.homeCardPreview}>{homePreview}</Text>
|
|
565
829
|
</View>
|
|
566
830
|
</View>
|
|
@@ -595,20 +859,22 @@ function RegantisChat(props) {
|
|
|
595
859
|
<Text style={styles.backIcon}>‹</Text>
|
|
596
860
|
</Pressable>
|
|
597
861
|
<Pressable accessibilityRole="button" accessibilityLabel={t('options')} style={styles.headerIconButton} onPress={() => setOptionsOpen(value => !value)}>
|
|
598
|
-
<
|
|
862
|
+
<View style={styles.menuDots}><View style={styles.menuDot} /><View style={styles.menuDot} /><View style={styles.menuDot} /></View>
|
|
599
863
|
</Pressable>
|
|
600
864
|
</View>
|
|
601
865
|
|
|
602
|
-
<
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
866
|
+
<View pointerEvents="box-none" style={styles.chatHeaderCenter}>
|
|
867
|
+
<Pressable
|
|
868
|
+
accessibilityRole="button"
|
|
869
|
+
accessibilityLabel={operatorDisplayName}
|
|
870
|
+
style={styles.agentTrigger}
|
|
871
|
+
onPress={() => {
|
|
872
|
+
if (state.settings.rating_enabled && contactConfirmed) setRatingOpen(true);
|
|
873
|
+
}}
|
|
874
|
+
>
|
|
875
|
+
{renderAgentVisual()}
|
|
876
|
+
</Pressable>
|
|
877
|
+
</View>
|
|
612
878
|
|
|
613
879
|
<View style={styles.chatHeaderRight}>
|
|
614
880
|
{typeof onClose === 'function' ? (
|
|
@@ -640,7 +906,7 @@ function RegantisChat(props) {
|
|
|
640
906
|
<Text style={styles.popoverChevron}>›</Text>
|
|
641
907
|
</Pressable>
|
|
642
908
|
) : null}
|
|
643
|
-
{state.
|
|
909
|
+
{state.soundAvailable ? (
|
|
644
910
|
<Pressable style={styles.popoverRow} onPress={() => client.setSoundEnabled(!state.settings.sound_enabled)}>
|
|
645
911
|
<Text style={styles.popoverIcon}>◖</Text>
|
|
646
912
|
<Text style={styles.popoverText}>{t('sounds')}</Text>
|
|
@@ -674,6 +940,20 @@ function RegantisChat(props) {
|
|
|
674
940
|
);
|
|
675
941
|
}
|
|
676
942
|
|
|
943
|
+
function renderContactInfoCard() {
|
|
944
|
+
if (!contactConfirmed || (!c.contact_name && !c.contact_email)) return null;
|
|
945
|
+
return (
|
|
946
|
+
<View style={styles.contactInfoRow}>
|
|
947
|
+
<View style={styles.contactInfoAvatar}><Text style={styles.contactInfoAvatarText}>i</Text></View>
|
|
948
|
+
<View style={styles.contactInfoBubble}>
|
|
949
|
+
<Text style={styles.contactInfoTitle}>{t('personal_details')}</Text>
|
|
950
|
+
{c.contact_name ? <Text style={styles.contactInfoLine}><Text style={styles.contactInfoLabel}>{t('name')}: </Text>{c.contact_name}</Text> : null}
|
|
951
|
+
{c.contact_email ? <Text style={styles.contactInfoLine}><Text style={styles.contactInfoLabel}>{t('email')}: </Text>{c.contact_email}</Text> : null}
|
|
952
|
+
</View>
|
|
953
|
+
</View>
|
|
954
|
+
);
|
|
955
|
+
}
|
|
956
|
+
|
|
677
957
|
function renderComposer() {
|
|
678
958
|
if (!active) {
|
|
679
959
|
return (
|
|
@@ -710,6 +990,17 @@ function RegantisChat(props) {
|
|
|
710
990
|
</View>
|
|
711
991
|
) : null}
|
|
712
992
|
|
|
993
|
+
{attachmentError ? (
|
|
994
|
+
<View style={styles.composerErrorRow}>
|
|
995
|
+
<Text style={styles.composerError}>{attachmentError}</Text>
|
|
996
|
+
{cameraSettingsNeeded ? (
|
|
997
|
+
<Pressable style={styles.composerErrorAction} onPress={() => Linking.openSettings().catch(() => {})}>
|
|
998
|
+
<Text style={styles.composerErrorActionText}>{t('open_settings')}</Text>
|
|
999
|
+
</Pressable>
|
|
1000
|
+
) : null}
|
|
1001
|
+
</View>
|
|
1002
|
+
) : null}
|
|
1003
|
+
|
|
713
1004
|
<View style={styles.composer}>
|
|
714
1005
|
<Pressable disabled={!canUpload || state.uploading} style={[styles.composerButton, (!canUpload || state.uploading) && styles.disabled]} onPress={() => { setEmojiOpen(false); setAttachOpen(value => !value); }}>
|
|
715
1006
|
<Text style={styles.composerPlus}>+</Text>
|
|
@@ -749,7 +1040,12 @@ function RegantisChat(props) {
|
|
|
749
1040
|
style={styles.messageList}
|
|
750
1041
|
contentContainerStyle={styles.messageListContent}
|
|
751
1042
|
onScrollBeginDrag={() => state.hasMore && client.loadOlder().catch(() => {})}
|
|
752
|
-
ListHeaderComponent={
|
|
1043
|
+
ListHeaderComponent={(
|
|
1044
|
+
<>
|
|
1045
|
+
{state.hasMore ? <View style={styles.historyLoader}><ActivityIndicator size="small" color={palette.primary} /></View> : null}
|
|
1046
|
+
{renderContactInfoCard()}
|
|
1047
|
+
</>
|
|
1048
|
+
)}
|
|
753
1049
|
ListEmptyComponent={<View style={styles.emptyState}><Text style={styles.emptyText}>{t('empty')}</Text></View>}
|
|
754
1050
|
/>
|
|
755
1051
|
{state.remoteTyping ? <View style={styles.typingRow}><Text style={styles.typingText}>{state.remoteTyping === 'chatbot' ? t('chatbot_typing') : t('typing')}</Text></View> : null}
|
|
@@ -825,7 +1121,7 @@ function RegantisChat(props) {
|
|
|
825
1121
|
<Modal transparent visible={ratingOpen} animationType="fade" onRequestClose={() => setRatingOpen(false)}>
|
|
826
1122
|
<View style={styles.modalBackdropTop}>
|
|
827
1123
|
<View style={styles.modalCard}>
|
|
828
|
-
<Text style={styles.modalTitle}>{
|
|
1124
|
+
<Text style={styles.modalTitle}>{operatorDisplayName}</Text>
|
|
829
1125
|
<Text style={styles.ratingLabel}>{t('rating_agent_label')}</Text>
|
|
830
1126
|
<View style={styles.ratingButtons}>
|
|
831
1127
|
<Pressable style={styles.ratingButton} onPress={() => rate(1)}><Text style={styles.ratingIcon}>👍</Text></Pressable>
|
|
@@ -847,6 +1143,8 @@ function RegantisChat(props) {
|
|
|
847
1143
|
onClose={() => setLightboxUri('')}
|
|
848
1144
|
closeLabel={t('lightbox_close')}
|
|
849
1145
|
resetLabel={t('lightbox_reset')}
|
|
1146
|
+
zoomInLabel={t('lightbox_zoom_in')}
|
|
1147
|
+
zoomOutLabel={t('lightbox_zoom_out')}
|
|
850
1148
|
/>
|
|
851
1149
|
</KeyboardAvoidingView>
|
|
852
1150
|
);
|
|
@@ -869,9 +1167,10 @@ function buildStyles(p) {
|
|
|
869
1167
|
errorText: { color: p.text, fontSize: 14, textAlign: 'center', marginBottom: 12 },
|
|
870
1168
|
|
|
871
1169
|
homeHero: { position: 'relative', overflow: 'hidden', paddingHorizontal: 18, paddingTop: 24, paddingBottom: 28, backgroundColor: p.heroBase },
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
1170
|
+
heroGlow: { position: 'absolute', opacity: p.dark ? 0.34 : 0.72 },
|
|
1171
|
+
heroGlowA: { width: 390, height: 390, top: -235, left: -165 },
|
|
1172
|
+
heroGlowB: { width: 360, height: 360, top: 20, right: -170, opacity: p.dark ? 0.26 : 0.62 },
|
|
1173
|
+
heroGlowC: { width: 300, height: 300, right: -135, bottom: -185, opacity: p.dark ? 0.09 : 0.34 },
|
|
875
1174
|
homeTop: { minHeight: 42, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', zIndex: 2 },
|
|
876
1175
|
homeTopLeft: { flexDirection: 'row', alignItems: 'center', gap: 9 },
|
|
877
1176
|
homeBackIcon: { color: p.text, fontSize: 37, lineHeight: 38, marginTop: -5 },
|
|
@@ -901,14 +1200,16 @@ function buildStyles(p) {
|
|
|
901
1200
|
bottomNavText: { marginTop: 2, color: p.muted, fontSize: 10, lineHeight: 12 },
|
|
902
1201
|
bottomNavTextActive: { color: p.text },
|
|
903
1202
|
|
|
904
|
-
chatHeader: { position: 'relative', zIndex: 20, minHeight: 64, flexDirection: 'row', alignItems: '
|
|
905
|
-
chatHeaderLeft: {
|
|
906
|
-
|
|
1203
|
+
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' },
|
|
1204
|
+
chatHeaderLeft: { zIndex: 30, width: 76, height: 64, flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', gap: 5 },
|
|
1205
|
+
chatHeaderCenter: { position: 'absolute', zIndex: 18, top: 0, left: 0, right: 0, height: 64, alignItems: 'center', justifyContent: 'flex-start' },
|
|
1206
|
+
chatHeaderRight: { zIndex: 30, width: 76, height: 64, flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 5 },
|
|
907
1207
|
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
|
-
|
|
1208
|
+
backIcon: { color: p.text, fontSize: 31, lineHeight: 31, transform: [{ translateY: -1 }] },
|
|
1209
|
+
menuDots: { width: 24, height: 24, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 3 },
|
|
1210
|
+
menuDot: { width: 3.5, height: 3.5, borderRadius: 2, backgroundColor: p.text },
|
|
1211
|
+
closeIcon: { color: p.text, fontSize: 23, lineHeight: 24, transform: [{ translateY: -1 }] },
|
|
1212
|
+
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
1213
|
agentVisual: { height: 30, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
|
|
913
1214
|
agentAvatar: { width: 28, height: 28, borderRadius: 14, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
|
|
914
1215
|
agentAvatarOverlap: { marginLeft: -8, borderWidth: 2, borderColor: p.surface },
|
|
@@ -942,6 +1243,13 @@ function buildStyles(p) {
|
|
|
942
1243
|
dangerButton: { flex: 1, minHeight: 40, borderRadius: 9, backgroundColor: p.danger, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center' },
|
|
943
1244
|
disabled: { opacity: 0.55 },
|
|
944
1245
|
|
|
1246
|
+
contactInfoRow: { width: '100%', flexDirection: 'row', alignItems: 'flex-start', marginBottom: 12, paddingRight: 44 },
|
|
1247
|
+
contactInfoAvatar: { width: 28, height: 28, borderRadius: 14, marginRight: 8, marginTop: 2, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
|
|
1248
|
+
contactInfoAvatarText: { color: p.muted, fontSize: 13, fontWeight: '800' },
|
|
1249
|
+
contactInfoBubble: { flex: 1, minWidth: 0, paddingHorizontal: 12, paddingVertical: 10, borderRadius: 14, borderBottomLeftRadius: 5, backgroundColor: p.agentBubble },
|
|
1250
|
+
contactInfoTitle: { color: p.agentText, fontSize: 12, lineHeight: 16, fontWeight: '800', marginBottom: 5 },
|
|
1251
|
+
contactInfoLine: { color: p.agentText, fontSize: 11, lineHeight: 16 },
|
|
1252
|
+
contactInfoLabel: { fontWeight: '700' },
|
|
945
1253
|
messageList: { flex: 1, minHeight: 0 },
|
|
946
1254
|
messageListContent: { paddingHorizontal: 16, paddingTop: 18, paddingBottom: 12, flexGrow: 1 },
|
|
947
1255
|
historyLoader: { minHeight: 34, alignItems: 'center', justifyContent: 'center', paddingBottom: 10 },
|
|
@@ -962,8 +1270,26 @@ function buildStyles(p) {
|
|
|
962
1270
|
systemRow: { alignSelf: 'center', paddingHorizontal: 16, paddingVertical: 7, marginVertical: 3 },
|
|
963
1271
|
systemText: { color: p.muted, fontSize: 11, lineHeight: 15, textAlign: 'center' },
|
|
964
1272
|
attachmentImage: { width: 220, maxWidth: '100%', height: 180, borderRadius: 10, marginBottom: 4, backgroundColor: p.surfaceAlt },
|
|
965
|
-
fileAttachment: { paddingVertical: 4 },
|
|
1273
|
+
fileAttachment: { minWidth: 180, maxWidth: 260, flexDirection: 'row', alignItems: 'center', gap: 10, paddingVertical: 4 },
|
|
1274
|
+
fileAttachmentIcon: { width: 34, height: 34, borderWidth: 1, borderRadius: 9, alignItems: 'center', justifyContent: 'center', opacity: 0.85 },
|
|
1275
|
+
fileAttachmentIconText: { fontSize: 10, fontWeight: '800' },
|
|
1276
|
+
fileAttachmentCopy: { flex: 1, minWidth: 0 },
|
|
966
1277
|
fileAttachmentText: { fontSize: 13, fontWeight: '600' },
|
|
1278
|
+
fileAttachmentMeta: { marginTop: 2, fontSize: 10, opacity: 0.72 },
|
|
1279
|
+
markdownRoot: { flexShrink: 1 },
|
|
1280
|
+
markdownParagraph: { marginBottom: 2 },
|
|
1281
|
+
markdownSpacer: { height: 7 },
|
|
1282
|
+
markdownStrong: { fontWeight: '800' },
|
|
1283
|
+
markdownEm: { fontStyle: 'italic' },
|
|
1284
|
+
markdownInlineCode: { fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontSize: 12, backgroundColor: p.dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)' },
|
|
1285
|
+
markdownLink: { textDecorationLine: 'underline' },
|
|
1286
|
+
markdownHeading: { marginTop: 2, marginBottom: 4, fontSize: 15, lineHeight: 20, fontWeight: '800' },
|
|
1287
|
+
markdownQuote: { marginVertical: 3, paddingLeft: 9, borderLeftWidth: 3, borderLeftColor: p.muted },
|
|
1288
|
+
markdownListRow: { flexDirection: 'row', alignItems: 'flex-start', marginVertical: 1 },
|
|
1289
|
+
markdownListPrefix: { width: 20, fontWeight: '700' },
|
|
1290
|
+
markdownListText: { flex: 1, minWidth: 0 },
|
|
1291
|
+
markdownCodeBlock: { marginVertical: 4, paddingHorizontal: 9, paddingVertical: 8, borderRadius: 8, backgroundColor: p.dark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.06)' },
|
|
1292
|
+
markdownCodeText: { fontFamily: Platform.OS === 'ios' ? 'Menlo' : 'monospace', fontSize: 12, lineHeight: 17 },
|
|
967
1293
|
typingRow: { flex: 0, paddingHorizontal: 18, paddingTop: 8, paddingBottom: 7 },
|
|
968
1294
|
typingText: { color: p.muted, fontSize: 11, lineHeight: 15 },
|
|
969
1295
|
|
|
@@ -978,6 +1304,10 @@ function buildStyles(p) {
|
|
|
978
1304
|
sendButton: { width: 38, height: 38, borderRadius: 19, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
|
|
979
1305
|
sendIcon: { color: '#FFFFFF', fontSize: 17, lineHeight: 18, transform: [{ rotate: '-3deg' }] },
|
|
980
1306
|
aiCompliance: { color: p.muted, fontSize: 9, lineHeight: 13, textAlign: 'center', paddingHorizontal: 5, paddingBottom: 8 },
|
|
1307
|
+
composerErrorRow: { alignItems: 'center', justifyContent: 'center', paddingHorizontal: 8, paddingBottom: 7 },
|
|
1308
|
+
composerError: { color: '#B42318', fontSize: 11, lineHeight: 15, textAlign: 'center' },
|
|
1309
|
+
composerErrorAction: { marginTop: 5, minHeight: 28, paddingHorizontal: 10, borderRadius: 14, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
|
|
1310
|
+
composerErrorActionText: { color: p.text, fontSize: 10, lineHeight: 13, fontWeight: '700' },
|
|
981
1311
|
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
1312
|
attachmentAction: { minHeight: 40, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, borderRadius: 9 },
|
|
983
1313
|
attachmentActionIcon: { width: 26, color: p.text, fontSize: 15 },
|
|
@@ -986,9 +1316,9 @@ function buildStyles(p) {
|
|
|
986
1316
|
emojiButton: { width: '11%', aspectRatio: 1, minHeight: 34, borderRadius: 8, alignItems: 'center', justifyContent: 'center' },
|
|
987
1317
|
emojiText: { fontSize: 20, lineHeight: 22 },
|
|
988
1318
|
|
|
989
|
-
poweredBy: { flex: 0, minHeight:
|
|
1319
|
+
poweredBy: { flex: 0, minHeight: 22, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 4, paddingBottom: 5, backgroundColor: p.background },
|
|
990
1320
|
poweredByText: { color: '#9398A2', fontSize: 9, lineHeight: 11, textAlign: 'center' },
|
|
991
|
-
|
|
1321
|
+
poweredByLogo: { width: 48, height: 15 },
|
|
992
1322
|
|
|
993
1323
|
modalBackdrop: { flex: 1, backgroundColor: 'rgba(16,20,27,0.55)', justifyContent: 'flex-end', padding: 16 },
|
|
994
1324
|
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
|
}
|