@ethora/chat-component-rn 26.4.1 → 26.4.2

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 (46) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/LICENSE +21 -661
  3. package/README.md +253 -7
  4. package/index.js +7 -0
  5. package/package.json +47 -16
  6. package/src/components/Header/HeaderRoomList.tsx +8 -7
  7. package/src/components/InputComponents/Search.tsx +7 -4
  8. package/src/components/InputComponents/TextInput.tsx +8 -1
  9. package/src/components/MainComponents/ChatRoom.tsx +16 -4
  10. package/src/components/MainComponents/ChatWrapper.tsx +10 -14
  11. package/src/components/MainComponents/MessageList.tsx +3 -20
  12. package/src/components/MainComponents/RoomList.tsx +7 -2
  13. package/src/components/MessageBubble/Message.tsx +7 -3
  14. package/src/components/MessageBubble/MessageInteractions.tsx +67 -63
  15. package/src/components/Modals/AttachSheet/AttachSheet.tsx +195 -0
  16. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +3 -0
  17. package/src/components/Modals/FilePreviewModal/PdfView.tsx +71 -0
  18. package/src/components/Modals/Modal/Modal.tsx +2 -4
  19. package/src/components/Modals/ModalSelectMedia/ModalSelectMedia.tsx +0 -2
  20. package/src/components/styled/Button.tsx +9 -0
  21. package/src/components/styled/SendInput.tsx +191 -107
  22. package/src/components/styled/StyledInputComponents/StyledInputComponents.tsx +1 -1
  23. package/src/context/xmppProvider.tsx +51 -2
  24. package/src/helpers/constants/MODAL_COMPONENTS.tsx +35 -0
  25. package/src/helpers/constants/MODAL_TYPES.ts +10 -39
  26. package/src/helpers/historyPreloadScheduler.ts +15 -1
  27. package/src/helpers/updateMessagesTillLast.tsx +20 -17
  28. package/src/hooks/useComposing.tsx +0 -9
  29. package/src/hooks/useLogout.tsx +129 -9
  30. package/src/hooks/useSendMessage.tsx +114 -60
  31. package/src/networking/api-requests/auth.api.ts +1 -1
  32. package/src/networking/api-requests/user.api.ts +1 -0
  33. package/src/networking/apiClient.ts +74 -38
  34. package/src/networking/stanzaHandlers.ts +18 -7
  35. package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +86 -0
  36. package/src/networking/xmpp/getHistory.xmpp.ts +6 -1
  37. package/src/networking/xmpp/presenceInRoom.xmpp.ts +8 -3
  38. package/src/networking/xmpp/sendMediaMessage.xmpp.ts +8 -10
  39. package/src/networking/xmpp/sendTextMessage.xmpp.ts +12 -12
  40. package/src/networking/xmppClient.ts +27 -0
  41. package/src/roomStore/Middleware/logoutMiddleware.tsx +17 -8
  42. package/src/roomStore/Middleware/unreadMidlleware.tsx +122 -28
  43. package/src/roomStore/persistence.ts +5 -2
  44. package/src/roomStore/roomsSlice.ts +115 -7
  45. package/src/types/types.ts +10 -9
  46. package/src/utils/devLogger.ts +11 -0
@@ -182,6 +182,11 @@ const RoomList: React.FC<RoomListProps> = ({
182
182
  keyExtractor={(item) => item.jid}
183
183
  renderItem={({ item }) => (
184
184
  <Pressable
185
+ // Stable testID so e2e drivers can target a room
186
+ // row by its jid local-part (e.g. Main chat under
187
+ // app id `..._...759` → testID `room-...759`).
188
+ testID={`room-${(item.jid || '').split('@')[0]}`}
189
+ accessibilityLabel={`room-${item.title || item.name}`}
185
190
  onPress={() => performClick(item)}
186
191
  onPressIn={handlePressIn}
187
192
  onPressOut={handlePressOut}
@@ -237,8 +242,8 @@ const styles = StyleSheet.create({
237
242
  },
238
243
  chatList: {
239
244
  flex: 1,
240
- paddingTop: 10,
241
- paddingHorizontal: 8,
245
+ paddingTop: 8,
246
+ paddingHorizontal: 16,
242
247
  backgroundColor: '#FAFAFA',
243
248
  },
244
249
  });
@@ -1,7 +1,7 @@
1
1
  import React, { useRef, useState } from 'react';
2
2
  import {
3
3
  View,
4
- TouchableWithoutFeedback,
4
+ Pressable,
5
5
  StyleSheet,
6
6
  findNodeHandle,
7
7
  UIManager,
@@ -117,6 +117,10 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
117
117
 
118
118
  const [isPressed, setIsPressed] = useState(false);
119
119
 
120
+ if (__DEV__ && message.id && !(globalThis as any).__loggedMsg?.[message.id]) {
121
+ ((globalThis as any).__loggedMsg ||= {})[message.id] = true;
122
+ }
123
+
120
124
  const [contextMenuPosition, setContextMenuPosition] = useState<{
121
125
  x: number;
122
126
  y: number;
@@ -264,7 +268,7 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
264
268
  )}
265
269
  </CustomMessagePhotoContainer>
266
270
  )}
267
- <TouchableWithoutFeedback
271
+ <Pressable
268
272
  // disableInteractions hides the long-press → context menu
269
273
  // (delete / edit / reply / react). Mirrors web's config gate.
270
274
  onLongPress={
@@ -354,7 +358,7 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
354
358
  )}
355
359
  </MessageFooter>
356
360
  </CustomMessageBubble>
357
- </TouchableWithoutFeedback>
361
+ </Pressable>
358
362
  </View>
359
363
  {!config?.disableInteractions && isPressed && (
360
364
  <MessageInteractions
@@ -1,10 +1,5 @@
1
1
  import React, { useMemo, useState } from 'react';
2
- import {
3
- ContextMenu,
4
- Delimeter,
5
- MenuItem,
6
- Overlay,
7
- } from '../ContextMenu/ContextMenuComponents';
2
+ import { Delimeter, MenuItem } from '../ContextMenu/ContextMenuComponents';
8
3
  import { useDispatch, useSelector } from 'react-redux';
9
4
  import { RootState } from '../../roomStore';
10
5
  import {
@@ -23,7 +18,7 @@ import {
23
18
  View,
24
19
  Pressable,
25
20
  } from 'react-native';
26
- import Clipboard from '@react-native-clipboard/clipboard';
21
+ import * as Clipboard from 'expo-clipboard';
27
22
  import { useToast } from '../../context/ToastContext';
28
23
  import EmojiPicker from 'react-native-emoji-selector';
29
24
 
@@ -94,14 +89,24 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
94
89
  // }
95
90
  };
96
91
 
97
- const handleCopyMessage = (text: string) => {
98
- Clipboard.setString(text);
99
- showToast({
100
- id: Date.now().toString(),
101
- title: 'Success',
102
- message: 'Copied to clipboard!',
103
- type: 'success',
104
- });
92
+ const handleCopyMessage = async (text: string) => {
93
+ try {
94
+ await Clipboard.setStringAsync(text);
95
+ showToast({
96
+ id: Date.now().toString(),
97
+ title: 'Success',
98
+ message: 'Copied to clipboard!',
99
+ type: 'success',
100
+ });
101
+ } catch (err) {
102
+ console.log(err);
103
+ showToast({
104
+ id: Date.now().toString(),
105
+ title: 'Copy failed',
106
+ message: (err as Error)?.message || 'Unknown error',
107
+ type: 'error',
108
+ });
109
+ }
105
110
  closeMenu();
106
111
  };
107
112
 
@@ -135,11 +140,9 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
135
140
  onRequestClose={closeMenu}
136
141
  >
137
142
  {!message.isDeleted && (
138
- <Overlay onPress={closeMenu}>
139
- <ContextMenu
140
- style={[styles.contextMenu, memoPosition]}
141
- // style={{ top: contextMenu.y, left: contextMenu.x }}
142
- >
143
+ <View style={styles.overlayFill}>
144
+ <Pressable style={StyleSheet.absoluteFill} onPress={closeMenu} />
145
+ <View style={[styles.contextMenu, memoPosition]}>
143
146
  {/* <MenuItem onClick={() => console.log(MESSAGE_INTERACTIONS.SEND_COINS)}>
144
147
  {MESSAGE_INTERACTIONS.SEND_COINS}
145
148
  <MESSAGE_INTERACTIONS_ICONS.SEND_COINS />{' '}
@@ -149,43 +152,41 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
149
152
  {MESSAGE_INTERACTIONS.SEND_ITEM}
150
153
  <MESSAGE_INTERACTIONS_ICONS.SEND_ITEM />{' '}
151
154
  </MenuItem> */}
152
- {/* Reaction row — hidden when consumer sets
153
- `config.disableReactions: true`. Mirrors the gate web
154
- applies inside its MessageInteractions panel. */}
155
+ {/*
155
156
  {!config?.disableReactions && (
156
- <View style={{ flexDirection: 'row', paddingBottom: 10 }}>
157
- {fixedEmojiIds.map((id) => (
158
- <Pressable
159
- key={id}
160
- onPress={() => handleReactionClick(id)}
161
- style={{ marginRight: 6 }}
162
- >
163
- <Text style={{ fontSize: 26 }}>{convertIdToEmoji(id)}</Text>
164
- </Pressable>
165
- ))}
166
-
167
- {/* ArrowButton */}
168
- <Pressable
169
- onPress={() => setPickerVisible(!pickerVisible)}
170
- style={{ marginLeft: 10 }}
171
- >
172
- <Text style={{ fontSize: 24 }}>⌄</Text>
173
- </Pressable>
174
- </View>
157
+ <View style={{ flexDirection: 'row', paddingBottom: 10 }}>
158
+ {fixedEmojiIds.map((id) => (
159
+ <Pressable
160
+ key={id}
161
+ onPress={() => handleReactionClick(id)}
162
+ style={{ marginRight: 6 }}
163
+ >
164
+ <Text style={{ fontSize: 26 }}>{convertIdToEmoji(id)}</Text>
165
+ </Pressable>
166
+ ))}
167
+ <Pressable
168
+ onPress={() => setPickerVisible(!pickerVisible)}
169
+ style={{ marginLeft: 10 }}
170
+ >
171
+ <Text style={{ fontSize: 24 }}>⌄</Text>
172
+ </Pressable>
173
+ </View>
175
174
  )}
176
175
 
177
- {!config?.disableReactions && pickerVisible && (
178
- <View style={{ height: 250 }}>
179
- <EmojiPicker
180
- onEmojiSelected={(emoji) => {
181
- handleReactionClick(emoji);
182
- }}
183
- showSearchBar={false}
184
- showTabs={false}
185
- />
186
- </View>
187
- )}
176
+ {!config?.disableReactions && pickerVisible && (
177
+ <View style={{ height: 250 }}>
178
+ <EmojiPicker
179
+ onEmojiSelected={(emoji) => {
180
+ handleReactionClick(emoji);
181
+ }}
182
+ showSearchBar={false}
183
+ showTabs={false}
184
+ />
185
+ </View>
186
+ )}
187
+ */}
188
188
 
189
+ {/*
189
190
  {!isReply && (
190
191
  <>
191
192
  <MenuItem onPress={handleReplyMessage}>
@@ -195,31 +196,32 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
195
196
  <Delimeter />
196
197
  </>
197
198
  )}
199
+ */}
198
200
  <MenuItem onPress={() => handleCopyMessage(message.body!)}>
199
201
  <Text>{MESSAGE_INTERACTIONS.COPY}</Text>
200
202
  <MESSAGE_INTERACTIONS_ICONS.COPY />
201
203
  </MenuItem>
202
- <Delimeter />
203
204
  {isUser && (
204
205
  <>
206
+ <Delimeter />
205
207
  <MenuItem onPress={handleEditMessage}>
206
208
  <Text>{MESSAGE_INTERACTIONS.EDIT}</Text>
207
209
  <MESSAGE_INTERACTIONS_ICONS.EDIT />
208
210
  </MenuItem>
209
211
  <Delimeter />
212
+ <MenuItem onPress={handleDeleteMessage}>
213
+ <Text>{MESSAGE_INTERACTIONS.DELETE}</Text>
214
+ <MESSAGE_INTERACTIONS_ICONS.DELETE />
215
+ </MenuItem>
210
216
  </>
211
217
  )}
212
- <MenuItem onPress={handleDeleteMessage}>
213
- <Text>{MESSAGE_INTERACTIONS.DELETE}</Text>
214
- <MESSAGE_INTERACTIONS_ICONS.DELETE />
215
- </MenuItem>
216
218
  {/* <Delimeter />
217
219
  <MenuItem onClick={() => console.log(MESSAGE_INTERACTIONS.REPORT)}>
218
220
  {MESSAGE_INTERACTIONS.REPORT}
219
221
  <MESSAGE_INTERACTIONS_ICONS.REPORT />{' '}
220
222
  </MenuItem> */}
221
- </ContextMenu>
222
- </Overlay>
223
+ </View>
224
+ </View>
223
225
  )}
224
226
 
225
227
  </Modal>
@@ -229,19 +231,21 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
229
231
  export default MessageInteractions;
230
232
 
231
233
  const styles = StyleSheet.create({
232
- overlay: {
234
+ overlayFill: {
233
235
  ...StyleSheet.absoluteFillObject,
234
- backgroundColor: 'rgba(0, 0, 0, 0.3)',
236
+ backgroundColor: 'transparent',
235
237
  },
236
238
  contextMenu: {
237
239
  position: 'absolute',
238
240
  backgroundColor: 'white',
239
- padding: 10,
241
+ paddingVertical: 8,
242
+ paddingHorizontal: 16,
240
243
  borderRadius: 8,
241
244
  shadowColor: '#000',
242
245
  shadowOffset: { width: 0, height: 2 },
243
246
  shadowOpacity: 0.25,
244
247
  shadowRadius: 4,
245
248
  elevation: 5,
249
+ minWidth: 180,
246
250
  },
247
251
  });
@@ -0,0 +1,195 @@
1
+ /** @format */
2
+
3
+ import React from 'react';
4
+ import {
5
+ Modal,
6
+ Platform,
7
+ StyleSheet,
8
+ Text,
9
+ TouchableOpacity,
10
+ View,
11
+ } from 'react-native';
12
+ import { CameraIcon, DocumentIcon, MediaIcon } from '../../../assets/icons';
13
+
14
+ interface AttachSheetProps {
15
+ visible: boolean;
16
+ onClose: () => void;
17
+ onCamera: () => void;
18
+ onGallery: () => void;
19
+ onDocument: () => void;
20
+ primaryColor?: string;
21
+ }
22
+
23
+ const AttachSheet: React.FC<AttachSheetProps> = ({
24
+ visible,
25
+ onClose,
26
+ onCamera,
27
+ onGallery,
28
+ onDocument,
29
+ primaryColor = '#0052CD',
30
+ }) => {
31
+ const trigger = (handler: () => void) => () => {
32
+ onClose();
33
+ setTimeout(handler, 150);
34
+ };
35
+
36
+ const rows: {
37
+ label: string;
38
+ hint: string;
39
+ Icon: React.ComponentType<any>;
40
+ handler: () => void;
41
+ }[] = [
42
+ {
43
+ label: 'Take photo',
44
+ hint: 'Capture with the camera',
45
+ Icon: CameraIcon,
46
+ handler: onCamera,
47
+ },
48
+ {
49
+ label: 'Photo or video',
50
+ hint: 'Pick from your library',
51
+ Icon: MediaIcon,
52
+ handler: onGallery,
53
+ },
54
+ {
55
+ label: 'Document',
56
+ hint: 'Choose a file',
57
+ Icon: DocumentIcon,
58
+ handler: onDocument,
59
+ },
60
+ ];
61
+
62
+ return (
63
+ <Modal
64
+ transparent
65
+ visible={visible}
66
+ animationType="slide"
67
+ onRequestClose={onClose}
68
+ >
69
+ <TouchableOpacity
70
+ style={styles.backdrop}
71
+ activeOpacity={1}
72
+ onPress={onClose}
73
+ >
74
+ <TouchableOpacity activeOpacity={1} style={styles.sheet}>
75
+ <View style={styles.grabber} />
76
+ <Text style={styles.title}>Attach</Text>
77
+ {rows.map(({ label, hint, Icon, handler }, idx) => (
78
+ <TouchableOpacity
79
+ key={label}
80
+ activeOpacity={0.6}
81
+ style={[
82
+ styles.row,
83
+ idx < rows.length - 1 && styles.rowDivider,
84
+ ]}
85
+ onPress={trigger(handler)}
86
+ >
87
+ <View
88
+ style={[
89
+ styles.iconBubble,
90
+ { backgroundColor: primaryColor + '14' },
91
+ ]}
92
+ >
93
+ <Icon color={primaryColor} width={20} height={20} />
94
+ </View>
95
+ <View style={styles.rowText}>
96
+ <Text style={styles.rowLabel}>{label}</Text>
97
+ <Text style={styles.rowHint}>{hint}</Text>
98
+ </View>
99
+ </TouchableOpacity>
100
+ ))}
101
+ <TouchableOpacity
102
+ activeOpacity={0.6}
103
+ style={styles.cancelRow}
104
+ onPress={onClose}
105
+ >
106
+ <Text style={styles.cancelLabel}>Cancel</Text>
107
+ </TouchableOpacity>
108
+ </TouchableOpacity>
109
+ </TouchableOpacity>
110
+ </Modal>
111
+ );
112
+ };
113
+
114
+ const styles = StyleSheet.create({
115
+ backdrop: {
116
+ flex: 1,
117
+ backgroundColor: 'rgba(15,15,20,0.45)',
118
+ justifyContent: 'flex-end',
119
+ },
120
+ sheet: {
121
+ backgroundColor: '#fff',
122
+ borderTopLeftRadius: 24,
123
+ borderTopRightRadius: 24,
124
+ paddingTop: 8,
125
+ paddingBottom: Platform.OS === 'ios' ? 32 : 20,
126
+ shadowColor: '#000',
127
+ shadowOffset: { width: 0, height: -2 },
128
+ shadowOpacity: 0.08,
129
+ shadowRadius: 12,
130
+ elevation: 16,
131
+ },
132
+ grabber: {
133
+ alignSelf: 'center',
134
+ width: 40,
135
+ height: 4,
136
+ borderRadius: 2,
137
+ backgroundColor: '#D9D9DE',
138
+ marginBottom: 12,
139
+ },
140
+ title: {
141
+ fontSize: 13,
142
+ fontWeight: '600',
143
+ color: '#8A8A8E',
144
+ letterSpacing: 0.4,
145
+ textTransform: 'uppercase',
146
+ paddingHorizontal: 20,
147
+ paddingBottom: 8,
148
+ },
149
+ row: {
150
+ flexDirection: 'row',
151
+ alignItems: 'center',
152
+ paddingHorizontal: 20,
153
+ paddingVertical: 14,
154
+ },
155
+ rowDivider: {
156
+ borderBottomWidth: StyleSheet.hairlineWidth,
157
+ borderBottomColor: '#EFEFF2',
158
+ },
159
+ iconBubble: {
160
+ width: 40,
161
+ height: 40,
162
+ borderRadius: 20,
163
+ alignItems: 'center',
164
+ justifyContent: 'center',
165
+ marginRight: 14,
166
+ },
167
+ rowText: {
168
+ flex: 1,
169
+ },
170
+ rowLabel: {
171
+ fontSize: 16,
172
+ fontWeight: '500',
173
+ color: '#1C1C1E',
174
+ },
175
+ rowHint: {
176
+ fontSize: 12,
177
+ color: '#8A8A8E',
178
+ marginTop: 2,
179
+ },
180
+ cancelRow: {
181
+ marginTop: 8,
182
+ marginHorizontal: 16,
183
+ paddingVertical: 14,
184
+ borderRadius: 14,
185
+ alignItems: 'center',
186
+ backgroundColor: '#F2F2F7',
187
+ },
188
+ cancelLabel: {
189
+ fontSize: 16,
190
+ fontWeight: '600',
191
+ color: '#1C1C1E',
192
+ },
193
+ });
194
+
195
+ export default AttachSheet;
@@ -17,6 +17,7 @@ import Video from 'react-native-video';
17
17
  import RNFS from 'react-native-fs';
18
18
  import { CameraRoll } from '@react-native-camera-roll/camera-roll';
19
19
  import { useToast } from '../../../context/ToastContext';
20
+ import PdfViewer from './PdfView';
20
21
 
21
22
  export const FullScreenVideo = styled.View`
22
23
  width: 100%;
@@ -245,6 +246,8 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
245
246
  paused={true}
246
247
  />
247
248
  );
249
+ case activeFile.mimetype === 'application/pdf':
250
+ return <PdfViewer pdfUrl={activeFile.fileURL} />;
248
251
  default:
249
252
  return (
250
253
  <View
@@ -0,0 +1,71 @@
1
+ import React, { useEffect, useState } from 'react';
2
+ import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
3
+ import { WebView } from 'react-native-webview';
4
+ import * as FileSystem from 'expo-file-system/legacy';
5
+
6
+ interface PdfViewerProps {
7
+ pdfUrl: string;
8
+ }
9
+
10
+ const PdfViewer: React.FC<PdfViewerProps> = ({ pdfUrl }) => {
11
+ const [localUri, setLocalUri] = useState<string | null>(null);
12
+ const [error, setError] = useState<string | null>(null);
13
+
14
+ useEffect(() => {
15
+ let cancelled = false;
16
+ const run = async () => {
17
+ try {
18
+ const target =
19
+ FileSystem.cacheDirectory + `pdf-${Date.now()}.pdf`;
20
+ const result = await FileSystem.downloadAsync(pdfUrl, target);
21
+ if (cancelled) {return;}
22
+ setLocalUri(result.uri);
23
+ } catch (e: any) {
24
+ if (cancelled) {return;}
25
+ console.log(e?.message);
26
+ setError(e?.message || 'Download failed');
27
+ }
28
+ };
29
+ run();
30
+ return () => {
31
+ cancelled = true;
32
+ };
33
+ }, [pdfUrl]);
34
+
35
+ if (error) {
36
+ return (
37
+ <View style={styles.center}>
38
+ <Text>Couldn't load PDF: {error}</Text>
39
+ </View>
40
+ );
41
+ }
42
+
43
+ if (!localUri) {
44
+ return (
45
+ <View style={styles.center}>
46
+ <ActivityIndicator size="large" />
47
+ </View>
48
+ );
49
+ }
50
+
51
+ return (
52
+ <View style={styles.container}>
53
+ <WebView
54
+ source={{ uri: localUri }}
55
+ style={styles.webview}
56
+ originWhitelist={['*']}
57
+ allowFileAccess
58
+ allowFileAccessFromFileURLs
59
+ allowUniversalAccessFromFileURLs
60
+ />
61
+ </View>
62
+ );
63
+ };
64
+
65
+ export default PdfViewer;
66
+
67
+ const styles = StyleSheet.create({
68
+ container: { flex: 1, width: '100%', backgroundColor: '#fff' },
69
+ webview: { flex: 1, backgroundColor: '#fff' },
70
+ center: { flex: 1, alignItems: 'center', justifyContent: 'center' },
71
+ });
@@ -3,10 +3,8 @@ import { ModalBackground } from '../styledModalComponents';
3
3
  import { ModalType } from '../../../types/types';
4
4
  import { useDispatch } from 'react-redux';
5
5
  import { setActiveModal } from '../../../roomStore/chatSettingsSlice';
6
- import {
7
- MODAL_COMPONENTS,
8
- MODAL_TYPES,
9
- } from '../../../helpers/constants/MODAL_TYPES';
6
+ import { MODAL_TYPES } from '../../../helpers/constants/MODAL_TYPES';
7
+ import { MODAL_COMPONENTS } from '../../../helpers/constants/MODAL_COMPONENTS';
10
8
 
11
9
  interface ModalProps {
12
10
  children?: React.ReactNode;
@@ -260,7 +260,6 @@ export const ModalSelectMedia: FC<ModalSelectMediaProps> = ({
260
260
  position="leftBottom"
261
261
  options={menuOptions}
262
262
  openButton={(onPress) => {
263
- console.log('🔵 [ModalSelectMedia] Rendering attach button');
264
263
  return (
265
264
  <Button
266
265
  style={{
@@ -274,7 +273,6 @@ export const ModalSelectMedia: FC<ModalSelectMediaProps> = ({
274
273
  }}
275
274
  EndIcon={<AttachIcon color={config?.colors?.primary || '#0052CD'} />}
276
275
  onPress={() => {
277
- console.log('🔵 [ModalSelectMedia] Attach button pressed');
278
276
  onPress();
279
277
  }}
280
278
  />
@@ -56,6 +56,11 @@ interface ButtonProps {
56
56
  style?: ViewStyle;
57
57
  onPress?: () => void;
58
58
  children?: React.ReactNode;
59
+ // Forwarded for e2e test drivers (Maestro / Detox / Appium). Without
60
+ // these the styled TouchableOpacity has no stable identifier for
61
+ // flow scripts to target.
62
+ testID?: string;
63
+ accessibilityLabel?: string;
59
64
  }
60
65
 
61
66
  const Button: React.FC<ButtonProps> = ({
@@ -71,9 +76,13 @@ const Button: React.FC<ButtonProps> = ({
71
76
  style: containerStyle,
72
77
  color,
73
78
  children,
79
+ testID,
80
+ accessibilityLabel,
74
81
  }) => {
75
82
  return (
76
83
  <CustomButton
84
+ testID={testID}
85
+ accessibilityLabel={accessibilityLabel}
77
86
  style={[containerStyle]}
78
87
  disabled={disabled || loading}
79
88
  onPress={onPress}