@ethora/chat-component-rn 26.5.5 → 26.5.7
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 +44 -0
- package/lib/commonjs/components/MainComponents/ChatHeader.js +7 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +8 -2
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +2 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +11 -5
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MessageList.js +10 -2
- package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js +60 -7
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +35 -16
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js +11 -2
- package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +1 -1
- package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/commonjs/components/styled/MediaFilePreview.js +96 -5
- package/lib/commonjs/components/styled/MediaFilePreview.js.map +1 -1
- package/lib/commonjs/components/styled/MessageImage.js +1 -2
- package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +4 -2
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +2 -10
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +71 -2
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/mimeToExtension.js +131 -0
- package/lib/commonjs/helpers/mimeToExtension.js.map +1 -0
- package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +73 -1
- package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
- package/lib/commonjs/hooks/useChatWrapperInit.js +22 -10
- package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/commonjs/hooks/useEventHandlers.js +9 -1
- package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
- package/lib/commonjs/hooks/useMessageHeapState.js +6 -2
- package/lib/commonjs/hooks/useMessageHeapState.js.map +1 -1
- package/lib/commonjs/hooks/useSendMessage.js +48 -4
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/networking/api-requests/auth.api.js +7 -1
- package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +87 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/roomHeapSlice.js +26 -8
- package/lib/commonjs/roomStore/roomHeapSlice.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +7 -1
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +8 -2
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +2 -1
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +11 -5
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/MessageList.js +11 -3
- package/lib/module/components/MainComponents/MessageList.js.map +1 -1
- package/lib/module/components/MessageBubble/Message.js +60 -7
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -17
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js +11 -2
- package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +1 -1
- package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
- package/lib/module/components/styled/MediaFilePreview.js +97 -6
- package/lib/module/components/styled/MediaFilePreview.js.map +1 -1
- package/lib/module/components/styled/MessageImage.js +1 -2
- package/lib/module/components/styled/MessageImage.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +4 -2
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +2 -10
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/xmppProvider.js +73 -3
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/mimeToExtension.js +124 -0
- package/lib/module/helpers/mimeToExtension.js.map +1 -0
- package/lib/module/helpers/resolveInitBeforeLoadUser.js +71 -0
- package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
- package/lib/module/hooks/useChatWrapperInit.js +22 -10
- package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/module/hooks/useEventHandlers.js +9 -1
- package/lib/module/hooks/useEventHandlers.js.map +1 -1
- package/lib/module/hooks/useMessageHeapState.js +6 -2
- package/lib/module/hooks/useMessageHeapState.js.map +1 -1
- package/lib/module/hooks/useSendMessage.js +49 -5
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/networking/api-requests/auth.api.js +7 -1
- package/lib/module/networking/api-requests/auth.api.js.map +1 -1
- package/lib/module/networking/xmppClient.js +87 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/roomHeapSlice.js +24 -7
- package/lib/module/roomStore/roomHeapSlice.js.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts +6 -0
- package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts.map +1 -1
- package/lib/typescript/components/styled/MediaFilePreview.d.ts.map +1 -1
- package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts +37 -0
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -0
- package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts +18 -0
- package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
- package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
- package/lib/typescript/hooks/useEventHandlers.d.ts +6 -0
- package/lib/typescript/hooks/useEventHandlers.d.ts.map +1 -1
- package/lib/typescript/hooks/useMessageHeapState.d.ts +2 -0
- package/lib/typescript/hooks/useMessageHeapState.d.ts.map +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts +2 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts +25 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomHeapSlice.d.ts +22 -1
- package/lib/typescript/roomStore/roomHeapSlice.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +6 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/components/MainComponents/ChatHeader.tsx +5 -1
- package/src/components/MainComponents/ChatRoom.tsx +6 -1
- package/src/components/MainComponents/ChatWrapper.tsx +1 -0
- package/src/components/MainComponents/MediaMessage.tsx +15 -8
- package/src/components/MainComponents/MessageList.tsx +10 -3
- package/src/components/MessageBubble/Message.tsx +64 -10
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +30 -12
- package/src/components/Modals/ModalWrapper/ModalWrapper.tsx +22 -1
- package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +1 -1
- package/src/components/styled/MediaFilePreview.tsx +91 -5
- package/src/components/styled/MessageImage.tsx +1 -2
- package/src/components/styled/SendInput.tsx +3 -0
- package/src/components/styled/VideoMessage.tsx +3 -10
- package/src/context/xmppProvider.tsx +90 -2
- package/src/helpers/mimeToExtension.ts +135 -0
- package/src/helpers/resolveInitBeforeLoadUser.ts +79 -0
- package/src/hooks/useChatWrapperInit.ts +51 -46
- package/src/hooks/useEventHandlers.tsx +24 -1
- package/src/hooks/useMessageHeapState.tsx +6 -2
- package/src/hooks/useSendMessage.tsx +75 -4
- package/src/networking/api-requests/auth.api.ts +5 -1
- package/src/networking/xmppClient.ts +95 -1
- package/src/roomStore/roomHeapSlice.ts +47 -4
- package/src/types/types.ts +10 -0
|
@@ -31,6 +31,7 @@ import { parseMessageBody } from '../../helpers/parseMessageBody';
|
|
|
31
31
|
import { useMessageHeapState } from '../../hooks/useMessageHeapState';
|
|
32
32
|
import { DoubleTick } from '../../assets/icons';
|
|
33
33
|
import { useXmppClient } from '../../context/xmppProvider';
|
|
34
|
+
import { useSendMessage } from '../../hooks/useSendMessage';
|
|
34
35
|
import { MessageReaction } from './MessageReaction';
|
|
35
36
|
import { MessageFooter } from '../styled/StyledComponents';
|
|
36
37
|
|
|
@@ -47,14 +48,17 @@ const CustomMessageBubble = styled.View<{
|
|
|
47
48
|
deleted?: boolean;
|
|
48
49
|
backgroundMessageUser?: string;
|
|
49
50
|
backgroundMessage?: string;
|
|
51
|
+
isMedia?: boolean;
|
|
50
52
|
}>`
|
|
51
53
|
position: relative;
|
|
52
54
|
max-width: 85%;
|
|
53
55
|
min-width: 30%;
|
|
54
|
-
padding:
|
|
56
|
+
padding: ${({isMedia}) => isMedia ? '0' : '10'}px;
|
|
57
|
+
padding-bottom: ${({isMedia}) => isMedia ? '6' : '10'}px;
|
|
55
58
|
margin-right: ${({isUser}) => isUser ? '0' : '10px'};
|
|
56
59
|
margin-left: ${({isUser}) => isUser ? '10px' : '0'};
|
|
57
60
|
border-radius: 10px;
|
|
61
|
+
overflow: hidden;
|
|
58
62
|
border-bottom-left-radius: ${({ isUser }) => (isUser ? '10' : '0')}px;
|
|
59
63
|
border-bottom-right-radius: ${({ isUser }) => (isUser ? '0' : '10')}px;
|
|
60
64
|
background-color: ${({
|
|
@@ -113,7 +117,8 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
|
|
|
113
117
|
const dispatch = useDispatch();
|
|
114
118
|
const { client } = useXmppClient();
|
|
115
119
|
const { config, langSource, user} = useChatSettingState();
|
|
116
|
-
const { idSet } = useMessageHeapState();
|
|
120
|
+
const { idSet, failedIdSet } = useMessageHeapState();
|
|
121
|
+
const { retryMessage } = useSendMessage();
|
|
117
122
|
|
|
118
123
|
const [isPressed, setIsPressed] = useState(false);
|
|
119
124
|
|
|
@@ -218,14 +223,41 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
|
|
|
218
223
|
if (messageRef.current) {
|
|
219
224
|
const nodeHandle = findNodeHandle(messageRef.current);
|
|
220
225
|
if (nodeHandle) {
|
|
221
|
-
UIManager.measure(nodeHandle, (
|
|
226
|
+
UIManager.measure(nodeHandle, (_x, _y, _width, height, pageX, pageY) => {
|
|
222
227
|
const screenHeight = Dimensions.get('window').height;
|
|
228
|
+
// MessageInteractions is ~5 rows (delete/edit/reply/react/copy)
|
|
229
|
+
// at ~48px each + padding, plus emoji bar on top. 280 covers
|
|
230
|
+
// both even with a couple of long labels. The legacy 150
|
|
231
|
+
// figure assumed a much shorter menu and stranded the popup
|
|
232
|
+
// off-screen on the last message of the room.
|
|
233
|
+
const MENU_HEIGHT = 280;
|
|
234
|
+
// Treat the consumer-supplied keyboardVerticalOffset as a
|
|
235
|
+
// proxy for tab bar + bottom safe-area when present; falls
|
|
236
|
+
// back to a reasonable default so the menu doesn't bump into
|
|
237
|
+
// the nav bar on phones without one in config.
|
|
238
|
+
const bottomReserve =
|
|
239
|
+
(config?.keyboardVerticalOffset ?? 0) + 24;
|
|
240
|
+
const topReserve = 16;
|
|
241
|
+
|
|
242
|
+
const spaceBelow = screenHeight - (pageY + height) - bottomReserve;
|
|
243
|
+
const spaceAbove = pageY - topReserve;
|
|
244
|
+
|
|
245
|
+
let menuY: number;
|
|
246
|
+
if (spaceBelow >= MENU_HEIGHT) {
|
|
247
|
+
menuY = pageY + height;
|
|
248
|
+
} else if (spaceAbove >= MENU_HEIGHT) {
|
|
249
|
+
menuY = pageY - MENU_HEIGHT;
|
|
250
|
+
} else {
|
|
251
|
+
// Neither side has room (e.g. very small phone, large
|
|
252
|
+
// menu) — clamp to the visible region so at least the
|
|
253
|
+
// top of the menu is reachable.
|
|
254
|
+
menuY = Math.max(
|
|
255
|
+
topReserve,
|
|
256
|
+
screenHeight - MENU_HEIGHT - bottomReserve
|
|
257
|
+
);
|
|
258
|
+
}
|
|
223
259
|
|
|
224
|
-
|
|
225
|
-
setContextMenuPosition({
|
|
226
|
-
x: pageX,
|
|
227
|
-
y: enoughSpaceBelow ? pageY + height : pageY - 150,
|
|
228
|
-
});
|
|
260
|
+
setContextMenuPosition({ x: pageX, y: menuY });
|
|
229
261
|
});
|
|
230
262
|
}
|
|
231
263
|
}
|
|
@@ -236,7 +268,13 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
|
|
|
236
268
|
? parseMessageBody(config?.messageTextFilter.filterFunction(message.body))
|
|
237
269
|
: parseMessageBody(message.body);
|
|
238
270
|
|
|
239
|
-
const
|
|
271
|
+
const isFailed = failedIdSet.has(message.id);
|
|
272
|
+
const isPending =
|
|
273
|
+
!isFailed && (idSet.has(message.id) || message?.pending || false);
|
|
274
|
+
|
|
275
|
+
const onRetryPress = () => {
|
|
276
|
+
if (isFailed) {retryMessage(message.id);}
|
|
277
|
+
};
|
|
240
278
|
|
|
241
279
|
return (
|
|
242
280
|
<View>
|
|
@@ -279,6 +317,7 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
|
|
|
279
317
|
<CustomMessageBubble
|
|
280
318
|
isUser={isUser}
|
|
281
319
|
deleted={message.isDeleted}
|
|
320
|
+
isMedia={message?.isMediafile === 'true' && !message?.isDeleted}
|
|
282
321
|
backgroundMessageUser={config?.messageColor?.backgroundMessageUser}
|
|
283
322
|
backgroundMessage={config?.messageColor?.backgroundMessage}
|
|
284
323
|
>
|
|
@@ -327,11 +366,21 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
|
|
|
327
366
|
)}
|
|
328
367
|
<CustomMessageTimestamp>
|
|
329
368
|
{!config?.disableSentLogic && isUser && isPending && <Text>sending...</Text>}
|
|
369
|
+
{!config?.disableSentLogic && isUser && isFailed && (
|
|
370
|
+
<Text
|
|
371
|
+
onPress={onRetryPress}
|
|
372
|
+
style={styles.failedText}
|
|
373
|
+
accessibilityRole="button"
|
|
374
|
+
accessibilityLabel="Retry sending message"
|
|
375
|
+
>
|
|
376
|
+
! Failed — tap to retry
|
|
377
|
+
</Text>
|
|
378
|
+
)}
|
|
330
379
|
{new Date(message.date).toLocaleTimeString([], {
|
|
331
380
|
hour: '2-digit',
|
|
332
381
|
minute: '2-digit',
|
|
333
382
|
})}
|
|
334
|
-
{!config?.disableSentLogic && isUser && !isPending && (
|
|
383
|
+
{!config?.disableSentLogic && isUser && !isPending && !isFailed && (
|
|
335
384
|
<DoubleTick />
|
|
336
385
|
)}
|
|
337
386
|
</CustomMessageTimestamp>
|
|
@@ -401,4 +450,9 @@ const styles = StyleSheet.create({
|
|
|
401
450
|
marginTop: 5,
|
|
402
451
|
alignSelf: 'flex-end',
|
|
403
452
|
},
|
|
453
|
+
failedText: {
|
|
454
|
+
color: '#E53935',
|
|
455
|
+
fontWeight: '600',
|
|
456
|
+
marginRight: 6,
|
|
457
|
+
},
|
|
404
458
|
});
|
|
@@ -17,6 +17,7 @@ import * as FileSystem from 'expo-file-system/legacy';
|
|
|
17
17
|
import * as MediaLibrary from 'expo-media-library';
|
|
18
18
|
import { useToast } from '../../../context/ToastContext';
|
|
19
19
|
import PdfViewer from './PdfView';
|
|
20
|
+
import { ensureFilenameHasExtension } from '../../../helpers/mimeToExtension';
|
|
20
21
|
|
|
21
22
|
export const FullScreenVideo = styled.View`
|
|
22
23
|
width: 100%;
|
|
@@ -61,10 +62,10 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
61
62
|
}
|
|
62
63
|
|
|
63
64
|
try {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
65
|
+
const fileName = ensureFilenameHasExtension(
|
|
66
|
+
activeFile.fileName,
|
|
67
|
+
activeFile.mimetype
|
|
68
|
+
);
|
|
68
69
|
|
|
69
70
|
const filePath = FileSystem.cacheDirectory + fileName;
|
|
70
71
|
const download = await FileSystem.downloadAsync(activeFile.fileURL, filePath);
|
|
@@ -87,7 +88,14 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
87
88
|
|
|
88
89
|
const saveFileToDownloads = async () => {
|
|
89
90
|
try {
|
|
90
|
-
|
|
91
|
+
// Always include a valid extension on the cache filename. expo-media-
|
|
92
|
+
// library's createAssetAsync inspects the URI's extension and throws
|
|
93
|
+
// "Could not get the file's extension" when missing — see bug #9 in
|
|
94
|
+
// sdk-bug-tracker.md.
|
|
95
|
+
const fileName = ensureFilenameHasExtension(
|
|
96
|
+
activeFile.fileName,
|
|
97
|
+
activeFile.mimetype
|
|
98
|
+
);
|
|
91
99
|
const filePath = FileSystem.cacheDirectory + fileName;
|
|
92
100
|
const download = await FileSystem.downloadAsync(activeFile.fileURL, filePath);
|
|
93
101
|
|
|
@@ -162,23 +170,33 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
162
170
|
);
|
|
163
171
|
case activeFile.mimetype === 'application/pdf':
|
|
164
172
|
return <PdfViewer pdfUrl={activeFile.fileURL} />;
|
|
165
|
-
default:
|
|
173
|
+
default: {
|
|
174
|
+
const displayName = ensureFilenameHasExtension(
|
|
175
|
+
activeFile.fileName,
|
|
176
|
+
activeFile.mimetype
|
|
177
|
+
);
|
|
166
178
|
return (
|
|
167
179
|
<View
|
|
168
180
|
style={{
|
|
169
181
|
backgroundColor: '#FFF8ED',
|
|
170
182
|
borderRadius: 16,
|
|
171
|
-
|
|
172
|
-
|
|
183
|
+
padding: 20,
|
|
184
|
+
gap: 8,
|
|
173
185
|
}}
|
|
174
186
|
>
|
|
175
|
-
<Text>
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
187
|
+
<Text style={{ fontSize: 16, fontWeight: '600' }}>
|
|
188
|
+
{displayName}
|
|
189
|
+
</Text>
|
|
190
|
+
<Text style={{ color: '#666' }}>
|
|
191
|
+
{activeFile.mimetype || 'unknown type'}
|
|
192
|
+
</Text>
|
|
193
|
+
<Text style={{ marginTop: 8 }}>
|
|
194
|
+
This file format can't be previewed inline. Tap the save icon
|
|
195
|
+
above to download it.
|
|
179
196
|
</Text>
|
|
180
197
|
</View>
|
|
181
198
|
);
|
|
199
|
+
}
|
|
182
200
|
}
|
|
183
201
|
}, [activeFile]);
|
|
184
202
|
|
|
@@ -22,6 +22,12 @@ interface ModalWrapperProps {
|
|
|
22
22
|
setTextarea?: (value: string) => void;
|
|
23
23
|
handleCloseModal: () => void;
|
|
24
24
|
handleClick: () => void;
|
|
25
|
+
/**
|
|
26
|
+
* When true, render a small centered dialog instead of the default
|
|
27
|
+
* full-screen container. Used for confirm-style modals (Delete, Leave
|
|
28
|
+
* room) where edge-to-edge feels too heavy.
|
|
29
|
+
*/
|
|
30
|
+
compact?: boolean;
|
|
25
31
|
}
|
|
26
32
|
|
|
27
33
|
export const ModalWrapper: FC<ModalWrapperProps> = ({
|
|
@@ -35,6 +41,7 @@ export const ModalWrapper: FC<ModalWrapperProps> = ({
|
|
|
35
41
|
setTextarea,
|
|
36
42
|
handleCloseModal,
|
|
37
43
|
handleClick,
|
|
44
|
+
compact = false,
|
|
38
45
|
}) => {
|
|
39
46
|
const textareaRef = useRef<TextInput>(null);
|
|
40
47
|
|
|
@@ -54,7 +61,21 @@ export const ModalWrapper: FC<ModalWrapperProps> = ({
|
|
|
54
61
|
onRequestClose={handleCloseModal}
|
|
55
62
|
>
|
|
56
63
|
<ModalBackground>
|
|
57
|
-
<ModalContainer
|
|
64
|
+
<ModalContainer
|
|
65
|
+
style={
|
|
66
|
+
compact
|
|
67
|
+
? {
|
|
68
|
+
flex: 0,
|
|
69
|
+
width: '85%',
|
|
70
|
+
maxWidth: 360,
|
|
71
|
+
height: 'auto',
|
|
72
|
+
padding: 24,
|
|
73
|
+
borderRadius: 16,
|
|
74
|
+
gap: 16,
|
|
75
|
+
}
|
|
76
|
+
: { maxWidth: 640 }
|
|
77
|
+
}
|
|
78
|
+
>
|
|
58
79
|
<CloseButton onPress={handleCloseModal}>
|
|
59
80
|
<Text style={{ fontSize: 24 }}>×</Text>
|
|
60
81
|
</CloseButton>
|
|
@@ -1,12 +1,13 @@
|
|
|
1
|
-
import { View } from 'react-native';
|
|
1
|
+
import { View, Text, StyleSheet, Modal, TouchableOpacity, SafeAreaView } from 'react-native';
|
|
2
2
|
import {
|
|
3
3
|
MediaContainer,
|
|
4
4
|
MediaImage,
|
|
5
5
|
} from './StyledInputComponents/StyledInputComponents';
|
|
6
6
|
import { FileIcon } from '../../assets/icons';
|
|
7
7
|
import { RemoveButton, RemoveButtonText } from './StyledComponents';
|
|
8
|
-
import { FC } from 'react';
|
|
8
|
+
import { FC, useState } from 'react';
|
|
9
9
|
import { MediaFile } from '../../types/types';
|
|
10
|
+
import { Video, ResizeMode } from 'expo-av';
|
|
10
11
|
|
|
11
12
|
interface MediaFilePreviewProps {
|
|
12
13
|
filePreviews: MediaFile[];
|
|
@@ -17,15 +18,34 @@ export const MediaFilePreview: FC<MediaFilePreviewProps> = ({
|
|
|
17
18
|
filePreviews,
|
|
18
19
|
handleRemoveImage,
|
|
19
20
|
}) => {
|
|
21
|
+
const [previewUri, setPreviewUri] = useState<string | null>(null);
|
|
22
|
+
|
|
20
23
|
return (
|
|
21
24
|
<MediaContainer>
|
|
22
25
|
{filePreviews.map((file, index) => {
|
|
23
|
-
const
|
|
24
|
-
|
|
26
|
+
const isImage = file.type?.startsWith('image');
|
|
27
|
+
const isVideo = file.type?.startsWith('video');
|
|
25
28
|
return (
|
|
26
29
|
<View key={`${file.name}_${index}`}>
|
|
27
|
-
{
|
|
30
|
+
{isImage ? (
|
|
28
31
|
<MediaImage source={{ uri: file.uri }} />
|
|
32
|
+
) : isVideo ? (
|
|
33
|
+
<TouchableOpacity
|
|
34
|
+
activeOpacity={0.8}
|
|
35
|
+
onPress={() => setPreviewUri(file.uri)}
|
|
36
|
+
style={styles.videoThumb}
|
|
37
|
+
>
|
|
38
|
+
<Video
|
|
39
|
+
source={{ uri: file.uri }}
|
|
40
|
+
style={StyleSheet.absoluteFill}
|
|
41
|
+
resizeMode={ResizeMode.COVER}
|
|
42
|
+
shouldPlay={false}
|
|
43
|
+
isMuted
|
|
44
|
+
/>
|
|
45
|
+
<View style={styles.playOverlay}>
|
|
46
|
+
<Text style={styles.playIcon}>▶</Text>
|
|
47
|
+
</View>
|
|
48
|
+
</TouchableOpacity>
|
|
29
49
|
) : (
|
|
30
50
|
<View style={{ width: 70, height: 70, borderRadius: 8 }}>
|
|
31
51
|
<FileIcon width={70} height={70} />
|
|
@@ -37,6 +57,72 @@ export const MediaFilePreview: FC<MediaFilePreviewProps> = ({
|
|
|
37
57
|
</View>
|
|
38
58
|
);
|
|
39
59
|
})}
|
|
60
|
+
|
|
61
|
+
<Modal visible={!!previewUri} animationType="fade" transparent>
|
|
62
|
+
<SafeAreaView style={styles.modalContainer}>
|
|
63
|
+
<TouchableOpacity
|
|
64
|
+
style={styles.closeButton}
|
|
65
|
+
onPress={() => setPreviewUri(null)}
|
|
66
|
+
>
|
|
67
|
+
<Text style={styles.closeText}>✕</Text>
|
|
68
|
+
</TouchableOpacity>
|
|
69
|
+
{previewUri && (
|
|
70
|
+
<Video
|
|
71
|
+
source={{ uri: previewUri }}
|
|
72
|
+
style={styles.fullVideo}
|
|
73
|
+
resizeMode={ResizeMode.CONTAIN}
|
|
74
|
+
useNativeControls
|
|
75
|
+
shouldPlay
|
|
76
|
+
/>
|
|
77
|
+
)}
|
|
78
|
+
</SafeAreaView>
|
|
79
|
+
</Modal>
|
|
40
80
|
</MediaContainer>
|
|
41
81
|
);
|
|
42
82
|
};
|
|
83
|
+
|
|
84
|
+
const styles = StyleSheet.create({
|
|
85
|
+
videoThumb: {
|
|
86
|
+
width: 70,
|
|
87
|
+
height: 70,
|
|
88
|
+
borderRadius: 8,
|
|
89
|
+
overflow: 'hidden',
|
|
90
|
+
backgroundColor: '#1a1a1a',
|
|
91
|
+
},
|
|
92
|
+
playOverlay: {
|
|
93
|
+
...StyleSheet.absoluteFillObject,
|
|
94
|
+
justifyContent: 'center',
|
|
95
|
+
alignItems: 'center',
|
|
96
|
+
backgroundColor: 'rgba(0,0,0,0.3)',
|
|
97
|
+
},
|
|
98
|
+
playIcon: {
|
|
99
|
+
fontSize: 24,
|
|
100
|
+
color: '#fff',
|
|
101
|
+
},
|
|
102
|
+
modalContainer: {
|
|
103
|
+
flex: 1,
|
|
104
|
+
backgroundColor: '#000',
|
|
105
|
+
justifyContent: 'center',
|
|
106
|
+
},
|
|
107
|
+
closeButton: {
|
|
108
|
+
position: 'absolute',
|
|
109
|
+
top: 60,
|
|
110
|
+
right: 20,
|
|
111
|
+
zIndex: 10,
|
|
112
|
+
width: 36,
|
|
113
|
+
height: 36,
|
|
114
|
+
borderRadius: 18,
|
|
115
|
+
backgroundColor: 'rgba(255,255,255,0.2)',
|
|
116
|
+
justifyContent: 'center',
|
|
117
|
+
alignItems: 'center',
|
|
118
|
+
},
|
|
119
|
+
closeText: {
|
|
120
|
+
fontSize: 18,
|
|
121
|
+
color: '#fff',
|
|
122
|
+
fontWeight: '600',
|
|
123
|
+
},
|
|
124
|
+
fullVideo: {
|
|
125
|
+
width: '100%',
|
|
126
|
+
height: '70%',
|
|
127
|
+
},
|
|
128
|
+
});
|
|
@@ -38,8 +38,7 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
|
|
|
38
38
|
'https://as2.ftcdn.net/v2/jpg/02/51/95/53/1000_F_251955356_FAQH0U1y1TZw3ZcdPGybwUkH90a3VAhb.jpg'
|
|
39
39
|
}
|
|
40
40
|
style={{
|
|
41
|
-
|
|
42
|
-
width: 150,
|
|
41
|
+
width: 250,
|
|
43
42
|
height: 200,
|
|
44
43
|
}}
|
|
45
44
|
onError={() => setError(true)}
|
|
@@ -117,6 +117,7 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
117
117
|
const result = await ImagePicker.launchCameraAsync({
|
|
118
118
|
mediaTypes: ['images', 'videos'],
|
|
119
119
|
quality: 0.9,
|
|
120
|
+
videoQuality: ImagePicker.UIImagePickerControllerQualityType.Medium,
|
|
120
121
|
});
|
|
121
122
|
if (result.canceled || !result.assets?.[0]) {return;}
|
|
122
123
|
const asset = result.assets[0];
|
|
@@ -151,6 +152,8 @@ const SendInput: React.FC<SendInputProps> = ({
|
|
|
151
152
|
mediaTypes: ['images', 'videos'],
|
|
152
153
|
allowsEditing: false,
|
|
153
154
|
quality: 0.9,
|
|
155
|
+
preferredAssetRepresentationMode:
|
|
156
|
+
ImagePicker.UIImagePickerPreferredAssetRepresentationMode.Compatible,
|
|
154
157
|
});
|
|
155
158
|
if (result.canceled || !result.assets?.[0]) {return;}
|
|
156
159
|
const asset = result.assets[0];
|
|
@@ -27,7 +27,7 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
|
27
27
|
};
|
|
28
28
|
|
|
29
29
|
return (
|
|
30
|
-
<TouchableOpacity
|
|
30
|
+
<TouchableOpacity onPress={handleOpen}>
|
|
31
31
|
<Video
|
|
32
32
|
source={{ uri: fileURL }}
|
|
33
33
|
style={styles.video}
|
|
@@ -42,16 +42,9 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
|
42
42
|
export default CustomMessageVideo;
|
|
43
43
|
|
|
44
44
|
const styles = StyleSheet.create({
|
|
45
|
-
container: {
|
|
46
|
-
margin: 0,
|
|
47
|
-
alignItems: 'center',
|
|
48
|
-
justifyContent: 'center',
|
|
49
|
-
width: '100%',
|
|
50
|
-
},
|
|
51
45
|
video: {
|
|
52
|
-
width:
|
|
53
|
-
|
|
54
|
-
borderRadius: 10,
|
|
46
|
+
width: '100%',
|
|
47
|
+
aspectRatio: 16 / 9,
|
|
55
48
|
backgroundColor: '#000',
|
|
56
49
|
},
|
|
57
50
|
});
|
|
@@ -4,11 +4,15 @@ import React, {
|
|
|
4
4
|
useCallback,
|
|
5
5
|
useContext,
|
|
6
6
|
useEffect,
|
|
7
|
+
useMemo,
|
|
7
8
|
useRef,
|
|
8
9
|
useState,
|
|
9
10
|
} from 'react';
|
|
10
11
|
import { AppState, DeviceEventEmitter } from 'react-native';
|
|
11
|
-
import XmppClient
|
|
12
|
+
import XmppClient, {
|
|
13
|
+
XmppCredentialsProvider,
|
|
14
|
+
} from '../networking/xmppClient';
|
|
15
|
+
import { refreshTokens } from '../roomStore/chatSettingsSlice';
|
|
12
16
|
import {
|
|
13
17
|
IConfig,
|
|
14
18
|
ProviderBootstrapStatus,
|
|
@@ -24,6 +28,7 @@ import {
|
|
|
24
28
|
} from '../utils/clientRegistry';
|
|
25
29
|
import {
|
|
26
30
|
applyResolvedUserToStore,
|
|
31
|
+
refreshUserCredentialsForXmpp,
|
|
27
32
|
resolveInitBeforeLoadUser,
|
|
28
33
|
} from '../helpers/resolveInitBeforeLoadUser';
|
|
29
34
|
import { ensureScopedChatCache } from '../helpers/ensureScopedChatCache';
|
|
@@ -77,6 +82,79 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config })
|
|
|
77
82
|
const completedBootstrapKeyRef = useRef<string>('');
|
|
78
83
|
const inflightBootstrapKeyRef = useRef<string>('');
|
|
79
84
|
|
|
85
|
+
// Callback XmppClient uses to fetch fresh credentials after a SASL
|
|
86
|
+
// `not-authorized` (typically JWT expiry during idle, bug #17).
|
|
87
|
+
// Always installed — the underlying `refreshUserCredentialsForXmpp`
|
|
88
|
+
// picks the right path based on config:
|
|
89
|
+
// - `jwtLogin` → re-exchange JWT via `/users/client`
|
|
90
|
+
// - `userLogin` / `customLogin` / store / persisted → `/users/my`
|
|
91
|
+
// with REST refresh chain (handles 401 → refresh → retry)
|
|
92
|
+
// Returns last-known cached creds if nothing usable is available so
|
|
93
|
+
// the XmppClient at least doesn't crash on `undefined.password`.
|
|
94
|
+
const credentialsProvider = useMemo<XmppCredentialsProvider>(() => {
|
|
95
|
+
return async () => {
|
|
96
|
+
// 1. Consumer-supplied REST refresh (when wired). This runs
|
|
97
|
+
// before the SDK's own refresh chain so non-Ethora backends
|
|
98
|
+
// can keep auth state in sync with their own token endpoint.
|
|
99
|
+
const customRefresh = config?.refreshTokens?.refreshFunction;
|
|
100
|
+
if (customRefresh) {
|
|
101
|
+
try {
|
|
102
|
+
const result = await customRefresh();
|
|
103
|
+
if (result?.accessToken) {
|
|
104
|
+
store.dispatch(
|
|
105
|
+
refreshTokens({
|
|
106
|
+
token: result.accessToken,
|
|
107
|
+
refreshToken: result.refreshToken || '',
|
|
108
|
+
})
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
} catch (err) {
|
|
112
|
+
devPushLog('warn', 'XMPP creds refresh: customRefresh failed', err);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// 2. Re-mint XMPP creds via the right priority chain for the
|
|
117
|
+
// current auth mode. This call ALWAYS hydrates (unlike
|
|
118
|
+
// `resolveInitBeforeLoadUser` which short-circuits when a
|
|
119
|
+
// cached user already has xmppCredentials).
|
|
120
|
+
const fresh = await refreshUserCredentialsForXmpp(config).catch(
|
|
121
|
+
(err) => {
|
|
122
|
+
devPushLog('warn', 'XMPP creds refresh: full chain failed', err);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
);
|
|
126
|
+
if (fresh) {
|
|
127
|
+
applyResolvedUserToStore(fresh);
|
|
128
|
+
return {
|
|
129
|
+
username:
|
|
130
|
+
fresh.xmppUsername ||
|
|
131
|
+
fresh.defaultWallet?.walletAddress ||
|
|
132
|
+
'',
|
|
133
|
+
password: fresh.xmppPassword || '',
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// 3. Last-resort: return the cached creds so reconnect() can
|
|
138
|
+
// still attempt a connection. If the original failure was
|
|
139
|
+
// a stale JWT this will fail again and the user has to
|
|
140
|
+
// re-mount the chat — but at least we don't break the
|
|
141
|
+
// transient-network-blip case where the cached creds are
|
|
142
|
+
// still valid.
|
|
143
|
+
const u = store.getState().chatSettingStore.user;
|
|
144
|
+
return {
|
|
145
|
+
username: u?.xmppUsername || u?.defaultWallet?.walletAddress || '',
|
|
146
|
+
password: u?.xmppPassword || '',
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
}, [
|
|
150
|
+
config?.jwtLogin?.enabled,
|
|
151
|
+
config?.jwtLogin?.token,
|
|
152
|
+
config?.refreshTokens?.refreshFunction,
|
|
153
|
+
config?.userLogin?.enabled,
|
|
154
|
+
config?.customLogin?.enabled,
|
|
155
|
+
config?.initBeforeLoadAuth?.myEndpoint,
|
|
156
|
+
]);
|
|
157
|
+
|
|
80
158
|
const initializeClient = useCallback(
|
|
81
159
|
async (
|
|
82
160
|
username: string,
|
|
@@ -104,6 +182,12 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config })
|
|
|
104
182
|
// concurrent calls for the same key.
|
|
105
183
|
const created = await withXmppClientInitLock(key, async () => {
|
|
106
184
|
const newClient = new XmppClient(username, password, settings);
|
|
185
|
+
// Wire up the credentialsProvider so SASL `not-authorized`
|
|
186
|
+
// after idle triggers a refresh chain (jwtLogin →
|
|
187
|
+
// /users/client; userLogin/customLogin/store → /users/my +
|
|
188
|
+
// REST refresh) instead of looping forever with stale creds.
|
|
189
|
+
// Bug #17 — full coverage in 26.5.6.
|
|
190
|
+
newClient.setCredentialsProvider(credentialsProvider);
|
|
107
191
|
setGlobalXmppClient(newClient, key);
|
|
108
192
|
await newClient.waitForOnline().catch((err) => {
|
|
109
193
|
console.error('Error waiting for xmpp online:', err);
|
|
@@ -112,12 +196,16 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config })
|
|
|
112
196
|
return newClient;
|
|
113
197
|
});
|
|
114
198
|
|
|
199
|
+
// Reused clients (steps 1+2 above) also need the provider — config
|
|
200
|
+
// may have changed since the singleton was created.
|
|
201
|
+
created.setCredentialsProvider(credentialsProvider);
|
|
202
|
+
|
|
115
203
|
setClient(created);
|
|
116
204
|
lastCredsRef.current = { username, password, settings };
|
|
117
205
|
setReconnectAttempts(0);
|
|
118
206
|
return created;
|
|
119
207
|
},
|
|
120
|
-
[client, config?.xmppSettings]
|
|
208
|
+
[client, config?.xmppSettings, credentialsProvider]
|
|
121
209
|
);
|
|
122
210
|
|
|
123
211
|
// -----------------------------------------------------------
|