@regantis-sdk/react-native-chat 0.1.2 → 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 +27 -7
- package/RegantisReactNativeChat.podspec +1 -1
- package/android/build.gradle +1 -0
- package/android/src/main/AndroidManifest.xml +22 -1
- package/android/src/main/java/technology/regantis/chat/RegantisChatNativeModule.java +161 -55
- package/android/src/main/res/xml/regantis_chat_file_paths.xml +4 -0
- package/assets/regantis_logo_black.png +0 -0
- package/assets/regantis_logo_white.png +0 -0
- package/assets/soft_glow.png +0 -0
- package/index.d.ts +2 -1
- package/ios/RegantisChatNative.h +1 -1
- package/ios/RegantisChatNative.m +113 -32
- package/package.json +2 -1
- package/src/RegantisChat.js +572 -55
- package/src/client.js +19 -4
- package/src/defaults.js +1 -1
- package/src/native.js +5 -5
package/src/RegantisChat.js
CHANGED
|
@@ -3,15 +3,19 @@
|
|
|
3
3
|
const React = require('react');
|
|
4
4
|
const {
|
|
5
5
|
ActivityIndicator,
|
|
6
|
+
Animated,
|
|
6
7
|
AppState,
|
|
7
8
|
FlatList,
|
|
8
9
|
Image,
|
|
9
10
|
KeyboardAvoidingView,
|
|
10
11
|
Linking,
|
|
11
12
|
Modal,
|
|
13
|
+
PanResponder,
|
|
12
14
|
Platform,
|
|
13
15
|
Pressable,
|
|
16
|
+
SafeAreaView,
|
|
14
17
|
ScrollView,
|
|
18
|
+
StatusBar,
|
|
15
19
|
StyleSheet,
|
|
16
20
|
Text,
|
|
17
21
|
TextInput,
|
|
@@ -19,8 +23,221 @@ const {
|
|
|
19
23
|
} = require('react-native');
|
|
20
24
|
const { RegantisChatClient } = require('./client');
|
|
21
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
|
+
|
|
22
30
|
const EMOJIS = ['🙂', '😁', '😂', '😊', '😍', '😐', '🤔', '😞', '😢', '😭', '🎉', '❤️', '👌', '👍', '👎', '🙏'];
|
|
23
31
|
|
|
32
|
+
|
|
33
|
+
const LOCAL_TEXTS = {
|
|
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' },
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
function localText(language, key) {
|
|
43
|
+
const code = String(language || 'en').trim().toLowerCase().split(/[-_]/)[0];
|
|
44
|
+
return (LOCAL_TEXTS[code] && LOCAL_TEXTS[code][key]) || LOCAL_TEXTS.en[key] || '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function touchDistance(touches) {
|
|
48
|
+
if (!touches || touches.length < 2) return 0;
|
|
49
|
+
const dx = Number(touches[0].pageX || 0) - Number(touches[1].pageX || 0);
|
|
50
|
+
const dy = Number(touches[0].pageY || 0) - Number(touches[1].pageY || 0);
|
|
51
|
+
return Math.sqrt(dx * dx + dy * dy);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel, zoomInLabel, zoomOutLabel }) {
|
|
55
|
+
const scale = React.useRef(new Animated.Value(1)).current;
|
|
56
|
+
const translateX = React.useRef(new Animated.Value(0)).current;
|
|
57
|
+
const translateY = React.useRef(new Animated.Value(0)).current;
|
|
58
|
+
const current = React.useRef({ scale: 1, x: 0, y: 0 });
|
|
59
|
+
const gesture = React.useRef({ mode: '', distance: 0, scale: 1, x: 0, y: 0, startX: 0, startY: 0 });
|
|
60
|
+
|
|
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 }),
|
|
81
|
+
];
|
|
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
|
+
);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Animated.parallel(animations).start();
|
|
91
|
+
}, [scale, translateX, translateY]);
|
|
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
|
+
|
|
102
|
+
React.useEffect(() => {
|
|
103
|
+
if (visible) resetZoom(false);
|
|
104
|
+
}, [visible, uri, resetZoom]);
|
|
105
|
+
|
|
106
|
+
const panResponder = React.useMemo(() => PanResponder.create({
|
|
107
|
+
onStartShouldSetPanResponder: () => true,
|
|
108
|
+
onMoveShouldSetPanResponder: () => true,
|
|
109
|
+
onPanResponderGrant: event => {
|
|
110
|
+
const touches = event.nativeEvent.touches || [];
|
|
111
|
+
if (touches.length >= 2) {
|
|
112
|
+
gesture.current = {
|
|
113
|
+
mode: 'pinch',
|
|
114
|
+
distance: Math.max(1, touchDistance(touches)),
|
|
115
|
+
scale: current.current.scale,
|
|
116
|
+
x: current.current.x,
|
|
117
|
+
y: current.current.y,
|
|
118
|
+
startX: 0,
|
|
119
|
+
startY: 0,
|
|
120
|
+
};
|
|
121
|
+
} else if (touches.length === 1) {
|
|
122
|
+
gesture.current = {
|
|
123
|
+
mode: 'pan',
|
|
124
|
+
distance: 0,
|
|
125
|
+
scale: current.current.scale,
|
|
126
|
+
x: current.current.x,
|
|
127
|
+
y: current.current.y,
|
|
128
|
+
startX: Number(touches[0].pageX || 0),
|
|
129
|
+
startY: Number(touches[0].pageY || 0),
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
onPanResponderMove: event => {
|
|
134
|
+
const touches = event.nativeEvent.touches || [];
|
|
135
|
+
if (touches.length >= 2) {
|
|
136
|
+
const distance = Math.max(1, touchDistance(touches));
|
|
137
|
+
if (gesture.current.mode !== 'pinch') {
|
|
138
|
+
gesture.current.mode = 'pinch';
|
|
139
|
+
gesture.current.distance = distance;
|
|
140
|
+
gesture.current.scale = current.current.scale;
|
|
141
|
+
}
|
|
142
|
+
const nextScale = Math.max(1, Math.min(5, gesture.current.scale * (distance / Math.max(1, gesture.current.distance))));
|
|
143
|
+
current.current.scale = nextScale;
|
|
144
|
+
scale.setValue(nextScale);
|
|
145
|
+
if (nextScale <= 1.01) {
|
|
146
|
+
current.current.x = 0;
|
|
147
|
+
current.current.y = 0;
|
|
148
|
+
translateX.setValue(0);
|
|
149
|
+
translateY.setValue(0);
|
|
150
|
+
}
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (touches.length === 1 && current.current.scale > 1.01) {
|
|
155
|
+
const x = Number(touches[0].pageX || 0);
|
|
156
|
+
const y = Number(touches[0].pageY || 0);
|
|
157
|
+
if (gesture.current.mode !== 'pan') {
|
|
158
|
+
gesture.current.mode = 'pan';
|
|
159
|
+
gesture.current.x = current.current.x;
|
|
160
|
+
gesture.current.y = current.current.y;
|
|
161
|
+
gesture.current.startX = x;
|
|
162
|
+
gesture.current.startY = y;
|
|
163
|
+
}
|
|
164
|
+
const nextX = gesture.current.x + (x - gesture.current.startX);
|
|
165
|
+
const nextY = gesture.current.y + (y - gesture.current.startY);
|
|
166
|
+
current.current.x = nextX;
|
|
167
|
+
current.current.y = nextY;
|
|
168
|
+
translateX.setValue(nextX);
|
|
169
|
+
translateY.setValue(nextY);
|
|
170
|
+
}
|
|
171
|
+
},
|
|
172
|
+
onPanResponderRelease: () => {
|
|
173
|
+
gesture.current.mode = '';
|
|
174
|
+
if (current.current.scale <= 1.05) resetZoom(true);
|
|
175
|
+
},
|
|
176
|
+
onPanResponderTerminate: () => {
|
|
177
|
+
gesture.current.mode = '';
|
|
178
|
+
if (current.current.scale <= 1.05) resetZoom(true);
|
|
179
|
+
},
|
|
180
|
+
}), [resetZoom, scale, translateX, translateY]);
|
|
181
|
+
|
|
182
|
+
return (
|
|
183
|
+
<Modal visible={visible} transparent animationType="fade" onRequestClose={onClose} statusBarTranslucent>
|
|
184
|
+
<View style={lightboxStyles.root}>
|
|
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>
|
|
209
|
+
<View style={lightboxStyles.stage} {...panResponder.panHandlers}>
|
|
210
|
+
{uri ? (
|
|
211
|
+
<Animated.Image
|
|
212
|
+
source={{ uri }}
|
|
213
|
+
resizeMode="contain"
|
|
214
|
+
style={[
|
|
215
|
+
lightboxStyles.image,
|
|
216
|
+
{ transform: [{ translateX }, { translateY }, { scale }] },
|
|
217
|
+
]}
|
|
218
|
+
/>
|
|
219
|
+
) : null}
|
|
220
|
+
</View>
|
|
221
|
+
</View>
|
|
222
|
+
</Modal>
|
|
223
|
+
);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const lightboxStyles = StyleSheet.create({
|
|
227
|
+
root: { flex: 1, backgroundColor: 'rgba(0,0,0,0.96)' },
|
|
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' },
|
|
230
|
+
toolbarButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: 'rgba(255,255,255,0.14)', alignItems: 'center', justifyContent: 'center' },
|
|
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' },
|
|
236
|
+
resetText: { color: '#FFFFFF', fontSize: 13, fontWeight: '700' },
|
|
237
|
+
stage: { flex: 1, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' },
|
|
238
|
+
image: { width: '100%', height: '100%' },
|
|
239
|
+
});
|
|
240
|
+
|
|
24
241
|
function colorWithOpacity(hex, opacity) {
|
|
25
242
|
const value = String(hex || '#F5F7FB').replace('#', '');
|
|
26
243
|
if (!/^[0-9a-f]{6}$/i.test(value)) return hex;
|
|
@@ -33,26 +250,29 @@ function colorWithOpacity(hex, opacity) {
|
|
|
33
250
|
|
|
34
251
|
function makePalette(settings) {
|
|
35
252
|
const dark = settings.theme_mode === 'dark';
|
|
36
|
-
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;
|
|
37
257
|
return {
|
|
38
258
|
dark,
|
|
39
259
|
primary,
|
|
40
|
-
background: colorWithOpacity(
|
|
260
|
+
background: colorWithOpacity(backgroundHex, backgroundOpacity),
|
|
41
261
|
surface: dark ? '#1D2127' : '#FFFFFF',
|
|
42
262
|
surfaceAlt: dark ? '#252A31' : '#F5F7FB',
|
|
43
263
|
header: dark ? '#181B20' : '#F8F9FC',
|
|
44
264
|
border: dark ? 'rgba(255,255,255,0.10)' : 'rgba(20,32,54,0.10)',
|
|
45
265
|
text: dark ? '#F7F8FA' : '#17181B',
|
|
46
|
-
muted: settings.system_text
|
|
47
|
-
customerBubble:
|
|
48
|
-
customerText:
|
|
49
|
-
agentBubble:
|
|
50
|
-
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'),
|
|
51
271
|
danger: '#DF2917',
|
|
52
|
-
heroBase: dark ? '#151B21' : '#
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
272
|
+
heroBase: dark ? '#151B21' : '#EEF5FC',
|
|
273
|
+
heroGlowA: dark ? '#495398' : '#7689FF',
|
|
274
|
+
heroGlowB: dark ? '#227EA8' : '#60DED7',
|
|
275
|
+
heroGlowC: dark ? '#293038' : '#FFFFFF',
|
|
56
276
|
};
|
|
57
277
|
}
|
|
58
278
|
|
|
@@ -68,6 +288,140 @@ function isSupportMessage(message) {
|
|
|
68
288
|
return sender === 'agent' || sender === 'admin' || sender === 'chatbot';
|
|
69
289
|
}
|
|
70
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
|
+
|
|
71
425
|
function RegantisChat(props) {
|
|
72
426
|
const {
|
|
73
427
|
apiKey,
|
|
@@ -81,6 +435,7 @@ function RegantisChat(props) {
|
|
|
81
435
|
style,
|
|
82
436
|
initialView = 'home',
|
|
83
437
|
onClose,
|
|
438
|
+
onBack,
|
|
84
439
|
onViewChange,
|
|
85
440
|
onMessage,
|
|
86
441
|
onUnreadChange,
|
|
@@ -119,12 +474,18 @@ function RegantisChat(props) {
|
|
|
119
474
|
const [ratingComment, setRatingComment] = React.useState('');
|
|
120
475
|
const [ratingStatus, setRatingStatus] = React.useState('');
|
|
121
476
|
const [busyAction, setBusyAction] = React.useState('');
|
|
477
|
+
const [attachmentError, setAttachmentError] = React.useState('');
|
|
478
|
+
const [cameraSettingsNeeded, setCameraSettingsNeeded] = React.useState(false);
|
|
479
|
+
const [lightboxUri, setLightboxUri] = React.useState('');
|
|
480
|
+
const [logoFailed, setLogoFailed] = React.useState(false);
|
|
122
481
|
const listRef = React.useRef(null);
|
|
482
|
+
const previousStatusRef = React.useRef(null);
|
|
483
|
+
const activeViewRef = React.useRef(initialView === 'chat' ? 'chat' : 'home');
|
|
123
484
|
|
|
124
485
|
React.useEffect(() => {
|
|
125
486
|
const unsubscribe = client.subscribe(next => setState(next));
|
|
126
487
|
client.start().catch(() => {});
|
|
127
|
-
const sub = AppState.addEventListener('change', value => client.setVisible(value === 'active'));
|
|
488
|
+
const sub = AppState.addEventListener('change', value => client.setVisible(value === 'active' && activeViewRef.current === 'chat'));
|
|
128
489
|
return () => {
|
|
129
490
|
if (sub && sub.remove) sub.remove();
|
|
130
491
|
unsubscribe();
|
|
@@ -132,6 +493,15 @@ function RegantisChat(props) {
|
|
|
132
493
|
};
|
|
133
494
|
}, [client]);
|
|
134
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
|
+
|
|
135
505
|
React.useEffect(() => {
|
|
136
506
|
const c = state.conversation || {};
|
|
137
507
|
if (c.contact_name && !contactName) setContactName(c.contact_name);
|
|
@@ -146,7 +516,7 @@ function RegantisChat(props) {
|
|
|
146
516
|
}
|
|
147
517
|
}, [activeView, state.messages.length]);
|
|
148
518
|
|
|
149
|
-
const t = key => state.texts[key] || key;
|
|
519
|
+
const t = key => state.texts[key] || localText(language, key) || key;
|
|
150
520
|
const palette = makePalette(state.settings);
|
|
151
521
|
const styles = React.useMemo(() => buildStyles(palette), [
|
|
152
522
|
palette.primary,
|
|
@@ -162,6 +532,9 @@ function RegantisChat(props) {
|
|
|
162
532
|
palette.agentBubble,
|
|
163
533
|
palette.agentText,
|
|
164
534
|
palette.heroBase,
|
|
535
|
+
palette.heroGlowA,
|
|
536
|
+
palette.heroGlowB,
|
|
537
|
+
palette.heroGlowC,
|
|
165
538
|
]);
|
|
166
539
|
|
|
167
540
|
const c = state.conversation || {};
|
|
@@ -173,9 +546,36 @@ function RegantisChat(props) {
|
|
|
173
546
|
const homePreview = lastSupport
|
|
174
547
|
? String(lastSupport.message || '').trim() || (lastSupport.attachment ? `[${String(lastSupport.message_type || 'file')}]` : t('empty'))
|
|
175
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]);
|
|
176
575
|
|
|
177
576
|
function showView(next) {
|
|
178
577
|
const view = next === 'chat' ? 'chat' : 'home';
|
|
578
|
+
activeViewRef.current = view;
|
|
179
579
|
setActiveView(view);
|
|
180
580
|
setOptionsOpen(false);
|
|
181
581
|
setLanguageOpen(false);
|
|
@@ -214,10 +614,23 @@ function RegantisChat(props) {
|
|
|
214
614
|
|
|
215
615
|
async function pick(kind) {
|
|
216
616
|
setAttachOpen(false);
|
|
617
|
+
setAttachmentError('');
|
|
618
|
+
setCameraSettingsNeeded(false);
|
|
217
619
|
setBusyAction('upload');
|
|
218
620
|
try {
|
|
219
621
|
await client.pickAndUpload(kind);
|
|
220
|
-
|
|
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);
|
|
221
634
|
} finally {
|
|
222
635
|
setBusyAction('');
|
|
223
636
|
}
|
|
@@ -295,8 +708,8 @@ function RegantisChat(props) {
|
|
|
295
708
|
|
|
296
709
|
function renderLogo(size, marginRight) {
|
|
297
710
|
if (!state.settings.show_logo) return null;
|
|
298
|
-
if (state.settings.logo_url) {
|
|
299
|
-
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" />;
|
|
300
713
|
}
|
|
301
714
|
return (
|
|
302
715
|
<View style={[styles.logoFallback, { width: size, height: size, borderRadius: size / 2, marginRight: marginRight || 0 }]}>
|
|
@@ -330,21 +743,35 @@ function RegantisChat(props) {
|
|
|
330
743
|
const textStyle = visitor ? styles.customerText : styles.agentText;
|
|
331
744
|
const attachment = item.attachment || null;
|
|
332
745
|
const isImage = attachment && String(attachment.mime || '').startsWith('image/');
|
|
333
|
-
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';
|
|
334
751
|
|
|
335
752
|
return (
|
|
336
753
|
<View style={[styles.messageRow, visitor ? styles.messageRowRight : styles.messageRowLeft]}>
|
|
337
754
|
<View style={[styles.bubble, bubbleStyle]}>
|
|
338
755
|
{isImage ? (
|
|
339
|
-
<Pressable onPress={() =>
|
|
756
|
+
<Pressable onPress={() => setLightboxUri(attachment.url)}>
|
|
340
757
|
<Image source={{ uri: attachment.url }} style={styles.attachmentImage} resizeMode="cover" />
|
|
341
758
|
</Pressable>
|
|
342
759
|
) : attachment ? (
|
|
343
|
-
<Pressable style={styles.fileAttachment} onPress={() => Linking.openURL(attachment.url)}>
|
|
344
|
-
<
|
|
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>
|
|
345
768
|
</Pressable>
|
|
346
769
|
) : null}
|
|
347
|
-
{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}
|
|
348
775
|
<View style={styles.messageMeta}>
|
|
349
776
|
{!visitor ? <Text numberOfLines={1} style={[styles.messageSender, textStyle]}>{senderName}</Text> : <View />}
|
|
350
777
|
<Text style={[styles.timeText, textStyle]}>{formatTime(item.date_added)}</Text>
|
|
@@ -356,19 +783,31 @@ function RegantisChat(props) {
|
|
|
356
783
|
|
|
357
784
|
function renderPoweredBy() {
|
|
358
785
|
if (state.settings.white_label) return null;
|
|
359
|
-
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
|
+
);
|
|
360
792
|
}
|
|
361
793
|
|
|
362
794
|
function renderHome() {
|
|
363
795
|
return (
|
|
364
796
|
<View style={styles.screen}>
|
|
365
797
|
<View style={styles.homeHero}>
|
|
366
|
-
<
|
|
367
|
-
<
|
|
368
|
-
<
|
|
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" />
|
|
369
801
|
|
|
370
802
|
<View style={styles.homeTop}>
|
|
371
|
-
<View
|
|
803
|
+
<View style={styles.homeTopLeft}>
|
|
804
|
+
{typeof onBack === 'function' || typeof onClose === 'function' ? (
|
|
805
|
+
<Pressable accessibilityRole="button" accessibilityLabel={t('back')} style={styles.iconButton} onPress={typeof onBack === 'function' ? onBack : onClose}>
|
|
806
|
+
<Text style={styles.homeBackIcon}>‹</Text>
|
|
807
|
+
</Pressable>
|
|
808
|
+
) : null}
|
|
809
|
+
<View>{renderLogo(42, 0)}</View>
|
|
810
|
+
</View>
|
|
372
811
|
{typeof onClose === 'function' ? (
|
|
373
812
|
<Pressable accessibilityRole="button" accessibilityLabel={t('minimize')} style={styles.iconButton} onPress={onClose}>
|
|
374
813
|
<Text style={styles.minimizeIcon}>−</Text>
|
|
@@ -385,7 +824,7 @@ function RegantisChat(props) {
|
|
|
385
824
|
<View style={styles.homeCardTop}>
|
|
386
825
|
{state.settings.show_agent_photo ? <View style={styles.homeAvatar}><Text style={styles.homeAvatarText}>R</Text></View> : null}
|
|
387
826
|
<View style={styles.homeCardCopy}>
|
|
388
|
-
<Text numberOfLines={1} style={styles.homeCardAgent}>{
|
|
827
|
+
<Text numberOfLines={1} style={styles.homeCardAgent}>{operatorDisplayName}</Text>
|
|
389
828
|
<Text numberOfLines={1} style={styles.homeCardPreview}>{homePreview}</Text>
|
|
390
829
|
</View>
|
|
391
830
|
</View>
|
|
@@ -420,20 +859,22 @@ function RegantisChat(props) {
|
|
|
420
859
|
<Text style={styles.backIcon}>‹</Text>
|
|
421
860
|
</Pressable>
|
|
422
861
|
<Pressable accessibilityRole="button" accessibilityLabel={t('options')} style={styles.headerIconButton} onPress={() => setOptionsOpen(value => !value)}>
|
|
423
|
-
<
|
|
862
|
+
<View style={styles.menuDots}><View style={styles.menuDot} /><View style={styles.menuDot} /><View style={styles.menuDot} /></View>
|
|
424
863
|
</Pressable>
|
|
425
864
|
</View>
|
|
426
865
|
|
|
427
|
-
<
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
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>
|
|
437
878
|
|
|
438
879
|
<View style={styles.chatHeaderRight}>
|
|
439
880
|
{typeof onClose === 'function' ? (
|
|
@@ -465,7 +906,7 @@ function RegantisChat(props) {
|
|
|
465
906
|
<Text style={styles.popoverChevron}>›</Text>
|
|
466
907
|
</Pressable>
|
|
467
908
|
) : null}
|
|
468
|
-
{state.
|
|
909
|
+
{state.soundAvailable ? (
|
|
469
910
|
<Pressable style={styles.popoverRow} onPress={() => client.setSoundEnabled(!state.settings.sound_enabled)}>
|
|
470
911
|
<Text style={styles.popoverIcon}>◖</Text>
|
|
471
912
|
<Text style={styles.popoverText}>{t('sounds')}</Text>
|
|
@@ -499,6 +940,20 @@ function RegantisChat(props) {
|
|
|
499
940
|
);
|
|
500
941
|
}
|
|
501
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
|
+
|
|
502
957
|
function renderComposer() {
|
|
503
958
|
if (!active) {
|
|
504
959
|
return (
|
|
@@ -517,9 +972,11 @@ function RegantisChat(props) {
|
|
|
517
972
|
|
|
518
973
|
{attachOpen ? (
|
|
519
974
|
<View style={styles.attachmentMenu}>
|
|
975
|
+
{Platform.OS === 'android' || Platform.OS === 'ios' ? (
|
|
976
|
+
<Pressable style={styles.attachmentAction} onPress={() => pick('camera')}><Text style={styles.attachmentActionIcon}>◉</Text><Text style={styles.attachmentActionText}>{t('attach_camera')}</Text></Pressable>
|
|
977
|
+
) : null}
|
|
520
978
|
<Pressable style={styles.attachmentAction} onPress={() => pick('image')}><Text style={styles.attachmentActionIcon}>▣</Text><Text style={styles.attachmentActionText}>{t('attach_image')}</Text></Pressable>
|
|
521
979
|
<Pressable style={styles.attachmentAction} onPress={() => pick('file')}><Text style={styles.attachmentActionIcon}>↥</Text><Text style={styles.attachmentActionText}>{t('attach_file')}</Text></Pressable>
|
|
522
|
-
<Pressable style={styles.attachmentAction} onPress={() => pick('screenshot')}><Text style={styles.attachmentActionIcon}>▧</Text><Text style={styles.attachmentActionText}>{t('attach_screenshot')}</Text></Pressable>
|
|
523
980
|
</View>
|
|
524
981
|
) : null}
|
|
525
982
|
|
|
@@ -533,6 +990,17 @@ function RegantisChat(props) {
|
|
|
533
990
|
</View>
|
|
534
991
|
) : null}
|
|
535
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
|
+
|
|
536
1004
|
<View style={styles.composer}>
|
|
537
1005
|
<Pressable disabled={!canUpload || state.uploading} style={[styles.composerButton, (!canUpload || state.uploading) && styles.disabled]} onPress={() => { setEmojiOpen(false); setAttachOpen(value => !value); }}>
|
|
538
1006
|
<Text style={styles.composerPlus}>+</Text>
|
|
@@ -572,7 +1040,12 @@ function RegantisChat(props) {
|
|
|
572
1040
|
style={styles.messageList}
|
|
573
1041
|
contentContainerStyle={styles.messageListContent}
|
|
574
1042
|
onScrollBeginDrag={() => state.hasMore && client.loadOlder().catch(() => {})}
|
|
575
|
-
ListHeaderComponent={
|
|
1043
|
+
ListHeaderComponent={(
|
|
1044
|
+
<>
|
|
1045
|
+
{state.hasMore ? <View style={styles.historyLoader}><ActivityIndicator size="small" color={palette.primary} /></View> : null}
|
|
1046
|
+
{renderContactInfoCard()}
|
|
1047
|
+
</>
|
|
1048
|
+
)}
|
|
576
1049
|
ListEmptyComponent={<View style={styles.emptyState}><Text style={styles.emptyText}>{t('empty')}</Text></View>}
|
|
577
1050
|
/>
|
|
578
1051
|
{state.remoteTyping ? <View style={styles.typingRow}><Text style={styles.typingText}>{state.remoteTyping === 'chatbot' ? t('chatbot_typing') : t('typing')}</Text></View> : null}
|
|
@@ -648,7 +1121,7 @@ function RegantisChat(props) {
|
|
|
648
1121
|
<Modal transparent visible={ratingOpen} animationType="fade" onRequestClose={() => setRatingOpen(false)}>
|
|
649
1122
|
<View style={styles.modalBackdropTop}>
|
|
650
1123
|
<View style={styles.modalCard}>
|
|
651
|
-
<Text style={styles.modalTitle}>{
|
|
1124
|
+
<Text style={styles.modalTitle}>{operatorDisplayName}</Text>
|
|
652
1125
|
<Text style={styles.ratingLabel}>{t('rating_agent_label')}</Text>
|
|
653
1126
|
<View style={styles.ratingButtons}>
|
|
654
1127
|
<Pressable style={styles.ratingButton} onPress={() => rate(1)}><Text style={styles.ratingIcon}>👍</Text></Pressable>
|
|
@@ -663,6 +1136,16 @@ function RegantisChat(props) {
|
|
|
663
1136
|
</View>
|
|
664
1137
|
</View>
|
|
665
1138
|
</Modal>
|
|
1139
|
+
|
|
1140
|
+
<ImageLightbox
|
|
1141
|
+
visible={!!lightboxUri}
|
|
1142
|
+
uri={lightboxUri}
|
|
1143
|
+
onClose={() => setLightboxUri('')}
|
|
1144
|
+
closeLabel={t('lightbox_close')}
|
|
1145
|
+
resetLabel={t('lightbox_reset')}
|
|
1146
|
+
zoomInLabel={t('lightbox_zoom_in')}
|
|
1147
|
+
zoomOutLabel={t('lightbox_zoom_out')}
|
|
1148
|
+
/>
|
|
666
1149
|
</KeyboardAvoidingView>
|
|
667
1150
|
);
|
|
668
1151
|
}
|
|
@@ -684,10 +1167,13 @@ function buildStyles(p) {
|
|
|
684
1167
|
errorText: { color: p.text, fontSize: 14, textAlign: 'center', marginBottom: 12 },
|
|
685
1168
|
|
|
686
1169
|
homeHero: { position: 'relative', overflow: 'hidden', paddingHorizontal: 18, paddingTop: 24, paddingBottom: 28, backgroundColor: p.heroBase },
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
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 },
|
|
690
1174
|
homeTop: { minHeight: 42, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', zIndex: 2 },
|
|
1175
|
+
homeTopLeft: { flexDirection: 'row', alignItems: 'center', gap: 9 },
|
|
1176
|
+
homeBackIcon: { color: p.text, fontSize: 37, lineHeight: 38, marginTop: -5 },
|
|
691
1177
|
homeTopPlaceholder: { width: 32, height: 32 },
|
|
692
1178
|
logoImage: { backgroundColor: p.surface },
|
|
693
1179
|
logoFallback: { backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center', shadowColor: p.primary, shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.22, shadowRadius: 10, elevation: 4 },
|
|
@@ -714,14 +1200,16 @@ function buildStyles(p) {
|
|
|
714
1200
|
bottomNavText: { marginTop: 2, color: p.muted, fontSize: 10, lineHeight: 12 },
|
|
715
1201
|
bottomNavTextActive: { color: p.text },
|
|
716
1202
|
|
|
717
|
-
chatHeader: { position: 'relative', zIndex: 20, minHeight: 64, flexDirection: 'row', alignItems: '
|
|
718
|
-
chatHeaderLeft: {
|
|
719
|
-
|
|
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 },
|
|
720
1207
|
headerIconButton: { width: 32, height: 32, borderRadius: 16, backgroundColor: p.dark ? 'rgba(255,255,255,0.08)' : '#FFFFFF', alignItems: 'center', justifyContent: 'center' },
|
|
721
|
-
backIcon: { color: p.text, fontSize: 31, lineHeight: 31,
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
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 },
|
|
725
1213
|
agentVisual: { height: 30, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
|
|
726
1214
|
agentAvatar: { width: 28, height: 28, borderRadius: 14, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
|
|
727
1215
|
agentAvatarOverlap: { marginLeft: -8, borderWidth: 2, borderColor: p.surface },
|
|
@@ -755,6 +1243,13 @@ function buildStyles(p) {
|
|
|
755
1243
|
dangerButton: { flex: 1, minHeight: 40, borderRadius: 9, backgroundColor: p.danger, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center' },
|
|
756
1244
|
disabled: { opacity: 0.55 },
|
|
757
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' },
|
|
758
1253
|
messageList: { flex: 1, minHeight: 0 },
|
|
759
1254
|
messageListContent: { paddingHorizontal: 16, paddingTop: 18, paddingBottom: 12, flexGrow: 1 },
|
|
760
1255
|
historyLoader: { minHeight: 34, alignItems: 'center', justifyContent: 'center', paddingBottom: 10 },
|
|
@@ -775,8 +1270,26 @@ function buildStyles(p) {
|
|
|
775
1270
|
systemRow: { alignSelf: 'center', paddingHorizontal: 16, paddingVertical: 7, marginVertical: 3 },
|
|
776
1271
|
systemText: { color: p.muted, fontSize: 11, lineHeight: 15, textAlign: 'center' },
|
|
777
1272
|
attachmentImage: { width: 220, maxWidth: '100%', height: 180, borderRadius: 10, marginBottom: 4, backgroundColor: p.surfaceAlt },
|
|
778
|
-
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 },
|
|
779
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 },
|
|
780
1293
|
typingRow: { flex: 0, paddingHorizontal: 18, paddingTop: 8, paddingBottom: 7 },
|
|
781
1294
|
typingText: { color: p.muted, fontSize: 11, lineHeight: 15 },
|
|
782
1295
|
|
|
@@ -791,6 +1304,10 @@ function buildStyles(p) {
|
|
|
791
1304
|
sendButton: { width: 38, height: 38, borderRadius: 19, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
|
|
792
1305
|
sendIcon: { color: '#FFFFFF', fontSize: 17, lineHeight: 18, transform: [{ rotate: '-3deg' }] },
|
|
793
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' },
|
|
794
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 },
|
|
795
1312
|
attachmentAction: { minHeight: 40, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, borderRadius: 9 },
|
|
796
1313
|
attachmentActionIcon: { width: 26, color: p.text, fontSize: 15 },
|
|
@@ -799,9 +1316,9 @@ function buildStyles(p) {
|
|
|
799
1316
|
emojiButton: { width: '11%', aspectRatio: 1, minHeight: 34, borderRadius: 8, alignItems: 'center', justifyContent: 'center' },
|
|
800
1317
|
emojiText: { fontSize: 20, lineHeight: 22 },
|
|
801
1318
|
|
|
802
|
-
poweredBy: { flex: 0, minHeight:
|
|
1319
|
+
poweredBy: { flex: 0, minHeight: 22, flexDirection: 'row', alignItems: 'center', justifyContent: 'center', gap: 4, paddingBottom: 5, backgroundColor: p.background },
|
|
803
1320
|
poweredByText: { color: '#9398A2', fontSize: 9, lineHeight: 11, textAlign: 'center' },
|
|
804
|
-
|
|
1321
|
+
poweredByLogo: { width: 48, height: 15 },
|
|
805
1322
|
|
|
806
1323
|
modalBackdrop: { flex: 1, backgroundColor: 'rgba(16,20,27,0.55)', justifyContent: 'flex-end', padding: 16 },
|
|
807
1324
|
modalBackdropTop: { flex: 1, backgroundColor: 'rgba(16,20,27,0.55)', justifyContent: 'flex-start', padding: 16 },
|