@developer_tribe/react-native-comnyx 0.14.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 (213) 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 +105 -19
  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.m +6 -0
  19. package/ios/ComnyxMediaPicker.swift +106 -26
  20. package/ios/ComnyxMessaging.swift +2 -0
  21. package/ios/PrivacyInfo.xcprivacy +32 -0
  22. package/ios/comnyx_post_install.rb +25 -0
  23. package/ios/generated/RCTAppDependencyProvider.h +25 -0
  24. package/ios/generated/RCTAppDependencyProvider.mm +55 -0
  25. package/ios/generated/RCTModulesConformingToProtocolsProvider.h +18 -0
  26. package/ios/generated/RCTModulesConformingToProtocolsProvider.mm +33 -0
  27. package/ios/generated/RCTThirdPartyComponentsProvider.h +16 -0
  28. package/ios/generated/RCTThirdPartyComponentsProvider.mm +23 -0
  29. package/ios/generated/RNComnyxSpec/RNComnyxSpec-generated.mm +53 -0
  30. package/ios/generated/RNComnyxSpec/RNComnyxSpec.h +67 -0
  31. package/ios/generated/RNComnyxSpecJSI-generated.cpp +38 -0
  32. package/ios/generated/RNComnyxSpecJSI.h +89 -0
  33. package/ios/generated/ReactAppDependencyProvider.podspec +34 -0
  34. package/lib/commonjs/NativeComnyxMediaPicker.js +46 -6
  35. package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -1
  36. package/lib/commonjs/api/conversations.js +6 -6
  37. package/lib/commonjs/api/conversations.js.map +1 -1
  38. package/lib/commonjs/api/customers.js +3 -2
  39. package/lib/commonjs/api/customers.js.map +1 -1
  40. package/lib/commonjs/api/media.js +20 -6
  41. package/lib/commonjs/api/media.js.map +1 -1
  42. package/lib/commonjs/api/messages.js +3 -2
  43. package/lib/commonjs/api/messages.js.map +1 -1
  44. package/lib/commonjs/assets/gallery.png +0 -0
  45. package/lib/commonjs/assets/video-play.png +0 -0
  46. package/lib/commonjs/components/ChatList.js +93 -45
  47. package/lib/commonjs/components/ChatList.js.map +1 -1
  48. package/lib/commonjs/components/ComnyxErrorBoundary.js +92 -0
  49. package/lib/commonjs/components/ComnyxErrorBoundary.js.map +1 -0
  50. package/lib/commonjs/components/CustomerForm.js +2 -2
  51. package/lib/commonjs/components/CustomerForm.js.map +1 -1
  52. package/lib/commonjs/components/MediaMessageItem.js +4 -3
  53. package/lib/commonjs/components/MediaMessageItem.js.map +1 -1
  54. package/lib/commonjs/components/MediaPickerButton.js +211 -14
  55. package/lib/commonjs/components/MediaPickerButton.js.map +1 -1
  56. package/lib/commonjs/components/MediaViewerModal.js +7 -0
  57. package/lib/commonjs/components/MediaViewerModal.js.map +1 -1
  58. package/lib/commonjs/components/MessageInput.js +63 -13
  59. package/lib/commonjs/components/MessageInput.js.map +1 -1
  60. package/lib/commonjs/components/MessageItem.js +1 -2
  61. package/lib/commonjs/components/MessageItem.js.map +1 -1
  62. package/lib/commonjs/constants/translations.js +87 -116
  63. package/lib/commonjs/constants/translations.js.map +1 -1
  64. package/lib/commonjs/hooks/usePolling.js +25 -21
  65. package/lib/commonjs/hooks/usePolling.js.map +1 -1
  66. package/lib/commonjs/hooks/useThemeColors.js +12 -1
  67. package/lib/commonjs/hooks/useThemeColors.js.map +1 -1
  68. package/lib/commonjs/index.js.map +1 -1
  69. package/lib/commonjs/notifications/initializeNotifications.js +19 -16
  70. package/lib/commonjs/notifications/initializeNotifications.js.map +1 -1
  71. package/lib/commonjs/register/Accumulator.js +19 -6
  72. package/lib/commonjs/register/Accumulator.js.map +1 -1
  73. package/lib/commonjs/register/collectData.js +1 -1
  74. package/lib/commonjs/register/collectData.js.map +1 -1
  75. package/lib/commonjs/store/store.js +6 -0
  76. package/lib/commonjs/store/store.js.map +1 -1
  77. package/lib/commonjs/support/ComnyxSupport.js +60 -15
  78. package/lib/commonjs/support/ComnyxSupport.js.map +1 -1
  79. package/lib/commonjs/support/SupportConfigContext.js +24 -0
  80. package/lib/commonjs/support/SupportConfigContext.js.map +1 -0
  81. package/lib/commonjs/types/Theme.js +30 -2
  82. package/lib/commonjs/types/Theme.js.map +1 -1
  83. package/lib/commonjs/version.js +1 -1
  84. package/lib/module/NativeComnyxMediaPicker.js +43 -6
  85. package/lib/module/NativeComnyxMediaPicker.js.map +1 -1
  86. package/lib/module/api/conversations.js +6 -6
  87. package/lib/module/api/conversations.js.map +1 -1
  88. package/lib/module/api/customers.js +3 -2
  89. package/lib/module/api/customers.js.map +1 -1
  90. package/lib/module/api/media.js +21 -6
  91. package/lib/module/api/media.js.map +1 -1
  92. package/lib/module/api/messages.js +3 -2
  93. package/lib/module/api/messages.js.map +1 -1
  94. package/lib/module/assets/gallery.png +0 -0
  95. package/lib/module/assets/video-play.png +0 -0
  96. package/lib/module/components/ChatList.js +94 -46
  97. package/lib/module/components/ChatList.js.map +1 -1
  98. package/lib/module/components/ComnyxErrorBoundary.js +87 -0
  99. package/lib/module/components/ComnyxErrorBoundary.js.map +1 -0
  100. package/lib/module/components/CustomerForm.js +2 -2
  101. package/lib/module/components/CustomerForm.js.map +1 -1
  102. package/lib/module/components/MediaMessageItem.js +4 -3
  103. package/lib/module/components/MediaMessageItem.js.map +1 -1
  104. package/lib/module/components/MediaPickerButton.js +215 -18
  105. package/lib/module/components/MediaPickerButton.js.map +1 -1
  106. package/lib/module/components/MediaViewerModal.js +8 -1
  107. package/lib/module/components/MediaViewerModal.js.map +1 -1
  108. package/lib/module/components/MessageInput.js +64 -14
  109. package/lib/module/components/MessageInput.js.map +1 -1
  110. package/lib/module/components/MessageItem.js +1 -2
  111. package/lib/module/components/MessageItem.js.map +1 -1
  112. package/lib/module/constants/translations.js +87 -116
  113. package/lib/module/constants/translations.js.map +1 -1
  114. package/lib/module/hooks/usePolling.js +25 -21
  115. package/lib/module/hooks/usePolling.js.map +1 -1
  116. package/lib/module/hooks/useThemeColors.js +13 -2
  117. package/lib/module/hooks/useThemeColors.js.map +1 -1
  118. package/lib/module/index.js +0 -1
  119. package/lib/module/index.js.map +1 -1
  120. package/lib/module/notifications/initializeNotifications.js +19 -16
  121. package/lib/module/notifications/initializeNotifications.js.map +1 -1
  122. package/lib/module/register/Accumulator.js +19 -6
  123. package/lib/module/register/Accumulator.js.map +1 -1
  124. package/lib/module/register/collectData.js +1 -1
  125. package/lib/module/register/collectData.js.map +1 -1
  126. package/lib/module/store/store.js +6 -0
  127. package/lib/module/store/store.js.map +1 -1
  128. package/lib/module/support/ComnyxSupport.js +61 -16
  129. package/lib/module/support/ComnyxSupport.js.map +1 -1
  130. package/lib/module/support/SupportConfigContext.js +19 -0
  131. package/lib/module/support/SupportConfigContext.js.map +1 -0
  132. package/lib/module/types/Theme.js +30 -2
  133. package/lib/module/types/Theme.js.map +1 -1
  134. package/lib/module/version.js +1 -1
  135. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +11 -0
  136. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -1
  137. package/lib/typescript/src/api/conversations.d.ts +2 -2
  138. package/lib/typescript/src/api/conversations.d.ts.map +1 -1
  139. package/lib/typescript/src/api/customers.d.ts +1 -1
  140. package/lib/typescript/src/api/customers.d.ts.map +1 -1
  141. package/lib/typescript/src/api/media.d.ts +3 -3
  142. package/lib/typescript/src/api/media.d.ts.map +1 -1
  143. package/lib/typescript/src/api/messages.d.ts +1 -1
  144. package/lib/typescript/src/api/messages.d.ts.map +1 -1
  145. package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
  146. package/lib/typescript/src/components/ComnyxErrorBoundary.d.ts +18 -0
  147. package/lib/typescript/src/components/ComnyxErrorBoundary.d.ts.map +1 -0
  148. package/lib/typescript/src/components/MediaMessageItem.d.ts.map +1 -1
  149. package/lib/typescript/src/components/MediaPickerButton.d.ts.map +1 -1
  150. package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -1
  151. package/lib/typescript/src/components/MessageInput.d.ts.map +1 -1
  152. package/lib/typescript/src/components/MessageItem.d.ts.map +1 -1
  153. package/lib/typescript/src/constants/translations.d.ts.map +1 -1
  154. package/lib/typescript/src/hooks/usePolling.d.ts.map +1 -1
  155. package/lib/typescript/src/hooks/useThemeColors.d.ts.map +1 -1
  156. package/lib/typescript/src/index.d.ts +2 -0
  157. package/lib/typescript/src/index.d.ts.map +1 -1
  158. package/lib/typescript/src/notifications/initializeNotifications.d.ts.map +1 -1
  159. package/lib/typescript/src/register/Accumulator.d.ts.map +1 -1
  160. package/lib/typescript/src/register/collectData.d.ts +4 -1
  161. package/lib/typescript/src/register/collectData.d.ts.map +1 -1
  162. package/lib/typescript/src/store/store.d.ts +6 -2
  163. package/lib/typescript/src/store/store.d.ts.map +1 -1
  164. package/lib/typescript/src/support/ComnyxSupport.d.ts +56 -2
  165. package/lib/typescript/src/support/ComnyxSupport.d.ts.map +1 -1
  166. package/lib/typescript/src/support/SupportConfigContext.d.ts +58 -0
  167. package/lib/typescript/src/support/SupportConfigContext.d.ts.map +1 -0
  168. package/lib/typescript/src/support/index.d.ts +1 -0
  169. package/lib/typescript/src/support/index.d.ts.map +1 -1
  170. package/lib/typescript/src/types/Conversation.d.ts +2 -2
  171. package/lib/typescript/src/types/Conversation.d.ts.map +1 -1
  172. package/lib/typescript/src/types/Customer.d.ts +1 -1
  173. package/lib/typescript/src/types/Customer.d.ts.map +1 -1
  174. package/lib/typescript/src/types/LocalizationKeys.d.ts +0 -1
  175. package/lib/typescript/src/types/LocalizationKeys.d.ts.map +1 -1
  176. package/lib/typescript/src/types/MessageResponse.d.ts +7 -4
  177. package/lib/typescript/src/types/MessageResponse.d.ts.map +1 -1
  178. package/lib/typescript/src/types/Theme.d.ts +26 -0
  179. package/lib/typescript/src/types/Theme.d.ts.map +1 -1
  180. package/lib/typescript/src/version.d.ts +1 -1
  181. package/package.json +12 -25
  182. package/src/NativeComnyxMediaPicker.ts +46 -7
  183. package/src/api/conversations.ts +6 -4
  184. package/src/api/customers.ts +3 -1
  185. package/src/api/media.ts +32 -10
  186. package/src/api/messages.ts +3 -1
  187. package/src/assets/gallery.png +0 -0
  188. package/src/assets/video-play.png +0 -0
  189. package/src/components/ChatList.tsx +115 -55
  190. package/src/components/ComnyxErrorBoundary.tsx +91 -0
  191. package/src/components/CustomerForm.tsx +2 -2
  192. package/src/components/MediaMessageItem.tsx +10 -3
  193. package/src/components/MediaPickerButton.tsx +238 -17
  194. package/src/components/MediaViewerModal.tsx +8 -1
  195. package/src/components/MessageInput.tsx +89 -16
  196. package/src/components/MessageItem.tsx +12 -14
  197. package/src/constants/translations.ts +87 -116
  198. package/src/hooks/usePolling.ts +21 -11
  199. package/src/hooks/useThemeColors.ts +11 -2
  200. package/src/index.ts +12 -0
  201. package/src/notifications/initializeNotifications.ts +22 -20
  202. package/src/register/Accumulator.ts +26 -9
  203. package/src/register/collectData.ts +10 -2
  204. package/src/store/store.ts +11 -3
  205. package/src/support/ComnyxSupport.tsx +128 -22
  206. package/src/support/SupportConfigContext.tsx +79 -0
  207. package/src/support/index.ts +7 -0
  208. package/src/types/Conversation.ts +2 -2
  209. package/src/types/Customer.ts +1 -2
  210. package/src/types/LocalizationKeys.ts +0 -1
  211. package/src/types/MessageResponse.ts +4 -4
  212. package/src/types/Theme.ts +38 -0
  213. 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);
@@ -1,12 +1,29 @@
1
- import { TouchableOpacity, Image } from 'react-native';
2
- import { useCallback } from 'react';
1
+ import {
2
+ TouchableOpacity,
3
+ Image,
4
+ View,
5
+ Modal,
6
+ Animated,
7
+ Dimensions,
8
+ TouchableWithoutFeedback,
9
+ Platform,
10
+ } from 'react-native';
11
+ import { useState, useCallback, useRef } from 'react';
3
12
  import { useThemeColors } from '../hooks/useThemeColors';
4
13
  import { ScaledSheet } from './ScaledSheet';
5
14
  import { activeOpacity } from '../constants/activeOpacity';
6
- import { pickMedia } from '../NativeComnyxMediaPicker';
15
+ import { pickImage, pickVideo } from '../NativeComnyxMediaPicker';
16
+ import { AppText } from './AppText';
17
+ import { useLocalize } from '../hooks/useLocalize';
7
18
  import type { MediaAsset } from '../types/MediaTypes';
8
19
 
9
20
  const paperclipIcon = require('../assets/attachment-01.png');
21
+ const galleryIcon = require('../assets/gallery.png');
22
+ const videoPlayIcon = require('../assets/video-play.png');
23
+
24
+ const closeCircleIcon = require('../assets/x-circle.png');
25
+
26
+ const SCREEN_HEIGHT = Dimensions.get('window').height;
10
27
 
11
28
  export function MediaPickerButton({
12
29
  onMediaSelected,
@@ -14,23 +31,166 @@ export function MediaPickerButton({
14
31
  onMediaSelected: (assets: MediaAsset[]) => void;
15
32
  }) {
16
33
  const themeColors = useThemeColors();
34
+ const localize = useLocalize();
35
+ const [visible, setVisible] = useState(false);
36
+ const slideAnim = useRef(new Animated.Value(SCREEN_HEIGHT)).current;
37
+
38
+ const open = useCallback(() => {
39
+ setVisible(true);
40
+ Animated.spring(slideAnim, {
41
+ toValue: 0,
42
+ useNativeDriver: true,
43
+ bounciness: 4,
44
+ speed: 14,
45
+ }).start();
46
+ }, [slideAnim]);
47
+
48
+ const close = useCallback(() => {
49
+ Animated.timing(slideAnim, {
50
+ toValue: SCREEN_HEIGHT,
51
+ duration: 250,
52
+ useNativeDriver: true,
53
+ }).start(() => {
54
+ setVisible(false);
55
+ });
56
+ }, [slideAnim]);
57
+
58
+ const pendingAction = useRef<(() => void) | null>(null);
59
+
60
+ const dismissAndRun = useCallback(
61
+ (action: () => void) => {
62
+ slideAnim.setValue(SCREEN_HEIGHT);
63
+ if (Platform.OS === 'ios') {
64
+ // Defer action until Modal's onDismiss fires to avoid
65
+ // "Attempt to present on a VC which is already presenting"
66
+ pendingAction.current = action;
67
+ setVisible(false);
68
+ } else {
69
+ setVisible(false);
70
+ action();
71
+ }
72
+ },
73
+ [slideAnim]
74
+ );
17
75
 
18
- const showPicker = useCallback(async () => {
19
- const assets = await pickMedia();
20
- if (assets.length > 0) onMediaSelected(assets);
21
- }, [onMediaSelected]);
76
+ const handlePickImage = useCallback(async () => {
77
+ dismissAndRun(async () => {
78
+ try {
79
+ const assets = await pickImage();
80
+ if (assets.length > 0) onMediaSelected(assets);
81
+ } catch (e) {
82
+ console.error('[Comnyx] pickImage error:', e);
83
+ }
84
+ });
85
+ }, [dismissAndRun, onMediaSelected]);
86
+
87
+ const handlePickVideo = useCallback(async () => {
88
+ dismissAndRun(async () => {
89
+ try {
90
+ const assets = await pickVideo();
91
+ if (assets.length > 0) onMediaSelected(assets);
92
+ } catch (e) {
93
+ console.error('[Comnyx] pickVideo error:', e);
94
+ }
95
+ });
96
+ }, [dismissAndRun, onMediaSelected]);
22
97
 
23
98
  return (
24
- <TouchableOpacity
25
- style={styles.button}
26
- onPress={showPicker}
27
- activeOpacity={activeOpacity}
28
- >
29
- <Image
30
- source={paperclipIcon}
31
- style={[styles.icon, { tintColor: themeColors.text }]}
32
- />
33
- </TouchableOpacity>
99
+ <>
100
+ <TouchableOpacity
101
+ style={styles.button}
102
+ onPress={open}
103
+ activeOpacity={activeOpacity}
104
+ >
105
+ <Image
106
+ source={paperclipIcon}
107
+ style={[styles.icon, { tintColor: themeColors.text }]}
108
+ />
109
+ </TouchableOpacity>
110
+
111
+ <Modal
112
+ visible={visible}
113
+ transparent
114
+ animationType="none"
115
+ onRequestClose={close}
116
+ onDismiss={() => {
117
+ if (pendingAction.current) {
118
+ const action = pendingAction.current;
119
+ pendingAction.current = null;
120
+ action();
121
+ }
122
+ }}
123
+ statusBarTranslucent
124
+ >
125
+ <TouchableWithoutFeedback onPress={close}>
126
+ <View style={styles.overlay}>
127
+ <TouchableWithoutFeedback>
128
+ <Animated.View
129
+ style={[
130
+ styles.sheet,
131
+ {
132
+ backgroundColor: themeColors.background,
133
+ transform: [{ translateY: slideAnim }],
134
+ },
135
+ ]}
136
+ >
137
+ <View style={styles.handle} />
138
+
139
+ <View style={styles.titleRow}>
140
+ <AppText style={[styles.title, { color: themeColors.text }]}>
141
+ {localize('chat.media.pick.title')}
142
+ </AppText>
143
+ {Platform.OS === 'android' && (
144
+ <TouchableOpacity
145
+ onPress={close}
146
+ activeOpacity={activeOpacity}
147
+ style={styles.closeButton}
148
+ >
149
+ <Image
150
+ source={closeCircleIcon}
151
+ style={styles.closeIcon}
152
+ />
153
+ </TouchableOpacity>
154
+ )}
155
+ </View>
156
+
157
+ <TouchableOpacity
158
+ style={styles.option}
159
+ onPress={handlePickImage}
160
+ activeOpacity={activeOpacity}
161
+ >
162
+ <Image
163
+ source={galleryIcon}
164
+ style={[styles.optionIcon, { tintColor: themeColors.text }]}
165
+ />
166
+ <AppText
167
+ style={[styles.optionText, { color: themeColors.text }]}
168
+ >
169
+ {localize('chat.media.pick.photo')}
170
+ </AppText>
171
+ </TouchableOpacity>
172
+
173
+ <TouchableOpacity
174
+ style={styles.option}
175
+ onPress={handlePickVideo}
176
+ activeOpacity={activeOpacity}
177
+ >
178
+ <Image
179
+ source={videoPlayIcon}
180
+ style={[styles.optionIcon, { tintColor: themeColors.text }]}
181
+ />
182
+ <AppText
183
+ style={[styles.optionText, { color: themeColors.text }]}
184
+ >
185
+ {localize('chat.media.pick.video')}
186
+ </AppText>
187
+ </TouchableOpacity>
188
+ </Animated.View>
189
+ </TouchableWithoutFeedback>
190
+ </View>
191
+ </TouchableWithoutFeedback>
192
+ </Modal>
193
+ </>
34
194
  );
35
195
  }
36
196
 
@@ -45,4 +205,65 @@ const styles = ScaledSheet.create({
45
205
  width: '28@vs',
46
206
  height: '28@vs',
47
207
  },
208
+ overlay: {
209
+ flex: 1,
210
+ backgroundColor: 'rgba(0,0,0,0.4)',
211
+ justifyContent: 'flex-end',
212
+ },
213
+ sheet: {
214
+ borderTopLeftRadius: '16@vs',
215
+ borderTopRightRadius: '16@vs',
216
+ paddingBottom: '34@vs',
217
+ paddingTop: '12@vs',
218
+ paddingHorizontal: '20@s',
219
+ },
220
+ handle: {
221
+ width: '36@s',
222
+ height: '4@vs',
223
+ backgroundColor: '#ccc',
224
+ borderRadius: '2@vs',
225
+ alignSelf: 'center',
226
+ marginBottom: '16@vs',
227
+ },
228
+ titleRow: {
229
+ flexDirection: 'row',
230
+ justifyContent: 'space-between',
231
+ alignItems: 'center',
232
+ marginBottom: '16@vs',
233
+ },
234
+ title: {
235
+ fontSize: '18@vs',
236
+ fontWeight: '600',
237
+ },
238
+ closeButton: {
239
+ padding: '4@vs',
240
+ },
241
+ closeIcon: {
242
+ width: '22@vs',
243
+ height: '22@vs',
244
+ },
245
+ optionIcon: {
246
+ width: '24@vs',
247
+ height: '24@vs',
248
+ marginRight: '12@s',
249
+ },
250
+ option: {
251
+ paddingVertical: '14@vs',
252
+ flexDirection: 'row',
253
+ alignItems: 'center',
254
+ },
255
+ optionText: {
256
+ fontSize: '16@vs',
257
+ },
258
+ cancelButton: {
259
+ marginTop: '8@vs',
260
+ paddingVertical: '14@vs',
261
+ alignItems: 'center',
262
+ borderTopWidth: 0.5,
263
+ borderTopColor: '#ddd',
264
+ },
265
+ cancelText: {
266
+ fontSize: '16@vs',
267
+ opacity: 0.6,
268
+ },
48
269
  });