@developer_tribe/react-native-comnyx 0.13.11 → 0.14.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 (121) hide show
  1. package/android/src/main/AndroidManifest.xml +15 -0
  2. package/android/src/main/AndroidManifestNew.xml +4 -0
  3. package/android/src/main/java/com/comnyx/ComnyxMediaPickerModule.kt +301 -0
  4. package/android/src/main/java/com/comnyx/ComnyxPackage.kt +1 -1
  5. package/android/src/main/java/com/comnyx/VideoPlayerActivity.kt +91 -0
  6. package/android/src/main/java/com/comnyx/src/messaging/notifications/NotificationsService.kt +12 -1
  7. package/ios/ComnyxMediaPicker.m +23 -0
  8. package/ios/ComnyxMediaPicker.swift +377 -0
  9. package/lib/commonjs/NativeComnyxMediaPicker.js +62 -0
  10. package/lib/commonjs/NativeComnyxMediaPicker.js.map +1 -0
  11. package/lib/commonjs/api/index.js +19 -0
  12. package/lib/commonjs/api/index.js.map +1 -1
  13. package/lib/commonjs/api/media.js +76 -0
  14. package/lib/commonjs/api/media.js.map +1 -0
  15. package/lib/commonjs/assets/attachment-01.png +0 -0
  16. package/lib/commonjs/assets/x-circle.png +0 -0
  17. package/lib/commonjs/components/ChatList.js +48 -22
  18. package/lib/commonjs/components/ChatList.js.map +1 -1
  19. package/lib/commonjs/components/LinkifyText.js +5 -1
  20. package/lib/commonjs/components/LinkifyText.js.map +1 -1
  21. package/lib/commonjs/components/MediaMessageItem.js +333 -0
  22. package/lib/commonjs/components/MediaMessageItem.js.map +1 -0
  23. package/lib/commonjs/components/MediaPickerButton.js +47 -0
  24. package/lib/commonjs/components/MediaPickerButton.js.map +1 -0
  25. package/lib/commonjs/components/MediaViewerModal.js +157 -0
  26. package/lib/commonjs/components/MediaViewerModal.js.map +1 -0
  27. package/lib/commonjs/components/MessageInput.js +344 -73
  28. package/lib/commonjs/components/MessageInput.js.map +1 -1
  29. package/lib/commonjs/components/MessageItem.js +17 -7
  30. package/lib/commonjs/components/MessageItem.js.map +1 -1
  31. package/lib/commonjs/constants/translations.js +203 -29
  32. package/lib/commonjs/constants/translations.js.map +1 -1
  33. package/lib/commonjs/data/fake/media.js +105 -0
  34. package/lib/commonjs/data/fake/media.js.map +1 -0
  35. package/lib/commonjs/notifications/initializeNotifications.js +1 -0
  36. package/lib/commonjs/notifications/initializeNotifications.js.map +1 -1
  37. package/lib/commonjs/types/MediaTypes.js +2 -0
  38. package/lib/commonjs/types/MediaTypes.js.map +1 -0
  39. package/lib/commonjs/version.js +1 -1
  40. package/lib/commonjs/version.js.map +1 -1
  41. package/lib/module/NativeComnyxMediaPicker.js +54 -0
  42. package/lib/module/NativeComnyxMediaPicker.js.map +1 -0
  43. package/lib/module/api/index.js +1 -0
  44. package/lib/module/api/index.js.map +1 -1
  45. package/lib/module/api/media.js +70 -0
  46. package/lib/module/api/media.js.map +1 -0
  47. package/lib/module/assets/attachment-01.png +0 -0
  48. package/lib/module/assets/x-circle.png +0 -0
  49. package/lib/module/components/ChatList.js +48 -22
  50. package/lib/module/components/ChatList.js.map +1 -1
  51. package/lib/module/components/LinkifyText.js +5 -1
  52. package/lib/module/components/LinkifyText.js.map +1 -1
  53. package/lib/module/components/MediaMessageItem.js +330 -0
  54. package/lib/module/components/MediaMessageItem.js.map +1 -0
  55. package/lib/module/components/MediaPickerButton.js +43 -0
  56. package/lib/module/components/MediaPickerButton.js.map +1 -0
  57. package/lib/module/components/MediaViewerModal.js +153 -0
  58. package/lib/module/components/MediaViewerModal.js.map +1 -0
  59. package/lib/module/components/MessageInput.js +347 -75
  60. package/lib/module/components/MessageInput.js.map +1 -1
  61. package/lib/module/components/MessageItem.js +17 -7
  62. package/lib/module/components/MessageItem.js.map +1 -1
  63. package/lib/module/constants/translations.js +203 -29
  64. package/lib/module/constants/translations.js.map +1 -1
  65. package/lib/module/data/fake/media.js +99 -0
  66. package/lib/module/data/fake/media.js.map +1 -0
  67. package/lib/module/notifications/initializeNotifications.js +1 -0
  68. package/lib/module/notifications/initializeNotifications.js.map +1 -1
  69. package/lib/module/types/MediaTypes.js +2 -0
  70. package/lib/module/types/MediaTypes.js.map +1 -0
  71. package/lib/module/version.js +1 -1
  72. package/lib/module/version.js.map +1 -1
  73. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts +7 -0
  74. package/lib/typescript/src/NativeComnyxMediaPicker.d.ts.map +1 -0
  75. package/lib/typescript/src/api/index.d.ts +1 -0
  76. package/lib/typescript/src/api/index.d.ts.map +1 -1
  77. package/lib/typescript/src/api/media.d.ts +7 -0
  78. package/lib/typescript/src/api/media.d.ts.map +1 -0
  79. package/lib/typescript/src/components/ChatList.d.ts.map +1 -1
  80. package/lib/typescript/src/components/LinkifyText.d.ts.map +1 -1
  81. package/lib/typescript/src/components/MediaMessageItem.d.ts +6 -0
  82. package/lib/typescript/src/components/MediaMessageItem.d.ts.map +1 -0
  83. package/lib/typescript/src/components/MediaPickerButton.d.ts +5 -0
  84. package/lib/typescript/src/components/MediaPickerButton.d.ts.map +1 -0
  85. package/lib/typescript/src/components/MediaViewerModal.d.ts +8 -0
  86. package/lib/typescript/src/components/MediaViewerModal.d.ts.map +1 -0
  87. package/lib/typescript/src/components/MessageInput.d.ts.map +1 -1
  88. package/lib/typescript/src/components/MessageItem.d.ts.map +1 -1
  89. package/lib/typescript/src/constants/translations.d.ts.map +1 -1
  90. package/lib/typescript/src/data/fake/media.d.ts +6 -0
  91. package/lib/typescript/src/data/fake/media.d.ts.map +1 -0
  92. package/lib/typescript/src/notifications/initializeNotifications.d.ts.map +1 -1
  93. package/lib/typescript/src/types/Conversation.d.ts +19 -0
  94. package/lib/typescript/src/types/Conversation.d.ts.map +1 -1
  95. package/lib/typescript/src/types/LocalizationKeys.d.ts +6 -0
  96. package/lib/typescript/src/types/LocalizationKeys.d.ts.map +1 -1
  97. package/lib/typescript/src/types/MediaTypes.d.ts +26 -0
  98. package/lib/typescript/src/types/MediaTypes.d.ts.map +1 -0
  99. package/lib/typescript/src/version.d.ts +1 -1
  100. package/lib/typescript/src/version.d.ts.map +1 -1
  101. package/package.json +1 -1
  102. package/src/NativeComnyxMediaPicker.ts +62 -0
  103. package/src/api/index.ts +1 -0
  104. package/src/api/media.ts +116 -0
  105. package/src/assets/attachment-01.png +0 -0
  106. package/src/assets/x-circle.png +0 -0
  107. package/src/components/ChatList.tsx +81 -24
  108. package/src/components/CustomerForm.tsx +1 -1
  109. package/src/components/LinkifyText.tsx +3 -2
  110. package/src/components/MediaMessageItem.tsx +390 -0
  111. package/src/components/MediaPickerButton.tsx +48 -0
  112. package/src/components/MediaViewerModal.tsx +161 -0
  113. package/src/components/MessageInput.tsx +396 -84
  114. package/src/components/MessageItem.tsx +19 -4
  115. package/src/constants/translations.ts +174 -0
  116. package/src/data/fake/media.ts +110 -0
  117. package/src/notifications/initializeNotifications.ts +1 -0
  118. package/src/types/Conversation.ts +20 -0
  119. package/src/types/LocalizationKeys.ts +6 -0
  120. package/src/types/MediaTypes.ts +27 -0
  121. package/src/version.ts +1 -1
@@ -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}>
@@ -0,0 +1,390 @@
1
+ import { View, Image, TouchableOpacity } from 'react-native';
2
+ import { useState, useEffect } from 'react';
3
+ import type { AppConversationMessage } from '../types/Conversation';
4
+ import { useThemeColors } from '../hooks/useThemeColors';
5
+ import { AppText } from './AppText';
6
+ import { ScaledSheet } from './ScaledSheet';
7
+ import { activeOpacity } from '../constants/activeOpacity';
8
+ import { MediaViewerModal } from './MediaViewerModal';
9
+ import { openVideo, generateThumbnail } from '../NativeComnyxMediaPicker';
10
+ import { LinkifyText } from './LinkifyText';
11
+ import { Linking } from 'react-native';
12
+ import type { MediaFileDisplay } from '../types/MediaTypes';
13
+
14
+ const infoIcon = require('../assets/info-circle.png');
15
+ const clockIcon = require('../assets/iconamoon_clock-fill.png');
16
+
17
+ function MediaThumbnail({
18
+ file,
19
+ isUploading,
20
+ onImagePress,
21
+ onVideoPress,
22
+ single,
23
+ }: {
24
+ file: MediaFileDisplay;
25
+ isUploading?: boolean;
26
+ onImagePress: (uri: string) => void;
27
+ onVideoPress: (uri: string) => void;
28
+ single?: boolean;
29
+ }) {
30
+ const [generatedThumb, setGeneratedThumb] = useState<string | null>(null);
31
+ const displayUri = file.local_uri || file.url;
32
+ const isVideo = file.type === 'video';
33
+
34
+ useEffect(() => {
35
+ if (isVideo && !file.thumbnail_uri && !isUploading && displayUri) {
36
+ generateThumbnail(displayUri)
37
+ .then((thumb) => {
38
+ if (thumb) setGeneratedThumb(thumb);
39
+ })
40
+ .catch((err) => {
41
+ console.warn('[Comnyx] Could not generate thumbnail:', err);
42
+ });
43
+ }
44
+ }, [isVideo, file.thumbnail_uri, isUploading, displayUri]);
45
+
46
+ const effectiveThumb = file.thumbnail_uri || generatedThumb;
47
+ const thumbSource = isVideo
48
+ ? effectiveThumb
49
+ ? { uri: effectiveThumb }
50
+ : undefined
51
+ : { uri: displayUri };
52
+
53
+ const handlePress = () => {
54
+ if (isUploading) return;
55
+ if (isVideo) {
56
+ onVideoPress(displayUri);
57
+ } else {
58
+ onImagePress(displayUri);
59
+ }
60
+ };
61
+
62
+ const wrapperStyle = single ? styles.singleMediaWrapper : styles.gridItem;
63
+ const imageStyle = single ? styles.singleMediaImage : styles.gridImage;
64
+ const placeholderStyle = single
65
+ ? [styles.singleMediaImage, { backgroundColor: '#1a1a2e' }]
66
+ : [styles.videoPlaceholder, { backgroundColor: '#1a1a2e' }];
67
+
68
+ return (
69
+ <TouchableOpacity
70
+ style={wrapperStyle}
71
+ onPress={handlePress}
72
+ activeOpacity={activeOpacity}
73
+ disabled={isUploading}
74
+ >
75
+ {thumbSource ? (
76
+ <Image source={thumbSource} style={imageStyle} resizeMode="cover" />
77
+ ) : (
78
+ <View style={placeholderStyle} />
79
+ )}
80
+ {isVideo && !isUploading && (
81
+ <View style={styles.playOverlay}>
82
+ <AppText style={styles.playIcon}>▶</AppText>
83
+ </View>
84
+ )}
85
+ </TouchableOpacity>
86
+ );
87
+ }
88
+
89
+ export function MediaMessageItem({
90
+ item,
91
+ onShowPopup,
92
+ }: {
93
+ item: AppConversationMessage;
94
+ onShowPopup: () => void;
95
+ }) {
96
+ const themeColors = useThemeColors();
97
+ const isDeviceOwner = !item.user && !item.bot;
98
+ const isUploading = item.is_uploading;
99
+ const uploadProgress = item.upload_progress ?? 0;
100
+
101
+ const [viewerVisible, setViewerVisible] = useState(false);
102
+ const [viewerUri, setViewerUri] = useState<string>('');
103
+
104
+ const mediaFiles: MediaFileDisplay[] = (() => {
105
+ if (item.media_files && item.media_files.length > 0) {
106
+ return item.media_files.map((f) => ({
107
+ url: f.url,
108
+ type: f.type,
109
+ local_uri: f.local_uri,
110
+ thumbnail_uri: f.thumbnail_uri,
111
+ }));
112
+ }
113
+
114
+ const singleUri = item.media_local_uri || item.media_url;
115
+ if (singleUri) {
116
+ return [
117
+ {
118
+ url: item.media_url || singleUri,
119
+ type: (item.media_type as 'image' | 'video') || 'image',
120
+ local_uri: item.media_local_uri || undefined,
121
+ thumbnail_uri: item.media_thumbnail_uri || undefined,
122
+ },
123
+ ];
124
+ }
125
+ return [];
126
+ })();
127
+
128
+ const isMultiMedia = mediaFiles.length > 1;
129
+ const hasContent = !!(
130
+ typeof item.content === 'string' && item.content.trim()
131
+ );
132
+
133
+ const formattedDate = new Date(item.created_at).toLocaleTimeString([], {
134
+ hour: '2-digit',
135
+ minute: '2-digit',
136
+ });
137
+
138
+ const handleImagePress = (uri: string) => {
139
+ setViewerUri(uri);
140
+ setViewerVisible(true);
141
+ };
142
+
143
+ const handleVideoPress = (uri: string) => {
144
+ openVideo(uri).catch((err) =>
145
+ console.warn('[Comnyx] Could not open video:', err)
146
+ );
147
+ };
148
+
149
+ const renderFooter = () => {
150
+ if (isDeviceOwner && item.error) {
151
+ return <Image source={infoIcon} style={styles.infoIcon} />;
152
+ } else if (isDeviceOwner && (isUploading || !item.approved)) {
153
+ return (
154
+ <View style={styles.footerRow}>
155
+ {isUploading && (
156
+ <AppText style={styles.uploadPercentText}>
157
+ %{uploadProgress}
158
+ </AppText>
159
+ )}
160
+ <Image source={clockIcon} style={styles.clockIcon} />
161
+ </View>
162
+ );
163
+ } else {
164
+ return (
165
+ <AppText
166
+ style={[
167
+ styles.timestamp,
168
+ isDeviceOwner
169
+ ? styles.rightTimestamp
170
+ : [styles.leftTimestamp, { color: themeColors.text }],
171
+ ]}
172
+ >
173
+ {formattedDate}
174
+ </AppText>
175
+ );
176
+ }
177
+ };
178
+
179
+ return (
180
+ <TouchableOpacity
181
+ activeOpacity={isDeviceOwner && item.error ? 0.8 : 1}
182
+ onLongPress={onShowPopup}
183
+ onPress={() => {
184
+ isDeviceOwner && item.error ? onShowPopup() : {};
185
+ }}
186
+ style={[
187
+ styles.container,
188
+ isDeviceOwner ? styles.rightMessage : styles.leftMessage,
189
+ ]}
190
+ >
191
+ <View
192
+ style={[
193
+ styles.mediaBubble,
194
+ isDeviceOwner
195
+ ? { backgroundColor: themeColors.light_green }
196
+ : { backgroundColor: themeColors.ghost },
197
+ ]}
198
+ >
199
+ {isMultiMedia ? (
200
+ <View style={styles.gridContent}>
201
+ {mediaFiles.map((file, index) => (
202
+ <MediaThumbnail
203
+ key={`${file.url}-${index}`}
204
+ file={file}
205
+ isUploading={isUploading}
206
+ onImagePress={handleImagePress}
207
+ onVideoPress={handleVideoPress}
208
+ />
209
+ ))}
210
+ </View>
211
+ ) : mediaFiles.length === 1 ? (
212
+ <MediaThumbnail
213
+ file={mediaFiles[0]!}
214
+ isUploading={isUploading}
215
+ onImagePress={handleImagePress}
216
+ onVideoPress={handleVideoPress}
217
+ single
218
+ />
219
+ ) : null}
220
+
221
+ {hasContent && (
222
+ <View style={styles.contentContainer}>
223
+ <AppText
224
+ selectable={true}
225
+ style={[
226
+ styles.contentText,
227
+ isDeviceOwner
228
+ ? styles.rightContentText
229
+ : [styles.leftContentText, { color: themeColors.text }],
230
+ ]}
231
+ >
232
+ <LinkifyText
233
+ patterns={[
234
+ {
235
+ regex:
236
+ /https?:\/\/(?:[-\w.])+(?::[0-9]+)?(?:\/(?:[\w/_.])*(?:\?(?:[\w&=%.])*)?(?:#(?:[\w.])*)?)?/g,
237
+ color: '#0066CC',
238
+ navigate: (url) => {
239
+ if (url) {
240
+ Linking.openURL(url);
241
+ }
242
+ },
243
+ },
244
+ ]}
245
+ >
246
+ {typeof item.content === 'string' ? item.content : ''}
247
+ </LinkifyText>
248
+ </AppText>
249
+ </View>
250
+ )}
251
+ <View style={styles.footer}>{renderFooter()}</View>
252
+ </View>
253
+ {viewerVisible && (
254
+ <MediaViewerModal
255
+ visible={viewerVisible}
256
+ onClose={() => {
257
+ setViewerVisible(false);
258
+ setViewerUri('');
259
+ }}
260
+ mediaUri={viewerUri}
261
+ mediaType={'image'}
262
+ />
263
+ )}
264
+ </TouchableOpacity>
265
+ );
266
+ }
267
+
268
+ const styles = ScaledSheet.create({
269
+ container: {
270
+ maxWidth: '80%',
271
+ minWidth: 100,
272
+ marginVertical: '5@vs',
273
+ },
274
+ leftMessage: {
275
+ alignSelf: 'flex-start',
276
+ },
277
+ rightMessage: {
278
+ alignSelf: 'flex-end',
279
+ },
280
+ mediaBubble: {
281
+ borderRadius: '16@vs',
282
+ overflow: 'hidden',
283
+ paddingTop: '4@vs',
284
+ paddingHorizontal: '4@vs',
285
+ },
286
+ gridContent: {
287
+ flexDirection: 'row',
288
+ flexWrap: 'wrap',
289
+ gap: '3@s',
290
+ },
291
+ gridItem: {
292
+ position: 'relative',
293
+ width: '40@vs',
294
+ height: '40@vs',
295
+ borderRadius: '10@vs',
296
+ overflow: 'hidden',
297
+ },
298
+ gridImage: {
299
+ width: '100%',
300
+ height: '100%',
301
+ borderRadius: '10@vs',
302
+ },
303
+ singleMediaWrapper: {
304
+ position: 'relative',
305
+ borderRadius: '12@vs',
306
+ overflow: 'hidden',
307
+ alignSelf: 'flex-start',
308
+ },
309
+ singleMediaImage: {
310
+ width: '40@s',
311
+ aspectRatio: 1,
312
+ borderRadius: '12@vs',
313
+ },
314
+ videoPlaceholder: {
315
+ width: '100%',
316
+ height: '100%',
317
+ borderRadius: '10@vs',
318
+ justifyContent: 'center',
319
+ alignItems: 'center',
320
+ },
321
+ playOverlay: {
322
+ ...({
323
+ position: 'absolute',
324
+ top: 0,
325
+ left: 0,
326
+ right: 0,
327
+ bottom: 0,
328
+ justifyContent: 'center',
329
+ alignItems: 'center',
330
+ } as any),
331
+ },
332
+ playIcon: {
333
+ fontSize: '20@vs',
334
+ color: '#E0E0E0',
335
+ marginLeft: '2@s',
336
+ },
337
+ // Text content
338
+ contentContainer: {
339
+ paddingHorizontal: '8@s',
340
+ paddingTop: '4@vs',
341
+ },
342
+ contentText: {
343
+ fontSize: '16@vs',
344
+ },
345
+ leftContentText: {
346
+ color: '#000000',
347
+ },
348
+ rightContentText: {
349
+ color: '#000000',
350
+ },
351
+ // Footer
352
+ footer: {
353
+ flexDirection: 'row',
354
+ justifyContent: 'flex-end',
355
+ alignItems: 'center',
356
+ marginTop: '4@vs',
357
+ paddingHorizontal: '8@s',
358
+ paddingBottom: '4@vs',
359
+ },
360
+ footerRow: {
361
+ flexDirection: 'row',
362
+ alignItems: 'center',
363
+ gap: '4@s',
364
+ },
365
+ uploadPercentText: {
366
+ fontSize: '10@vs',
367
+ opacity: 0.7,
368
+ color: '#000000',
369
+ },
370
+ timestamp: {
371
+ fontSize: '10@vs',
372
+ opacity: 0.7,
373
+ },
374
+ leftTimestamp: {
375
+ color: '#666666',
376
+ },
377
+ rightTimestamp: {
378
+ color: '#000000',
379
+ },
380
+ infoIcon: {
381
+ width: '12@vs',
382
+ height: '12@vs',
383
+ marginLeft: '4@s',
384
+ },
385
+ clockIcon: {
386
+ width: '12@vs',
387
+ height: '12@vs',
388
+ marginLeft: '4@s',
389
+ },
390
+ });
@@ -0,0 +1,48 @@
1
+ import { TouchableOpacity, Image } from 'react-native';
2
+ import { useCallback } from 'react';
3
+ import { useThemeColors } from '../hooks/useThemeColors';
4
+ import { ScaledSheet } from './ScaledSheet';
5
+ import { activeOpacity } from '../constants/activeOpacity';
6
+ import { pickMedia } from '../NativeComnyxMediaPicker';
7
+ import type { MediaAsset } from '../types/MediaTypes';
8
+
9
+ const paperclipIcon = require('../assets/attachment-01.png');
10
+
11
+ export function MediaPickerButton({
12
+ onMediaSelected,
13
+ }: {
14
+ onMediaSelected: (assets: MediaAsset[]) => void;
15
+ }) {
16
+ const themeColors = useThemeColors();
17
+
18
+ const showPicker = useCallback(async () => {
19
+ const assets = await pickMedia();
20
+ if (assets.length > 0) onMediaSelected(assets);
21
+ }, [onMediaSelected]);
22
+
23
+ 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>
34
+ );
35
+ }
36
+
37
+ const styles = ScaledSheet.create({
38
+ button: {
39
+ justifyContent: 'center',
40
+ alignItems: 'center',
41
+ marginRight: '6@s',
42
+ paddingVertical: '6@vs',
43
+ },
44
+ icon: {
45
+ width: '28@vs',
46
+ height: '28@vs',
47
+ },
48
+ });