@regantis-sdk/react-native-chat 0.1.1 โ†’ 0.1.3

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,12 +3,14 @@
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,
14
16
  ScrollView,
@@ -19,7 +21,171 @@ const {
19
21
  } = require('react-native');
20
22
  const { RegantisChatClient } = require('./client');
21
23
 
22
- const EMOJIS = ['๐Ÿ™‚', '๐Ÿ˜', '๐Ÿ˜‚', '๐Ÿ˜Š', '๐Ÿ˜', '๐Ÿค”', '๐Ÿ˜ž', '๐Ÿ˜ข', '๐ŸŽ‰', 'โค๏ธ', '๐Ÿ‘Œ', '๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿ™'];
24
+ const EMOJIS = ['๐Ÿ™‚', '๐Ÿ˜', '๐Ÿ˜‚', '๐Ÿ˜Š', '๐Ÿ˜', '๐Ÿ˜', '๐Ÿค”', '๐Ÿ˜ž', '๐Ÿ˜ข', '๐Ÿ˜ญ', '๐ŸŽ‰', 'โค๏ธ', '๐Ÿ‘Œ', '๐Ÿ‘', '๐Ÿ‘Ž', '๐Ÿ™'];
25
+
26
+
27
+ const LOCAL_TEXTS = {
28
+ en: { attach_camera: 'Take photo', lightbox_close: 'Close', lightbox_reset: 'Reset zoom', back: 'Back' },
29
+ ro: { attach_camera: 'Fฤƒ o fotografie', lightbox_close: 'รŽnchide', lightbox_reset: 'Reseteazฤƒ zoom', back: 'รŽnapoi' },
30
+ de: { attach_camera: 'Foto aufnehmen', lightbox_close: 'SchlieรŸen', lightbox_reset: 'Zoom zurรผcksetzen', back: 'Zurรผck' },
31
+ fr: { attach_camera: 'Prendre une photo', lightbox_close: 'Fermer', lightbox_reset: 'Rรฉinitialiser le zoom', back: 'Retour' },
32
+ es: { attach_camera: 'Tomar una foto', lightbox_close: 'Cerrar', lightbox_reset: 'Restablecer zoom', back: 'Atrรกs' },
33
+ it: { attach_camera: 'Scatta una foto', lightbox_close: 'Chiudi', lightbox_reset: 'Reimposta zoom', back: 'Indietro' },
34
+ };
35
+
36
+ function localText(language, key) {
37
+ const code = String(language || 'en').trim().toLowerCase().split(/[-_]/)[0];
38
+ return (LOCAL_TEXTS[code] && LOCAL_TEXTS[code][key]) || LOCAL_TEXTS.en[key] || '';
39
+ }
40
+
41
+ function touchDistance(touches) {
42
+ if (!touches || touches.length < 2) return 0;
43
+ const dx = Number(touches[0].pageX || 0) - Number(touches[1].pageX || 0);
44
+ const dy = Number(touches[0].pageY || 0) - Number(touches[1].pageY || 0);
45
+ return Math.sqrt(dx * dx + dy * dy);
46
+ }
47
+
48
+ function ImageLightbox({ visible, uri, onClose, closeLabel, resetLabel }) {
49
+ const scale = React.useRef(new Animated.Value(1)).current;
50
+ const translateX = React.useRef(new Animated.Value(0)).current;
51
+ const translateY = React.useRef(new Animated.Value(0)).current;
52
+ const current = React.useRef({ scale: 1, x: 0, y: 0 });
53
+ const gesture = React.useRef({ mode: '', distance: 0, scale: 1, x: 0, y: 0, startX: 0, startY: 0 });
54
+
55
+ const resetZoom = React.useCallback((animated = true) => {
56
+ current.current = { scale: 1, x: 0, y: 0 };
57
+ gesture.current.mode = '';
58
+ const values = [
59
+ Animated.spring(scale, { toValue: 1, useNativeDriver: true, friction: 8, tension: 70 }),
60
+ Animated.spring(translateX, { toValue: 0, useNativeDriver: true, friction: 8, tension: 70 }),
61
+ Animated.spring(translateY, { toValue: 0, useNativeDriver: true, friction: 8, tension: 70 }),
62
+ ];
63
+ if (animated) Animated.parallel(values).start();
64
+ else {
65
+ scale.setValue(1);
66
+ translateX.setValue(0);
67
+ translateY.setValue(0);
68
+ }
69
+ }, [scale, translateX, translateY]);
70
+
71
+ React.useEffect(() => {
72
+ if (visible) resetZoom(false);
73
+ }, [visible, uri, resetZoom]);
74
+
75
+ const panResponder = React.useMemo(() => PanResponder.create({
76
+ onStartShouldSetPanResponder: () => true,
77
+ onMoveShouldSetPanResponder: () => true,
78
+ onPanResponderGrant: event => {
79
+ const touches = event.nativeEvent.touches || [];
80
+ if (touches.length >= 2) {
81
+ gesture.current = {
82
+ mode: 'pinch',
83
+ distance: Math.max(1, touchDistance(touches)),
84
+ scale: current.current.scale,
85
+ x: current.current.x,
86
+ y: current.current.y,
87
+ startX: 0,
88
+ startY: 0,
89
+ };
90
+ } else if (touches.length === 1) {
91
+ gesture.current = {
92
+ mode: 'pan',
93
+ distance: 0,
94
+ scale: current.current.scale,
95
+ x: current.current.x,
96
+ y: current.current.y,
97
+ startX: Number(touches[0].pageX || 0),
98
+ startY: Number(touches[0].pageY || 0),
99
+ };
100
+ }
101
+ },
102
+ onPanResponderMove: event => {
103
+ const touches = event.nativeEvent.touches || [];
104
+ if (touches.length >= 2) {
105
+ const distance = Math.max(1, touchDistance(touches));
106
+ if (gesture.current.mode !== 'pinch') {
107
+ gesture.current.mode = 'pinch';
108
+ gesture.current.distance = distance;
109
+ gesture.current.scale = current.current.scale;
110
+ }
111
+ const nextScale = Math.max(1, Math.min(5, gesture.current.scale * (distance / Math.max(1, gesture.current.distance))));
112
+ current.current.scale = nextScale;
113
+ scale.setValue(nextScale);
114
+ if (nextScale <= 1.01) {
115
+ current.current.x = 0;
116
+ current.current.y = 0;
117
+ translateX.setValue(0);
118
+ translateY.setValue(0);
119
+ }
120
+ return;
121
+ }
122
+
123
+ if (touches.length === 1 && current.current.scale > 1.01) {
124
+ const x = Number(touches[0].pageX || 0);
125
+ const y = Number(touches[0].pageY || 0);
126
+ if (gesture.current.mode !== 'pan') {
127
+ gesture.current.mode = 'pan';
128
+ gesture.current.x = current.current.x;
129
+ gesture.current.y = current.current.y;
130
+ gesture.current.startX = x;
131
+ gesture.current.startY = y;
132
+ }
133
+ const nextX = gesture.current.x + (x - gesture.current.startX);
134
+ const nextY = gesture.current.y + (y - gesture.current.startY);
135
+ current.current.x = nextX;
136
+ current.current.y = nextY;
137
+ translateX.setValue(nextX);
138
+ translateY.setValue(nextY);
139
+ }
140
+ },
141
+ onPanResponderRelease: () => {
142
+ gesture.current.mode = '';
143
+ if (current.current.scale <= 1.05) resetZoom(true);
144
+ },
145
+ onPanResponderTerminate: () => {
146
+ gesture.current.mode = '';
147
+ if (current.current.scale <= 1.05) resetZoom(true);
148
+ },
149
+ }), [resetZoom, scale, translateX, translateY]);
150
+
151
+ return (
152
+ <Modal visible={visible} transparent animationType="fade" onRequestClose={onClose} statusBarTranslucent>
153
+ <View style={lightboxStyles.root}>
154
+ <View style={lightboxStyles.toolbar}>
155
+ <Pressable accessibilityRole="button" accessibilityLabel={closeLabel} style={lightboxStyles.toolbarButton} onPress={onClose}>
156
+ <Text style={lightboxStyles.closeIcon}>โ€น</Text>
157
+ </Pressable>
158
+ <Pressable accessibilityRole="button" accessibilityLabel={resetLabel} style={lightboxStyles.resetButton} onPress={() => resetZoom(true)}>
159
+ <Text style={lightboxStyles.resetText}>1:1</Text>
160
+ </Pressable>
161
+ </View>
162
+ <View style={lightboxStyles.stage} {...panResponder.panHandlers}>
163
+ {uri ? (
164
+ <Animated.Image
165
+ source={{ uri }}
166
+ resizeMode="contain"
167
+ style={[
168
+ lightboxStyles.image,
169
+ { transform: [{ translateX }, { translateY }, { scale }] },
170
+ ]}
171
+ />
172
+ ) : null}
173
+ </View>
174
+ </View>
175
+ </Modal>
176
+ );
177
+ }
178
+
179
+ const lightboxStyles = StyleSheet.create({
180
+ root: { flex: 1, backgroundColor: 'rgba(0,0,0,0.96)' },
181
+ toolbar: { position: 'absolute', zIndex: 20, top: Platform.OS === 'ios' ? 48 : 18, left: 14, right: 14, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
182
+ toolbarButton: { width: 44, height: 44, borderRadius: 22, backgroundColor: 'rgba(255,255,255,0.14)', alignItems: 'center', justifyContent: 'center' },
183
+ closeIcon: { color: '#FFFFFF', fontSize: 39, lineHeight: 40, marginTop: -6 },
184
+ resetButton: { minWidth: 50, height: 40, paddingHorizontal: 12, borderRadius: 20, backgroundColor: 'rgba(255,255,255,0.14)', alignItems: 'center', justifyContent: 'center' },
185
+ resetText: { color: '#FFFFFF', fontSize: 13, fontWeight: '700' },
186
+ stage: { flex: 1, overflow: 'hidden', alignItems: 'center', justifyContent: 'center' },
187
+ image: { width: '100%', height: '100%' },
188
+ });
23
189
 
24
190
  function colorWithOpacity(hex, opacity) {
25
191
  const value = String(hex || '#F5F7FB').replace('#', '');
@@ -35,18 +201,24 @@ function makePalette(settings) {
35
201
  const dark = settings.theme_mode === 'dark';
36
202
  const primary = settings.color_mode === 'advanced' ? settings.primary_color : settings.theme_color;
37
203
  return {
204
+ dark,
38
205
  primary,
39
206
  background: colorWithOpacity(settings.chat_background, settings.chat_background_opacity),
40
- surface: dark ? '#17181B' : '#FFFFFF',
41
- surfaceAlt: dark ? '#22252A' : '#EEF1F5',
42
- border: dark ? '#30343B' : '#E0E4EA',
207
+ surface: dark ? '#1D2127' : '#FFFFFF',
208
+ surfaceAlt: dark ? '#252A31' : '#F5F7FB',
209
+ header: dark ? '#181B20' : '#F8F9FC',
210
+ border: dark ? 'rgba(255,255,255,0.10)' : 'rgba(20,32,54,0.10)',
43
211
  text: dark ? '#F7F8FA' : '#17181B',
44
212
  muted: settings.system_text || (dark ? '#A7ADB7' : '#6F7480'),
45
213
  customerBubble: settings.color_mode === 'advanced' ? settings.customer_bubble : primary,
46
214
  customerText: settings.color_mode === 'advanced' ? settings.customer_text : '#FFFFFF',
47
215
  agentBubble: settings.color_mode === 'advanced' ? settings.agent_bubble : (dark ? '#22252A' : '#FFFFFF'),
48
216
  agentText: settings.color_mode === 'advanced' ? settings.agent_text : (dark ? '#F7F8FA' : '#17181B'),
49
- danger: '#C93A3A',
217
+ danger: '#DF2917',
218
+ heroBase: dark ? '#151B21' : '#EEF7FC',
219
+ heroBlobA: dark ? 'rgba(73,83,152,0.25)' : 'rgba(118,137,255,0.30)',
220
+ heroBlobB: dark ? 'rgba(34,126,168,0.22)' : 'rgba(96,222,215,0.28)',
221
+ heroBlobC: dark ? 'rgba(255,255,255,0.035)' : 'rgba(255,255,255,0.52)',
50
222
  };
51
223
  }
52
224
 
@@ -57,6 +229,11 @@ function formatTime(value) {
57
229
  return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
58
230
  }
59
231
 
232
+ function isSupportMessage(message) {
233
+ const sender = String(message && message.sender_type || '').toLowerCase();
234
+ return sender === 'agent' || sender === 'admin' || sender === 'chatbot';
235
+ }
236
+
60
237
  function RegantisChat(props) {
61
238
  const {
62
239
  apiKey,
@@ -68,6 +245,10 @@ function RegantisChat(props) {
68
245
  texts,
69
246
  screen = 'chat',
70
247
  style,
248
+ initialView = 'home',
249
+ onClose,
250
+ onBack,
251
+ onViewChange,
71
252
  onMessage,
72
253
  onUnreadChange,
73
254
  onError,
@@ -88,11 +269,13 @@ function RegantisChat(props) {
88
269
  }), [apiKey, serverUrl, language, translationLanguage, customer, theme, texts, screen, onMessage, onUnreadChange, onError]);
89
270
 
90
271
  const [state, setState] = React.useState(client.getState());
272
+ const [activeView, setActiveView] = React.useState(initialView === 'chat' ? 'chat' : 'home');
91
273
  const [draft, setDraft] = React.useState('');
92
274
  const [contactName, setContactName] = React.useState(customer && customer.name || '');
93
275
  const [contactEmail, setContactEmail] = React.useState(customer && customer.email || '');
94
276
  const [contactError, setContactError] = React.useState('');
95
277
  const [optionsOpen, setOptionsOpen] = React.useState(false);
278
+ const [languageOpen, setLanguageOpen] = React.useState(false);
96
279
  const [attachOpen, setAttachOpen] = React.useState(false);
97
280
  const [emojiOpen, setEmojiOpen] = React.useState(false);
98
281
  const [closeOpen, setCloseOpen] = React.useState(false);
@@ -103,6 +286,7 @@ function RegantisChat(props) {
103
286
  const [ratingComment, setRatingComment] = React.useState('');
104
287
  const [ratingStatus, setRatingStatus] = React.useState('');
105
288
  const [busyAction, setBusyAction] = React.useState('');
289
+ const [lightboxUri, setLightboxUri] = React.useState('');
106
290
  const listRef = React.useRef(null);
107
291
 
108
292
  React.useEffect(() => {
@@ -124,19 +308,20 @@ function RegantisChat(props) {
124
308
  }, [state.conversation, contactName, contactEmail, transcriptEmail]);
125
309
 
126
310
  React.useEffect(() => {
127
- if (state.messages.length) {
311
+ if (activeView === 'chat' && state.messages.length) {
128
312
  const timer = setTimeout(() => listRef.current && listRef.current.scrollToEnd({ animated: true }), 50);
129
313
  return () => clearTimeout(timer);
130
314
  }
131
- }, [state.messages.length]);
315
+ }, [activeView, state.messages.length]);
132
316
 
133
- const t = key => state.texts[key] || key;
317
+ const t = key => state.texts[key] || localText(language, key) || key;
134
318
  const palette = makePalette(state.settings);
135
319
  const styles = React.useMemo(() => buildStyles(palette), [
136
320
  palette.primary,
137
321
  palette.background,
138
322
  palette.surface,
139
323
  palette.surfaceAlt,
324
+ palette.header,
140
325
  palette.border,
141
326
  palette.text,
142
327
  palette.muted,
@@ -144,8 +329,29 @@ function RegantisChat(props) {
144
329
  palette.customerText,
145
330
  palette.agentBubble,
146
331
  palette.agentText,
332
+ palette.heroBase,
147
333
  ]);
148
334
 
335
+ const c = state.conversation || {};
336
+ const active = Number(c.status || 0) === 1;
337
+ const contactConfirmed = Number(c.contact_confirmed || 0) === 1;
338
+ const canUpload = !!(state.upload && state.upload.enabled);
339
+ const hasExisting = state.messages.length > 0 || contactConfirmed;
340
+ const lastSupport = [...state.messages].reverse().find(isSupportMessage);
341
+ const homePreview = lastSupport
342
+ ? String(lastSupport.message || '').trim() || (lastSupport.attachment ? `[${String(lastSupport.message_type || 'file')}]` : t('empty'))
343
+ : t('empty');
344
+
345
+ function showView(next) {
346
+ const view = next === 'chat' ? 'chat' : 'home';
347
+ setActiveView(view);
348
+ setOptionsOpen(false);
349
+ setLanguageOpen(false);
350
+ setAttachOpen(false);
351
+ setEmojiOpen(false);
352
+ if (typeof onViewChange === 'function') onViewChange(view);
353
+ }
354
+
149
355
  async function submitContact() {
150
356
  setContactError('');
151
357
  if (!contactName.trim() || !/^\S+@\S+\.\S+$/.test(contactEmail.trim())) {
@@ -189,6 +395,7 @@ function RegantisChat(props) {
189
395
  setBusyAction('language');
190
396
  try {
191
397
  await client.setTranslationLanguage(code);
398
+ setLanguageOpen(false);
192
399
  setOptionsOpen(false);
193
400
  } finally {
194
401
  setBusyAction('');
@@ -254,6 +461,30 @@ function RegantisChat(props) {
254
461
  }
255
462
  }
256
463
 
464
+ function renderLogo(size, marginRight) {
465
+ if (!state.settings.show_logo) return null;
466
+ if (state.settings.logo_url) {
467
+ return <Image source={{ uri: state.settings.logo_url }} style={[styles.logoImage, { width: size, height: size, borderRadius: size / 2, marginRight: marginRight || 0 }]} resizeMode="cover" />;
468
+ }
469
+ return (
470
+ <View style={[styles.logoFallback, { width: size, height: size, borderRadius: size / 2, marginRight: marginRight || 0 }]}>
471
+ <Text style={[styles.logoFallbackText, { fontSize: Math.max(12, Math.round(size * 0.38)) }]}>R</Text>
472
+ </View>
473
+ );
474
+ }
475
+
476
+ function renderAgentVisual() {
477
+ const showLogo = !!state.settings.show_logo;
478
+ const showAgent = !!state.settings.show_agent_photo;
479
+ return (
480
+ <View style={styles.agentVisual}>
481
+ {showLogo ? renderLogo(28, 0) : null}
482
+ {showAgent ? <View style={[styles.agentAvatar, showLogo && styles.agentAvatarOverlap]}><Text style={styles.agentAvatarText}>R</Text></View> : null}
483
+ {!showLogo && !showAgent ? <View style={styles.agentFallback}><Text style={styles.agentFallbackText}>โ—</Text></View> : null}
484
+ </View>
485
+ );
486
+ }
487
+
257
488
  function renderMessage({ item }) {
258
489
  const sender = String(item.sender_type || '').toLowerCase();
259
490
  const system = sender === 'system' || String(item.message_type || '').startsWith('routing_') || String(item.message_type || '').startsWith('rating_');
@@ -267,66 +498,168 @@ function RegantisChat(props) {
267
498
  const textStyle = visitor ? styles.customerText : styles.agentText;
268
499
  const attachment = item.attachment || null;
269
500
  const isImage = attachment && String(attachment.mime || '').startsWith('image/');
501
+ const senderName = sender === 'chatbot' ? 'Chatbot' : (item.sender_name || state.settings.operator_name || t('support_name'));
270
502
 
271
503
  return (
272
504
  <View style={[styles.messageRow, visitor ? styles.messageRowRight : styles.messageRowLeft]}>
273
- {!visitor && state.settings.show_agent_photo ? <View style={styles.avatar}><Text style={styles.avatarText}>{String(item.sender_name || state.settings.operator_name || 'S').slice(0, 1).toUpperCase()}</Text></View> : null}
274
505
  <View style={[styles.bubble, bubbleStyle]}>
275
- {!visitor && item.sender_name ? <Text style={[styles.senderName, textStyle]}>{item.sender_name}</Text> : null}
276
506
  {isImage ? (
277
- <Pressable onPress={() => Linking.openURL(attachment.url)}>
507
+ <Pressable onPress={() => setLightboxUri(attachment.url)}>
278
508
  <Image source={{ uri: attachment.url }} style={styles.attachmentImage} resizeMode="cover" />
279
509
  </Pressable>
280
510
  ) : attachment ? (
281
511
  <Pressable style={styles.fileAttachment} onPress={() => Linking.openURL(attachment.url)}>
282
- <Text style={textStyle}>๐Ÿ“Ž {attachment.name || t('attach_file')}</Text>
512
+ <Text style={[styles.fileAttachmentText, textStyle]}>๐Ÿ“Ž {attachment.name || t('attach_file')}</Text>
283
513
  </Pressable>
284
514
  ) : null}
285
515
  {item.message ? <Text style={[styles.messageText, textStyle]}>{item.message}</Text> : null}
286
- <Text style={[styles.timeText, textStyle]}>{formatTime(item.date_added)}</Text>
516
+ <View style={styles.messageMeta}>
517
+ {!visitor ? <Text numberOfLines={1} style={[styles.messageSender, textStyle]}>{senderName}</Text> : <View />}
518
+ <Text style={[styles.timeText, textStyle]}>{formatTime(item.date_added)}</Text>
519
+ </View>
287
520
  </View>
288
521
  </View>
289
522
  );
290
523
  }
291
524
 
292
- if (state.loading) {
293
- return <View style={[styles.root, styles.center, style]}><ActivityIndicator size="large" color={palette.primary} /><Text style={styles.loadingText}>{t('loading')}</Text></View>;
525
+ function renderPoweredBy() {
526
+ if (state.settings.white_label) return null;
527
+ return <View style={styles.poweredBy}><Text style={styles.poweredByText}>{t('powered_by')} <Text style={styles.poweredByBrand}>Regantis</Text></Text></View>;
294
528
  }
295
529
 
296
- if (state.error) {
530
+ function renderHome() {
297
531
  return (
298
- <View style={[styles.root, styles.center, style]}>
299
- <Text style={styles.errorText}>{t('error')}</Text>
300
- <Pressable style={styles.primaryButton} onPress={() => client.start().catch(() => {})}><Text style={styles.primaryButtonText}>{t('restart_chat')}</Text></Pressable>
532
+ <View style={styles.screen}>
533
+ <View style={styles.homeHero}>
534
+ <View pointerEvents="none" style={styles.heroDecorationA} />
535
+ <View pointerEvents="none" style={styles.heroDecorationB} />
536
+ <View pointerEvents="none" style={styles.heroDecorationC} />
537
+
538
+ <View style={styles.homeTop}>
539
+ <View style={styles.homeTopLeft}>
540
+ {typeof onBack === 'function' || typeof onClose === 'function' ? (
541
+ <Pressable accessibilityRole="button" accessibilityLabel={t('back')} style={styles.iconButton} onPress={typeof onBack === 'function' ? onBack : onClose}>
542
+ <Text style={styles.homeBackIcon}>โ€น</Text>
543
+ </Pressable>
544
+ ) : null}
545
+ <View>{renderLogo(42, 0)}</View>
546
+ </View>
547
+ {typeof onClose === 'function' ? (
548
+ <Pressable accessibilityRole="button" accessibilityLabel={t('minimize')} style={styles.iconButton} onPress={onClose}>
549
+ <Text style={styles.minimizeIcon}>โˆ’</Text>
550
+ </Pressable>
551
+ ) : <View style={styles.homeTopPlaceholder} />}
552
+ </View>
553
+
554
+ <View style={styles.homeTitle}>
555
+ <Text style={styles.homeTitleLine}>{t('welcome_title')}</Text>
556
+ <Text style={styles.homeTitleLine}>{t('welcome_text')}</Text>
557
+ </View>
558
+
559
+ <Pressable style={styles.homeCard} onPress={() => showView('chat')}>
560
+ <View style={styles.homeCardTop}>
561
+ {state.settings.show_agent_photo ? <View style={styles.homeAvatar}><Text style={styles.homeAvatarText}>R</Text></View> : null}
562
+ <View style={styles.homeCardCopy}>
563
+ <Text numberOfLines={1} style={styles.homeCardAgent}>{state.settings.operator_name || t('support_name')}</Text>
564
+ <Text numberOfLines={1} style={styles.homeCardPreview}>{homePreview}</Text>
565
+ </View>
566
+ </View>
567
+ <View style={styles.homeCardAction}>
568
+ <Text style={styles.homeCardActionText}>{hasExisting ? t('back_to_chat') : t('lets_chat')}</Text>
569
+ </View>
570
+ </Pressable>
571
+ </View>
572
+
573
+ <View style={styles.homeSpacer} />
574
+
575
+ <View style={styles.bottomNav}>
576
+ <Pressable style={[styles.bottomNavButton, styles.bottomNavButtonActive]} onPress={() => showView('home')}>
577
+ <Text style={[styles.bottomNavIcon, styles.bottomNavIconActive]}>โŒ‚</Text>
578
+ <Text style={[styles.bottomNavText, styles.bottomNavTextActive]}>{t('home')}</Text>
579
+ </Pressable>
580
+ <Pressable style={styles.bottomNavButton} onPress={() => showView('chat')}>
581
+ <Text style={styles.bottomNavIcon}>โ—Œ</Text>
582
+ <Text style={styles.bottomNavText}>{t('chat')}</Text>
583
+ </Pressable>
584
+ </View>
585
+ {renderPoweredBy()}
301
586
  </View>
302
587
  );
303
588
  }
304
589
 
305
- if (state.banned) return <View style={[styles.root, styles.center, style]}><Text style={styles.errorText}>{t('error')}</Text></View>;
590
+ function renderChatHeader() {
591
+ return (
592
+ <View style={styles.chatHeader}>
593
+ <View style={styles.chatHeaderLeft}>
594
+ <Pressable accessibilityRole="button" accessibilityLabel={t('back')} style={styles.headerIconButton} onPress={() => showView('home')}>
595
+ <Text style={styles.backIcon}>โ€น</Text>
596
+ </Pressable>
597
+ <Pressable accessibilityRole="button" accessibilityLabel={t('options')} style={styles.headerIconButton} onPress={() => setOptionsOpen(value => !value)}>
598
+ <Text style={styles.menuIcon}>โ€ขโ€ขโ€ข</Text>
599
+ </Pressable>
600
+ </View>
306
601
 
307
- const c = state.conversation || {};
308
- const active = Number(c.status || 0) === 1;
309
- const contactConfirmed = Number(c.contact_confirmed || 0) === 1;
310
- const canUpload = !!(state.upload && state.upload.enabled);
602
+ <Pressable
603
+ accessibilityRole="button"
604
+ accessibilityLabel={state.settings.operator_name || t('support_name')}
605
+ style={styles.agentTrigger}
606
+ onPress={() => {
607
+ if (state.settings.rating_enabled && contactConfirmed) setRatingOpen(true);
608
+ }}
609
+ >
610
+ {renderAgentVisual()}
611
+ </Pressable>
311
612
 
312
- return (
313
- <KeyboardAvoidingView style={[styles.root, style]} behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={props.keyboardVerticalOffset || 0}>
314
- <View style={styles.header}>
315
- <View style={styles.headerBrand}>
316
- {state.settings.show_logo && state.settings.logo_url ? <Image source={{ uri: state.settings.logo_url }} style={styles.logo} resizeMode="contain" /> : <View style={styles.logoFallback}><Text style={styles.logoFallbackText}>R</Text></View>}
317
- <View style={styles.headerCopy}>
318
- <Text numberOfLines={1} style={styles.headerTitle}>{state.settings.operator_name || t('support_name')}</Text>
319
- <Text numberOfLines={1} style={styles.headerStatus}>{t('welcome_text')}</Text>
320
- </View>
321
- </View>
322
- <View style={styles.headerActions}>
323
- <Pressable style={styles.iconButton} onPress={() => setOptionsOpen(true)}><Text style={styles.iconText}>โ‹ฏ</Text></Pressable>
324
- {active && state.conversation.conversation_id ? <Pressable style={styles.iconButton} onPress={() => setCloseOpen(true)}><Text style={styles.iconText}>ร—</Text></Pressable> : null}
613
+ <View style={styles.chatHeaderRight}>
614
+ {typeof onClose === 'function' ? (
615
+ <Pressable accessibilityRole="button" accessibilityLabel={t('minimize')} style={styles.headerIconButton} onPress={onClose}>
616
+ <Text style={styles.minimizeIcon}>โˆ’</Text>
617
+ </Pressable>
618
+ ) : null}
619
+ {active && Number(c.conversation_id || 0) > 0 ? (
620
+ <Pressable accessibilityRole="button" accessibilityLabel={t('close')} style={styles.headerIconButton} onPress={() => setCloseOpen(true)}>
621
+ <Text style={styles.closeIcon}>ร—</Text>
622
+ </Pressable>
623
+ ) : null}
325
624
  </View>
625
+
626
+ {optionsOpen ? (
627
+ <>
628
+ <Pressable style={styles.inlineDismiss} onPress={() => setOptionsOpen(false)} />
629
+ <View style={styles.optionsPopover}>
630
+ {state.settings.transcript_enabled && contactConfirmed ? (
631
+ <Pressable style={styles.popoverRow} onPress={() => { setOptionsOpen(false); setTranscriptOpen(true); }}>
632
+ <Text style={styles.popoverIcon}>โœ‰</Text>
633
+ <Text style={styles.popoverText}>{t('send_transcript')}</Text>
634
+ </Pressable>
635
+ ) : null}
636
+ {state.translationCustomerEnabled ? (
637
+ <Pressable style={styles.popoverRow} onPress={() => { setOptionsOpen(false); setLanguageOpen(true); }}>
638
+ <Text style={styles.popoverIcon}>โ—Ž</Text>
639
+ <Text style={styles.popoverText}>{t('language')}</Text>
640
+ <Text style={styles.popoverChevron}>โ€บ</Text>
641
+ </Pressable>
642
+ ) : null}
643
+ {state.settings.sound_enabled !== undefined ? (
644
+ <Pressable style={styles.popoverRow} onPress={() => client.setSoundEnabled(!state.settings.sound_enabled)}>
645
+ <Text style={styles.popoverIcon}>โ—–</Text>
646
+ <Text style={styles.popoverText}>{t('sounds')}</Text>
647
+ <View style={[styles.switchTrack, state.settings.sound_enabled && styles.switchTrackActive]}>
648
+ <View style={[styles.switchThumb, state.settings.sound_enabled && styles.switchThumbActive]} />
649
+ </View>
650
+ </Pressable>
651
+ ) : null}
652
+ </View>
653
+ </>
654
+ ) : null}
326
655
  </View>
656
+ );
657
+ }
327
658
 
328
- {!contactConfirmed ? (
329
- <ScrollView contentContainerStyle={styles.contactStage} keyboardShouldPersistTaps="handled">
659
+ function renderContactStage() {
660
+ return (
661
+ <ScrollView style={styles.contactStageScroll} contentContainerStyle={styles.contactStage} keyboardShouldPersistTaps="handled">
662
+ <View style={styles.contactCard}>
330
663
  <Text style={styles.contactTitle}>{t('start_chat_title')}</Text>
331
664
  <Text style={styles.label}>{t('name')}</Text>
332
665
  <TextInput style={styles.input} value={contactName} onChangeText={setContactName} maxLength={190} placeholder={t('name')} placeholderTextColor={palette.muted} autoCapitalize="words" />
@@ -336,203 +669,347 @@ function RegantisChat(props) {
336
669
  <Pressable disabled={busyAction === 'contact'} style={[styles.primaryButton, busyAction === 'contact' && styles.disabled]} onPress={submitContact}>
337
670
  {busyAction === 'contact' ? <ActivityIndicator color="#FFFFFF" /> : <Text style={styles.primaryButtonText}>{t('start_chat')}</Text>}
338
671
  </Pressable>
339
- </ScrollView>
340
- ) : (
341
- <>
342
- <FlatList
343
- ref={listRef}
344
- data={state.messages}
345
- keyExtractor={item => String(item.chat_message_id)}
346
- renderItem={renderMessage}
347
- style={styles.messageList}
348
- contentContainerStyle={styles.messageListContent}
349
- onScrollBeginDrag={() => state.hasMore && client.loadOlder().catch(() => {})}
350
- ListEmptyComponent={<View style={styles.emptyState}><Text style={styles.emptyText}>{t('empty')}</Text></View>}
351
- />
672
+ </View>
673
+ </ScrollView>
674
+ );
675
+ }
352
676
 
353
- {state.remoteTyping ? <View style={styles.typingRow}><Text style={styles.typingText}>{state.remoteTyping === 'chatbot' ? t('chatbot_typing') : t('typing')}</Text></View> : null}
677
+ function renderComposer() {
678
+ if (!active) {
679
+ return (
680
+ <View style={styles.closedStage}>
681
+ <Text style={styles.closedText}>{c.closed_by === 'visitor' ? t('chat_closed') : t('chat_closed_agent')}</Text>
682
+ <Pressable disabled={busyAction === 'reopen'} style={styles.primaryButton} onPress={reopenChat}>
683
+ <Text style={styles.primaryButtonText}>{t('restart_chat')}</Text>
684
+ </Pressable>
685
+ </View>
686
+ );
687
+ }
354
688
 
355
- {!active ? (
356
- <View style={styles.closedStage}>
357
- <Text style={styles.closedText}>{c.closed_by === 'visitor' ? t('chat_closed') : t('chat_closed_agent')}</Text>
358
- <Pressable disabled={busyAction === 'reopen'} style={styles.primaryButton} onPress={reopenChat}><Text style={styles.primaryButtonText}>{t('restart_chat')}</Text></Pressable>
359
- {state.settings.rating_enabled ? <Pressable style={styles.secondaryButton} onPress={() => setRatingOpen(true)}><Text style={styles.secondaryButtonText}>{t('rating_agent_label')}</Text></Pressable> : null}
360
- </View>
361
- ) : (
362
- <View style={styles.composerArea}>
363
- {emojiOpen ? <ScrollView horizontal showsHorizontalScrollIndicator={false} contentContainerStyle={styles.emojiRow}>{EMOJIS.map(emoji => <Pressable key={emoji} style={styles.emojiButton} onPress={() => { setDraft(value => value + emoji); setEmojiOpen(false); }}><Text style={styles.emojiText}>{emoji}</Text></Pressable>)}</ScrollView> : null}
364
- <View style={styles.composer}>
365
- <Pressable disabled={!canUpload || state.uploading} style={styles.composerButton} onPress={() => setAttachOpen(true)}><Text style={styles.composerIcon}>๏ผ‹</Text></Pressable>
366
- <Pressable style={styles.composerButton} onPress={() => setEmojiOpen(value => !value)}><Text style={styles.composerIcon}>โ˜บ</Text></Pressable>
367
- <TextInput
368
- style={styles.composerInput}
369
- value={draft}
370
- onChangeText={value => { setDraft(value); client.sendTyping(value); }}
371
- placeholder={t('placeholder')}
372
- placeholderTextColor={palette.muted}
373
- multiline
374
- maxLength={5000}
375
- />
376
- <Pressable disabled={!draft.trim() || state.sending} style={[styles.sendButton, (!draft.trim() || state.sending) && styles.disabled]} onPress={submitMessage}>
377
- {state.sending ? <ActivityIndicator size="small" color="#FFFFFF" /> : <Text style={styles.sendIcon}>โžค</Text>}
378
- </Pressable>
379
- </View>
380
- {String(c.routing_mode || '') === 'chatbot' ? <Text style={styles.aiCompliance}>{t('ai_compliance')}</Text> : null}
381
- </View>
689
+ return (
690
+ <View style={styles.composerArea}>
691
+ {String(c.routing_mode || '') === 'chatbot' ? <Text style={styles.aiCompliance}>โ“˜ {t('ai_compliance')}</Text> : null}
692
+
693
+ {attachOpen ? (
694
+ <View style={styles.attachmentMenu}>
695
+ {Platform.OS === 'android' || Platform.OS === 'ios' ? (
696
+ <Pressable style={styles.attachmentAction} onPress={() => pick('camera')}><Text style={styles.attachmentActionIcon}>โ—‰</Text><Text style={styles.attachmentActionText}>{t('attach_camera')}</Text></Pressable>
697
+ ) : null}
698
+ <Pressable style={styles.attachmentAction} onPress={() => pick('image')}><Text style={styles.attachmentActionIcon}>โ–ฃ</Text><Text style={styles.attachmentActionText}>{t('attach_image')}</Text></Pressable>
699
+ <Pressable style={styles.attachmentAction} onPress={() => pick('file')}><Text style={styles.attachmentActionIcon}>โ†ฅ</Text><Text style={styles.attachmentActionText}>{t('attach_file')}</Text></Pressable>
700
+ </View>
701
+ ) : null}
702
+
703
+ {emojiOpen ? (
704
+ <View style={styles.emojiPicker}>
705
+ {EMOJIS.map(emoji => (
706
+ <Pressable key={emoji} style={styles.emojiButton} onPress={() => { setDraft(value => value + emoji); setEmojiOpen(false); }}>
707
+ <Text style={styles.emojiText}>{emoji}</Text>
708
+ </Pressable>
709
+ ))}
710
+ </View>
711
+ ) : null}
712
+
713
+ <View style={styles.composer}>
714
+ <Pressable disabled={!canUpload || state.uploading} style={[styles.composerButton, (!canUpload || state.uploading) && styles.disabled]} onPress={() => { setEmojiOpen(false); setAttachOpen(value => !value); }}>
715
+ <Text style={styles.composerPlus}>๏ผ‹</Text>
716
+ </Pressable>
717
+ <Pressable style={styles.composerButton} onPress={() => { setAttachOpen(false); setEmojiOpen(value => !value); }}>
718
+ <Text style={styles.composerEmoji}>โ˜บ</Text>
719
+ </Pressable>
720
+ <TextInput
721
+ style={styles.composerInput}
722
+ value={draft}
723
+ onChangeText={value => { setDraft(value); client.sendTyping(value); }}
724
+ placeholder={t('placeholder')}
725
+ placeholderTextColor={palette.muted}
726
+ multiline
727
+ maxLength={5000}
728
+ />
729
+ <Pressable disabled={!draft.trim() || state.sending} style={[styles.sendButton, (!draft.trim() || state.sending) && styles.disabled]} onPress={submitMessage}>
730
+ {state.sending ? <ActivityIndicator size="small" color="#FFFFFF" /> : <Text style={styles.sendIcon}>โžค</Text>}
731
+ </Pressable>
732
+ </View>
733
+ </View>
734
+ );
735
+ }
736
+
737
+ function renderChat() {
738
+ return (
739
+ <View style={styles.screen}>
740
+ {renderChatHeader()}
741
+ <View style={styles.chatContent}>
742
+ {!contactConfirmed ? renderContactStage() : (
743
+ <>
744
+ <FlatList
745
+ ref={listRef}
746
+ data={state.messages}
747
+ keyExtractor={item => String(item.chat_message_id)}
748
+ renderItem={renderMessage}
749
+ style={styles.messageList}
750
+ contentContainerStyle={styles.messageListContent}
751
+ onScrollBeginDrag={() => state.hasMore && client.loadOlder().catch(() => {})}
752
+ ListHeaderComponent={state.hasMore ? <View style={styles.historyLoader}><ActivityIndicator size="small" color={palette.primary} /></View> : null}
753
+ ListEmptyComponent={<View style={styles.emptyState}><Text style={styles.emptyText}>{t('empty')}</Text></View>}
754
+ />
755
+ {state.remoteTyping ? <View style={styles.typingRow}><Text style={styles.typingText}>{state.remoteTyping === 'chatbot' ? t('chatbot_typing') : t('typing')}</Text></View> : null}
756
+ {renderComposer()}
757
+ </>
382
758
  )}
383
- </>
384
- )}
759
+ {renderPoweredBy()}
760
+ </View>
761
+ </View>
762
+ );
763
+ }
385
764
 
386
- {!state.settings.white_label ? <View style={styles.poweredBy}><Text style={styles.poweredByText}>{t('powered_by')} Regantis</Text></View> : null}
765
+ if (state.loading) {
766
+ return <View style={[styles.root, styles.center, style]}><ActivityIndicator size="large" color={palette.primary} /><Text style={styles.loadingText}>{t('loading')}</Text></View>;
767
+ }
387
768
 
388
- <Modal transparent visible={attachOpen} animationType="fade" onRequestClose={() => setAttachOpen(false)}>
389
- <Pressable style={styles.modalBackdrop} onPress={() => setAttachOpen(false)}>
390
- <View style={styles.sheet}>
391
- <Pressable style={styles.sheetRow} onPress={() => pick('image')}><Text style={styles.sheetText}>โ–ฃ {t('attach_image')}</Text></Pressable>
392
- <Pressable style={styles.sheetRow} onPress={() => pick('file')}><Text style={styles.sheetText}>๐Ÿ“Ž {t('attach_file')}</Text></Pressable>
393
- <Pressable style={styles.sheetRow} onPress={() => pick('screenshot')}><Text style={styles.sheetText}>โ–ง {t('attach_screenshot')}</Text></Pressable>
394
- </View>
395
- </Pressable>
396
- </Modal>
769
+ if (state.error) {
770
+ return (
771
+ <View style={[styles.root, styles.center, style]}>
772
+ <Text style={styles.errorText}>{t('error')}</Text>
773
+ <Pressable style={styles.primaryButton} onPress={() => client.start().catch(() => {})}><Text style={styles.primaryButtonText}>{t('restart_chat')}</Text></Pressable>
774
+ </View>
775
+ );
776
+ }
397
777
 
398
- <Modal transparent visible={optionsOpen} animationType="fade" onRequestClose={() => setOptionsOpen(false)}>
399
- <Pressable style={styles.modalBackdrop} onPress={() => setOptionsOpen(false)}>
400
- <View style={styles.modalCard} onStartShouldSetResponder={() => true}>
401
- <Text style={styles.modalTitle}>{t('options')}</Text>
402
- <Pressable style={styles.optionRow} onPress={() => client.setSoundEnabled(!state.settings.sound_enabled)}><Text style={styles.optionText}>{t('sounds')}</Text><Text style={styles.optionValue}>{state.settings.sound_enabled ? 'โœ“' : 'โ€”'}</Text></Pressable>
403
- {state.translationCustomerEnabled ? (
404
- <View style={styles.optionSection}>
405
- <Text style={styles.optionSectionTitle}>{t('language')}</Text>
406
- <ScrollView style={styles.languageList}>
407
- <Pressable style={styles.languageRow} onPress={() => selectLanguage('')}><Text style={styles.optionText}>{t('language_auto')}</Text><Text style={styles.optionValue}>{!c.translation_language_code ? 'โœ“' : ''}</Text></Pressable>
408
- {state.languages.map(item => {
409
- const code = String(item.code || item.language_code || '').toLowerCase();
410
- const name = item.name || item.language_name || code.toUpperCase();
411
- return <Pressable key={code} style={styles.languageRow} onPress={() => selectLanguage(code)}><Text style={styles.optionText}>{name}</Text><Text style={styles.optionValue}>{String(c.translation_language_code || '').toLowerCase() === code ? 'โœ“' : code.toUpperCase()}</Text></Pressable>;
412
- })}
413
- </ScrollView>
414
- </View>
415
- ) : null}
416
- {state.settings.transcript_enabled && contactConfirmed ? <Pressable style={styles.optionRow} onPress={() => { setOptionsOpen(false); setTranscriptOpen(true); }}><Text style={styles.optionText}>{t('send_transcript')}</Text><Text style={styles.optionValue}>โ€บ</Text></Pressable> : null}
417
- {state.settings.rating_enabled && contactConfirmed ? <Pressable style={styles.optionRow} onPress={() => { setOptionsOpen(false); setRatingOpen(true); }}><Text style={styles.optionText}>{t('rating_agent_label')}</Text><Text style={styles.optionValue}>โ€บ</Text></Pressable> : null}
778
+ if (state.banned) return <View style={[styles.root, styles.center, style]}><Text style={styles.errorText}>{t('error')}</Text></View>;
779
+
780
+ return (
781
+ <KeyboardAvoidingView style={[styles.root, style]} behavior={Platform.OS === 'ios' ? 'padding' : undefined} keyboardVerticalOffset={props.keyboardVerticalOffset || 0}>
782
+ {activeView === 'home' ? renderHome() : renderChat()}
783
+
784
+ <Modal transparent visible={languageOpen} animationType="fade" onRequestClose={() => setLanguageOpen(false)}>
785
+ <Pressable style={styles.modalBackdrop} onPress={() => setLanguageOpen(false)}>
786
+ <View style={styles.sheet} onStartShouldSetResponder={() => true}>
787
+ <Text style={styles.sheetTitle}>{t('language')}</Text>
788
+ <ScrollView style={styles.languageList}>
789
+ <Pressable style={styles.languageRow} onPress={() => selectLanguage('')}><Text style={styles.optionText}>{t('language_auto')}</Text><Text style={styles.optionValue}>{!c.translation_language_code ? 'โœ“' : ''}</Text></Pressable>
790
+ {state.languages.map(item => {
791
+ const code = String(item.code || item.language_code || '').toLowerCase();
792
+ const name = item.name || item.language_name || code.toUpperCase();
793
+ return <Pressable key={code} style={styles.languageRow} onPress={() => selectLanguage(code)}><Text style={styles.optionText}>{name}</Text><Text style={styles.optionValue}>{String(c.translation_language_code || '').toLowerCase() === code ? 'โœ“' : code.toUpperCase()}</Text></Pressable>;
794
+ })}
795
+ </ScrollView>
418
796
  </View>
419
797
  </Pressable>
420
798
  </Modal>
421
799
 
422
800
  <Modal transparent visible={closeOpen} animationType="fade" onRequestClose={() => setCloseOpen(false)}>
423
- <View style={styles.modalBackdrop}><View style={styles.modalCard}>
424
- <Text style={styles.modalTitle}>{t('close_chat_question')}</Text>
425
- <View style={styles.modalActions}><Pressable style={styles.secondaryButton} onPress={() => setCloseOpen(false)}><Text style={styles.secondaryButtonText}>{t('cancel')}</Text></Pressable><Pressable style={styles.dangerButton} onPress={closeChat}><Text style={styles.primaryButtonText}>{t('close_chat_confirm')}</Text></Pressable></View>
426
- </View></View>
801
+ <View style={styles.modalBackdropTop}>
802
+ <View style={styles.modalCard}>
803
+ <View style={styles.modalIcon}><Text style={styles.modalIconText}>โ†ช</Text></View>
804
+ <Text style={styles.modalText}>{t('close_chat_question')}</Text>
805
+ <View style={styles.modalActions}>
806
+ <Pressable style={styles.secondaryButton} onPress={() => setCloseOpen(false)}><Text style={styles.secondaryButtonText}>{t('cancel')}</Text></Pressable>
807
+ <Pressable style={styles.dangerButton} onPress={closeChat}><Text style={styles.primaryButtonText}>{t('close_chat_confirm')}</Text></Pressable>
808
+ </View>
809
+ </View>
810
+ </View>
427
811
  </Modal>
428
812
 
429
813
  <Modal transparent visible={transcriptOpen} animationType="fade" onRequestClose={() => setTranscriptOpen(false)}>
430
- <View style={styles.modalBackdrop}><View style={styles.modalCard}>
431
- <Text style={styles.modalTitle}>{t('send_transcript_title')}</Text>
432
- <TextInput style={styles.input} value={transcriptEmail} onChangeText={setTranscriptEmail} keyboardType="email-address" autoCapitalize="none" placeholder={t('email')} placeholderTextColor={palette.muted} />
433
- {transcriptStatus ? <Text style={styles.modalStatus}>{transcriptStatus}</Text> : null}
434
- <View style={styles.modalActions}><Pressable style={styles.secondaryButton} onPress={() => setTranscriptOpen(false)}><Text style={styles.secondaryButtonText}>{t('cancel')}</Text></Pressable><Pressable style={styles.primaryButton} onPress={sendTranscript}><Text style={styles.primaryButtonText}>{t('send_transcript')}</Text></Pressable></View>
435
- </View></View>
814
+ <View style={styles.modalBackdropTop}>
815
+ <View style={styles.modalCard}>
816
+ <View style={styles.modalIcon}><Text style={styles.modalIconText}>โœ‰</Text></View>
817
+ <Text style={styles.modalText}>{t('send_transcript_title')}</Text>
818
+ <TextInput style={styles.input} value={transcriptEmail} onChangeText={setTranscriptEmail} keyboardType="email-address" autoCapitalize="none" placeholder={t('email')} placeholderTextColor={palette.muted} />
819
+ {transcriptStatus ? <Text style={styles.modalStatus}>{transcriptStatus}</Text> : null}
820
+ <Pressable style={styles.primaryButton} onPress={sendTranscript}><Text style={styles.primaryButtonText}>{t('send_transcript')}</Text></Pressable>
821
+ </View>
822
+ </View>
436
823
  </Modal>
437
824
 
438
825
  <Modal transparent visible={ratingOpen} animationType="fade" onRequestClose={() => setRatingOpen(false)}>
439
- <View style={styles.modalBackdrop}><View style={styles.modalCard}>
440
- <Text style={styles.modalTitle}>{t('rating_agent_label')}</Text>
441
- <View style={styles.ratingButtons}><Pressable style={styles.ratingButton} onPress={() => rate(1)}><Text style={styles.ratingIcon}>๐Ÿ‘</Text></Pressable><Pressable style={styles.ratingButton} onPress={() => rate(0)}><Text style={styles.ratingIcon}>๐Ÿ‘Ž</Text></Pressable></View>
442
- {ratingStatus ? <Text style={styles.modalStatus}>{ratingStatus}</Text> : null}
443
- <TextInput style={[styles.input, styles.commentInput]} value={ratingComment} onChangeText={setRatingComment} multiline maxLength={2000} placeholder={t('rating_comment_placeholder')} placeholderTextColor={palette.muted} />
444
- <View style={styles.modalActions}><Pressable style={styles.secondaryButton} onPress={() => setRatingOpen(false)}><Text style={styles.secondaryButtonText}>{t('cancel')}</Text></Pressable><Pressable style={styles.primaryButton} onPress={sendRatingComment}><Text style={styles.primaryButtonText}>{t('rating_comment_send')}</Text></Pressable></View>
445
- </View></View>
826
+ <View style={styles.modalBackdropTop}>
827
+ <View style={styles.modalCard}>
828
+ <Text style={styles.modalTitle}>{state.settings.operator_name || t('support_name')}</Text>
829
+ <Text style={styles.ratingLabel}>{t('rating_agent_label')}</Text>
830
+ <View style={styles.ratingButtons}>
831
+ <Pressable style={styles.ratingButton} onPress={() => rate(1)}><Text style={styles.ratingIcon}>๐Ÿ‘</Text></Pressable>
832
+ <Pressable style={styles.ratingButton} onPress={() => rate(0)}><Text style={styles.ratingIcon}>๐Ÿ‘Ž</Text></Pressable>
833
+ </View>
834
+ {ratingStatus ? <Text style={styles.modalStatus}>{ratingStatus}</Text> : null}
835
+ <TextInput style={[styles.input, styles.commentInput]} value={ratingComment} onChangeText={setRatingComment} multiline maxLength={2000} placeholder={t('rating_comment_placeholder')} placeholderTextColor={palette.muted} />
836
+ <View style={styles.modalActions}>
837
+ <Pressable style={styles.secondaryButton} onPress={() => setRatingOpen(false)}><Text style={styles.secondaryButtonText}>{t('cancel')}</Text></Pressable>
838
+ <Pressable style={styles.primaryButtonCompact} onPress={sendRatingComment}><Text style={styles.primaryButtonText}>{t('rating_comment_send')}</Text></Pressable>
839
+ </View>
840
+ </View>
841
+ </View>
446
842
  </Modal>
843
+
844
+ <ImageLightbox
845
+ visible={!!lightboxUri}
846
+ uri={lightboxUri}
847
+ onClose={() => setLightboxUri('')}
848
+ closeLabel={t('lightbox_close')}
849
+ resetLabel={t('lightbox_reset')}
850
+ />
447
851
  </KeyboardAvoidingView>
448
852
  );
449
853
  }
450
854
 
451
855
  function buildStyles(p) {
856
+ const shadow = {
857
+ shadowColor: '#172236',
858
+ shadowOffset: { width: 0, height: 8 },
859
+ shadowOpacity: p.dark ? 0.24 : 0.10,
860
+ shadowRadius: 18,
861
+ elevation: 5,
862
+ };
863
+
452
864
  return StyleSheet.create({
453
- root: { flex: 1, backgroundColor: p.background },
865
+ root: { flex: 1, minWidth: 0, minHeight: 0, overflow: 'hidden', backgroundColor: p.background },
866
+ screen: { flex: 1, minWidth: 0, minHeight: 0, backgroundColor: p.background },
454
867
  center: { alignItems: 'center', justifyContent: 'center', padding: 24, gap: 14 },
455
- loadingText: { color: p.muted, fontSize: 14 },
456
- errorText: { color: p.text, fontSize: 15, textAlign: 'center', marginBottom: 16 },
457
- header: { minHeight: 68, paddingHorizontal: 14, paddingVertical: 10, backgroundColor: p.surface, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: p.border, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between' },
458
- headerBrand: { flex: 1, flexDirection: 'row', alignItems: 'center', minWidth: 0 },
459
- headerCopy: { flex: 1, minWidth: 0 },
460
- headerTitle: { color: p.text, fontSize: 16, fontWeight: '700' },
461
- headerStatus: { color: p.muted, fontSize: 11, marginTop: 2 },
462
- headerActions: { flexDirection: 'row', alignItems: 'center', gap: 6 },
463
- logo: { width: 38, height: 38, borderRadius: 10, marginRight: 10 },
464
- logoFallback: { width: 38, height: 38, borderRadius: 10, marginRight: 10, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
465
- logoFallbackText: { color: '#FFFFFF', fontWeight: '800', fontSize: 18 },
466
- iconButton: { width: 38, height: 38, borderRadius: 10, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
467
- iconText: { color: p.text, fontSize: 24, lineHeight: 26 },
468
- contactStage: { flexGrow: 1, justifyContent: 'center', padding: 22 },
469
- contactTitle: { color: p.text, fontSize: 20, lineHeight: 27, fontWeight: '700', marginBottom: 24 },
470
- label: { color: p.text, fontSize: 13, fontWeight: '600', marginBottom: 7, marginTop: 10 },
471
- input: { minHeight: 48, borderWidth: 1, borderColor: p.border, borderRadius: 12, backgroundColor: p.surface, color: p.text, paddingHorizontal: 13, paddingVertical: 10, fontSize: 15 },
472
- inlineError: { color: p.danger, fontSize: 12, marginTop: 8 },
473
- primaryButton: { minHeight: 48, borderRadius: 12, backgroundColor: p.primary, paddingHorizontal: 18, alignItems: 'center', justifyContent: 'center', marginTop: 14 },
474
- primaryButtonText: { color: '#FFFFFF', fontSize: 14, fontWeight: '700' },
475
- secondaryButton: { minHeight: 46, borderRadius: 12, backgroundColor: p.surfaceAlt, paddingHorizontal: 16, alignItems: 'center', justifyContent: 'center', marginTop: 10 },
476
- secondaryButtonText: { color: p.text, fontSize: 14, fontWeight: '700' },
477
- dangerButton: { minHeight: 46, borderRadius: 12, backgroundColor: p.danger, paddingHorizontal: 16, alignItems: 'center', justifyContent: 'center', marginTop: 10 },
868
+ loadingText: { color: p.muted, fontSize: 13 },
869
+ errorText: { color: p.text, fontSize: 14, textAlign: 'center', marginBottom: 12 },
870
+
871
+ homeHero: { position: 'relative', overflow: 'hidden', paddingHorizontal: 18, paddingTop: 24, paddingBottom: 28, backgroundColor: p.heroBase },
872
+ heroDecorationA: { position: 'absolute', width: 240, height: 240, borderRadius: 120, top: -138, left: -74, backgroundColor: p.heroBlobA },
873
+ heroDecorationB: { position: 'absolute', width: 230, height: 230, borderRadius: 115, top: 80, right: -92, backgroundColor: p.heroBlobB },
874
+ heroDecorationC: { position: 'absolute', width: 210, height: 210, borderRadius: 105, right: -82, bottom: -120, backgroundColor: p.heroBlobC },
875
+ homeTop: { minHeight: 42, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', zIndex: 2 },
876
+ homeTopLeft: { flexDirection: 'row', alignItems: 'center', gap: 9 },
877
+ homeBackIcon: { color: p.text, fontSize: 37, lineHeight: 38, marginTop: -5 },
878
+ homeTopPlaceholder: { width: 32, height: 32 },
879
+ logoImage: { backgroundColor: p.surface },
880
+ logoFallback: { backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center', shadowColor: p.primary, shadowOffset: { width: 0, height: 6 }, shadowOpacity: 0.22, shadowRadius: 10, elevation: 4 },
881
+ logoFallbackText: { color: '#FFFFFF', fontWeight: '800' },
882
+ iconButton: { width: 38, height: 38, borderRadius: 19, backgroundColor: p.dark ? 'rgba(255,255,255,0.08)' : 'rgba(255,255,255,0.82)', alignItems: 'center', justifyContent: 'center', zIndex: 3 },
883
+ minimizeIcon: { color: p.text, fontSize: 25, lineHeight: 27, fontWeight: '400', marginTop: -3 },
884
+ homeTitle: { marginTop: 24, zIndex: 2 },
885
+ homeTitleLine: { color: p.text, fontSize: 34, lineHeight: 36, letterSpacing: -1.1, fontWeight: '800' },
886
+ homeCard: { marginTop: 22, padding: 14, borderWidth: 1, borderColor: p.border, borderRadius: 18, backgroundColor: p.dark ? p.surface : 'rgba(255,255,255,0.96)', zIndex: 2, ...shadow },
887
+ homeCardTop: { minHeight: 38, flexDirection: 'row', alignItems: 'center' },
888
+ homeAvatar: { width: 38, height: 38, borderRadius: 19, marginRight: 10, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
889
+ homeAvatarText: { color: '#FFFFFF', fontSize: 14, fontWeight: '700' },
890
+ homeCardCopy: { flex: 1, minWidth: 0 },
891
+ homeCardAgent: { color: p.muted, fontSize: 11, lineHeight: 15, fontWeight: '700' },
892
+ homeCardPreview: { marginTop: 3, color: p.text, fontSize: 13, lineHeight: 18 },
893
+ homeCardAction: { minHeight: 41, marginTop: 12, paddingHorizontal: 12, borderRadius: 11, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
894
+ homeCardActionText: { color: '#FFFFFF', fontSize: 13, lineHeight: 18, fontWeight: '700', textAlign: 'center' },
895
+ homeSpacer: { flex: 1, minHeight: 20, backgroundColor: p.background },
896
+ bottomNav: { minHeight: 64, flexDirection: 'row', gap: 6, marginHorizontal: 16, marginBottom: 10, padding: 7, borderWidth: 1, borderColor: p.border, borderRadius: 18, backgroundColor: p.surface, ...shadow },
897
+ bottomNavButton: { flex: 1, minHeight: 48, borderRadius: 12, alignItems: 'center', justifyContent: 'center' },
898
+ bottomNavButtonActive: { backgroundColor: p.dark ? 'rgba(255,255,255,0.04)' : '#FAFBFC' },
899
+ bottomNavIcon: { color: p.muted, fontSize: 22, lineHeight: 23 },
900
+ bottomNavIconActive: { color: p.text },
901
+ bottomNavText: { marginTop: 2, color: p.muted, fontSize: 10, lineHeight: 12 },
902
+ bottomNavTextActive: { color: p.text },
903
+
904
+ 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' },
905
+ chatHeaderLeft: { position: 'absolute', zIndex: 30, left: 12, top: 16, flexDirection: 'row', alignItems: 'center', gap: 5 },
906
+ chatHeaderRight: { position: 'absolute', zIndex: 30, right: 12, top: 16, flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-end', gap: 5 },
907
+ headerIconButton: { width: 32, height: 32, borderRadius: 16, backgroundColor: p.dark ? 'rgba(255,255,255,0.08)' : '#FFFFFF', alignItems: 'center', justifyContent: 'center' },
908
+ backIcon: { color: p.text, fontSize: 31, lineHeight: 31, marginTop: -4 },
909
+ menuIcon: { color: p.text, fontSize: 17, lineHeight: 18, letterSpacing: -1.5, fontWeight: '700', marginTop: -4 },
910
+ closeIcon: { color: p.text, fontSize: 23, lineHeight: 24, marginTop: -2 },
911
+ 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 },
912
+ agentVisual: { height: 30, flexDirection: 'row', alignItems: 'center', justifyContent: 'center' },
913
+ agentAvatar: { width: 28, height: 28, borderRadius: 14, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
914
+ agentAvatarOverlap: { marginLeft: -8, borderWidth: 2, borderColor: p.surface },
915
+ agentAvatarText: { color: '#FFFFFF', fontSize: 11, fontWeight: '700' },
916
+ agentFallback: { width: 28, height: 28, borderRadius: 14, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
917
+ agentFallbackText: { color: p.muted, fontSize: 12 },
918
+ inlineDismiss: { position: 'absolute', zIndex: 23, top: 0, left: -12, right: -12, bottom: -1000 },
919
+ optionsPopover: { position: 'absolute', zIndex: 40, top: 52, left: 46, width: 230, padding: 7, borderWidth: 1, borderColor: p.border, borderRadius: 14, backgroundColor: p.surface, ...shadow },
920
+ popoverRow: { minHeight: 43, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 9, borderRadius: 9 },
921
+ popoverIcon: { width: 26, color: p.text, fontSize: 16 },
922
+ popoverText: { flex: 1, color: p.text, fontSize: 13 },
923
+ popoverChevron: { color: p.muted, fontSize: 20 },
924
+ switchTrack: { width: 34, height: 20, borderRadius: 10, padding: 2, backgroundColor: p.dark ? '#3D434B' : '#D7DBE1' },
925
+ switchTrackActive: { backgroundColor: '#24935B' },
926
+ switchThumb: { width: 16, height: 16, borderRadius: 8, backgroundColor: '#FFFFFF' },
927
+ switchThumbActive: { transform: [{ translateX: 14 }] },
928
+
929
+ chatContent: { flex: 1, minHeight: 0, backgroundColor: p.background },
930
+ contactStageScroll: { flex: 1 },
931
+ contactStage: { paddingHorizontal: 16, paddingTop: 18, paddingBottom: 24 },
932
+ contactCard: { padding: 18, borderWidth: 1, borderColor: p.border, borderRadius: 18, backgroundColor: p.surface, ...shadow },
933
+ contactTitle: { color: p.text, fontSize: 14, lineHeight: 20, marginBottom: 13 },
934
+ label: { color: p.text, fontSize: 12, lineHeight: 16, marginBottom: 5, marginTop: 9 },
935
+ input: { minHeight: 42, borderWidth: 1, borderColor: p.dark ? '#555B64' : '#B7BDC7', borderRadius: 8, backgroundColor: p.surface, color: p.text, paddingHorizontal: 11, paddingVertical: 9, fontSize: 13 },
936
+ inlineError: { color: '#B42318', fontSize: 11, lineHeight: 15, marginTop: 8 },
937
+ primaryButton: { minHeight: 40, borderRadius: 9, backgroundColor: p.primary, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center', marginTop: 12 },
938
+ primaryButtonCompact: { flex: 1, minHeight: 40, borderRadius: 9, backgroundColor: p.primary, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center' },
939
+ primaryButtonText: { color: '#FFFFFF', fontSize: 13, fontWeight: '700', textAlign: 'center' },
940
+ secondaryButton: { flex: 1, minHeight: 40, borderWidth: 1, borderColor: p.dark ? '#555B64' : '#C7CCD4', borderRadius: 9, backgroundColor: p.surface, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center' },
941
+ secondaryButtonText: { color: p.text, fontSize: 13, fontWeight: '700', textAlign: 'center' },
942
+ dangerButton: { flex: 1, minHeight: 40, borderRadius: 9, backgroundColor: p.danger, paddingHorizontal: 14, alignItems: 'center', justifyContent: 'center' },
478
943
  disabled: { opacity: 0.55 },
479
- messageList: { flex: 1 },
480
- messageListContent: { paddingHorizontal: 12, paddingVertical: 14, flexGrow: 1 },
481
- emptyState: { flex: 1, alignItems: 'center', justifyContent: 'center', padding: 30 },
482
- emptyText: { color: p.muted, textAlign: 'center', lineHeight: 21 },
483
- messageRow: { marginBottom: 8, flexDirection: 'row', alignItems: 'flex-end', maxWidth: '88%' },
484
- messageRowLeft: { alignSelf: 'flex-start' },
485
- messageRowRight: { alignSelf: 'flex-end', justifyContent: 'flex-end' },
486
- avatar: { width: 28, height: 28, borderRadius: 14, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center', marginRight: 6 },
487
- avatarText: { color: '#FFFFFF', fontSize: 12, fontWeight: '800' },
488
- bubble: { maxWidth: '100%', borderRadius: 16, paddingHorizontal: 12, paddingVertical: 9 },
944
+
945
+ messageList: { flex: 1, minHeight: 0 },
946
+ messageListContent: { paddingHorizontal: 16, paddingTop: 18, paddingBottom: 12, flexGrow: 1 },
947
+ historyLoader: { minHeight: 34, alignItems: 'center', justifyContent: 'center', paddingBottom: 10 },
948
+ emptyState: { flex: 1, alignItems: 'center', justifyContent: 'center', paddingHorizontal: 30, paddingVertical: 20 },
949
+ emptyText: { maxWidth: 260, paddingHorizontal: 16, paddingVertical: 14, borderRadius: 15, backgroundColor: p.dark ? p.surface : 'rgba(255,255,255,0.84)', color: p.muted, fontSize: 13, lineHeight: 19, textAlign: 'center' },
950
+ messageRow: { width: '100%', marginBottom: 11, flexDirection: 'row' },
951
+ messageRowLeft: { justifyContent: 'flex-start', paddingRight: 52 },
952
+ messageRowRight: { justifyContent: 'flex-end', paddingLeft: 52 },
953
+ bubble: { maxWidth: '100%', paddingHorizontal: 12, paddingTop: 10, paddingBottom: 8, borderRadius: 15, shadowColor: '#172236', shadowOffset: { width: 0, height: 4 }, shadowOpacity: p.dark ? 0.16 : 0.07, shadowRadius: 7, elevation: 2 },
489
954
  customerBubble: { backgroundColor: p.customerBubble, borderBottomRightRadius: 5 },
490
- agentBubble: { backgroundColor: p.agentBubble, borderBottomLeftRadius: 5, borderWidth: StyleSheet.hairlineWidth, borderColor: p.border },
955
+ agentBubble: { backgroundColor: p.agentBubble, borderBottomLeftRadius: 5 },
491
956
  customerText: { color: p.customerText },
492
957
  agentText: { color: p.agentText },
493
- senderName: { fontSize: 11, fontWeight: '700', marginBottom: 4, opacity: 0.85 },
494
- messageText: { fontSize: 15, lineHeight: 20 },
495
- timeText: { fontSize: 9, marginTop: 4, opacity: 0.6, alignSelf: 'flex-end' },
958
+ messageText: { fontSize: 14, lineHeight: 20 },
959
+ messageMeta: { minHeight: 13, marginTop: 5, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 6 },
960
+ messageSender: { flex: 1, fontSize: 10, lineHeight: 12, fontWeight: '600', opacity: 0.62 },
961
+ timeText: { fontSize: 10, lineHeight: 12, opacity: 0.62 },
496
962
  systemRow: { alignSelf: 'center', paddingHorizontal: 16, paddingVertical: 7, marginVertical: 3 },
497
- systemText: { color: p.muted, fontSize: 11, textAlign: 'center' },
498
- attachmentImage: { width: 210, height: 160, borderRadius: 10, marginBottom: 5, backgroundColor: p.surfaceAlt },
499
- fileAttachment: { paddingVertical: 5 },
500
- typingRow: { paddingHorizontal: 16, paddingVertical: 5 },
501
- typingText: { color: p.muted, fontSize: 12, fontStyle: 'italic' },
502
- closedStage: { padding: 14, backgroundColor: p.surface, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: p.border },
503
- closedText: { color: p.muted, textAlign: 'center', marginBottom: 4 },
504
- composerArea: { backgroundColor: p.surface, borderTopWidth: StyleSheet.hairlineWidth, borderTopColor: p.border, paddingHorizontal: 8, paddingTop: 7, paddingBottom: 8 },
505
- composer: { flexDirection: 'row', alignItems: 'flex-end', gap: 5 },
506
- composerButton: { width: 38, height: 42, borderRadius: 11, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
507
- composerIcon: { color: p.text, fontSize: 21 },
508
- composerInput: { flex: 1, minHeight: 42, maxHeight: 120, borderRadius: 13, backgroundColor: p.surfaceAlt, color: p.text, paddingHorizontal: 12, paddingTop: 10, paddingBottom: 9, fontSize: 15 },
509
- sendButton: { width: 42, height: 42, borderRadius: 13, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
510
- sendIcon: { color: '#FFFFFF', fontSize: 18 },
511
- aiCompliance: { color: p.muted, fontSize: 9, textAlign: 'center', marginTop: 5, paddingHorizontal: 8 },
512
- emojiRow: { paddingVertical: 6, gap: 4 },
513
- emojiButton: { width: 39, height: 39, borderRadius: 10, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
514
- emojiText: { fontSize: 21 },
515
- poweredBy: { backgroundColor: p.surface, paddingBottom: 4, alignItems: 'center' },
516
- poweredByText: { color: p.muted, fontSize: 8 },
517
- modalBackdrop: { flex: 1, backgroundColor: 'rgba(0,0,0,0.45)', justifyContent: 'center', padding: 20 },
518
- modalCard: { backgroundColor: p.surface, borderRadius: 18, padding: 16, maxHeight: '82%' },
519
- modalTitle: { color: p.text, fontSize: 17, fontWeight: '700', marginBottom: 12 },
520
- modalActions: { flexDirection: 'row', gap: 8, justifyContent: 'flex-end' },
521
- modalStatus: { color: p.muted, fontSize: 12, marginTop: 9 },
522
- sheet: { alignSelf: 'stretch', backgroundColor: p.surface, borderRadius: 18, paddingVertical: 6 },
523
- sheetRow: { minHeight: 54, justifyContent: 'center', paddingHorizontal: 18, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: p.border },
524
- sheetText: { color: p.text, fontSize: 16 },
525
- optionRow: { minHeight: 50, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: p.border },
526
- optionText: { color: p.text, fontSize: 14 },
963
+ systemText: { color: p.muted, fontSize: 11, lineHeight: 15, textAlign: 'center' },
964
+ attachmentImage: { width: 220, maxWidth: '100%', height: 180, borderRadius: 10, marginBottom: 4, backgroundColor: p.surfaceAlt },
965
+ fileAttachment: { paddingVertical: 4 },
966
+ fileAttachmentText: { fontSize: 13, fontWeight: '600' },
967
+ typingRow: { flex: 0, paddingHorizontal: 18, paddingTop: 8, paddingBottom: 7 },
968
+ typingText: { color: p.muted, fontSize: 11, lineHeight: 15 },
969
+
970
+ closedStage: { flex: 0, paddingHorizontal: 12, paddingTop: 10, paddingBottom: 12, borderTopWidth: 1, borderTopColor: p.border, backgroundColor: p.background },
971
+ closedText: { color: p.muted, fontSize: 11, lineHeight: 15, textAlign: 'center' },
972
+ composerArea: { position: 'relative', flex: 0, paddingHorizontal: 12, paddingTop: 10, paddingBottom: 12, backgroundColor: p.background },
973
+ composer: { minHeight: 54, flexDirection: 'row', alignItems: 'flex-end', gap: 8, paddingLeft: 13, paddingRight: 7, paddingVertical: 7, borderWidth: 1, borderColor: p.border, borderRadius: 18, backgroundColor: p.surface, shadowColor: '#172236', shadowOffset: { width: 0, height: 8 }, shadowOpacity: p.dark ? 0.22 : 0.10, shadowRadius: 12, elevation: 4 },
974
+ composerButton: { width: 34, height: 38, borderRadius: 19, backgroundColor: 'transparent', alignItems: 'center', justifyContent: 'center' },
975
+ composerPlus: { color: p.text, fontSize: 25, lineHeight: 27, fontWeight: '300', marginTop: -2 },
976
+ composerEmoji: { color: p.text, fontSize: 23, lineHeight: 25 },
977
+ composerInput: { flex: 1, minWidth: 0, minHeight: 38, maxHeight: 110, backgroundColor: 'transparent', color: p.text, paddingHorizontal: 0, paddingTop: 9, paddingBottom: 7, fontSize: 14, lineHeight: 19, textAlignVertical: 'top' },
978
+ sendButton: { width: 38, height: 38, borderRadius: 19, backgroundColor: p.primary, alignItems: 'center', justifyContent: 'center' },
979
+ sendIcon: { color: '#FFFFFF', fontSize: 17, lineHeight: 18, transform: [{ rotate: '-3deg' }] },
980
+ aiCompliance: { color: p.muted, fontSize: 9, lineHeight: 13, textAlign: 'center', paddingHorizontal: 5, paddingBottom: 8 },
981
+ attachmentMenu: { position: 'absolute', zIndex: 12, left: 12, bottom: 72, width: 210, padding: 7, borderWidth: 1, borderColor: p.border, borderRadius: 14, backgroundColor: p.surface, ...shadow },
982
+ attachmentAction: { minHeight: 40, flexDirection: 'row', alignItems: 'center', paddingHorizontal: 10, borderRadius: 9 },
983
+ attachmentActionIcon: { width: 26, color: p.text, fontSize: 15 },
984
+ attachmentActionText: { flex: 1, color: p.text, fontSize: 13 },
985
+ emojiPicker: { position: 'absolute', zIndex: 12, left: 12, right: 12, bottom: 72, flexDirection: 'row', flexWrap: 'wrap', gap: 5, padding: 10, borderWidth: 1, borderColor: p.border, borderRadius: 16, backgroundColor: p.surface, ...shadow },
986
+ emojiButton: { width: '11%', aspectRatio: 1, minHeight: 34, borderRadius: 8, alignItems: 'center', justifyContent: 'center' },
987
+ emojiText: { fontSize: 20, lineHeight: 22 },
988
+
989
+ poweredBy: { flex: 0, minHeight: 17, alignItems: 'center', justifyContent: 'center', paddingBottom: 5, backgroundColor: p.background },
990
+ poweredByText: { color: '#9398A2', fontSize: 9, lineHeight: 11, textAlign: 'center' },
991
+ poweredByBrand: { color: p.text, fontWeight: '700' },
992
+
993
+ modalBackdrop: { flex: 1, backgroundColor: 'rgba(16,20,27,0.55)', justifyContent: 'flex-end', padding: 16 },
994
+ modalBackdropTop: { flex: 1, backgroundColor: 'rgba(16,20,27,0.55)', justifyContent: 'flex-start', padding: 16 },
995
+ modalCard: { width: '100%', maxHeight: '82%', marginTop: 34, paddingHorizontal: 16, paddingTop: 24, paddingBottom: 16, borderRadius: 14, backgroundColor: p.surface, ...shadow },
996
+ modalIcon: { width: 48, height: 48, borderRadius: 24, alignSelf: 'center', alignItems: 'center', justifyContent: 'center', backgroundColor: p.surfaceAlt },
997
+ modalIconText: { color: p.text, fontSize: 22 },
998
+ modalText: { marginHorizontal: 4, marginVertical: 16, color: p.text, fontSize: 14, lineHeight: 20, textAlign: 'center' },
999
+ modalTitle: { color: p.text, fontSize: 16, fontWeight: '700', textAlign: 'center' },
1000
+ modalActions: { flexDirection: 'row', gap: 8, marginTop: 12 },
1001
+ modalStatus: { color: p.muted, fontSize: 11, lineHeight: 15, marginTop: 9, textAlign: 'center' },
1002
+ sheet: { maxHeight: '72%', borderRadius: 18, paddingHorizontal: 14, paddingTop: 14, paddingBottom: 8, backgroundColor: p.surface, ...shadow },
1003
+ sheetTitle: { color: p.text, fontSize: 16, fontWeight: '700', marginBottom: 8 },
1004
+ languageList: { maxHeight: 360 },
1005
+ languageRow: { minHeight: 46, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderBottomColor: p.border },
1006
+ optionText: { flex: 1, color: p.text, fontSize: 14 },
527
1007
  optionValue: { color: p.muted, fontSize: 12, marginLeft: 12 },
528
- optionSection: { paddingTop: 10 },
529
- optionSectionTitle: { color: p.muted, fontSize: 11, fontWeight: '700', marginBottom: 4 },
530
- languageList: { maxHeight: 220 },
531
- languageRow: { minHeight: 44, flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingLeft: 8 },
532
- ratingButtons: { flexDirection: 'row', gap: 12, justifyContent: 'center', marginVertical: 8 },
533
- ratingButton: { width: 66, height: 54, borderRadius: 14, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
534
- ratingIcon: { fontSize: 27 },
535
- commentInput: { minHeight: 82, textAlignVertical: 'top', marginTop: 10 },
1008
+ ratingLabel: { color: p.muted, fontSize: 11, lineHeight: 15, textAlign: 'center', marginTop: 3 },
1009
+ ratingButtons: { flexDirection: 'row', gap: 12, justifyContent: 'center', marginVertical: 12 },
1010
+ ratingButton: { width: 60, height: 48, borderWidth: 1, borderColor: p.border, borderRadius: 12, backgroundColor: p.surfaceAlt, alignItems: 'center', justifyContent: 'center' },
1011
+ ratingIcon: { fontSize: 24 },
1012
+ commentInput: { minHeight: 72, textAlignVertical: 'top', marginTop: 6 },
536
1013
  });
537
1014
  }
538
1015