@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.
@@ -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 primary = settings.color_mode === 'advanced' ? settings.primary_color : settings.theme_color;
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(settings.chat_background, settings.chat_background_opacity),
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 || (dark ? '#A7ADB7' : '#6F7480'),
47
- customerBubble: settings.color_mode === 'advanced' ? settings.customer_bubble : primary,
48
- customerText: settings.color_mode === 'advanced' ? settings.customer_text : '#FFFFFF',
49
- agentBubble: settings.color_mode === 'advanced' ? settings.agent_bubble : (dark ? '#22252A' : '#FFFFFF'),
50
- agentText: settings.color_mode === 'advanced' ? settings.agent_text : (dark ? '#F7F8FA' : '#17181B'),
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' : '#EEF7FC',
53
- heroBlobA: dark ? 'rgba(73,83,152,0.25)' : 'rgba(118,137,255,0.30)',
54
- heroBlobB: dark ? 'rgba(34,126,168,0.22)' : 'rgba(96,222,215,0.28)',
55
- heroBlobC: dark ? 'rgba(255,255,255,0.035)' : 'rgba(255,255,255,0.52)',
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
- } catch (_) {
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 senderName = sender === 'chatbot' ? 'Chatbot' : (item.sender_name || state.settings.operator_name || t('support_name'));
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={() => Linking.openURL(attachment.url)}>
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
- <Text style={[styles.fileAttachmentText, textStyle]}>📎 {attachment.name || t('attach_file')}</Text>
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 ? <Text style={[styles.messageText, textStyle]}>{item.message}</Text> : null}
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 <View style={styles.poweredBy}><Text style={styles.poweredByText}>{t('powered_by')} <Text style={styles.poweredByBrand}>Regantis</Text></Text></View>;
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
- <View pointerEvents="none" style={styles.heroDecorationA} />
367
- <View pointerEvents="none" style={styles.heroDecorationB} />
368
- <View pointerEvents="none" style={styles.heroDecorationC} />
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>{renderLogo(42, 0)}</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}>{state.settings.operator_name || t('support_name')}</Text>
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
- <Text style={styles.menuIcon}>•••</Text>
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
- <Pressable
428
- accessibilityRole="button"
429
- accessibilityLabel={state.settings.operator_name || t('support_name')}
430
- style={styles.agentTrigger}
431
- onPress={() => {
432
- if (state.settings.rating_enabled && contactConfirmed) setRatingOpen(true);
433
- }}
434
- >
435
- {renderAgentVisual()}
436
- </Pressable>
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.settings.sound_enabled !== undefined ? (
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={state.hasMore ? <View style={styles.historyLoader}><ActivityIndicator size="small" color={palette.primary} /></View> : null}
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}>{state.settings.operator_name || t('support_name')}</Text>
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
- heroDecorationA: { position: 'absolute', width: 240, height: 240, borderRadius: 120, top: -138, left: -74, backgroundColor: p.heroBlobA },
688
- heroDecorationB: { position: 'absolute', width: 230, height: 230, borderRadius: 115, top: 80, right: -92, backgroundColor: p.heroBlobB },
689
- heroDecorationC: { position: 'absolute', width: 210, height: 210, borderRadius: 105, right: -82, bottom: -120, backgroundColor: p.heroBlobC },
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: 'center', justifyContent: 'space-between', paddingHorizontal: 12, borderBottomWidth: 1, borderBottomColor: p.border, backgroundColor: p.header, overflow: 'visible' },
718
- chatHeaderLeft: { position: 'absolute', zIndex: 30, left: 12, top: 16, flexDirection: 'row', alignItems: 'center', gap: 5 },
719
- chatHeaderRight: { position: 'absolute', zIndex: 30, right: 12, top: 16, flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 5 },
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, marginTop: -4 },
722
- menuIcon: { color: p.text, fontSize: 17, lineHeight: 18, letterSpacing: -1.5, fontWeight: '700', marginTop: -4 },
723
- closeIcon: { color: p.text, fontSize: 23, lineHeight: 24, marginTop: -2 },
724
- agentTrigger: { position: 'absolute', zIndex: 18, top: 0, left: '50%', width: 218, height: 48, marginLeft: -109, 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 },
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: 17, alignItems: 'center', justifyContent: 'center', paddingBottom: 5, backgroundColor: p.background },
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
- poweredByBrand: { color: p.text, fontWeight: '700' },
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 },