connect-x-reactnative-livechat-sdk 0.2.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.
Files changed (38) hide show
  1. package/ConnectXReactnativeLivechatSdk.podspec +20 -0
  2. package/LICENSE +20 -0
  3. package/README.md +34 -0
  4. package/android/build.gradle +83 -0
  5. package/android/gradle.properties +5 -0
  6. package/android/src/main/AndroidManifest.xml +2 -0
  7. package/android/src/main/java/com/connectxreactnativelivechatsdk/ConnectXReactnativeLivechatSdkModule.kt +23 -0
  8. package/android/src/main/java/com/connectxreactnativelivechatsdk/ConnectXReactnativeLivechatSdkPackage.kt +33 -0
  9. package/ios/ConnectXReactnativeLivechatSdk.h +5 -0
  10. package/ios/ConnectXReactnativeLivechatSdk.mm +18 -0
  11. package/lib/module/ChatScreen.js +834 -0
  12. package/lib/module/ChatScreen.js.map +1 -0
  13. package/lib/module/NativeConnectXReactnativeLivechatSdk.js +5 -0
  14. package/lib/module/NativeConnectXReactnativeLivechatSdk.js.map +1 -0
  15. package/lib/module/SendIcon.js +35 -0
  16. package/lib/module/SendIcon.js.map +1 -0
  17. package/lib/module/api.js +66 -0
  18. package/lib/module/api.js.map +1 -0
  19. package/lib/module/index.js +78 -0
  20. package/lib/module/index.js.map +1 -0
  21. package/lib/module/package.json +1 -0
  22. package/lib/typescript/package.json +1 -0
  23. package/lib/typescript/src/ChatScreen.d.ts +7 -0
  24. package/lib/typescript/src/ChatScreen.d.ts.map +1 -0
  25. package/lib/typescript/src/NativeConnectXReactnativeLivechatSdk.d.ts +7 -0
  26. package/lib/typescript/src/NativeConnectXReactnativeLivechatSdk.d.ts.map +1 -0
  27. package/lib/typescript/src/SendIcon.d.ts +3 -0
  28. package/lib/typescript/src/SendIcon.d.ts.map +1 -0
  29. package/lib/typescript/src/api.d.ts +5 -0
  30. package/lib/typescript/src/api.d.ts.map +1 -0
  31. package/lib/typescript/src/index.d.ts +20 -0
  32. package/lib/typescript/src/index.d.ts.map +1 -0
  33. package/package.json +176 -0
  34. package/src/ChatScreen.tsx +914 -0
  35. package/src/NativeConnectXReactnativeLivechatSdk.ts +8 -0
  36. package/src/SendIcon.tsx +25 -0
  37. package/src/api.ts +79 -0
  38. package/src/index.tsx +91 -0
@@ -0,0 +1,914 @@
1
+ // src/ChatScreen.tsx
2
+ import { useState, useEffect, useRef, useMemo } from 'react';
3
+ import {
4
+ View,
5
+ Text,
6
+ TextInput,
7
+ TouchableOpacity,
8
+ SectionList,
9
+ StyleSheet,
10
+ SafeAreaView,
11
+ KeyboardAvoidingView,
12
+ Platform,
13
+ Image,
14
+ ActivityIndicator,
15
+ Alert,
16
+ Linking,
17
+ Dimensions,
18
+ } from 'react-native';
19
+ import io from 'socket.io-client';
20
+ import type { Socket } from 'socket.io-client';
21
+ import {
22
+ pick,
23
+ types,
24
+ type DocumentPickerResponse,
25
+ } from '@react-native-documents/picker';
26
+ import { ConnectXMobileSdk } from 'connect-x-react-native-sdk';
27
+ import { apiGet, uploadFile, BASE_URL_LIVE_CHAT, apiPost } from './api';
28
+ import Svg, { Path } from 'react-native-svg';
29
+ import hexToRgba from 'hex-to-rgba';
30
+
31
+ interface Message {
32
+ id?: string;
33
+ text?: string;
34
+ sender: 'user' | 'agent';
35
+ timestamp: number;
36
+ type: 'text' | 'image' | 'file' | 'liveChatButtons';
37
+ url?: string;
38
+ fileName?: string;
39
+ buttons?: any[];
40
+ [key: string]: any; // for other properties
41
+ }
42
+
43
+ const SendIcon = ({ color, size }: { color: string; size: number }) => (
44
+ <Svg width={size} height={size} viewBox="0 0 24 24" fill="none">
45
+ <Path
46
+ d="M22 2L15 22L11 13L2 9L22 2Z"
47
+ stroke={color}
48
+ strokeWidth="2"
49
+ strokeLinecap="round"
50
+ strokeLinejoin="round"
51
+ />
52
+ </Svg>
53
+ );
54
+
55
+ // Component หลักของหน้าแชท
56
+ export const ChatScreen = ({
57
+ onClose,
58
+ token,
59
+ orgId,
60
+ channelId,
61
+ }: {
62
+ onClose: () => void;
63
+ token: string;
64
+ orgId: string;
65
+ channelId: string;
66
+ }) => {
67
+ const [messages, setMessages] = useState<Message[]>([]);
68
+ const [inputText, setInputText] = useState('');
69
+ const [loading, setLoading] = useState(true);
70
+ const socketRef = useRef<typeof Socket | null>(null);
71
+ const [userId, setUserId] = useState<string | null>(null);
72
+ const [selectedFile, setSelectedFile] =
73
+ useState<DocumentPickerResponse | null>(null);
74
+ const [styleSettings, setStyleSettings] = useState<any>(null);
75
+ const [styles, setStyles] = useState<any>({});
76
+ const sectionListRef = useRef<SectionList<Message>>(null);
77
+ const [chatPhase, setChatPhase] = useState<'pre-chat' | 'chatting'>(
78
+ 'chatting'
79
+ );
80
+
81
+ useEffect(() => {
82
+ const findCustomer = async (unknownId: String) => {
83
+ try {
84
+ const res = await apiPost(
85
+ `liveChat/${orgId}/${channelId}/setLiveChatConnector`,
86
+ {
87
+ externalValue: "Siwakorn'", //'ปุ้นซู่ซ่า', //sdf0001111111111 //78e839c38397b8cac97437d17b1c91b568a5adb4
88
+ userId: unknownId,
89
+ },
90
+ token
91
+ );
92
+ console.log('Found customer:', res);
93
+ if (res) {
94
+ setUserId(res?.userId);
95
+ } else {
96
+ // initializeUserId();
97
+ }
98
+ } catch (error) {
99
+ // initializeUserId();
100
+ console.log('Error finding customer:', error);
101
+ }
102
+ };
103
+
104
+ const initializeUserId = async () => {
105
+ try {
106
+ const res = await ConnectXMobileSdk.getUnknownId();
107
+ console.log('Generated userId:', res);
108
+ // setUserId(res);
109
+ findCustomer(res);
110
+ } catch (error) {
111
+ console.log('Error generating userId:', error);
112
+ }
113
+ };
114
+ if (!userId) {
115
+ initializeUserId();
116
+ }
117
+ });
118
+
119
+ useEffect(() => {
120
+ const initializeChat = async () => {
121
+ if (!userId) return;
122
+ try {
123
+ console.log('Checking domain/app permissions...');
124
+
125
+ const settings = await apiGet(
126
+ `liveChat/${orgId}/${channelId}/getSettingConnector`,
127
+ token
128
+ );
129
+ setStyleSettings(settings);
130
+ setStyles(getDynamicStyles(settings.styles));
131
+
132
+ const history = await apiGet(
133
+ `liveChat/${orgId}/${channelId}/getChatHistory/${userId}`,
134
+ token
135
+ );
136
+ console.log('Chat history:', history);
137
+ setMessages(history);
138
+
139
+ const socket = io(`${BASE_URL_LIVE_CHAT}/liveChat`, {
140
+ transports: ['websocket'],
141
+ });
142
+ socketRef.current = socket;
143
+
144
+ socket.on('connect', () => {
145
+ console.log('Socket connected!');
146
+ socket.emit('joinRoom', { userId, socketId: socket.id });
147
+ });
148
+
149
+ socket.on('receiveMessage', (payload: { message: Message }) => {
150
+ console.log('Received message:', payload.message);
151
+ setMessages((prev) => [...prev, payload.message]);
152
+ });
153
+
154
+ socket.on('disconnect', (reason: any) => {
155
+ console.log(`Socket disconnected: ${reason}`);
156
+ });
157
+ } catch (error) {
158
+ console.log('Initialization failed:', error);
159
+ // Alert.alert('Error', 'Could not initialize chat.');
160
+ } finally {
161
+ setLoading(false);
162
+ }
163
+ };
164
+
165
+ initializeChat();
166
+
167
+ return () => {
168
+ socketRef.current?.disconnect();
169
+ };
170
+ }, [userId, orgId, channelId, token]);
171
+
172
+ const messageSections = useMemo(() => {
173
+ if (!messages || messages.length === 0) return [];
174
+
175
+ const groupedMessages = messages.reduce(
176
+ (acc, message) => {
177
+ const dateKey = new Date(message.timestamp).toLocaleDateString('en-CA'); // YYYY-MM-DD format for reliable sorting
178
+ if (!acc[dateKey]) {
179
+ acc[dateKey] = [];
180
+ }
181
+ acc[dateKey].push(message);
182
+ return acc;
183
+ },
184
+ {} as { [key: string]: Message[] }
185
+ );
186
+
187
+ return Object.keys(groupedMessages)
188
+ .sort()
189
+ .map((date) => ({
190
+ title: new Date(date).toLocaleDateString('en-GB', {
191
+ day: '2-digit',
192
+ month: '2-digit',
193
+ year: 'numeric',
194
+ }),
195
+ data: groupedMessages[date] ?? [],
196
+ }));
197
+ }, [messages]);
198
+
199
+ // eslint-disable-next-line react/no-unstable-nested-components
200
+ const PreChatForm = ({
201
+ onSubmit,
202
+ headerColor,
203
+ }: {
204
+ onSubmit: (data: any) => void;
205
+ // agentProfile: any;
206
+ // styles: any;
207
+ headerColor: string;
208
+ }) => {
209
+ const [formData, setFormData] = useState<any>({});
210
+ const [consentChecked, setConsentChecked] = useState(false);
211
+
212
+ const handleInputChange = (field: string, value: string) => {
213
+ setFormData((prev: any) => ({ ...prev, [field]: value }));
214
+ };
215
+
216
+ const handleStartChat = () => {
217
+ if (!consentChecked) {
218
+ Alert.alert(
219
+ 'Consent Required',
220
+ 'Please click the consent checkbox to continue.'
221
+ );
222
+ return;
223
+ }
224
+ onSubmit(formData);
225
+ };
226
+
227
+ return (
228
+ <SafeAreaView style={styles.safeArea}>
229
+ <View style={styles.formContainer}>
230
+ <Text style={styles.formWelcomeText}>
231
+ Hello there! Are you looking for an automation program in branding
232
+ for your product?
233
+ </Text>
234
+
235
+ <View style={styles.formInputContainer}>
236
+ <Text style={styles.formLabel}>First Name</Text>
237
+ <TextInput
238
+ style={styles.formInput}
239
+ placeholder="First Name"
240
+ placeholderTextColor={'#BBC1C3'}
241
+ onChangeText={(text) => handleInputChange('firstName', text)}
242
+ />
243
+ </View>
244
+
245
+ <View style={styles.formInputContainer}>
246
+ <Text style={styles.formLabel}>Lorem</Text>
247
+ <TextInput
248
+ style={styles.formInput}
249
+ placeholder="Lorem"
250
+ placeholderTextColor={'#BBC1C3'}
251
+ onChangeText={(text) => handleInputChange('lorem', text)}
252
+ />
253
+ </View>
254
+ </View>
255
+
256
+ <View style={styles.consentContainer}>
257
+ <TouchableOpacity
258
+ onPress={() => setConsentChecked(!consentChecked)}
259
+ style={[
260
+ styles.checkboxBase,
261
+ { backgroundColor: consentChecked ? '#3761DF' : '#fff' },
262
+ ]}
263
+ accessibilityRole="checkbox"
264
+ accessibilityState={{ checked: consentChecked }}
265
+ >
266
+ {consentChecked && <Text style={styles.checkmark}>✓</Text>}
267
+ </TouchableOpacity>
268
+
269
+ <Text style={styles.consentText}>
270
+ <Text style={{ color: 'red' }}>*</Text> Click{' '}
271
+ <Text style={styles.consentLink}>Consent</Text>
272
+ </Text>
273
+ </View>
274
+
275
+ <View style={styles.formFooter}>
276
+ <TouchableOpacity
277
+ style={[styles.startButton, { backgroundColor: headerColor }]}
278
+ onPress={handleStartChat}
279
+ >
280
+ <Text style={styles.startButtonText}>Start Chat</Text>
281
+ </TouchableOpacity>
282
+ </View>
283
+ </SafeAreaView>
284
+ );
285
+ };
286
+
287
+ useEffect(() => {
288
+ console.log('Message sections updated:', messageSections);
289
+ try {
290
+ if (sectionListRef.current && messageSections.length > 0) {
291
+ setTimeout(() => {
292
+ const lastSectionIndex = messageSections.length - 1;
293
+ const lastSection = messageSections[lastSectionIndex];
294
+ console.log(
295
+ 'Scrolling to last section:',
296
+ lastSectionIndex,
297
+ lastSection
298
+ );
299
+ if (
300
+ lastSection &&
301
+ lastSection?.data &&
302
+ lastSection?.data?.length > 0
303
+ ) {
304
+ sectionListRef.current?.scrollToLocation({
305
+ sectionIndex: lastSectionIndex,
306
+ itemIndex: lastSection?.data?.length - 1,
307
+ viewPosition: 0,
308
+ animated: true,
309
+ });
310
+ }
311
+ }, 400);
312
+ }
313
+ } catch (error) {
314
+ console.error('Error scrolling to last section:', error);
315
+ }
316
+ }, [messageSections]);
317
+
318
+ const handleScrollToIndexFailed = (info: {
319
+ index: number;
320
+ highestMeasuredFrameIndex: number;
321
+ averageItemLength: number;
322
+ }) => {
323
+ console.warn('Scroll to index failed:', info);
324
+ // As a fallback, scroll to the last rendered item, which is a safer option.
325
+ try {
326
+ sectionListRef.current?.scrollToLocation({
327
+ sectionIndex: info.highestMeasuredFrameIndex,
328
+ itemIndex: 0,
329
+ viewPosition: 0,
330
+ animated: false,
331
+ });
332
+ } catch (error) {
333
+ console.log('Fallback scroll failed:', error);
334
+ }
335
+ };
336
+
337
+ const sendMessage = (messageBody: Omit<Message, 'sender' | 'timestamp'>) => {
338
+ if (!socketRef.current) return;
339
+ const fullMessage: Message = {
340
+ ...messageBody,
341
+ sender: 'user',
342
+ timestamp: Date.now(),
343
+ type: messageBody.type || 'text',
344
+ };
345
+ socketRef.current.emit(
346
+ 'sendMessage',
347
+ {
348
+ organizeId: orgId,
349
+ channelId: channelId,
350
+ userId: userId,
351
+ message: fullMessage,
352
+ },
353
+ () => {
354
+ if (fullMessage.type === 'text') {
355
+ setInputText('');
356
+ }
357
+ }
358
+ );
359
+ };
360
+
361
+ const handleSendText = () => {
362
+ if (inputText.trim()) {
363
+ sendMessage({ type: 'text', text: inputText.trim() });
364
+ }
365
+ };
366
+
367
+ const handleSelectFile = async () => {
368
+ try {
369
+ const result = await pick({
370
+ type: [types.images, types.doc, types.pdf],
371
+ allowMultiSelection: false,
372
+ });
373
+ setSelectedFile(result[0]);
374
+ } catch (err) {
375
+ if ((err as any)?.code !== 'DOCUMENT_PICKER_CANCELED') {
376
+ Alert.alert(
377
+ 'File-Picker Error',
378
+ 'An error occurred during file selection.'
379
+ );
380
+ }
381
+ }
382
+ };
383
+
384
+ const handleCancelUpload = () => {
385
+ setSelectedFile(null);
386
+ };
387
+
388
+ const handleConfirmUpload = async () => {
389
+ if (!selectedFile) return;
390
+ try {
391
+ const fileUrl = await uploadFile(selectedFile, orgId, channelId);
392
+ const type = selectedFile.type?.startsWith('image/') ? 'image' : 'file';
393
+ sendMessage({
394
+ type,
395
+ fileName: selectedFile.name || 'file',
396
+ url: fileUrl,
397
+ });
398
+ } catch (error) {
399
+ Alert.alert('Upload Error', 'Failed to upload the file.');
400
+ console.error('Upload failed', error);
401
+ } finally {
402
+ setSelectedFile(null);
403
+ }
404
+ };
405
+
406
+ const formatTimestamp = (timestamp: number) => {
407
+ const now = Date.now();
408
+ const messageTime = new Date(timestamp).getTime();
409
+ const differenceInSeconds = (now - messageTime) / 1000;
410
+
411
+ if (differenceInSeconds < 60) {
412
+ return 'Just now';
413
+ }
414
+
415
+ return new Date(timestamp).toLocaleTimeString('en-US', {
416
+ hour: '2-digit',
417
+ minute: '2-digit',
418
+ hour12: false,
419
+ });
420
+ };
421
+
422
+ const renderMessage = ({ item }: { item: Message }) => {
423
+ const handleFilePress = () => {
424
+ if (item.url) {
425
+ Linking.openURL(item.url).catch((err) => {
426
+ console.log('Failed to open URL:', err);
427
+ // Alert.alert('Cannot open file');
428
+ });
429
+ }
430
+ };
431
+
432
+ return (
433
+ <View
434
+ style={[
435
+ styles.messageRow,
436
+ item.sender === 'user' ? styles.userRow : styles.agentRow,
437
+ ]}
438
+ >
439
+ {item.sender === 'agent' && styleSettings?.styles?.styleLogoUrl && (
440
+ <Image
441
+ source={{ uri: styleSettings.styles.styleLogoUrl }}
442
+ style={styles.avatar}
443
+ />
444
+ )}
445
+ <View
446
+ style={
447
+ item.sender === 'agent'
448
+ ? styles.messageAgentContainer
449
+ : styles.messageContainer
450
+ }
451
+ >
452
+ <Text style={styles.timestampText}>
453
+ {formatTimestamp(item.timestamp)}
454
+ </Text>
455
+ <View
456
+ style={[
457
+ styles.messageBubble,
458
+ item.sender === 'user' ? styles.userBubble : styles.agentBubble,
459
+ ]}
460
+ >
461
+ {item.type === 'text' && (
462
+ <Text
463
+ style={
464
+ item.sender === 'user'
465
+ ? styles.customerChatText
466
+ : styles.agentChatText
467
+ }
468
+ >
469
+ {item.text}
470
+ </Text>
471
+ )}
472
+ {item.type === 'image' && (
473
+ <Image
474
+ source={{ uri: item.url }}
475
+ style={styles.imageMessage}
476
+ resizeMode="cover"
477
+ />
478
+ )}
479
+ {item.type === 'file' && (
480
+ <TouchableOpacity onPress={handleFilePress}>
481
+ <Text
482
+ style={[
483
+ styles.fileLink,
484
+ item.sender === 'user'
485
+ ? styles.customerChatText
486
+ : styles.agentChatText,
487
+ ]}
488
+ >
489
+ {item.fileName}
490
+ </Text>
491
+ </TouchableOpacity>
492
+ )}
493
+ </View>
494
+ </View>
495
+ </View>
496
+ );
497
+ };
498
+
499
+ if (loading) {
500
+ return (
501
+ <View style={{ flex: 1, justifyContent: 'center', alignItems: 'center' }}>
502
+ <ActivityIndicator size="large" color={'#3761DF'} />
503
+ </View>
504
+ );
505
+ }
506
+
507
+ // const agentProfile = {
508
+ // name: styleSettings?.name,
509
+ // avatarUrl: styleSettings?.styleLogoUrl,
510
+ // };
511
+
512
+ return (
513
+ <SafeAreaView style={styles.safeArea}>
514
+ <View style={styles.header}>
515
+ <View style={styles.headerProfile}>
516
+ {styleSettings?.styles?.styleLogoUrl && (
517
+ <Image
518
+ source={{ uri: styleSettings.styles.styleLogoUrl }}
519
+ style={styles.headerAvatar}
520
+ />
521
+ )}
522
+ <Text style={styles.headerTitle}>
523
+ {styleSettings?.styles.name || 'Live Chat'}
524
+ </Text>
525
+ </View>
526
+ <TouchableOpacity onPress={onClose}>
527
+ <Text style={styles.closeButtonText}>✕</Text>
528
+ </TouchableOpacity>
529
+ </View>
530
+ <KeyboardAvoidingView style={styles.container}>
531
+ {chatPhase === 'pre-chat' ? (
532
+ <PreChatForm
533
+ onSubmit={() => setChatPhase('chatting')}
534
+ // agentProfile={agentProfile}
535
+ // styles={styles}
536
+ headerColor={styleSettings?.styles?.styleHeadColor}
537
+ />
538
+ ) : (
539
+ <>
540
+ <SectionList
541
+ ref={sectionListRef}
542
+ sections={messageSections}
543
+ renderItem={renderMessage}
544
+ renderSectionHeader={({ section: { title } }) => (
545
+ <View style={styles.datePartition}>
546
+ <Text style={styles.datePartitionText}>{title}</Text>
547
+ </View>
548
+ )}
549
+ keyExtractor={(item) => item.timestamp.toString()}
550
+ style={styles.messageList}
551
+ onScrollToIndexFailed={handleScrollToIndexFailed}
552
+ />
553
+ <View style={styles.inputContainer}>
554
+ {selectedFile ? (
555
+ <View style={styles.uploadPreviewContainer}>
556
+ <View style={styles.fileInfoContainer}>
557
+ <Text style={{ fontSize: 20 }}>📎</Text>
558
+ <Text style={styles.fileNameText} numberOfLines={1}>
559
+ {selectedFile.name}
560
+ </Text>
561
+ </View>
562
+ <View style={styles.buttonRow}>
563
+ <TouchableOpacity
564
+ style={[styles.actionButton, styles.uploadButton]}
565
+ onPress={handleConfirmUpload}
566
+ >
567
+ <Text style={styles.uploadButtonText}>Upload a file</Text>
568
+ </TouchableOpacity>
569
+ <TouchableOpacity
570
+ style={[styles.actionButton, styles.cancelButton]}
571
+ onPress={handleCancelUpload}
572
+ >
573
+ <Text style={styles.cancelButtonText}>Cancel</Text>
574
+ </TouchableOpacity>
575
+ </View>
576
+ </View>
577
+ ) : (
578
+ <View style={styles.normalInputContainer}>
579
+ <TouchableOpacity
580
+ onPress={handleSelectFile}
581
+ style={styles.attachmentButton}
582
+ >
583
+ <View style={styles.plusIconContainer}>
584
+ <View
585
+ style={[
586
+ styles.plusIconBar,
587
+ styles.plusIconBarHorizontal,
588
+ ]}
589
+ />
590
+ <View
591
+ style={[styles.plusIconBar, styles.plusIconBarVertical]}
592
+ />
593
+ </View>
594
+ </TouchableOpacity>
595
+ <TextInput
596
+ style={styles.input}
597
+ value={inputText}
598
+ onChangeText={setInputText}
599
+ placeholder="Enter a message..."
600
+ placeholderTextColor="#9E9E9E"
601
+ />
602
+ <TouchableOpacity
603
+ onPress={handleSendText}
604
+ style={styles.sendButton}
605
+ >
606
+ <SendIcon
607
+ size={22}
608
+ color={styleSettings?.styles?.styleTextColor || '#FFFFFF'}
609
+ />
610
+ </TouchableOpacity>
611
+ </View>
612
+ )}
613
+ </View>
614
+ </>
615
+ )}
616
+ </KeyboardAvoidingView>
617
+ </SafeAreaView>
618
+ );
619
+ };
620
+
621
+ const getDynamicStyles = (settingStyle: any = {}) => {
622
+ const screenWidth = Dimensions.get('window').width;
623
+ // const screenHeight = Dimensions.get('window').height;
624
+ return StyleSheet.create({
625
+ safeArea: {
626
+ flex: 1,
627
+ backgroundColor: '#FFFFFF',
628
+ // padding: 12,
629
+ },
630
+ container: { flex: 1, backgroundColor: '#F4F7FC' },
631
+ centered: { flex: 1, justifyContent: 'center', alignItems: 'center' },
632
+ header: {
633
+ flexDirection: 'row',
634
+ justifyContent: 'space-between',
635
+ alignItems: 'center',
636
+ backgroundColor: settingStyle?.styleHeadColor || '#3761DF',
637
+ paddingVertical: 10,
638
+ paddingHorizontal: 15,
639
+ paddingTop: Platform.OS === 'android' ? 15 : 15,
640
+ },
641
+ headerProfile: {
642
+ flexDirection: 'row',
643
+ alignItems: 'center',
644
+ },
645
+ headerAvatar: {
646
+ width: 40,
647
+ height: 40,
648
+ borderRadius: 20,
649
+ marginRight: 10,
650
+ },
651
+ headerTitle: {
652
+ fontSize: 18,
653
+ fontWeight: 'bold',
654
+ color: settingStyle?.styleTextColor || '#FFFFFF',
655
+ },
656
+ closeButtonText: {
657
+ fontSize: 20,
658
+ color: settingStyle?.styleTextColor || '#FFFFFF',
659
+ fontWeight: 'bold',
660
+ },
661
+ messageList: { flex: 1, paddingHorizontal: 10 },
662
+ messageRow: {
663
+ flexDirection: 'row',
664
+ alignItems: 'flex-end',
665
+ marginVertical: 5,
666
+ },
667
+ agentRow: { justifyContent: 'flex-start' },
668
+ userRow: { justifyContent: 'flex-end' },
669
+ messageContainer: {
670
+ maxWidth: '80%',
671
+ alignItems: 'flex-start',
672
+ flexDirection: 'row',
673
+ // marginBottom: 10,
674
+ },
675
+ messageAgentContainer: {
676
+ maxWidth: '80%',
677
+ alignItems: 'flex-start',
678
+ flexDirection: 'row-reverse',
679
+ marginVertical: 0,
680
+ },
681
+ avatar: { width: 36, height: 36, borderRadius: 20, marginRight: 8 },
682
+ messageBubble: {
683
+ paddingHorizontal: 8,
684
+ paddingVertical: 8,
685
+ borderRadius: 12,
686
+ // width: '80%',
687
+ },
688
+ agentBubble: {
689
+ backgroundColor: settingStyle.styleAgentColor || '#E0E0E0',
690
+ },
691
+ userBubble: {
692
+ backgroundColor: settingStyle.styleCustomerColor || '#E0E0E0',
693
+ },
694
+ customerChatText: {
695
+ color: settingStyle.styleCustomerTextColor || '#000000',
696
+ },
697
+ agentChatText: {
698
+ color: settingStyle.styleAgentTextColor || '#000000',
699
+ },
700
+ fileLink: {
701
+ textDecorationLine: 'underline',
702
+ elevation: 4,
703
+ width: screenWidth * 0.6,
704
+ },
705
+ timestampText: {
706
+ fontSize: 12,
707
+ color: '#888',
708
+ marginTop: 5,
709
+ alignSelf: 'flex-end',
710
+ marginHorizontal: 8,
711
+ },
712
+ datePartition: {
713
+ alignSelf: 'center',
714
+ backgroundColor: 'rgb(244, 243, 244)',
715
+ borderRadius: 16,
716
+ paddingVertical: 5,
717
+ paddingHorizontal: 12,
718
+ marginVertical: 15,
719
+ },
720
+ datePartitionText: {
721
+ color: 'rgb(130, 135, 143)',
722
+ fontSize: 12,
723
+ fontWeight: '600',
724
+ },
725
+ inputContainer: {
726
+ paddingVertical: 10,
727
+ paddingHorizontal: 15,
728
+ borderTopWidth: 1,
729
+ borderTopColor: '#E0E0E0',
730
+ backgroundColor: 'white',
731
+ },
732
+ normalInputContainer: {
733
+ flexDirection: 'row',
734
+ alignItems: 'center',
735
+ },
736
+ input: {
737
+ flex: 1,
738
+ height: 42,
739
+ backgroundColor: '#F0F0F0',
740
+ borderRadius: 22.5,
741
+ paddingHorizontal: 15,
742
+ fontSize: 16,
743
+ marginHorizontal: 10,
744
+ },
745
+ attachmentButton: { padding: 5 },
746
+ sendButton: {
747
+ paddingRight: 3,
748
+ paddingTop: 2,
749
+ width: 42,
750
+ height: 42,
751
+ borderRadius: 24,
752
+ backgroundColor: settingStyle?.styleHeadColor || '#3761DF',
753
+ justifyContent: 'center',
754
+ alignItems: 'center',
755
+ },
756
+ uploadPreviewContainer: {
757
+ alignItems: 'center',
758
+ },
759
+ fileInfoContainer: {
760
+ flexDirection: 'row',
761
+ alignItems: 'center',
762
+ backgroundColor: '#F0F0F0',
763
+ borderRadius: 22.5,
764
+ paddingHorizontal: 15,
765
+ paddingVertical: 12,
766
+ width: '100%',
767
+ },
768
+ fileNameText: {
769
+ flex: 1,
770
+ marginLeft: 10,
771
+ fontSize: 16,
772
+ color: '#333',
773
+ },
774
+ buttonRow: {
775
+ flexDirection: 'row',
776
+ justifyContent: 'center',
777
+ marginTop: 10,
778
+ width: '100%',
779
+ },
780
+ actionButton: {
781
+ flex: 1,
782
+ paddingVertical: 10,
783
+ borderRadius: 8,
784
+ alignItems: 'center',
785
+ marginHorizontal: 5,
786
+ },
787
+ uploadButton: {
788
+ backgroundColor: settingStyle?.styleHeadColor || '#3761DF',
789
+ },
790
+ uploadButtonText: {
791
+ color: settingStyle?.styleTextColor || '#FFFFFF',
792
+ fontWeight: 'bold',
793
+ fontSize: 16,
794
+ },
795
+ cancelButton: {
796
+ backgroundColor: '#E0E0E0',
797
+ },
798
+ cancelButtonText: {
799
+ color: '#333',
800
+ fontWeight: 'bold',
801
+ fontSize: 16,
802
+ },
803
+ plusIconContainer: {
804
+ width: 24,
805
+ height: 24,
806
+ justifyContent: 'center',
807
+ alignItems: 'center',
808
+ },
809
+ plusIconBar: {
810
+ position: 'absolute',
811
+ backgroundColor: '#8A8A8A',
812
+ },
813
+ plusIconBarHorizontal: {
814
+ width: 20,
815
+ height: 2,
816
+ borderRadius: 1,
817
+ },
818
+ plusIconBarVertical: {
819
+ width: 2,
820
+ height: 20,
821
+ borderRadius: 1,
822
+ },
823
+ imageMessage: {
824
+ width: 200,
825
+ height: 200,
826
+ borderRadius: 10,
827
+ },
828
+ formContainer: {
829
+ // flex: 1,
830
+ backgroundColor:
831
+ hexToRgba(settingStyle?.styleHeadColor, '0.5') ||
832
+ hexToRgba('#3761DF', '0.5'),
833
+ // opacity: 0.5,
834
+ padding: 14,
835
+ margin: 14,
836
+ borderRadius: 6,
837
+ },
838
+ formWelcomeText: {
839
+ fontSize: 16,
840
+ color: '#2D3335',
841
+ marginBottom: 20,
842
+ borderRadius: 10,
843
+ },
844
+ formInputContainer: {
845
+ marginBottom: 15,
846
+ },
847
+ formLabel: {
848
+ fontSize: 14,
849
+ color: '#2D3335',
850
+ marginBottom: 5,
851
+ fontWeight: '600',
852
+ },
853
+ formInput: {
854
+ borderWidth: 1,
855
+ borderColor: '#ccc',
856
+ backgroundColor: '#fff',
857
+ color: '#2D3335',
858
+ borderRadius: 8,
859
+ padding: 12,
860
+ fontSize: 16,
861
+ },
862
+ consentContainer: {
863
+ flexDirection: 'row',
864
+ alignItems: 'center',
865
+ marginHorizontal: 14,
866
+ },
867
+ consentText: {
868
+ marginLeft: 2,
869
+ fontSize: 14,
870
+ color: '#333',
871
+ },
872
+ consentLink: {
873
+ color: '#3761DF',
874
+ textDecorationLine: 'underline',
875
+ },
876
+
877
+ checkboxChecked: {
878
+ width: 14,
879
+ height: 14,
880
+ backgroundColor: settingStyle?.styleHeadColor || '#3761DF',
881
+ },
882
+ formFooter: {
883
+ flex: 1,
884
+ justifyContent: 'flex-end',
885
+ padding: 16,
886
+ backgroundColor: 'white',
887
+ },
888
+ startButton: {
889
+ padding: 12,
890
+ borderRadius: 12,
891
+ alignItems: 'center',
892
+ },
893
+ startButtonText: {
894
+ color: 'white',
895
+ fontSize: 16,
896
+ // fontWeight: 'bold',
897
+ },
898
+ checkboxBase: {
899
+ width: 20,
900
+ height: 20,
901
+ borderWidth: 2,
902
+ borderColor: '#3761DF',
903
+ borderRadius: 4,
904
+ alignItems: 'center',
905
+ justifyContent: 'center',
906
+ marginRight: 3,
907
+ },
908
+ checkmark: {
909
+ color: '#fff',
910
+ fontSize: 12,
911
+ fontWeight: 'bold',
912
+ },
913
+ });
914
+ };