@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.
- package/README.md +157 -0
- package/RegantisReactNativeChat.podspec +17 -0
- package/android/build.gradle +24 -0
- package/android/src/main/AndroidManifest.xml +1 -0
- package/android/src/main/java/technology/regantis/chat/RegantisChatNativeModule.java +244 -0
- package/android/src/main/java/technology/regantis/chat/RegantisChatPackage.java +28 -0
- package/index.d.ts +91 -0
- package/index.js +10 -0
- package/ios/RegantisChatNative.h +5 -0
- package/ios/RegantisChatNative.m +191 -0
- package/package.json +35 -0
- package/src/RegantisChat.js +539 -0
- package/src/client.js +630 -0
- package/src/defaults.js +43 -0
- package/src/native.js +56 -0
|
@@ -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;
|