@developer_tribe/react-native-comnyx 0.15.0 → 0.16.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 (185) hide show
  1. package/Comnyx.podspec +10 -2
  2. package/README.md +50 -0
  3. package/android/build.gradle +1 -0
  4. package/android/consumer-rules.pro +23 -0
  5. package/android/generated/java/com/comnyx/NativeComnyxSpec.java +46 -0
  6. package/android/generated/jni/RNComnyxSpec-generated.cpp +23 -1
  7. package/android/generated/jni/RNComnyxSpec.h +7 -0
  8. package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI-generated.cpp +21 -0
  9. package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI.h +70 -0
  10. package/android/src/main/AndroidManifest.xml +11 -1
  11. package/android/src/main/AndroidManifestNew.xml +11 -1
  12. package/android/src/main/java/com/comnyx/ComnyxMediaPickerModule.kt +91 -51
  13. package/android/src/main/java/com/comnyx/ComnyxModule.kt +7 -0
  14. package/android/src/main/java/com/comnyx/src/messaging/firebase/FirebaseMessagingService.kt +4 -6
  15. package/android/src/main/res/xml/comnyx_file_paths.xml +12 -0
  16. package/ios/APNService.swift +9 -9
  17. package/ios/Comnyx.swift +17 -8
  18. package/ios/ComnyxMediaPicker.swift +47 -26
  19. package/ios/ComnyxMessaging.swift +2 -0
  20. package/ios/PrivacyInfo.xcprivacy +32 -0
  21. package/ios/comnyx_post_install.rb +25 -0
  22. package/ios/generated/RCTAppDependencyProvider.h +25 -0
  23. package/ios/generated/RCTAppDependencyProvider.mm +55 -0
  24. package/ios/generated/RCTModulesConformingToProtocolsProvider.h +18 -0
  25. package/ios/generated/RCTModulesConformingToProtocolsProvider.mm +33 -0
  26. package/ios/generated/RCTThirdPartyComponentsProvider.h +16 -0
  27. package/ios/generated/RCTThirdPartyComponentsProvider.mm +23 -0
  28. package/ios/generated/RNComnyxSpec/RNComnyxSpec-generated.mm +53 -0
  29. package/ios/generated/RNComnyxSpec/RNComnyxSpec.h +67 -0
  30. package/ios/generated/RNComnyxSpecJSI-generated.cpp +38 -0
  31. package/ios/generated/RNComnyxSpecJSI.h +89 -0
  32. package/ios/generated/ReactAppDependencyProvider.podspec +34 -0
  33. package/lib/commonjs/NativeComnyxMediaPicker.js +19 -0
  34. package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -1
  35. package/lib/commonjs/api/conversations.js +6 -6
  36. package/lib/commonjs/api/conversations.js.map +1 -1
  37. package/lib/commonjs/api/customers.js +3 -2
  38. package/lib/commonjs/api/customers.js.map +1 -1
  39. package/lib/commonjs/api/media.js +20 -6
  40. package/lib/commonjs/api/media.js.map +1 -1
  41. package/lib/commonjs/api/messages.js +3 -2
  42. package/lib/commonjs/api/messages.js.map +1 -1
  43. package/lib/commonjs/components/ChatList.js +93 -45
  44. package/lib/commonjs/components/ChatList.js.map +1 -1
  45. package/lib/commonjs/components/ComnyxErrorBoundary.js +92 -0
  46. package/lib/commonjs/components/ComnyxErrorBoundary.js.map +1 -0
  47. package/lib/commonjs/components/CustomerForm.js +2 -2
  48. package/lib/commonjs/components/CustomerForm.js.map +1 -1
  49. package/lib/commonjs/components/MediaMessageItem.js +4 -3
  50. package/lib/commonjs/components/MediaMessageItem.js.map +1 -1
  51. package/lib/commonjs/components/MessageInput.js +63 -13
  52. package/lib/commonjs/components/MessageInput.js.map +1 -1
  53. package/lib/commonjs/components/MessageItem.js +1 -1
  54. package/lib/commonjs/components/MessageItem.js.map +1 -1
  55. package/lib/commonjs/hooks/usePolling.js +25 -21
  56. package/lib/commonjs/hooks/usePolling.js.map +1 -1
  57. package/lib/commonjs/hooks/useThemeColors.js +12 -1
  58. package/lib/commonjs/hooks/useThemeColors.js.map +1 -1
  59. package/lib/commonjs/index.js.map +1 -1
  60. package/lib/commonjs/notifications/initializeNotifications.js +19 -16
  61. package/lib/commonjs/notifications/initializeNotifications.js.map +1 -1
  62. package/lib/commonjs/register/Accumulator.js +19 -6
  63. package/lib/commonjs/register/Accumulator.js.map +1 -1
  64. package/lib/commonjs/register/collectData.js +1 -1
  65. package/lib/commonjs/register/collectData.js.map +1 -1
  66. package/lib/commonjs/store/store.js +6 -0
  67. package/lib/commonjs/store/store.js.map +1 -1
  68. package/lib/commonjs/support/ComnyxSupport.js +60 -15
  69. package/lib/commonjs/support/ComnyxSupport.js.map +1 -1
  70. package/lib/commonjs/support/SupportConfigContext.js +24 -0
  71. package/lib/commonjs/support/SupportConfigContext.js.map +1 -0
  72. package/lib/commonjs/types/Theme.js +30 -2
  73. package/lib/commonjs/types/Theme.js.map +1 -1
  74. package/lib/commonjs/version.js +1 -1
  75. package/lib/module/NativeComnyxMediaPicker.js +18 -0
  76. package/lib/module/NativeComnyxMediaPicker.js.map +1 -1
  77. package/lib/module/api/conversations.js +6 -6
  78. package/lib/module/api/conversations.js.map +1 -1
  79. package/lib/module/api/customers.js +3 -2
  80. package/lib/module/api/customers.js.map +1 -1
  81. package/lib/module/api/media.js +21 -6
  82. package/lib/module/api/media.js.map +1 -1
  83. package/lib/module/api/messages.js +3 -2
  84. package/lib/module/api/messages.js.map +1 -1
  85. package/lib/module/components/ChatList.js +94 -46
  86. package/lib/module/components/ChatList.js.map +1 -1
  87. package/lib/module/components/ComnyxErrorBoundary.js +87 -0
  88. package/lib/module/components/ComnyxErrorBoundary.js.map +1 -0
  89. package/lib/module/components/CustomerForm.js +2 -2
  90. package/lib/module/components/CustomerForm.js.map +1 -1
  91. package/lib/module/components/MediaMessageItem.js +4 -3
  92. package/lib/module/components/MediaMessageItem.js.map +1 -1
  93. package/lib/module/components/MessageInput.js +64 -14
  94. package/lib/module/components/MessageInput.js.map +1 -1
  95. package/lib/module/components/MessageItem.js +1 -1
  96. package/lib/module/components/MessageItem.js.map +1 -1
  97. package/lib/module/hooks/usePolling.js +25 -21
  98. package/lib/module/hooks/usePolling.js.map +1 -1
  99. package/lib/module/hooks/useThemeColors.js +13 -2
  100. package/lib/module/hooks/useThemeColors.js.map +1 -1
  101. package/lib/module/index.js +0 -1
  102. package/lib/module/index.js.map +1 -1
  103. package/lib/module/notifications/initializeNotifications.js +19 -16
  104. package/lib/module/notifications/initializeNotifications.js.map +1 -1
  105. package/lib/module/register/Accumulator.js +19 -6
  106. package/lib/module/register/Accumulator.js.map +1 -1
  107. package/lib/module/register/collectData.js +1 -1
  108. package/lib/module/register/collectData.js.map +1 -1
  109. package/lib/module/store/store.js +6 -0
  110. package/lib/module/store/store.js.map +1 -1
  111. package/lib/module/support/ComnyxSupport.js +61 -16
  112. package/lib/module/support/ComnyxSupport.js.map +1 -1
  113. package/lib/module/support/SupportConfigContext.js +19 -0
  114. package/lib/module/support/SupportConfigContext.js.map +1 -0
  115. package/lib/module/types/Theme.js +30 -2
  116. package/lib/module/types/Theme.js.map +1 -1
  117. package/lib/module/version.js +1 -1
  118. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +9 -0
  119. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -1
  120. package/lib/typescript/src/api/conversations.d.ts +2 -2
  121. package/lib/typescript/src/api/conversations.d.ts.map +1 -1
  122. package/lib/typescript/src/api/customers.d.ts +1 -1
  123. package/lib/typescript/src/api/customers.d.ts.map +1 -1
  124. package/lib/typescript/src/api/media.d.ts +3 -3
  125. package/lib/typescript/src/api/media.d.ts.map +1 -1
  126. package/lib/typescript/src/api/messages.d.ts +1 -1
  127. package/lib/typescript/src/api/messages.d.ts.map +1 -1
  128. package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
  129. package/lib/typescript/src/components/ComnyxErrorBoundary.d.ts +18 -0
  130. package/lib/typescript/src/components/ComnyxErrorBoundary.d.ts.map +1 -0
  131. package/lib/typescript/src/components/MediaMessageItem.d.ts.map +1 -1
  132. package/lib/typescript/src/components/MessageInput.d.ts.map +1 -1
  133. package/lib/typescript/src/components/MessageItem.d.ts.map +1 -1
  134. package/lib/typescript/src/hooks/usePolling.d.ts.map +1 -1
  135. package/lib/typescript/src/hooks/useThemeColors.d.ts.map +1 -1
  136. package/lib/typescript/src/index.d.ts +2 -0
  137. package/lib/typescript/src/index.d.ts.map +1 -1
  138. package/lib/typescript/src/notifications/initializeNotifications.d.ts.map +1 -1
  139. package/lib/typescript/src/register/Accumulator.d.ts.map +1 -1
  140. package/lib/typescript/src/register/collectData.d.ts +4 -1
  141. package/lib/typescript/src/register/collectData.d.ts.map +1 -1
  142. package/lib/typescript/src/store/store.d.ts +6 -2
  143. package/lib/typescript/src/store/store.d.ts.map +1 -1
  144. package/lib/typescript/src/support/ComnyxSupport.d.ts +56 -2
  145. package/lib/typescript/src/support/ComnyxSupport.d.ts.map +1 -1
  146. package/lib/typescript/src/support/SupportConfigContext.d.ts +58 -0
  147. package/lib/typescript/src/support/SupportConfigContext.d.ts.map +1 -0
  148. package/lib/typescript/src/support/index.d.ts +1 -0
  149. package/lib/typescript/src/support/index.d.ts.map +1 -1
  150. package/lib/typescript/src/types/Conversation.d.ts +2 -2
  151. package/lib/typescript/src/types/Conversation.d.ts.map +1 -1
  152. package/lib/typescript/src/types/Customer.d.ts +1 -1
  153. package/lib/typescript/src/types/Customer.d.ts.map +1 -1
  154. package/lib/typescript/src/types/MessageResponse.d.ts +7 -4
  155. package/lib/typescript/src/types/MessageResponse.d.ts.map +1 -1
  156. package/lib/typescript/src/types/Theme.d.ts +26 -0
  157. package/lib/typescript/src/types/Theme.d.ts.map +1 -1
  158. package/lib/typescript/src/version.d.ts +1 -1
  159. package/package.json +12 -25
  160. package/src/NativeComnyxMediaPicker.ts +18 -0
  161. package/src/api/conversations.ts +6 -4
  162. package/src/api/customers.ts +3 -1
  163. package/src/api/media.ts +32 -10
  164. package/src/api/messages.ts +3 -1
  165. package/src/components/ChatList.tsx +115 -55
  166. package/src/components/ComnyxErrorBoundary.tsx +91 -0
  167. package/src/components/CustomerForm.tsx +2 -2
  168. package/src/components/MediaMessageItem.tsx +10 -3
  169. package/src/components/MessageInput.tsx +89 -16
  170. package/src/components/MessageItem.tsx +12 -13
  171. package/src/hooks/usePolling.ts +21 -11
  172. package/src/hooks/useThemeColors.ts +11 -2
  173. package/src/index.ts +12 -0
  174. package/src/notifications/initializeNotifications.ts +22 -20
  175. package/src/register/Accumulator.ts +26 -9
  176. package/src/register/collectData.ts +10 -2
  177. package/src/store/store.ts +11 -3
  178. package/src/support/ComnyxSupport.tsx +128 -22
  179. package/src/support/SupportConfigContext.tsx +79 -0
  180. package/src/support/index.ts +7 -0
  181. package/src/types/Conversation.ts +2 -2
  182. package/src/types/Customer.ts +1 -2
  183. package/src/types/MessageResponse.ts +4 -4
  184. package/src/types/Theme.ts +38 -0
  185. package/src/version.ts +1 -1
@@ -31,6 +31,7 @@ import { ScaledSheet } from './ScaledSheet';
31
31
  import { formatDate, getDateKey } from '../utils/formatDate';
32
32
  import { activeOpacity } from '../constants/activeOpacity';
33
33
  import { useAppStore } from '../store/store';
34
+ import { useSupportConfig } from '../support/SupportConfigContext';
34
35
 
35
36
  const headphonesIcon = require('../assets/headphones-01.png');
36
37
  const closeIcon = require('../assets/x-close.png');
@@ -110,6 +111,7 @@ function CustomToast() {
110
111
  const { toastMessage } = useAppStore((s) => ({
111
112
  toastMessage: s.toastMessage,
112
113
  }));
114
+ const themeColors = useThemeColors();
113
115
 
114
116
  const fadeAnim = useRef(new Animated.Value(0)).current;
115
117
  const translateY = useRef(new Animated.Value(50)).current;
@@ -157,8 +159,16 @@ function CustomToast() {
157
159
  },
158
160
  ]}
159
161
  >
160
- <View style={styles.toastContent}>
161
- <AppText style={styles.toastText} localization={toastMessage} />
162
+ <View
163
+ style={[
164
+ styles.toastContent,
165
+ { backgroundColor: themeColors.toastBackground },
166
+ ]}
167
+ >
168
+ <AppText
169
+ style={[styles.toastText, { color: themeColors.toastText }]}
170
+ localization={toastMessage}
171
+ />
162
172
  </View>
163
173
  </Animated.View>
164
174
  );
@@ -174,6 +184,12 @@ export function ChatList({
174
184
  const themeColors = useThemeColors();
175
185
  const { height: windowHeight } = useWindowDimensions();
176
186
  const insets = useSafeAreaInsets();
187
+ const {
188
+ renderHeader: renderHeaderOverride,
189
+ renderEmptyState: renderEmptyStateOverride,
190
+ renderErrorState: renderErrorStateOverride,
191
+ renderMessage: renderMessageOverride,
192
+ } = useSupportConfig();
177
193
  const MESSAGE_MIN_HEIGHT = 60;
178
194
  const FLATLIST_PADDING = 20;
179
195
  const MESSAGES_PER_PAGE = Math.ceil(
@@ -184,6 +200,17 @@ export function ChatList({
184
200
  setLoading((l) => l && initLoading);
185
201
  }, [initLoading]);
186
202
 
203
+ const lifecycleAbortRef = useRef<AbortController | null>(null);
204
+ if (lifecycleAbortRef.current === null) {
205
+ lifecycleAbortRef.current = new AbortController();
206
+ }
207
+ useEffect(() => {
208
+ const controller = lifecycleAbortRef.current;
209
+ return () => {
210
+ controller?.abort();
211
+ };
212
+ }, []);
213
+
187
214
  const { data, setData, customer, language } = useAppStore((s) => ({
188
215
  data: s.data,
189
216
  setData: s.setData,
@@ -192,7 +219,9 @@ export function ChatList({
192
219
  }));
193
220
  const ref = useRef<SectionList<AppConversationMessage>>(null);
194
221
  const [page, setPage] = useState(1);
195
- const nextPageStatus = useRef<'fail' | 'loading' | 'empty'>();
222
+ const nextPageStatus = useRef<'fail' | 'loading' | 'empty' | undefined>(
223
+ undefined
224
+ );
196
225
  const [nexPageFailed, setNexPageFailed] = useState(false);
197
226
  const [initFailed, setInitFailed] = useState(false);
198
227
  const [showScrollDownButton, setShowScrollDownButton] = useState(false);
@@ -398,7 +427,8 @@ export function ChatList({
398
427
  {
399
428
  fake: useAppStore.getState().fake,
400
429
  per_page: MESSAGES_PER_PAGE,
401
- }
430
+ },
431
+ lifecycleAbortRef.current?.signal
402
432
  )
403
433
  .then((newData) => {
404
434
  listChangedRef.current = true;
@@ -445,7 +475,7 @@ export function ChatList({
445
475
 
446
476
  const renderItem = useCallback(
447
477
  ({ item }: { item: AppConversationMessage }) => {
448
- return (
478
+ const defaultNode = (
449
479
  <MessageItem
450
480
  item={item}
451
481
  onShowPopup={() => {
@@ -454,8 +484,12 @@ export function ChatList({
454
484
  }}
455
485
  />
456
486
  );
487
+ if (renderMessageOverride) {
488
+ return <>{renderMessageOverride({ message: item, defaultNode })}</>;
489
+ }
490
+ return defaultNode;
457
491
  },
458
- []
492
+ [renderMessageOverride]
459
493
  );
460
494
 
461
495
  const renderSectionFooter = useCallback(
@@ -515,10 +549,16 @@ export function ChatList({
515
549
 
516
550
  useEffect(() => {
517
551
  if (customer?.external_id && !initFailed) {
518
- getCustomerConversation(customer?.external_id, new Date(), 1, {
519
- fake: useAppStore.getState().fake,
520
- per_page: MESSAGES_PER_PAGE,
521
- })
552
+ getCustomerConversation(
553
+ customer?.external_id,
554
+ new Date(),
555
+ 1,
556
+ {
557
+ fake: useAppStore.getState().fake,
558
+ per_page: MESSAGES_PER_PAGE,
559
+ },
560
+ lifecycleAbortRef.current?.signal
561
+ )
522
562
  .then((newData) => {
523
563
  setData((prevData) => {
524
564
  const newMessages = newData?.page?.data ?? [];
@@ -609,6 +649,11 @@ export function ChatList({
609
649
  }
610
650
 
611
651
  if (initFailed) {
652
+ if (renderErrorStateOverride) {
653
+ return (
654
+ <>{renderErrorStateOverride({ retry: () => setInitFailed(false) })}</>
655
+ );
656
+ }
612
657
  return <InitFailed setInitFailed={setInitFailed} />;
613
658
  }
614
659
 
@@ -633,49 +678,60 @@ export function ChatList({
633
678
  },
634
679
  ]}
635
680
  >
636
- <TouchableOpacity
637
- style={[styles.iconContainer, { top: insets.top || 40 }]}
638
- onPress={onBack}
639
- activeOpacity={activeOpacity}
640
- >
641
- <Image
642
- source={closeIcon}
643
- style={[styles.closeIcon, { tintColor: themeColors.text }]}
644
- />
645
- </TouchableOpacity>
646
- <View
647
- style={[
648
- styles.headerContainer,
649
- {
650
- backgroundColor: themeColors.background,
651
- borderBottomColor: themeColors.lavender,
652
- },
653
- ]}
654
- >
655
- <AppText
656
- localization="chat.support-team"
657
- weight={'600'}
658
- style={[styles.header, { color: themeColors.text }]}
659
- />
660
- <View
661
- style={[
662
- styles.headerText,
663
- { backgroundColor: themeColors.background },
664
- ]}
665
- >
666
- <Image
667
- source={headphonesIcon}
668
- style={[styles.headphonesIcon, { tintColor: themeColors.text }]}
669
- />
670
- <AppText
671
- localization="chat.live"
672
- style={[styles.liveChat, { color: themeColors.text }]}
673
- />
681
+ {renderHeaderOverride ? (
682
+ <>{renderHeaderOverride({ onBack })}</>
683
+ ) : (
684
+ <>
685
+ <TouchableOpacity
686
+ style={[styles.iconContainer, { top: insets.top || 40 }]}
687
+ onPress={onBack}
688
+ activeOpacity={activeOpacity}
689
+ accessibilityRole="button"
690
+ accessibilityLabel="Close support"
691
+ >
692
+ <Image
693
+ source={closeIcon}
694
+ style={[styles.closeIcon, { tintColor: themeColors.text }]}
695
+ />
696
+ </TouchableOpacity>
674
697
  <View
675
- style={[styles.dot, { backgroundColor: themeColors.green }]}
676
- />
677
- </View>
678
- </View>
698
+ style={[
699
+ styles.headerContainer,
700
+ {
701
+ backgroundColor: themeColors.background,
702
+ borderBottomColor: themeColors.lavender,
703
+ },
704
+ ]}
705
+ >
706
+ <AppText
707
+ localization="chat.support-team"
708
+ weight={'600'}
709
+ style={[styles.header, { color: themeColors.text }]}
710
+ />
711
+ <View
712
+ style={[
713
+ styles.headerText,
714
+ { backgroundColor: themeColors.background },
715
+ ]}
716
+ >
717
+ <Image
718
+ source={headphonesIcon}
719
+ style={[
720
+ styles.headphonesIcon,
721
+ { tintColor: themeColors.text },
722
+ ]}
723
+ />
724
+ <AppText
725
+ localization="chat.live"
726
+ style={[styles.liveChat, { color: themeColors.text }]}
727
+ />
728
+ <View
729
+ style={[styles.dot, { backgroundColor: themeColors.green }]}
730
+ />
731
+ </View>
732
+ </View>
733
+ </>
734
+ )}
679
735
 
680
736
  <View style={styles.listContainer}>
681
737
  <SectionList
@@ -691,7 +747,11 @@ export function ChatList({
691
747
  style={styles.list}
692
748
  ListEmptyComponent={
693
749
  !loading && (!sections || sections.length === 0) ? (
694
- <EmptyList />
750
+ renderEmptyStateOverride ? (
751
+ <>{renderEmptyStateOverride()}</>
752
+ ) : (
753
+ <EmptyList />
754
+ )
695
755
  ) : null
696
756
  }
697
757
  ListFooterComponent={loading ? <LoadingItem /> : null}
@@ -722,6 +782,8 @@ export function ChatList({
722
782
  isKeyboardVisible && styles.scrollDownButtonWithKeyboard,
723
783
  ]}
724
784
  onPress={() => scrollToBottom(true)}
785
+ accessibilityRole="button"
786
+ accessibilityLabel="Scroll to latest message"
725
787
  >
726
788
  <Image
727
789
  source={require('../assets/down.png')}
@@ -880,7 +942,6 @@ const styles = ScaledSheet.create({
880
942
  zIndex: 9999,
881
943
  },
882
944
  toastContent: {
883
- backgroundColor: '#333333',
884
945
  borderRadius: '8@s',
885
946
  paddingHorizontal: '16@s',
886
947
  paddingVertical: '12@vs',
@@ -895,7 +956,6 @@ const styles = ScaledSheet.create({
895
956
  maxWidth: '90%',
896
957
  },
897
958
  toastText: {
898
- color: '#FFFFFF',
899
959
  fontSize: '14@vs',
900
960
  fontWeight: '400',
901
961
  textAlign: 'center',
@@ -0,0 +1,91 @@
1
+ import { Component, type ReactNode, type ErrorInfo } from 'react';
2
+ import { View, TouchableOpacity, Image, Text } from 'react-native';
3
+ import { ScaledSheet } from './ScaledSheet';
4
+
5
+ const closeIcon = require('../assets/x-close.png');
6
+
7
+ interface Props {
8
+ children: ReactNode;
9
+ onBack?: () => void;
10
+ fallback?: (reset: () => void) => ReactNode;
11
+ }
12
+
13
+ interface State {
14
+ hasError: boolean;
15
+ }
16
+
17
+ export class ComnyxErrorBoundary extends Component<Props, State> {
18
+ state: State = { hasError: false };
19
+
20
+ static getDerivedStateFromError(): State {
21
+ return { hasError: true };
22
+ }
23
+
24
+ componentDidCatch(error: Error, info: ErrorInfo) {
25
+ console.error('[Comnyx] UI error caught by boundary', error, info);
26
+ }
27
+
28
+ reset = () => {
29
+ this.setState({ hasError: false });
30
+ };
31
+
32
+ render() {
33
+ if (!this.state.hasError) {
34
+ return this.props.children;
35
+ }
36
+
37
+ if (this.props.fallback) {
38
+ return this.props.fallback(this.reset);
39
+ }
40
+
41
+ return (
42
+ <View style={styles.container}>
43
+ {this.props.onBack ? (
44
+ <TouchableOpacity
45
+ activeOpacity={1}
46
+ style={styles.iconContainer}
47
+ onPress={this.props.onBack}
48
+ >
49
+ <Image source={closeIcon} style={styles.closeIcon} />
50
+ </TouchableOpacity>
51
+ ) : null}
52
+ <Text style={styles.message}>Something went wrong.</Text>
53
+ <TouchableOpacity onPress={this.reset} style={styles.retryButton}>
54
+ <Text style={styles.retryLabel}>Retry</Text>
55
+ </TouchableOpacity>
56
+ </View>
57
+ );
58
+ }
59
+ }
60
+
61
+ const styles = ScaledSheet.create({
62
+ container: {
63
+ flex: 1,
64
+ alignItems: 'center',
65
+ justifyContent: 'center',
66
+ paddingHorizontal: '20@s',
67
+ paddingVertical: '20@vs',
68
+ },
69
+ iconContainer: {
70
+ position: 'absolute',
71
+ top: '60@vs',
72
+ left: '24@s',
73
+ },
74
+ closeIcon: {
75
+ width: '24@vs',
76
+ height: '24@vs',
77
+ },
78
+ message: {
79
+ textAlign: 'center',
80
+ fontSize: '14@vs',
81
+ },
82
+ retryButton: {
83
+ marginTop: '16@vs',
84
+ paddingHorizontal: '16@s',
85
+ paddingVertical: '10@vs',
86
+ },
87
+ retryLabel: {
88
+ fontSize: '14@vs',
89
+ fontWeight: '500',
90
+ },
91
+ });
@@ -244,7 +244,7 @@ export function CustomerForm({
244
244
  },
245
245
  ]}
246
246
  >
247
- Email
247
+ {localize('customer.form.email')}
248
248
  </AppText>
249
249
  <Controller
250
250
  control={control}
@@ -291,7 +291,7 @@ export function CustomerForm({
291
291
  },
292
292
  ]}
293
293
  >
294
- Phone
294
+ {localize('customer.form.phone')}
295
295
  </AppText>
296
296
  <Controller
297
297
  control={control}
@@ -30,6 +30,7 @@ function MediaThumbnail({
30
30
  const [generatedThumb, setGeneratedThumb] = useState<string | null>(null);
31
31
  const displayUri = file.local_uri || file.url;
32
32
  const isVideo = file.type === 'video';
33
+ const themeColors = useThemeColors();
33
34
 
34
35
  useEffect(() => {
35
36
  if (isVideo && !file.thumbnail_uri && !isUploading && displayUri) {
@@ -62,8 +63,14 @@ function MediaThumbnail({
62
63
  const wrapperStyle = single ? styles.singleMediaWrapper : styles.gridItem;
63
64
  const imageStyle = single ? styles.singleMediaImage : styles.gridImage;
64
65
  const placeholderStyle = single
65
- ? [styles.singleMediaImage, { backgroundColor: '#1a1a2e' }]
66
- : [styles.videoPlaceholder, { backgroundColor: '#1a1a2e' }];
66
+ ? [
67
+ styles.singleMediaImage,
68
+ { backgroundColor: themeColors.videoPlaceholder },
69
+ ]
70
+ : [
71
+ styles.videoPlaceholder,
72
+ { backgroundColor: themeColors.videoPlaceholder },
73
+ ];
67
74
 
68
75
  return (
69
76
  <TouchableOpacity
@@ -234,7 +241,7 @@ export function MediaMessageItem({
234
241
  {
235
242
  regex:
236
243
  /https?:\/\/(?:[-\w.])+(?::[0-9]+)?(?:\/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:[\w.])*)?)?/g,
237
- color: '#0066CC',
244
+ color: themeColors.link,
238
245
  navigate: (url) => {
239
246
  if (url) {
240
247
  Linking.openURL(url);
@@ -5,7 +5,7 @@ import {
5
5
  TouchableOpacity,
6
6
  ScrollView,
7
7
  } from 'react-native';
8
- import { useState, useCallback } from 'react';
8
+ import { useState, useCallback, useEffect, useRef } from 'react';
9
9
  import { sendCustomerMessage } from '../api';
10
10
  import { getUploadUrl, uploadFileToS3, sendMediaMessage } from '../api';
11
11
  import { deleteTempFile } from '../NativeComnyxMediaPicker';
@@ -17,6 +17,7 @@ import { useIsRtl } from '../hooks/isRtl';
17
17
  import { useAppStore } from '../store/store';
18
18
  import { MediaPickerButton } from './MediaPickerButton';
19
19
  import type { MediaAsset } from '../types/MediaTypes';
20
+ import { useSupportConfig } from '../support/SupportConfigContext';
20
21
 
21
22
  const sendDark = require('../assets/arrow-right.png');
22
23
  const circleXIcon = require('../assets/x-circle.png');
@@ -34,6 +35,20 @@ export function MessageInput({
34
35
  const themeColors = useThemeColors();
35
36
  const localize = useLocalize();
36
37
  const isRtl = useIsRtl();
38
+ const { onBeforeSend } = useSupportConfig();
39
+
40
+ const uploadAbortRef = useRef<AbortController | null>(null);
41
+ const lifecycleAbortRef = useRef<AbortController | null>(null);
42
+ if (lifecycleAbortRef.current === null) {
43
+ lifecycleAbortRef.current = new AbortController();
44
+ }
45
+ useEffect(() => {
46
+ const controller = lifecycleAbortRef.current;
47
+ return () => {
48
+ uploadAbortRef.current?.abort();
49
+ controller?.abort();
50
+ };
51
+ }, []);
37
52
 
38
53
  const sendTextOnlyMessage = useCallback(() => {
39
54
  if (!value.trim()) return;
@@ -53,10 +68,14 @@ export function MessageInput({
53
68
  },
54
69
  ...(data ?? []),
55
70
  ]);
56
- sendCustomerMessage(customer.external_id as string, value, {
57
- fake: useAppStore.getState().fake,
58
- })
71
+ sendCustomerMessage(
72
+ customer.external_id as string,
73
+ value,
74
+ { fake: useAppStore.getState().fake },
75
+ lifecycleAbortRef.current?.signal
76
+ )
59
77
  .then((res) => {
78
+ if (lifecycleAbortRef.current?.signal.aborted) return;
60
79
  const data = useAppStore.getState().data;
61
80
  if (data) {
62
81
  const itemIndex = data.findIndex((item) => item.local_id === localId);
@@ -114,6 +133,7 @@ export function MessageInput({
114
133
  async (
115
134
  asset: MediaAsset,
116
135
  isFake: boolean,
136
+ signal: AbortSignal,
117
137
  onProgress?: (percentage: number) => void
118
138
  ): Promise<string> => {
119
139
  const presignResponse = await getUploadUrl(
@@ -124,9 +144,14 @@ export function MessageInput({
124
144
  const uploadUrl = presignResponse.data.url;
125
145
  const filePath = presignResponse.data.path;
126
146
 
127
- await uploadFileToS3(uploadUrl, asset.uri, asset.mimeType, onProgress, {
128
- fake: isFake,
129
- });
147
+ await uploadFileToS3(
148
+ uploadUrl,
149
+ asset.uri,
150
+ asset.mimeType,
151
+ onProgress,
152
+ { fake: isFake },
153
+ signal
154
+ );
130
155
 
131
156
  return isFake ? asset.uri : filePath;
132
157
  },
@@ -166,6 +191,10 @@ export function MessageInput({
166
191
  ]);
167
192
  scrollToBottom(false);
168
193
 
194
+ uploadAbortRef.current?.abort();
195
+ const controller = new AbortController();
196
+ uploadAbortRef.current = controller;
197
+
169
198
  try {
170
199
  const fileProgresses = new Array(assets.length).fill(0);
171
200
  let lastProgressUpdate = 0;
@@ -198,6 +227,7 @@ export function MessageInput({
198
227
  const path = await uploadSingleMedia(
199
228
  assets[i]!,
200
229
  isFake,
230
+ controller.signal,
201
231
  (percentage) => {
202
232
  fileProgresses[i] = percentage;
203
233
  updateOverallProgress();
@@ -211,7 +241,8 @@ export function MessageInput({
211
241
  uploadedPaths,
212
242
  assets[0]!.type,
213
243
  content,
214
- { fake: isFake }
244
+ { fake: isFake },
245
+ controller.signal
215
246
  );
216
247
 
217
248
  const currentData = useAppStore.getState().data;
@@ -265,6 +296,10 @@ export function MessageInput({
265
296
  }
266
297
  }
267
298
  console.error('[Comnyx] Media upload failed:', error);
299
+ } finally {
300
+ if (uploadAbortRef.current === controller) {
301
+ uploadAbortRef.current = null;
302
+ }
268
303
  }
269
304
  },
270
305
  [customer, scrollToBottom, uploadSingleMedia]
@@ -294,18 +329,45 @@ export function MessageInput({
294
329
  const handleSend = useCallback(async () => {
295
330
  if (isSending) return;
296
331
 
297
- if (pendingMedia.length > 0) {
298
- setIsSending(true);
332
+ const hasMedia = pendingMedia.length > 0;
333
+ const hasText = !!value.trim();
334
+ if (!hasMedia && !hasText) return;
335
+
336
+ if (onBeforeSend) {
337
+ try {
338
+ setIsSending(true);
339
+ await onBeforeSend({
340
+ content: value.trim(),
341
+ mediaCount: pendingMedia.length,
342
+ mediaTypes: pendingMedia.map((m) => m.type),
343
+ });
344
+ } catch (err) {
345
+ console.warn('[Comnyx] onBeforeSend cancelled the message:', err);
346
+ setIsSending(false);
347
+ return;
348
+ }
349
+ }
350
+
351
+ if (hasMedia) {
352
+ if (!onBeforeSend) setIsSending(true);
299
353
  const content = value.trim();
300
354
  const assets = [...pendingMedia];
301
355
  setPendingMedia([]);
302
356
  setValue('');
303
357
  await sendMediaMessages(assets, content);
304
358
  setIsSending(false);
305
- } else if (value.trim()) {
359
+ } else if (hasText) {
306
360
  sendTextOnlyMessage();
361
+ if (onBeforeSend) setIsSending(false);
307
362
  }
308
- }, [isSending, pendingMedia, value, sendMediaMessages, sendTextOnlyMessage]);
363
+ }, [
364
+ isSending,
365
+ pendingMedia,
366
+ value,
367
+ sendMediaMessages,
368
+ sendTextOnlyMessage,
369
+ onBeforeSend,
370
+ ]);
309
371
 
310
372
  const hasPendingMedia = pendingMedia.length > 0;
311
373
 
@@ -335,8 +397,18 @@ export function MessageInput({
335
397
  />
336
398
  {asset.type === 'video' && (
337
399
  <View style={styles.playIconOverlay}>
338
- <View style={styles.playIcon}>
339
- <View style={styles.playTriangle} />
400
+ <View
401
+ style={[
402
+ styles.playIcon,
403
+ { borderColor: themeColors.playIcon },
404
+ ]}
405
+ >
406
+ <View
407
+ style={[
408
+ styles.playTriangle,
409
+ { borderLeftColor: themeColors.playIcon },
410
+ ]}
411
+ />
340
412
  </View>
341
413
  </View>
342
414
  )}
@@ -375,6 +447,9 @@ export function MessageInput({
375
447
  onPress={handleSend}
376
448
  activeOpacity={activeOpacity}
377
449
  disabled={isSending}
450
+ accessibilityRole="button"
451
+ accessibilityLabel={localize('chat.messageInput.placeholder')}
452
+ accessibilityState={{ disabled: isSending, busy: isSending }}
378
453
  >
379
454
  <Image
380
455
  style={[
@@ -441,7 +516,6 @@ const styles = ScaledSheet.create({
441
516
  height: '22@vs',
442
517
  borderRadius: '11@vs',
443
518
  borderWidth: 1.5,
444
- borderColor: '#fff',
445
519
  justifyContent: 'center',
446
520
  alignItems: 'center',
447
521
  },
@@ -451,7 +525,6 @@ const styles = ScaledSheet.create({
451
525
  borderLeftWidth: '7@vs',
452
526
  borderTopWidth: '5@vs',
453
527
  borderBottomWidth: '5@vs',
454
- borderLeftColor: '#fff',
455
528
  borderTopColor: 'transparent',
456
529
  borderBottomColor: 'transparent',
457
530
  marginLeft: '2@s',
@@ -1,4 +1,11 @@
1
- import { View, Image, TouchableOpacity, Linking } from 'react-native';
1
+ import {
2
+ View,
3
+ Image,
4
+ TouchableOpacity,
5
+ Linking,
6
+ type NativeSyntheticEvent,
7
+ type TextLayoutEventData,
8
+ } from 'react-native';
2
9
  import { AppText } from './AppText';
3
10
  import type { AppConversationMessage } from '../types/Conversation';
4
11
  import { useThemeColors } from '../hooks/useThemeColors';
@@ -11,14 +18,6 @@ import { MediaMessageItem } from './MediaMessageItem';
11
18
  const clockIcon = require('../assets/iconamoon_clock-fill.png');
12
19
  const infoIcon = require('../assets/info-circle.png');
13
20
 
14
- interface TextLine {
15
- width: number;
16
- height: number;
17
- x: number;
18
- y: number;
19
- text: string;
20
- }
21
-
22
21
  interface MessageLayout {
23
22
  lines: number;
24
23
  lastLineWidth: number;
@@ -90,7 +89,7 @@ function TextMessageItem({
90
89
  }
91
90
  };
92
91
 
93
- const onTextLayout = (event: any) => {
92
+ const onTextLayout = (event: NativeSyntheticEvent<TextLayoutEventData>) => {
94
93
  const { lines } = event.nativeEvent;
95
94
 
96
95
  const numLines = lines.length;
@@ -99,9 +98,9 @@ function TextMessageItem({
99
98
 
100
99
  if (numLines >= 1) {
101
100
  // Get the width of the last line
102
- lastLineWidth = lines[numLines - 1].width;
101
+ lastLineWidth = lines[numLines - 1]!.width;
103
102
  // Use the widest line as the total width
104
- totalWidth = Math.max(...lines.map((line: TextLine) => line.width));
103
+ totalWidth = Math.max(...lines.map((line) => line.width));
105
104
  }
106
105
 
107
106
  layoutRef.current = {
@@ -183,7 +182,7 @@ function TextMessageItem({
183
182
  {
184
183
  regex:
185
184
  /https?:\/\/(?:[-\w.])+(?::[0-9]+)?(?:\/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:[\w.])*)?)?/g,
186
- color: '#0066CC',
185
+ color: themeColors.link,
187
186
  navigate: (url) => {
188
187
  if (url) {
189
188
  Linking.openURL(url);