@developer_tribe/react-native-comnyx 0.15.0 → 0.16.1

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 (210) 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 +132 -90
  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 +7 -3
  48. package/lib/commonjs/components/CustomerForm.js.map +1 -1
  49. package/lib/commonjs/components/InitFailed.js +77 -21
  50. package/lib/commonjs/components/InitFailed.js.map +1 -1
  51. package/lib/commonjs/components/MediaMessageItem.js +37 -10
  52. package/lib/commonjs/components/MediaMessageItem.js.map +1 -1
  53. package/lib/commonjs/components/MediaViewerModal.js +16 -3
  54. package/lib/commonjs/components/MediaViewerModal.js.map +1 -1
  55. package/lib/commonjs/components/MessageInput.js +83 -15
  56. package/lib/commonjs/components/MessageInput.js.map +1 -1
  57. package/lib/commonjs/components/MessageItem.js +1 -1
  58. package/lib/commonjs/components/MessageItem.js.map +1 -1
  59. package/lib/commonjs/hooks/usePolling.js +30 -21
  60. package/lib/commonjs/hooks/usePolling.js.map +1 -1
  61. package/lib/commonjs/hooks/useThemeColors.js +12 -1
  62. package/lib/commonjs/hooks/useThemeColors.js.map +1 -1
  63. package/lib/commonjs/index.js +6 -0
  64. package/lib/commonjs/index.js.map +1 -1
  65. package/lib/commonjs/notifications/initializeNotifications.js +19 -16
  66. package/lib/commonjs/notifications/initializeNotifications.js.map +1 -1
  67. package/lib/commonjs/register/Accumulator.js +19 -6
  68. package/lib/commonjs/register/Accumulator.js.map +1 -1
  69. package/lib/commonjs/register/collectData.js +1 -1
  70. package/lib/commonjs/register/collectData.js.map +1 -1
  71. package/lib/commonjs/register/login.js +5 -0
  72. package/lib/commonjs/register/login.js.map +1 -1
  73. package/lib/commonjs/store/store.js +6 -0
  74. package/lib/commonjs/store/store.js.map +1 -1
  75. package/lib/commonjs/support/ComnyxSupport.js +77 -16
  76. package/lib/commonjs/support/ComnyxSupport.js.map +1 -1
  77. package/lib/commonjs/support/SupportConfigContext.js +66 -0
  78. package/lib/commonjs/support/SupportConfigContext.js.map +1 -0
  79. package/lib/commonjs/support/index.js +7 -0
  80. package/lib/commonjs/support/index.js.map +1 -1
  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 +18 -0
  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/components/ChatList.js +133 -91
  95. package/lib/module/components/ChatList.js.map +1 -1
  96. package/lib/module/components/ComnyxErrorBoundary.js +87 -0
  97. package/lib/module/components/ComnyxErrorBoundary.js.map +1 -0
  98. package/lib/module/components/CustomerForm.js +7 -3
  99. package/lib/module/components/CustomerForm.js.map +1 -1
  100. package/lib/module/components/InitFailed.js +79 -23
  101. package/lib/module/components/InitFailed.js.map +1 -1
  102. package/lib/module/components/MediaMessageItem.js +37 -11
  103. package/lib/module/components/MediaMessageItem.js.map +1 -1
  104. package/lib/module/components/MediaViewerModal.js +15 -3
  105. package/lib/module/components/MediaViewerModal.js.map +1 -1
  106. package/lib/module/components/MessageInput.js +84 -16
  107. package/lib/module/components/MessageInput.js.map +1 -1
  108. package/lib/module/components/MessageItem.js +1 -1
  109. package/lib/module/components/MessageItem.js.map +1 -1
  110. package/lib/module/hooks/usePolling.js +30 -21
  111. package/lib/module/hooks/usePolling.js.map +1 -1
  112. package/lib/module/hooks/useThemeColors.js +13 -2
  113. package/lib/module/hooks/useThemeColors.js.map +1 -1
  114. package/lib/module/index.js +1 -0
  115. package/lib/module/index.js.map +1 -1
  116. package/lib/module/notifications/initializeNotifications.js +19 -16
  117. package/lib/module/notifications/initializeNotifications.js.map +1 -1
  118. package/lib/module/register/Accumulator.js +19 -6
  119. package/lib/module/register/Accumulator.js.map +1 -1
  120. package/lib/module/register/collectData.js +1 -1
  121. package/lib/module/register/collectData.js.map +1 -1
  122. package/lib/module/register/login.js +5 -0
  123. package/lib/module/register/login.js.map +1 -1
  124. package/lib/module/store/store.js +6 -0
  125. package/lib/module/store/store.js.map +1 -1
  126. package/lib/module/support/ComnyxSupport.js +79 -18
  127. package/lib/module/support/ComnyxSupport.js.map +1 -1
  128. package/lib/module/support/SupportConfigContext.js +59 -0
  129. package/lib/module/support/SupportConfigContext.js.map +1 -0
  130. package/lib/module/support/index.js +1 -0
  131. package/lib/module/support/index.js.map +1 -1
  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 +9 -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/CustomerForm.d.ts.map +1 -1
  149. package/lib/typescript/src/components/InitFailed.d.ts +5 -2
  150. package/lib/typescript/src/components/InitFailed.d.ts.map +1 -1
  151. package/lib/typescript/src/components/MediaMessageItem.d.ts.map +1 -1
  152. package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -1
  153. package/lib/typescript/src/components/MessageInput.d.ts.map +1 -1
  154. package/lib/typescript/src/components/MessageItem.d.ts.map +1 -1
  155. package/lib/typescript/src/hooks/usePolling.d.ts.map +1 -1
  156. package/lib/typescript/src/hooks/useThemeColors.d.ts.map +1 -1
  157. package/lib/typescript/src/index.d.ts +3 -0
  158. package/lib/typescript/src/index.d.ts.map +1 -1
  159. package/lib/typescript/src/notifications/initializeNotifications.d.ts.map +1 -1
  160. package/lib/typescript/src/register/Accumulator.d.ts.map +1 -1
  161. package/lib/typescript/src/register/collectData.d.ts +4 -1
  162. package/lib/typescript/src/register/collectData.d.ts.map +1 -1
  163. package/lib/typescript/src/register/login.d.ts.map +1 -1
  164. package/lib/typescript/src/store/store.d.ts +6 -2
  165. package/lib/typescript/src/store/store.d.ts.map +1 -1
  166. package/lib/typescript/src/support/ComnyxSupport.d.ts +80 -3
  167. package/lib/typescript/src/support/ComnyxSupport.d.ts.map +1 -1
  168. package/lib/typescript/src/support/SupportConfigContext.d.ts +98 -0
  169. package/lib/typescript/src/support/SupportConfigContext.d.ts.map +1 -0
  170. package/lib/typescript/src/support/index.d.ts +2 -0
  171. package/lib/typescript/src/support/index.d.ts.map +1 -1
  172. package/lib/typescript/src/types/Conversation.d.ts +2 -2
  173. package/lib/typescript/src/types/Conversation.d.ts.map +1 -1
  174. package/lib/typescript/src/types/Customer.d.ts +1 -1
  175. package/lib/typescript/src/types/Customer.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 +19 -30
  182. package/src/NativeComnyxMediaPicker.ts +18 -0
  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/components/ChatList.tsx +147 -99
  188. package/src/components/ComnyxErrorBoundary.tsx +91 -0
  189. package/src/components/CustomerForm.tsx +7 -3
  190. package/src/components/InitFailed.tsx +80 -16
  191. package/src/components/MediaMessageItem.tsx +48 -11
  192. package/src/components/MediaViewerModal.tsx +21 -8
  193. package/src/components/MessageInput.tsx +108 -17
  194. package/src/components/MessageItem.tsx +12 -13
  195. package/src/hooks/usePolling.ts +26 -11
  196. package/src/hooks/useThemeColors.ts +11 -2
  197. package/src/index.ts +16 -0
  198. package/src/notifications/initializeNotifications.ts +22 -20
  199. package/src/register/Accumulator.ts +26 -9
  200. package/src/register/collectData.ts +10 -2
  201. package/src/register/login.ts +5 -0
  202. package/src/store/store.ts +11 -3
  203. package/src/support/ComnyxSupport.tsx +172 -23
  204. package/src/support/SupportConfigContext.tsx +157 -0
  205. package/src/support/index.ts +11 -0
  206. package/src/types/Conversation.ts +2 -2
  207. package/src/types/Customer.ts +1 -2
  208. package/src/types/MessageResponse.ts +4 -4
  209. package/src/types/Theme.ts +38 -0
  210. package/src/version.ts +1 -1
@@ -1,4 +1,5 @@
1
- import { View, Image, TouchableOpacity } from 'react-native';
1
+ import { View, Image, TouchableOpacity, ActivityIndicator } from 'react-native';
2
+ import FastImage from '@d11/react-native-fast-image';
2
3
  import { useState, useEffect } from 'react';
3
4
  import type { AppConversationMessage } from '../types/Conversation';
4
5
  import { useThemeColors } from '../hooks/useThemeColors';
@@ -30,6 +31,7 @@ function MediaThumbnail({
30
31
  const [generatedThumb, setGeneratedThumb] = useState<string | null>(null);
31
32
  const displayUri = file.local_uri || file.url;
32
33
  const isVideo = file.type === 'video';
34
+ const themeColors = useThemeColors();
33
35
 
34
36
  useEffect(() => {
35
37
  if (isVideo && !file.thumbnail_uri && !isUploading && displayUri) {
@@ -44,11 +46,8 @@ function MediaThumbnail({
44
46
  }, [isVideo, file.thumbnail_uri, isUploading, displayUri]);
45
47
 
46
48
  const effectiveThumb = file.thumbnail_uri || generatedThumb;
47
- const thumbSource = isVideo
48
- ? effectiveThumb
49
- ? { uri: effectiveThumb }
50
- : undefined
51
- : { uri: displayUri };
49
+ const thumbUri = isVideo ? effectiveThumb : displayUri;
50
+ const isRemote = !!thumbUri && /^https?:\/\//i.test(thumbUri);
52
51
 
53
52
  const handlePress = () => {
54
53
  if (isUploading) return;
@@ -62,8 +61,14 @@ function MediaThumbnail({
62
61
  const wrapperStyle = single ? styles.singleMediaWrapper : styles.gridItem;
63
62
  const imageStyle = single ? styles.singleMediaImage : styles.gridImage;
64
63
  const placeholderStyle = single
65
- ? [styles.singleMediaImage, { backgroundColor: '#1a1a2e' }]
66
- : [styles.videoPlaceholder, { backgroundColor: '#1a1a2e' }];
64
+ ? [
65
+ styles.singleMediaImage,
66
+ { backgroundColor: themeColors.videoPlaceholder },
67
+ ]
68
+ : [
69
+ styles.videoPlaceholder,
70
+ { backgroundColor: themeColors.videoPlaceholder },
71
+ ];
67
72
 
68
73
  return (
69
74
  <TouchableOpacity
@@ -72,8 +77,23 @@ function MediaThumbnail({
72
77
  activeOpacity={activeOpacity}
73
78
  disabled={isUploading}
74
79
  >
75
- {thumbSource ? (
76
- <Image source={thumbSource} style={imageStyle} resizeMode="cover" />
80
+ {thumbUri ? (
81
+ isRemote ? (
82
+ <FastImage
83
+ source={{
84
+ uri: thumbUri,
85
+ cache: FastImage.cacheControl.immutable,
86
+ }}
87
+ style={imageStyle}
88
+ resizeMode={FastImage.resizeMode.cover}
89
+ />
90
+ ) : (
91
+ <Image
92
+ source={{ uri: thumbUri }}
93
+ style={imageStyle}
94
+ resizeMode="cover"
95
+ />
96
+ )
77
97
  ) : (
78
98
  <View style={placeholderStyle} />
79
99
  )}
@@ -82,6 +102,11 @@ function MediaThumbnail({
82
102
  <AppText style={styles.playIcon}>▶</AppText>
83
103
  </View>
84
104
  )}
105
+ {isUploading && (
106
+ <View style={styles.uploadOverlay}>
107
+ <ActivityIndicator size="small" color="#FFFFFF" />
108
+ </View>
109
+ )}
85
110
  </TouchableOpacity>
86
111
  );
87
112
  }
@@ -234,7 +259,7 @@ export function MediaMessageItem({
234
259
  {
235
260
  regex:
236
261
  /https?:\/\/(?:[-\w.])+(?::[0-9]+)?(?:\/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:[\w.])*)?)?/g,
237
- color: '#0066CC',
262
+ color: themeColors.link,
238
263
  navigate: (url) => {
239
264
  if (url) {
240
265
  Linking.openURL(url);
@@ -329,6 +354,18 @@ const styles = ScaledSheet.create({
329
354
  alignItems: 'center',
330
355
  } as any),
331
356
  },
357
+ uploadOverlay: {
358
+ ...({
359
+ position: 'absolute',
360
+ top: 0,
361
+ left: 0,
362
+ right: 0,
363
+ bottom: 0,
364
+ justifyContent: 'center',
365
+ alignItems: 'center',
366
+ backgroundColor: 'rgba(0,0,0,0.35)',
367
+ } as any),
368
+ },
332
369
  playIcon: {
333
370
  fontSize: '20@vs',
334
371
  color: '#E0E0E0',
@@ -6,6 +6,7 @@ import {
6
6
  StatusBar,
7
7
  useWindowDimensions,
8
8
  } from 'react-native';
9
+ import FastImage from '@d11/react-native-fast-image';
9
10
  import { useState, useEffect } from 'react';
10
11
  import { AppText } from './AppText';
11
12
  import { ScaledSheet } from './ScaledSheet';
@@ -61,15 +62,27 @@ export function MediaViewerModal({
61
62
  >
62
63
  <AppText style={styles.closeIcon}>✕</AppText>
63
64
  </TouchableOpacity>
64
- <View style={[styles.mediaContainer, { width, height: height * 0.7 }]}>
65
+ <View style={[styles.mediaContainer, { width, height }]}>
65
66
  {displayUri && !imageError ? (
66
- <Image
67
- source={{ uri: displayUri }}
68
- style={{ width, height: height * 0.9 }}
69
- resizeMode="contain"
70
- resizeMethod="resize"
71
- onError={() => setImageError(true)}
72
- />
67
+ /^https?:\/\//i.test(displayUri) ? (
68
+ <FastImage
69
+ source={{
70
+ uri: displayUri,
71
+ cache: FastImage.cacheControl.immutable,
72
+ }}
73
+ style={{ width, height }}
74
+ resizeMode={FastImage.resizeMode.contain}
75
+ onError={() => setImageError(true)}
76
+ />
77
+ ) : (
78
+ <Image
79
+ source={{ uri: displayUri }}
80
+ style={{ width, height }}
81
+ resizeMode="contain"
82
+ resizeMethod="resize"
83
+ onError={() => setImageError(true)}
84
+ />
85
+ )
73
86
  ) : (
74
87
  <View
75
88
  style={[
@@ -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,10 @@ 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 {
21
+ useSupportConfig,
22
+ reportSupportError,
23
+ } from '../support/SupportConfigContext';
20
24
 
21
25
  const sendDark = require('../assets/arrow-right.png');
22
26
  const circleXIcon = require('../assets/x-circle.png');
@@ -34,6 +38,20 @@ export function MessageInput({
34
38
  const themeColors = useThemeColors();
35
39
  const localize = useLocalize();
36
40
  const isRtl = useIsRtl();
41
+ const { onBeforeSend } = useSupportConfig();
42
+
43
+ const uploadAbortRef = useRef<AbortController | null>(null);
44
+ const lifecycleAbortRef = useRef<AbortController | null>(null);
45
+ if (lifecycleAbortRef.current === null) {
46
+ lifecycleAbortRef.current = new AbortController();
47
+ }
48
+ useEffect(() => {
49
+ const controller = lifecycleAbortRef.current;
50
+ return () => {
51
+ uploadAbortRef.current?.abort();
52
+ controller?.abort();
53
+ };
54
+ }, []);
37
55
 
38
56
  const sendTextOnlyMessage = useCallback(() => {
39
57
  if (!value.trim()) return;
@@ -53,10 +71,14 @@ export function MessageInput({
53
71
  },
54
72
  ...(data ?? []),
55
73
  ]);
56
- sendCustomerMessage(customer.external_id as string, value, {
57
- fake: useAppStore.getState().fake,
58
- })
74
+ sendCustomerMessage(
75
+ customer.external_id as string,
76
+ value,
77
+ { fake: useAppStore.getState().fake },
78
+ lifecycleAbortRef.current?.signal
79
+ )
59
80
  .then((res) => {
81
+ if (lifecycleAbortRef.current?.signal.aborted) return;
60
82
  const data = useAppStore.getState().data;
61
83
  if (data) {
62
84
  const itemIndex = data.findIndex((item) => item.local_id === localId);
@@ -81,7 +103,7 @@ export function MessageInput({
81
103
  //TODO: ??
82
104
  }
83
105
  })
84
- .catch(() => {
106
+ .catch((err) => {
85
107
  const data = useAppStore.getState().data;
86
108
  if (data) {
87
109
  const itemIndex = data.findIndex((item) => item.local_id === localId);
@@ -106,6 +128,11 @@ export function MessageInput({
106
128
  } else {
107
129
  //TODO: ??
108
130
  }
131
+ reportSupportError(err, {
132
+ section: 'send',
133
+ recoverable: true,
134
+ extras: { contentLength: value.length },
135
+ });
109
136
  });
110
137
  setValue('');
111
138
  }, [value, customer, scrollToBottom]);
@@ -114,6 +141,7 @@ export function MessageInput({
114
141
  async (
115
142
  asset: MediaAsset,
116
143
  isFake: boolean,
144
+ signal: AbortSignal,
117
145
  onProgress?: (percentage: number) => void
118
146
  ): Promise<string> => {
119
147
  const presignResponse = await getUploadUrl(
@@ -124,9 +152,14 @@ export function MessageInput({
124
152
  const uploadUrl = presignResponse.data.url;
125
153
  const filePath = presignResponse.data.path;
126
154
 
127
- await uploadFileToS3(uploadUrl, asset.uri, asset.mimeType, onProgress, {
128
- fake: isFake,
129
- });
155
+ await uploadFileToS3(
156
+ uploadUrl,
157
+ asset.uri,
158
+ asset.mimeType,
159
+ onProgress,
160
+ { fake: isFake },
161
+ signal
162
+ );
130
163
 
131
164
  return isFake ? asset.uri : filePath;
132
165
  },
@@ -166,6 +199,10 @@ export function MessageInput({
166
199
  ]);
167
200
  scrollToBottom(false);
168
201
 
202
+ uploadAbortRef.current?.abort();
203
+ const controller = new AbortController();
204
+ uploadAbortRef.current = controller;
205
+
169
206
  try {
170
207
  const fileProgresses = new Array(assets.length).fill(0);
171
208
  let lastProgressUpdate = 0;
@@ -198,6 +235,7 @@ export function MessageInput({
198
235
  const path = await uploadSingleMedia(
199
236
  assets[i]!,
200
237
  isFake,
238
+ controller.signal,
201
239
  (percentage) => {
202
240
  fileProgresses[i] = percentage;
203
241
  updateOverallProgress();
@@ -211,7 +249,8 @@ export function MessageInput({
211
249
  uploadedPaths,
212
250
  assets[0]!.type,
213
251
  content,
214
- { fake: isFake }
252
+ { fake: isFake },
253
+ controller.signal
215
254
  );
216
255
 
217
256
  const currentData = useAppStore.getState().data;
@@ -265,6 +304,19 @@ export function MessageInput({
265
304
  }
266
305
  }
267
306
  console.error('[Comnyx] Media upload failed:', error);
307
+ reportSupportError(error, {
308
+ section: 'upload',
309
+ recoverable: true,
310
+ extras: {
311
+ assetCount: assets.length,
312
+ mediaTypes: assets.map((a) => a.type),
313
+ aborted: controller.signal.aborted,
314
+ },
315
+ });
316
+ } finally {
317
+ if (uploadAbortRef.current === controller) {
318
+ uploadAbortRef.current = null;
319
+ }
268
320
  }
269
321
  },
270
322
  [customer, scrollToBottom, uploadSingleMedia]
@@ -294,18 +346,45 @@ export function MessageInput({
294
346
  const handleSend = useCallback(async () => {
295
347
  if (isSending) return;
296
348
 
297
- if (pendingMedia.length > 0) {
298
- setIsSending(true);
349
+ const hasMedia = pendingMedia.length > 0;
350
+ const hasText = !!value.trim();
351
+ if (!hasMedia && !hasText) return;
352
+
353
+ if (onBeforeSend) {
354
+ try {
355
+ setIsSending(true);
356
+ await onBeforeSend({
357
+ content: value.trim(),
358
+ mediaCount: pendingMedia.length,
359
+ mediaTypes: pendingMedia.map((m) => m.type),
360
+ });
361
+ } catch (err) {
362
+ console.warn('[Comnyx] onBeforeSend cancelled the message:', err);
363
+ setIsSending(false);
364
+ return;
365
+ }
366
+ }
367
+
368
+ if (hasMedia) {
369
+ if (!onBeforeSend) setIsSending(true);
299
370
  const content = value.trim();
300
371
  const assets = [...pendingMedia];
301
372
  setPendingMedia([]);
302
373
  setValue('');
303
374
  await sendMediaMessages(assets, content);
304
375
  setIsSending(false);
305
- } else if (value.trim()) {
376
+ } else if (hasText) {
306
377
  sendTextOnlyMessage();
378
+ if (onBeforeSend) setIsSending(false);
307
379
  }
308
- }, [isSending, pendingMedia, value, sendMediaMessages, sendTextOnlyMessage]);
380
+ }, [
381
+ isSending,
382
+ pendingMedia,
383
+ value,
384
+ sendMediaMessages,
385
+ sendTextOnlyMessage,
386
+ onBeforeSend,
387
+ ]);
309
388
 
310
389
  const hasPendingMedia = pendingMedia.length > 0;
311
390
 
@@ -335,8 +414,18 @@ export function MessageInput({
335
414
  />
336
415
  {asset.type === 'video' && (
337
416
  <View style={styles.playIconOverlay}>
338
- <View style={styles.playIcon}>
339
- <View style={styles.playTriangle} />
417
+ <View
418
+ style={[
419
+ styles.playIcon,
420
+ { borderColor: themeColors.playIcon },
421
+ ]}
422
+ >
423
+ <View
424
+ style={[
425
+ styles.playTriangle,
426
+ { borderLeftColor: themeColors.playIcon },
427
+ ]}
428
+ />
340
429
  </View>
341
430
  </View>
342
431
  )}
@@ -375,10 +464,14 @@ export function MessageInput({
375
464
  onPress={handleSend}
376
465
  activeOpacity={activeOpacity}
377
466
  disabled={isSending}
467
+ accessibilityRole="button"
468
+ accessibilityLabel={localize('chat.messageInput.placeholder')}
469
+ accessibilityState={{ disabled: isSending, busy: isSending }}
378
470
  >
379
471
  <Image
380
472
  style={[
381
473
  styles.sendIcon,
474
+ { tintColor: themeColors.text },
382
475
  isRtl && { transform: [{ rotate: '180deg' }] },
383
476
  isSending && { opacity: 0.4 },
384
477
  ]}
@@ -441,7 +534,6 @@ const styles = ScaledSheet.create({
441
534
  height: '22@vs',
442
535
  borderRadius: '11@vs',
443
536
  borderWidth: 1.5,
444
- borderColor: '#fff',
445
537
  justifyContent: 'center',
446
538
  alignItems: 'center',
447
539
  },
@@ -451,7 +543,6 @@ const styles = ScaledSheet.create({
451
543
  borderLeftWidth: '7@vs',
452
544
  borderTopWidth: '5@vs',
453
545
  borderBottomWidth: '5@vs',
454
- borderLeftColor: '#fff',
455
546
  borderTopColor: 'transparent',
456
547
  borderBottomColor: 'transparent',
457
548
  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);
@@ -1,6 +1,7 @@
1
1
  import { useEffect } from 'react';
2
2
  import { getNewCustomerConversation } from '../api';
3
3
  import { useAppStore } from '../store/store';
4
+ import { reportSupportError } from '../support/SupportConfigContext';
4
5
 
5
6
  const NEW_MESSAGES_CHECK_INTERVAL = 10000;
6
7
 
@@ -13,12 +14,19 @@ export function usePolling() {
13
14
  const lastMessage = data ? data[data.length - 1] : null;
14
15
  useEffect(() => {
15
16
  const created_at = lastMessage?.created_at;
16
- let interval: ReturnType<typeof setInterval> | null = null;
17
- if (customer && created_at) {
18
- interval = setInterval(() => {
19
- getNewCustomerConversation(customer?.external_id, created_at, {
20
- fake: useAppStore.getState().fake,
21
- }).then((newData) => {
17
+ if (!customer || !created_at) {
18
+ return undefined;
19
+ }
20
+ const controller = new AbortController();
21
+ const interval = setInterval(() => {
22
+ getNewCustomerConversation(
23
+ customer.external_id,
24
+ created_at,
25
+ { fake: useAppStore.getState().fake },
26
+ controller.signal
27
+ )
28
+ .then((newData) => {
29
+ if (controller.signal.aborted) return;
22
30
  setData((prevData) => {
23
31
  const newMessages = newData.page.data;
24
32
  const existingIds = new Set(prevData?.map((msg) => msg.id));
@@ -34,13 +42,20 @@ export function usePolling() {
34
42
  ...(prevData ?? []),
35
43
  ];
36
44
  });
45
+ })
46
+ .catch((err) => {
47
+ if (controller.signal.aborted) return;
48
+ console.warn('[Comnyx] Polling failed:', err);
49
+ reportSupportError(err, {
50
+ section: 'polling',
51
+ recoverable: true,
52
+ });
37
53
  });
38
- }, NEW_MESSAGES_CHECK_INTERVAL);
39
- }
54
+ }, NEW_MESSAGES_CHECK_INTERVAL);
55
+
40
56
  return () => {
41
- if (interval) {
42
- clearInterval(interval);
43
- }
57
+ clearInterval(interval);
58
+ controller.abort();
44
59
  };
45
60
  }, [customer, lastMessage?.created_at, setData]);
46
61
  }
@@ -1,5 +1,14 @@
1
- import { lightTheme, type ThemeColors } from '../types/Theme';
1
+ import { useMemo } from 'react';
2
+ import { darkTheme, lightTheme, type ThemeColors } from '../types/Theme';
3
+ import { useAppStore } from '../store/store';
2
4
 
3
5
  export function useThemeColors(): ThemeColors {
4
- return lightTheme;
6
+ const theme = useAppStore((s) => s.theme);
7
+ const override = useAppStore((s) => s.themeOverride);
8
+
9
+ return useMemo(() => {
10
+ const base = theme === 'dark' ? darkTheme : lightTheme;
11
+ const patch = override?.[theme];
12
+ return patch ? { ...base, ...patch } : base;
13
+ }, [theme, override]);
5
14
  }
package/src/index.ts CHANGED
@@ -5,6 +5,22 @@ export { ComnyxSupport } from './support';
5
5
  export { ComnyxNotifications } from './notifications';
6
6
  //types
7
7
  export { NotificationPermissionStatus } from './NativeComnyx';
8
+ export type {
9
+ ThemeColors,
10
+ ThemeColorsOverride,
11
+ ThemeOverrideConfig,
12
+ } from './types/Theme';
13
+ export type {
14
+ SupportConfig,
15
+ SupportHeaderRenderProps,
16
+ SupportErrorRenderProps,
17
+ SupportMessageRenderProps,
18
+ SupportSendPayload,
19
+ SupportErrorSection,
20
+ SupportErrorContext,
21
+ SupportErrorReporter,
22
+ } from './support';
23
+ export { ComnyxErrorBoundary } from './support';
8
24
 
9
25
  //deprecated
10
26
  export { registerOneSignalForComnyx } from './register/collectData';
@@ -81,10 +81,11 @@ function changePermissionStatus(status: NotificationPermissionStatus) {
81
81
 
82
82
  async function initializeNativeNotifications(options: InitializeOptions) {
83
83
  const state = useAppStore.getState();
84
- //TODO: better event listener. Event listener doesn't work if its set after initialize
84
+ // Listeners are attached BEFORE nativeComnyx.initialize so that a
85
+ // synchronous TOKEN_INIT emission (e.g. cached APN token on iOS) is not
86
+ // lost between native resolve and JS subscription.
85
87
  globalSubscriptions.subscriptionsForNotification = [
86
88
  ComnyxNotifications.addEventListener('TOKEN_INIT', (data) => {
87
- console.log('TOKEN_INIT', data);
88
89
  useAppStore.getState().setToken(data.token);
89
90
  if (Platform.OS === 'ios') {
90
91
  accumulator.add({
@@ -168,7 +169,11 @@ export async function initializeNotifications(
168
169
  globalSubscriptions.subscriptionsForOptIn = AppState.addEventListener(
169
170
  'change',
170
171
  async (nextAppState) => {
171
- if (nextAppState === 'active') {
172
+ // AppState callbacks are fire-and-forget — any throw here becomes an
173
+ // unhandled rejection that can surface as a red-screen in the host app.
174
+ // Keep the whole body inside a guard.
175
+ try {
176
+ if (nextAppState !== 'active') return;
172
177
  const permissionGivenInForeground =
173
178
  await ComnyxNotifications.checkOptIn();
174
179
  changePermissionStatus(permissionGivenInForeground);
@@ -179,24 +184,21 @@ export async function initializeNotifications(
179
184
  if (!state.notificationInitialized) {
180
185
  await initializeNativeNotifications(options);
181
186
  }
182
- } else {
183
- if (params.showOptInOnForeground) {
184
- ComnyxNotifications.optIn().then(async (permissionResultInner) => {
185
- changePermissionStatus(permissionResultInner);
186
- if (
187
- permissionResultInner === NotificationPermissionStatus.GRANTED
188
- ) {
189
- await initializeNativeNotifications(options);
190
- } else if (
191
- permissionResultInner === NotificationPermissionStatus.BLOCKED
192
- ) {
193
- if (params.linkToSettings) {
194
- showAlertDialog();
195
- }
196
- }
197
- });
198
- }
187
+ return;
188
+ }
189
+ if (!params.showOptInOnForeground) return;
190
+ const permissionResultInner = await ComnyxNotifications.optIn();
191
+ changePermissionStatus(permissionResultInner);
192
+ if (permissionResultInner === NotificationPermissionStatus.GRANTED) {
193
+ await initializeNativeNotifications(options);
194
+ } else if (
195
+ permissionResultInner === NotificationPermissionStatus.BLOCKED &&
196
+ params.linkToSettings
197
+ ) {
198
+ showAlertDialog();
199
199
  }
200
+ } catch (err) {
201
+ console.warn('[Comnyx] AppState handler failed', err);
200
202
  }
201
203
  }
202
204
  );