@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.
- package/CHANGELOG.md +42 -0
- package/LICENSE +21 -661
- package/README.md +253 -7
- package/index.js +7 -0
- package/package.json +47 -16
- package/src/components/Header/HeaderRoomList.tsx +8 -7
- package/src/components/InputComponents/Search.tsx +7 -4
- package/src/components/InputComponents/TextInput.tsx +8 -1
- package/src/components/MainComponents/ChatRoom.tsx +16 -4
- package/src/components/MainComponents/ChatWrapper.tsx +10 -14
- package/src/components/MainComponents/MessageList.tsx +3 -20
- package/src/components/MainComponents/RoomList.tsx +7 -2
- package/src/components/MessageBubble/Message.tsx +7 -3
- package/src/components/MessageBubble/MessageInteractions.tsx +67 -63
- package/src/components/Modals/AttachSheet/AttachSheet.tsx +195 -0
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +3 -0
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +71 -0
- package/src/components/Modals/Modal/Modal.tsx +2 -4
- package/src/components/Modals/ModalSelectMedia/ModalSelectMedia.tsx +0 -2
- package/src/components/styled/Button.tsx +9 -0
- package/src/components/styled/SendInput.tsx +191 -107
- package/src/components/styled/StyledInputComponents/StyledInputComponents.tsx +1 -1
- package/src/context/xmppProvider.tsx +51 -2
- package/src/helpers/constants/MODAL_COMPONENTS.tsx +35 -0
- package/src/helpers/constants/MODAL_TYPES.ts +10 -39
- package/src/helpers/historyPreloadScheduler.ts +15 -1
- package/src/helpers/updateMessagesTillLast.tsx +20 -17
- package/src/hooks/useComposing.tsx +0 -9
- package/src/hooks/useLogout.tsx +129 -9
- package/src/hooks/useSendMessage.tsx +114 -60
- package/src/networking/api-requests/auth.api.ts +1 -1
- package/src/networking/api-requests/user.api.ts +1 -0
- package/src/networking/apiClient.ts +74 -38
- package/src/networking/stanzaHandlers.ts +18 -7
- package/src/networking/xmpp/flushLastViewedToPrivateStore.ts +86 -0
- package/src/networking/xmpp/getHistory.xmpp.ts +6 -1
- package/src/networking/xmpp/presenceInRoom.xmpp.ts +8 -3
- package/src/networking/xmpp/sendMediaMessage.xmpp.ts +8 -10
- package/src/networking/xmpp/sendTextMessage.xmpp.ts +12 -12
- package/src/networking/xmppClient.ts +27 -0
- package/src/roomStore/Middleware/logoutMiddleware.tsx +17 -8
- package/src/roomStore/Middleware/unreadMidlleware.tsx +122 -28
- package/src/roomStore/persistence.ts +5 -2
- package/src/roomStore/roomsSlice.ts +115 -7
- package/src/types/types.ts +10 -9
- 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:
|
|
241
|
-
paddingHorizontal:
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
-
</
|
|
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 '
|
|
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
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
<
|
|
139
|
-
<
|
|
140
|
-
|
|
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
|
-
{/*
|
|
153
|
-
`config.disableReactions: true`. Mirrors the gate web
|
|
154
|
-
applies inside its MessageInteractions panel. */}
|
|
155
|
+
{/*
|
|
155
156
|
{!config?.disableReactions && (
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
-
</
|
|
222
|
-
</
|
|
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
|
-
|
|
234
|
+
overlayFill: {
|
|
233
235
|
...StyleSheet.absoluteFillObject,
|
|
234
|
-
backgroundColor: '
|
|
236
|
+
backgroundColor: 'transparent',
|
|
235
237
|
},
|
|
236
238
|
contextMenu: {
|
|
237
239
|
position: 'absolute',
|
|
238
240
|
backgroundColor: 'white',
|
|
239
|
-
|
|
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
|
-
|
|
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}
|