@developer_tribe/react-native-comnyx 0.13.13 → 0.15.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 (132) hide show
  1. package/android/generated/RCTAppDependencyProvider.h +25 -0
  2. package/android/generated/RCTAppDependencyProvider.mm +55 -0
  3. package/android/generated/RCTModulesConformingToProtocolsProvider.h +18 -0
  4. package/android/generated/RCTModulesConformingToProtocolsProvider.mm +33 -0
  5. package/android/generated/RCTThirdPartyComponentsProvider.h +16 -0
  6. package/android/generated/RCTThirdPartyComponentsProvider.mm +23 -0
  7. package/android/generated/ReactAppDependencyProvider.podspec +34 -0
  8. package/android/generated/jni/CMakeLists.txt +36 -0
  9. package/android/generated/jni/RNComnyxSpec-generated.cpp +22 -0
  10. package/android/generated/jni/RNComnyxSpec.h +24 -0
  11. package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI-generated.cpp +17 -0
  12. package/android/generated/jni/react/renderer/components/RNComnyxSpec/RNComnyxSpecJSI.h +19 -0
  13. package/android/src/main/AndroidManifest.xml +15 -0
  14. package/android/src/main/AndroidManifestNew.xml +4 -0
  15. package/android/src/main/java/com/comnyx/ComnyxMediaPickerModule.kt +347 -0
  16. package/android/src/main/java/com/comnyx/ComnyxPackage.kt +1 -1
  17. package/android/src/main/java/com/comnyx/VideoPlayerActivity.kt +91 -0
  18. package/ios/ComnyxMediaPicker.m +29 -0
  19. package/ios/ComnyxMediaPicker.swift +436 -0
  20. package/lib/commonjs/NativeComnyxMediaPicker.js +83 -0
  21. package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -0
  22. package/lib/commonjs/api/index.js +19 -0
  23. package/lib/commonjs/api/index.js.map +1 -1
  24. package/lib/commonjs/api/media.js +76 -0
  25. package/lib/commonjs/api/media.js.map +1 -0
  26. package/lib/commonjs/assets/attachment-01.png +0 -0
  27. package/lib/commonjs/assets/gallery.png +0 -0
  28. package/lib/commonjs/assets/video-play.png +0 -0
  29. package/lib/commonjs/assets/x-circle.png +0 -0
  30. package/lib/commonjs/components/ChatList.js +48 -22
  31. package/lib/commonjs/components/ChatList.js.map +1 -1
  32. package/lib/commonjs/components/LinkifyText.js +5 -1
  33. package/lib/commonjs/components/LinkifyText.js.map +1 -1
  34. package/lib/commonjs/components/MediaMessageItem.js +333 -0
  35. package/lib/commonjs/components/MediaMessageItem.js.map +1 -0
  36. package/lib/commonjs/components/MediaPickerButton.js +244 -0
  37. package/lib/commonjs/components/MediaPickerButton.js.map +1 -0
  38. package/lib/commonjs/components/MediaViewerModal.js +164 -0
  39. package/lib/commonjs/components/MediaViewerModal.js.map +1 -0
  40. package/lib/commonjs/components/MessageInput.js +344 -73
  41. package/lib/commonjs/components/MessageInput.js.map +1 -1
  42. package/lib/commonjs/components/MessageItem.js +17 -8
  43. package/lib/commonjs/components/MessageItem.js.map +1 -1
  44. package/lib/commonjs/constants/translations.js +174 -29
  45. package/lib/commonjs/constants/translations.js.map +1 -1
  46. package/lib/commonjs/data/fake/media.js +105 -0
  47. package/lib/commonjs/data/fake/media.js.map +1 -0
  48. package/lib/commonjs/types/MediaTypes.js +2 -0
  49. package/lib/commonjs/types/MediaTypes.js.map +1 -0
  50. package/lib/commonjs/version.js +1 -1
  51. package/lib/commonjs/version.js.map +1 -1
  52. package/lib/module/NativeComnyxMediaPicker.js +73 -0
  53. package/lib/module/NativeComnyxMediaPicker.js.map +1 -0
  54. package/lib/module/api/index.js +1 -0
  55. package/lib/module/api/index.js.map +1 -1
  56. package/lib/module/api/media.js +70 -0
  57. package/lib/module/api/media.js.map +1 -0
  58. package/lib/module/assets/attachment-01.png +0 -0
  59. package/lib/module/assets/gallery.png +0 -0
  60. package/lib/module/assets/video-play.png +0 -0
  61. package/lib/module/assets/x-circle.png +0 -0
  62. package/lib/module/components/ChatList.js +48 -22
  63. package/lib/module/components/ChatList.js.map +1 -1
  64. package/lib/module/components/LinkifyText.js +5 -1
  65. package/lib/module/components/LinkifyText.js.map +1 -1
  66. package/lib/module/components/MediaMessageItem.js +330 -0
  67. package/lib/module/components/MediaMessageItem.js.map +1 -0
  68. package/lib/module/components/MediaPickerButton.js +240 -0
  69. package/lib/module/components/MediaPickerButton.js.map +1 -0
  70. package/lib/module/components/MediaViewerModal.js +160 -0
  71. package/lib/module/components/MediaViewerModal.js.map +1 -0
  72. package/lib/module/components/MessageInput.js +347 -75
  73. package/lib/module/components/MessageInput.js.map +1 -1
  74. package/lib/module/components/MessageItem.js +17 -8
  75. package/lib/module/components/MessageItem.js.map +1 -1
  76. package/lib/module/constants/translations.js +174 -29
  77. package/lib/module/constants/translations.js.map +1 -1
  78. package/lib/module/data/fake/media.js +99 -0
  79. package/lib/module/data/fake/media.js.map +1 -0
  80. package/lib/module/types/MediaTypes.js +2 -0
  81. package/lib/module/types/MediaTypes.js.map +1 -0
  82. package/lib/module/version.js +1 -1
  83. package/lib/module/version.js.map +1 -1
  84. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +9 -0
  85. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -0
  86. package/lib/typescript/src/api/index.d.ts +1 -0
  87. package/lib/typescript/src/api/index.d.ts.map +1 -1
  88. package/lib/typescript/src/api/media.d.ts +7 -0
  89. package/lib/typescript/src/api/media.d.ts.map +1 -0
  90. package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
  91. package/lib/typescript/src/components/LinkifyText.d.ts.map +1 -1
  92. package/lib/typescript/src/components/MediaMessageItem.d.ts +6 -0
  93. package/lib/typescript/src/components/MediaMessageItem.d.ts.map +1 -0
  94. package/lib/typescript/src/components/MediaPickerButton.d.ts +5 -0
  95. package/lib/typescript/src/components/MediaPickerButton.d.ts.map +1 -0
  96. package/lib/typescript/src/components/MediaViewerModal.d.ts +8 -0
  97. package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -0
  98. package/lib/typescript/src/components/MessageInput.d.ts.map +1 -1
  99. package/lib/typescript/src/components/MessageItem.d.ts.map +1 -1
  100. package/lib/typescript/src/constants/translations.d.ts.map +1 -1
  101. package/lib/typescript/src/data/fake/media.d.ts +6 -0
  102. package/lib/typescript/src/data/fake/media.d.ts.map +1 -0
  103. package/lib/typescript/src/types/Conversation.d.ts +19 -0
  104. package/lib/typescript/src/types/Conversation.d.ts.map +1 -1
  105. package/lib/typescript/src/types/LocalizationKeys.d.ts +5 -0
  106. package/lib/typescript/src/types/LocalizationKeys.d.ts.map +1 -1
  107. package/lib/typescript/src/types/MediaTypes.d.ts +26 -0
  108. package/lib/typescript/src/types/MediaTypes.d.ts.map +1 -0
  109. package/lib/typescript/src/version.d.ts +1 -1
  110. package/lib/typescript/src/version.d.ts.map +1 -1
  111. package/package.json +1 -1
  112. package/src/NativeComnyxMediaPicker.ts +83 -0
  113. package/src/api/index.ts +1 -0
  114. package/src/api/media.ts +116 -0
  115. package/src/assets/attachment-01.png +0 -0
  116. package/src/assets/gallery.png +0 -0
  117. package/src/assets/video-play.png +0 -0
  118. package/src/assets/x-circle.png +0 -0
  119. package/src/components/ChatList.tsx +81 -24
  120. package/src/components/CustomerForm.tsx +1 -1
  121. package/src/components/LinkifyText.tsx +3 -2
  122. package/src/components/MediaMessageItem.tsx +390 -0
  123. package/src/components/MediaPickerButton.tsx +269 -0
  124. package/src/components/MediaViewerModal.tsx +168 -0
  125. package/src/components/MessageInput.tsx +396 -84
  126. package/src/components/MessageItem.tsx +19 -5
  127. package/src/constants/translations.ts +145 -0
  128. package/src/data/fake/media.ts +110 -0
  129. package/src/types/Conversation.ts +20 -0
  130. package/src/types/LocalizationKeys.ts +5 -0
  131. package/src/types/MediaTypes.ts +27 -0
  132. package/src/version.ts +1 -1
@@ -0,0 +1,83 @@
1
+ import { NativeModules } from 'react-native';
2
+ import type { MediaAsset } from './types/MediaTypes';
3
+
4
+ const { ComnyxMediaPicker } = NativeModules;
5
+
6
+ function mapResults(results: any[]): MediaAsset[] {
7
+ return results.map((result: any) => {
8
+ const isImage =
9
+ result.type === 'image' ||
10
+ (result.mimeType && result.mimeType.startsWith('image'));
11
+
12
+ return {
13
+ uri: result.uri,
14
+ type: isImage ? ('image' as const) : ('video' as const),
15
+ fileName: result.fileName,
16
+ fileSize: result.fileSize,
17
+ mimeType: result.mimeType,
18
+ width: isImage ? result.width : undefined,
19
+ height: isImage ? result.height : undefined,
20
+ duration: isImage ? undefined : result.duration,
21
+ thumbnailUri: isImage ? undefined : result.thumbnailUri,
22
+ };
23
+ });
24
+ }
25
+
26
+ export async function pickMedia(): Promise<MediaAsset[]> {
27
+ if (!ComnyxMediaPicker) {
28
+ return [];
29
+ }
30
+ const results = await ComnyxMediaPicker.pickMedia();
31
+ if (!results || !Array.isArray(results) || results.length === 0) return [];
32
+ return mapResults(results);
33
+ }
34
+
35
+ export async function pickImage(): Promise<MediaAsset[]> {
36
+ if (!ComnyxMediaPicker) {
37
+ return [];
38
+ }
39
+ const results = await ComnyxMediaPicker.pickImage();
40
+ if (!results || !Array.isArray(results) || results.length === 0) return [];
41
+ return mapResults(results);
42
+ }
43
+
44
+ export async function pickVideo(): Promise<MediaAsset[]> {
45
+ if (!ComnyxMediaPicker) {
46
+ return [];
47
+ }
48
+ const results = await ComnyxMediaPicker.pickVideo();
49
+ if (!results || !Array.isArray(results) || results.length === 0) return [];
50
+ return mapResults(results);
51
+ }
52
+
53
+ export async function openVideo(uri: string): Promise<void> {
54
+ if (!ComnyxMediaPicker) {
55
+ console.warn('[Comnyx] ComnyxMediaPicker native module is not available');
56
+ return;
57
+ }
58
+ await ComnyxMediaPicker.openVideo(uri);
59
+ }
60
+
61
+ export async function generateThumbnail(
62
+ videoUrl: string
63
+ ): Promise<string | null> {
64
+ if (!ComnyxMediaPicker) {
65
+ console.warn('[Comnyx] ComnyxMediaPicker native module is not available');
66
+ return null;
67
+ }
68
+ return ComnyxMediaPicker.generateThumbnail(videoUrl);
69
+ }
70
+
71
+ export async function deleteTempFile(uri: string): Promise<void> {
72
+ if (!ComnyxMediaPicker) {
73
+ return;
74
+ }
75
+ return ComnyxMediaPicker.deleteTempFile(uri);
76
+ }
77
+
78
+ export async function cleanupTempFiles(): Promise<void> {
79
+ if (!ComnyxMediaPicker) {
80
+ return;
81
+ }
82
+ return ComnyxMediaPicker.cleanupTempFiles();
83
+ }
package/src/api/index.ts CHANGED
@@ -4,3 +4,4 @@ export {
4
4
  getNewCustomerConversation,
5
5
  } from './conversations';
6
6
  export { sendCustomerMessage } from './messages';
7
+ export { getUploadUrl, uploadFileToS3, sendMediaMessage } from './media';
@@ -0,0 +1,116 @@
1
+ import { Platform } from 'react-native';
2
+ import type { UploadUrlResponse } from '../types/MediaTypes';
3
+ import type { MessageResponse } from '../types/MessageResponse';
4
+ import type { ApiOptions } from '../types/ApiOptions';
5
+ import { axiosInstance } from './api';
6
+ import {
7
+ getFakeUploadUrlResponse,
8
+ fakeUploadToS3,
9
+ getFakeMediaMessageResponse,
10
+ } from '../data/fake/media';
11
+
12
+ export function getUploadUrl(
13
+ fileName: string,
14
+ contentType: string,
15
+ options: ApiOptions
16
+ ): Promise<UploadUrlResponse> {
17
+ if (options.fake) {
18
+ return getFakeUploadUrlResponse(fileName);
19
+ }
20
+ return axiosInstance
21
+ .post<UploadUrlResponse>('/api/media/presign', {
22
+ fileName,
23
+ fileType: contentType,
24
+ })
25
+ .then((res) => res.data);
26
+ }
27
+
28
+ export function uploadFileToS3(
29
+ uploadUrl: string,
30
+ fileUri: string,
31
+ contentType: string,
32
+ onProgress?: (percentage: number) => void,
33
+ options?: ApiOptions
34
+ ): Promise<void> {
35
+ if (options?.fake) {
36
+ return fakeUploadToS3(onProgress);
37
+ }
38
+
39
+ return new Promise((resolve, reject) => {
40
+ const xhr = new XMLHttpRequest();
41
+
42
+ xhr.upload.addEventListener('progress', (event) => {
43
+ if (event.lengthComputable && onProgress) {
44
+ const percentage = Math.min(
45
+ 100,
46
+ Math.round((event.loaded / event.total) * 100)
47
+ );
48
+ onProgress(percentage);
49
+ }
50
+ });
51
+
52
+ xhr.addEventListener('load', () => {
53
+ if (xhr.status >= 200 && xhr.status < 300) {
54
+ resolve();
55
+ } else {
56
+ console.error('[Comnyx] Upload failed:', xhr.status, xhr.responseText);
57
+ reject(new Error(`Upload failed with status ${xhr.status}`));
58
+ }
59
+ });
60
+
61
+ xhr.addEventListener('error', () => {
62
+ console.error(
63
+ '[Comnyx] Upload XHR error, status:',
64
+ xhr.status,
65
+ 'response:',
66
+ xhr.responseText
67
+ );
68
+ reject(new Error('Upload failed'));
69
+ });
70
+
71
+ xhr.addEventListener('abort', () => {
72
+ reject(new Error('Upload aborted'));
73
+ });
74
+
75
+ xhr.open('PUT', uploadUrl);
76
+ xhr.setRequestHeader('Content-Type', contentType);
77
+
78
+ // React Native supports sending file URIs directly via XHR
79
+ const file: any = {
80
+ uri: Platform.OS === 'android' ? fileUri : fileUri.replace('file://', ''),
81
+ type: contentType,
82
+ name: fileUri.split('/').pop() || 'file',
83
+ };
84
+
85
+ xhr.send(file);
86
+ });
87
+ }
88
+
89
+ export function sendMediaMessage(
90
+ externalId: string,
91
+ fileUrls: string[],
92
+ mediaType: 'image' | 'video',
93
+ content: string,
94
+ options: ApiOptions
95
+ ): Promise<MessageResponse> {
96
+ if (options.fake) {
97
+ return getFakeMediaMessageResponse(fileUrls, mediaType);
98
+ }
99
+ return axiosInstance
100
+ .post<MessageResponse>(
101
+ '/api/customers/message',
102
+ {
103
+ externalId,
104
+ content,
105
+ files: fileUrls,
106
+ media_type: mediaType,
107
+ },
108
+ {
109
+ headers: {
110
+ 'Accept': 'application/json',
111
+ 'Content-Type': 'application/json',
112
+ },
113
+ }
114
+ )
115
+ .then((res) => res.data);
116
+ }
Binary file
Binary file
Binary file
Binary file
@@ -15,7 +15,10 @@ import {
15
15
  } from 'react-native';
16
16
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
17
17
  import { getCustomerConversation, sendCustomerMessage } from '../api';
18
- import type { AppConversationMessage } from '../types/Conversation';
18
+ import type {
19
+ AppConversationMessage,
20
+ ConversationMessage,
21
+ } from '../types/Conversation';
19
22
  import { MessageItem } from './MessageItem';
20
23
  import { MessageInput } from './MessageInput';
21
24
  import { useThemeColors } from '../hooks/useThemeColors';
@@ -32,6 +35,66 @@ import { useAppStore } from '../store/store';
32
35
  const headphonesIcon = require('../assets/headphones-01.png');
33
36
  const closeIcon = require('../assets/x-close.png');
34
37
 
38
+ const VIDEO_EXTENSIONS = [
39
+ '.mp4',
40
+ '.mov',
41
+ '.m4v',
42
+ '.avi',
43
+ '.webm',
44
+ '.mkv',
45
+ '.3gp',
46
+ ];
47
+ const IMAGE_EXTENSIONS = [
48
+ '.jpg',
49
+ '.jpeg',
50
+ '.png',
51
+ '.gif',
52
+ '.heic',
53
+ '.heif',
54
+ '.webp',
55
+ '.bmp',
56
+ ];
57
+
58
+ function getMediaTypeFromFile(
59
+ file: { mime_type?: string; url?: string } | null
60
+ ): 'image' | 'video' | undefined {
61
+ if (!file) return undefined;
62
+ // 1. Try mime_type first
63
+ if (file.mime_type) {
64
+ if (file.mime_type.startsWith('image')) return 'image';
65
+ if (file.mime_type.startsWith('video')) return 'video';
66
+ }
67
+ // 2. Fallback: check URL for known extensions
68
+ if (file.url) {
69
+ // Strip query params from URL for extension check
70
+ const urlPath = file.url.split('?')[0] || '';
71
+ const lower = urlPath.toLowerCase();
72
+ if (VIDEO_EXTENSIONS.some((ext) => lower.endsWith(ext))) return 'video';
73
+ if (IMAGE_EXTENSIONS.some((ext) => lower.endsWith(ext))) return 'image';
74
+ }
75
+ // 3. Default to image if file exists but can't determine type
76
+ return file.url ? 'image' : undefined;
77
+ }
78
+
79
+ function processRawMessage(msg: ConversationMessage): AppConversationMessage {
80
+ const file = msg.files && msg.files.length > 0 ? msg.files[0] : null;
81
+ const mediaFiles =
82
+ msg.files && msg.files.length > 0
83
+ ? msg.files.map((f) => ({
84
+ url: f.url,
85
+ type: getMediaTypeFromFile(f) || ('image' as const),
86
+ }))
87
+ : undefined;
88
+ return {
89
+ ...msg,
90
+ created_at: new Date(msg.created_at),
91
+ approved: true,
92
+ media_url: file?.url || undefined,
93
+ media_type: getMediaTypeFromFile(file),
94
+ media_files: mediaFiles,
95
+ };
96
+ }
97
+
35
98
  function LoadingItem() {
36
99
  const themeColors = useThemeColors();
37
100
  return (
@@ -141,7 +204,15 @@ export function ChatList({
141
204
  const sections = useMemo(() => {
142
205
  if (!data || data.length === 0) return [];
143
206
 
144
- const validData = data.map((message) => {
207
+ const seenIds = new Set<number>();
208
+ const dedupedData = data.filter((msg) => {
209
+ if (msg.id == null) return true;
210
+ if (seenIds.has(msg.id)) return false;
211
+ seenIds.add(msg.id);
212
+ return true;
213
+ });
214
+
215
+ const validData = dedupedData.map((message) => {
145
216
  if (!message.created_at || !(message.created_at instanceof Date)) {
146
217
  return {
147
218
  ...message,
@@ -340,11 +411,8 @@ export function ChatList({
340
411
  (msg) => !existingIds.has(msg.id)
341
412
  );
342
413
 
343
- const processedMessages = uniqueNewMessages.map((u) => ({
344
- ...u,
345
- created_at: new Date(u.created_at),
346
- approved: true,
347
- }));
414
+ const processedMessages =
415
+ uniqueNewMessages.map(processRawMessage);
348
416
 
349
417
  if (processedMessages.length === 0) {
350
418
  nextPageStatus.current = 'empty';
@@ -355,11 +423,7 @@ export function ChatList({
355
423
  }
356
424
  } else if (newMessages.length > 0) {
357
425
  // Only new messages, no previous data
358
- const processedMessages = newMessages.map((u) => ({
359
- ...u,
360
- created_at: new Date(u.created_at),
361
- approved: true,
362
- }));
426
+ const processedMessages = newMessages.map(processRawMessage);
363
427
  nextPageStatus.current = undefined;
364
428
  return processedMessages;
365
429
  } else {
@@ -464,11 +528,8 @@ export function ChatList({
464
528
  const uniqueNewMessages = newMessages.filter(
465
529
  (msg) => !existingIds.has(msg.id)
466
530
  );
467
- const processedMessages = uniqueNewMessages.map((u) => ({
468
- ...u,
469
- created_at: new Date(u.created_at),
470
- approved: true,
471
- }));
531
+ const processedMessages =
532
+ uniqueNewMessages.map(processRawMessage);
472
533
  useAppStore.setState({
473
534
  firstMessage: processedMessages[0] || prevData[0],
474
535
  });
@@ -478,11 +539,7 @@ export function ChatList({
478
539
  return [...prevData, ...processedMessages];
479
540
  } else {
480
541
  // Handle the case where there's no previous data
481
- const processedMessages = newMessages.map((u) => ({
482
- ...u,
483
- created_at: new Date(u.created_at),
484
- approved: true,
485
- }));
542
+ const processedMessages = newMessages.map(processRawMessage);
486
543
  useAppStore.setState({
487
544
  firstMessage: processedMessages[0],
488
545
  });
@@ -638,8 +695,8 @@ export function ChatList({
638
695
  ) : null
639
696
  }
640
697
  ListFooterComponent={loading ? <LoadingItem /> : null}
641
- keyExtractor={(item: AppConversationMessage) =>
642
- item.id + '-' + item.created_at
698
+ keyExtractor={(item: AppConversationMessage, index: number) =>
699
+ item.id?.toString() ?? item.local_id ?? `fake-${index}`
643
700
  }
644
701
  removeClippedSubviews={false}
645
702
  maxToRenderPerBatch={20}
@@ -38,7 +38,7 @@ interface InfoButtonProps {
38
38
 
39
39
  const closeIcon = require('../assets/x-close.png');
40
40
  const infoIcon = require('../assets/info-circle.png');
41
- const BORDER_BOTTOM_WIDTH = 1
41
+ const BORDER_BOTTOM_WIDTH = 1;
42
42
 
43
43
  export function CustomerForm({
44
44
  loading,
@@ -15,7 +15,8 @@ interface Props {
15
15
  patterns: Pattern[];
16
16
  }
17
17
 
18
- function parseText(text: string, patterns: Pattern[]) {
18
+ function parseText(text: string | null | undefined, patterns: Pattern[]) {
19
+ if (!text) return [{ type: 'text' as const, value: '' }];
19
20
  type Segment =
20
21
  | { type: 'text'; value: string }
21
22
  | { type: 'match'; pattern: Pattern; value: string; matchedText: string };
@@ -74,7 +75,7 @@ export function LinkifyText({
74
75
  containerStyle,
75
76
  ...props
76
77
  }: Props) {
77
- const parts = parseText(children, patterns);
78
+ const parts = parseText(children || '', patterns);
78
79
 
79
80
  return (
80
81
  <AppText style={containerStyle} {...props}>