@regantis-sdk/react-native-chat 0.1.0

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.
@@ -0,0 +1,539 @@
1
+ 'use strict';
2
+
3
+ const React = require('react');
4
+ const {
5
+ ActivityIndicator,
6
+ AppState,
7
+ FlatList,
8
+ Image,
9
+ KeyboardAvoidingView,
10
+ Linking,
11
+ Modal,
12
+ Platform,
13
+ Pressable,
14
+ ScrollView,
15
+ StyleSheet,
16
+ Text,
17
+ TextInput,
18
+ View,
19
+ } = require('react-native');
20
+ const { RegantisChatClient } = require('./client');
21
+
22
+ const EMOJIS = ['🙂', '😁', '😂', '😊', '😍', '🤔', '😞', '😢', '🎉', '❤️', '👌', '👍', '👎', '🙏'];
23
+
24
+ function colorWithOpacity(hex, opacity) {
25
+ const value = String(hex || '#F5F7FB').replace('#', '');
26
+ if (!/^[0-9a-f]{6}$/i.test(value)) return hex;
27
+ const alpha = Math.max(0, Math.min(100, Number(opacity))) / 100;
28
+ const r = parseInt(value.slice(0, 2), 16);
29
+ const g = parseInt(value.slice(2, 4), 16);
30
+ const b = parseInt(value.slice(4, 6), 16);
31
+ return `rgba(${r},${g},${b},${alpha})`;
32
+ }
33
+
34
+ function makePalette(settings) {
35
+ const dark = settings.theme_mode === 'dark';
36
+ const primary = settings.color_mode === 'advanced' ? settings.primary_color : settings.theme_color;
37
+ return {
38
+ primary,
39
+ background: colorWithOpacity(settings.chat_background, settings.chat_background_opacity),
40
+ surface: dark ? '#17181B' : '#FFFFFF',
41
+ surfaceAlt: dark ? '#22252A' : '#EEF1F5',
42
+ border: dark ? '#30343B' : '#E0E4EA',
43
+ text: dark ? '#F7F8FA' : '#17181B',
44
+ muted: settings.system_text || (dark ? '#A7ADB7' : '#6F7480'),
45
+ customerBubble: settings.color_mode === 'advanced' ? settings.customer_bubble : primary,
46
+ customerText: settings.color_mode === 'advanced' ? settings.customer_text : '#FFFFFF',
47
+ agentBubble: settings.color_mode === 'advanced' ? settings.agent_bubble : (dark ? '#22252A' : '#FFFFFF'),
48
+ agentText: settings.color_mode === 'advanced' ? settings.agent_text : (dark ? '#F7F8FA' : '#17181B'),
49
+ danger: '#C93A3A',
50
+ };
51
+ }
52
+
53
+ function formatTime(value) {
54
+ if (!value) return '';
55
+ const date = new Date(value);
56
+ if (Number.isNaN(date.getTime())) return '';
57
+ return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
58
+ }
59
+
60
+ function RegantisChat(props) {
61
+ const {
62
+ apiKey,
63
+ serverUrl,
64
+ language = 'en',
65
+ translationLanguage = '',
66
+ customer,
67
+ theme,
68
+ texts,
69
+ screen = 'chat',
70
+ style,
71
+ onMessage,
72
+ onUnreadChange,
73
+ onError,
74
+ } = props;
75
+
76
+ const client = React.useMemo(() => new RegantisChatClient({
77
+ apiKey,
78
+ serverUrl,
79
+ language,
80
+ translationLanguage,
81
+ customer,
82
+ theme,
83
+ texts,
84
+ screen,
85
+ onMessage,
86
+ onUnreadChange,
87
+ onError,
88
+ }), [apiKey, serverUrl, language, translationLanguage, customer, theme, texts, screen, onMessage, onUnreadChange, onError]);
89
+
90
+ const [state, setState] = React.useState(client.getState());
91
+ const [draft, setDraft] = React.useState('');
92
+ const [contactName, setContactName] = React.useState(customer && customer.name || '');
93
+ const [contactEmail, setContactEmail] = React.useState(customer && customer.email || '');
94
+ const [contactError, setContactError] = React.useState('');
95
+ const [optionsOpen, setOptionsOpen] = React.useState(false);
96
+ const [attachOpen, setAttachOpen] = React.useState(false);
97
+ const [emojiOpen, setEmojiOpen] = React.useState(false);
98
+ const [closeOpen, setCloseOpen] = React.useState(false);
99
+ const [transcriptOpen, setTranscriptOpen] = React.useState(false);
100
+ const [transcriptEmail, setTranscriptEmail] = React.useState(customer && customer.email || '');
101
+ const [transcriptStatus, setTranscriptStatus] = React.useState('');
102
+ const [ratingOpen, setRatingOpen] = React.useState(false);
103
+ const [ratingComment, setRatingComment] = React.useState('');
104
+ const [ratingStatus, setRatingStatus] = React.useState('');
105
+ const [busyAction, setBusyAction] = React.useState('');
106
+ const listRef = React.useRef(null);
107
+
108
+ React.useEffect(() => {
109
+ const unsubscribe = client.subscribe(next => setState(next));
110
+ client.start().catch(() => {});
111
+ const sub = AppState.addEventListener('change', value => client.setVisible(value === 'active'));
112
+ return () => {
113
+ if (sub && sub.remove) sub.remove();
114
+ unsubscribe();
115
+ client.stop();
116
+ };
117
+ }, [client]);
118
+
119
+ React.useEffect(() => {
120
+ const c = state.conversation || {};
121
+ if (c.contact_name && !contactName) setContactName(c.contact_name);
122
+ if (c.contact_email && !contactEmail) setContactEmail(c.contact_email);
123
+ if (c.contact_email && !transcriptEmail) setTranscriptEmail(c.contact_email);
124
+ }, [state.conversation, contactName, contactEmail, transcriptEmail]);
125
+
126
+ React.useEffect(() => {
127
+ if (state.messages.length) {
128
+ const timer = setTimeout(() => listRef.current && listRef.current.scrollToEnd({ animated: true }), 50);
129
+ return () => clearTimeout(timer);
130
+ }
131
+ }, [state.messages.length]);
132
+
133
+ const t = key => state.texts[key] || key;
134
+ const palette = makePalette(state.settings);
135
+ const styles = React.useMemo(() => buildStyles(palette), [
136
+ palette.primary,
137
+ palette.background,
138
+ palette.surface,
139
+ palette.surfaceAlt,
140
+ palette.border,
141
+ palette.text,
142
+ palette.muted,
143
+ palette.customerBubble,
144
+ palette.customerText,
145
+ palette.agentBubble,
146
+ palette.agentText,
147
+ ]);
148
+
149
+ async function submitContact() {
150
+ setContactError('');
151
+ if (!contactName.trim() || !/^\S+@\S+\.\S+$/.test(contactEmail.trim())) {
152
+ setContactError(t('invalid_contact'));
153
+ return;
154
+ }
155
+ setBusyAction('contact');
156
+ try {
157
+ await client.saveContact(contactName.trim(), contactEmail.trim());
158
+ } catch (error) {
159
+ setContactError(error.message || t('invalid_contact'));
160
+ } finally {
161
+ setBusyAction('');
162
+ }
163
+ }
164
+
165
+ async function submitMessage() {
166
+ const message = draft.trim();
167
+ if (!message || state.sending) return;
168
+ setDraft('');
169
+ client.sendTyping('');
170
+ try {
171
+ await client.sendMessage(message);
172
+ } catch (_) {
173
+ setDraft(message);
174
+ }
175
+ }
176
+
177
+ async function pick(kind) {
178
+ setAttachOpen(false);
179
+ setBusyAction('upload');
180
+ try {
181
+ await client.pickAndUpload(kind);
182
+ } catch (_) {
183
+ } finally {
184
+ setBusyAction('');
185
+ }
186
+ }
187
+
188
+ async function selectLanguage(code) {
189
+ setBusyAction('language');
190
+ try {
191
+ await client.setTranslationLanguage(code);
192
+ setOptionsOpen(false);
193
+ } finally {
194
+ setBusyAction('');
195
+ }
196
+ }
197
+
198
+ async function closeChat() {
199
+ setBusyAction('close');
200
+ try {
201
+ await client.closeChat();
202
+ setCloseOpen(false);
203
+ } finally {
204
+ setBusyAction('');
205
+ }
206
+ }
207
+
208
+ async function reopenChat() {
209
+ setBusyAction('reopen');
210
+ try {
211
+ await client.reopenChat();
212
+ } finally {
213
+ setBusyAction('');
214
+ }
215
+ }
216
+
217
+ async function sendTranscript() {
218
+ setTranscriptStatus('');
219
+ setBusyAction('transcript');
220
+ try {
221
+ const data = await client.sendTranscript(transcriptEmail.trim());
222
+ setTranscriptStatus(data.verification_required ? t('transcript_verification_sent') : t('transcript_sent'));
223
+ } catch (error) {
224
+ setTranscriptStatus(error.message || t('send_error'));
225
+ } finally {
226
+ setBusyAction('');
227
+ }
228
+ }
229
+
230
+ async function rate(value) {
231
+ setBusyAction('rating');
232
+ setRatingStatus('');
233
+ try {
234
+ await client.rate(value);
235
+ setRatingStatus(value ? t('rating_event_good') : t('rating_event_bad'));
236
+ } catch (_) {
237
+ setRatingStatus(t('rating_error'));
238
+ } finally {
239
+ setBusyAction('');
240
+ }
241
+ }
242
+
243
+ async function sendRatingComment() {
244
+ if (!ratingComment.trim()) return;
245
+ setBusyAction('rating-comment');
246
+ try {
247
+ await client.rateComment(ratingComment.trim());
248
+ setRatingComment('');
249
+ setRatingOpen(false);
250
+ } catch (_) {
251
+ setRatingStatus(t('rating_error'));
252
+ } finally {
253
+ setBusyAction('');
254
+ }
255
+ }
256
+
257
+ function renderMessage({ item }) {
258
+ const sender = String(item.sender_type || '').toLowerCase();
259
+ const system = sender === 'system' || String(item.message_type || '').startsWith('routing_') || String(item.message_type || '').startsWith('rating_');
260
+ const visitor = sender === 'visitor';
261
+
262
+ if (system) {
263
+ return <View style={styles.systemRow}><Text style={styles.systemText}>{item.message}</Text></View>;
264
+ }
265
+
266
+ const bubbleStyle = visitor ? styles.customerBubble : styles.agentBubble;
267
+ const textStyle = visitor ? styles.customerText : styles.agentText;
268
+ const attachment = item.attachment || null;
269
+ const isImage = attachment && String(attachment.mime || '').startsWith('image/');
270
+
271
+ return (
272
+ <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
+ <View style={[styles.bubble, bubbleStyle]}>
275
+ {!visitor && item.sender_name ? <Text style={[styles.senderName, textStyle]}>{item.sender_name}</Text> : null}
276
+ {isImage ? (
277
+ <Pressable onPress={() => Linking.openURL(attachment.url)}>
278
+ <Image source={{ uri: attachment.url }} style={styles.attachmentImage} resizeMode="cover" />
279
+ </Pressable>
280
+ ) : attachment ? (
281
+ <Pressable style={styles.fileAttachment} onPress={() => Linking.openURL(attachment.url)}>
282
+ <Text style={textStyle}>📎 {attachment.name || t('attach_file')}</Text>
283
+ </Pressable>
284
+ ) : null}
285
+ {item.message ? <Text style={[styles.messageText, textStyle]}>{item.message}</Text> : null}
286
+ <Text style={[styles.timeText, textStyle]}>{formatTime(item.date_added)}</Text>
287
+ </View>
288
+ </View>
289
+ );
290
+ }
291
+
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>;
294
+ }
295
+
296
+ if (state.error) {
297
+ 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>
301
+ </View>
302
+ );
303
+ }
304
+
305
+ if (state.banned) return <View style={[styles.root, styles.center, style]}><Text style={styles.errorText}>{t('error')}</Text></View>;
306
+
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);
311
+
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 style={styles.headerStatus}>{state.realtimeConnected ? '● ' : '○ '}{state.realtimeConnected ? t('chat') : t('offline')}</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}
325
+ </View>
326
+ </View>
327
+
328
+ {!contactConfirmed ? (
329
+ <ScrollView contentContainerStyle={styles.contactStage} keyboardShouldPersistTaps="handled">
330
+ <Text style={styles.contactTitle}>{t('start_chat_title')}</Text>
331
+ <Text style={styles.label}>{t('name')}</Text>
332
+ <TextInput style={styles.input} value={contactName} onChangeText={setContactName} maxLength={190} placeholder={t('name')} placeholderTextColor={palette.muted} autoCapitalize="words" />
333
+ <Text style={styles.label}>{t('email')}</Text>
334
+ <TextInput style={styles.input} value={contactEmail} onChangeText={setContactEmail} maxLength={190} placeholder={t('email')} placeholderTextColor={palette.muted} keyboardType="email-address" autoCapitalize="none" />
335
+ {contactError ? <Text style={styles.inlineError}>{contactError}</Text> : null}
336
+ <Pressable disabled={busyAction === 'contact'} style={[styles.primaryButton, busyAction === 'contact' && styles.disabled]} onPress={submitContact}>
337
+ {busyAction === 'contact' ? <ActivityIndicator color="#FFFFFF" /> : <Text style={styles.primaryButtonText}>{t('start_chat')}</Text>}
338
+ </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
+ />
352
+
353
+ {state.remoteTyping ? <View style={styles.typingRow}><Text style={styles.typingText}>{state.remoteTyping === 'chatbot' ? t('chatbot_typing') : t('typing')}</Text></View> : null}
354
+
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>
382
+ )}
383
+ </>
384
+ )}
385
+
386
+ {!state.settings.white_label ? <View style={styles.poweredBy}><Text style={styles.poweredByText}>{t('powered_by')} Regantis</Text></View> : null}
387
+
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>
397
+
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}
418
+ </View>
419
+ </Pressable>
420
+ </Modal>
421
+
422
+ <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>
427
+ </Modal>
428
+
429
+ <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>
436
+ </Modal>
437
+
438
+ <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>
446
+ </Modal>
447
+ </KeyboardAvoidingView>
448
+ );
449
+ }
450
+
451
+ function buildStyles(p) {
452
+ return StyleSheet.create({
453
+ root: { flex: 1, backgroundColor: p.background },
454
+ 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 },
478
+ 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 },
489
+ customerBubble: { backgroundColor: p.customerBubble, borderBottomRightRadius: 5 },
490
+ agentBubble: { backgroundColor: p.agentBubble, borderBottomLeftRadius: 5, borderWidth: StyleSheet.hairlineWidth, borderColor: p.border },
491
+ customerText: { color: p.customerText },
492
+ 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' },
496
+ 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 },
527
+ 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 },
536
+ });
537
+ }
538
+
539
+ module.exports = RegantisChat;