@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.
Files changed (149) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/lib/commonjs/components/MainComponents/ChatHeader.js +7 -1
  3. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  4. package/lib/commonjs/components/MainComponents/ChatRoom.js +8 -2
  5. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  6. package/lib/commonjs/components/MainComponents/ChatWrapper.js +2 -1
  7. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  8. package/lib/commonjs/components/MainComponents/MediaMessage.js +11 -5
  9. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  10. package/lib/commonjs/components/MainComponents/MessageList.js +10 -2
  11. package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
  12. package/lib/commonjs/components/MessageBubble/Message.js +60 -7
  13. package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
  14. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +35 -16
  15. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  16. package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js +11 -2
  17. package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
  18. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +1 -1
  19. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  20. package/lib/commonjs/components/styled/MediaFilePreview.js +96 -5
  21. package/lib/commonjs/components/styled/MediaFilePreview.js.map +1 -1
  22. package/lib/commonjs/components/styled/MessageImage.js +1 -2
  23. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  24. package/lib/commonjs/components/styled/SendInput.js +4 -2
  25. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  26. package/lib/commonjs/components/styled/VideoMessage.js +2 -10
  27. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  28. package/lib/commonjs/context/xmppProvider.js +71 -2
  29. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  30. package/lib/commonjs/helpers/mimeToExtension.js +131 -0
  31. package/lib/commonjs/helpers/mimeToExtension.js.map +1 -0
  32. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +73 -1
  33. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  34. package/lib/commonjs/hooks/useChatWrapperInit.js +22 -10
  35. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  36. package/lib/commonjs/hooks/useEventHandlers.js +9 -1
  37. package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
  38. package/lib/commonjs/hooks/useMessageHeapState.js +6 -2
  39. package/lib/commonjs/hooks/useMessageHeapState.js.map +1 -1
  40. package/lib/commonjs/hooks/useSendMessage.js +48 -4
  41. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  42. package/lib/commonjs/networking/api-requests/auth.api.js +7 -1
  43. package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
  44. package/lib/commonjs/networking/xmppClient.js +87 -1
  45. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  46. package/lib/commonjs/roomStore/roomHeapSlice.js +26 -8
  47. package/lib/commonjs/roomStore/roomHeapSlice.js.map +1 -1
  48. package/lib/module/components/MainComponents/ChatHeader.js +7 -1
  49. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  50. package/lib/module/components/MainComponents/ChatRoom.js +8 -2
  51. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  52. package/lib/module/components/MainComponents/ChatWrapper.js +2 -1
  53. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  54. package/lib/module/components/MainComponents/MediaMessage.js +11 -5
  55. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  56. package/lib/module/components/MainComponents/MessageList.js +11 -3
  57. package/lib/module/components/MainComponents/MessageList.js.map +1 -1
  58. package/lib/module/components/MessageBubble/Message.js +60 -7
  59. package/lib/module/components/MessageBubble/Message.js.map +1 -1
  60. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -17
  61. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  62. package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js +11 -2
  63. package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
  64. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +1 -1
  65. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  66. package/lib/module/components/styled/MediaFilePreview.js +97 -6
  67. package/lib/module/components/styled/MediaFilePreview.js.map +1 -1
  68. package/lib/module/components/styled/MessageImage.js +1 -2
  69. package/lib/module/components/styled/MessageImage.js.map +1 -1
  70. package/lib/module/components/styled/SendInput.js +4 -2
  71. package/lib/module/components/styled/SendInput.js.map +1 -1
  72. package/lib/module/components/styled/VideoMessage.js +2 -10
  73. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  74. package/lib/module/context/xmppProvider.js +73 -3
  75. package/lib/module/context/xmppProvider.js.map +1 -1
  76. package/lib/module/helpers/mimeToExtension.js +124 -0
  77. package/lib/module/helpers/mimeToExtension.js.map +1 -0
  78. package/lib/module/helpers/resolveInitBeforeLoadUser.js +71 -0
  79. package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  80. package/lib/module/hooks/useChatWrapperInit.js +22 -10
  81. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  82. package/lib/module/hooks/useEventHandlers.js +9 -1
  83. package/lib/module/hooks/useEventHandlers.js.map +1 -1
  84. package/lib/module/hooks/useMessageHeapState.js +6 -2
  85. package/lib/module/hooks/useMessageHeapState.js.map +1 -1
  86. package/lib/module/hooks/useSendMessage.js +49 -5
  87. package/lib/module/hooks/useSendMessage.js.map +1 -1
  88. package/lib/module/networking/api-requests/auth.api.js +7 -1
  89. package/lib/module/networking/api-requests/auth.api.js.map +1 -1
  90. package/lib/module/networking/xmppClient.js +87 -1
  91. package/lib/module/networking/xmppClient.js.map +1 -1
  92. package/lib/module/roomStore/roomHeapSlice.js +24 -7
  93. package/lib/module/roomStore/roomHeapSlice.js.map +1 -1
  94. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  95. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  96. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  97. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  98. package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
  99. package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
  100. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  101. package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts +6 -0
  102. package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts.map +1 -1
  103. package/lib/typescript/components/styled/MediaFilePreview.d.ts.map +1 -1
  104. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  105. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  106. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  107. package/lib/typescript/helpers/mimeToExtension.d.ts +37 -0
  108. package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -0
  109. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts +18 -0
  110. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
  111. package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
  112. package/lib/typescript/hooks/useEventHandlers.d.ts +6 -0
  113. package/lib/typescript/hooks/useEventHandlers.d.ts.map +1 -1
  114. package/lib/typescript/hooks/useMessageHeapState.d.ts +2 -0
  115. package/lib/typescript/hooks/useMessageHeapState.d.ts.map +1 -1
  116. package/lib/typescript/hooks/useSendMessage.d.ts +2 -1
  117. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  118. package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
  119. package/lib/typescript/networking/xmppClient.d.ts +25 -1
  120. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  121. package/lib/typescript/roomStore/roomHeapSlice.d.ts +22 -1
  122. package/lib/typescript/roomStore/roomHeapSlice.d.ts.map +1 -1
  123. package/lib/typescript/types/types.d.ts +6 -0
  124. package/lib/typescript/types/types.d.ts.map +1 -1
  125. package/package.json +1 -1
  126. package/src/components/MainComponents/ChatHeader.tsx +5 -1
  127. package/src/components/MainComponents/ChatRoom.tsx +6 -1
  128. package/src/components/MainComponents/ChatWrapper.tsx +1 -0
  129. package/src/components/MainComponents/MediaMessage.tsx +15 -8
  130. package/src/components/MainComponents/MessageList.tsx +10 -3
  131. package/src/components/MessageBubble/Message.tsx +64 -10
  132. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +30 -12
  133. package/src/components/Modals/ModalWrapper/ModalWrapper.tsx +22 -1
  134. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +1 -1
  135. package/src/components/styled/MediaFilePreview.tsx +91 -5
  136. package/src/components/styled/MessageImage.tsx +1 -2
  137. package/src/components/styled/SendInput.tsx +3 -0
  138. package/src/components/styled/VideoMessage.tsx +3 -10
  139. package/src/context/xmppProvider.tsx +90 -2
  140. package/src/helpers/mimeToExtension.ts +135 -0
  141. package/src/helpers/resolveInitBeforeLoadUser.ts +79 -0
  142. package/src/hooks/useChatWrapperInit.ts +51 -46
  143. package/src/hooks/useEventHandlers.tsx +24 -1
  144. package/src/hooks/useMessageHeapState.tsx +6 -2
  145. package/src/hooks/useSendMessage.tsx +75 -4
  146. package/src/networking/api-requests/auth.api.ts +5 -1
  147. package/src/networking/xmppClient.ts +95 -1
  148. package/src/roomStore/roomHeapSlice.ts +47 -4
  149. 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: 10px;
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, (x, y, width, height, pageX, pageY) => {
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
- const enoughSpaceBelow = screenHeight - pageY - height > 150;
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 isPending = idSet.has(message.id) || message?.pending || false;
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
- let fileName = activeFile.fileName || `media_${Date.now()}`;
65
- if (!fileName.includes('.')) {
66
- fileName += activeFile.mimetype.startsWith('image/') ? '.jpg' : '.mp4';
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
- const fileName = activeFile.fileName || `file_${Date.now()}`;
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
- display: 'flex',
172
- padding: 16,
183
+ padding: 20,
184
+ gap: 8,
173
185
  }}
174
186
  >
175
- <Text>
176
- Unable to open the uploaded document. The file format is not
177
- supported by the system. Please upload a file in a compatible
178
- format. You still can download this file.
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 style={{ maxWidth: 640 }}>
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 }}>&times;</Text>
60
81
  </CloseButton>
@@ -285,7 +285,7 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
285
285
  </LabelData>
286
286
  </BorderedContainer>
287
287
 
288
- {selectedUser && (
288
+ {selectedUser && !config?.disableProfilesInteractions && (
289
289
  <>
290
290
  <ActionButton
291
291
  StartIcon={<ChatIcon />}
@@ -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 isImageOrVideo =
24
- file.type?.startsWith('image') || file.type?.startsWith('video');
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
- {isImageOrVideo ? (
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
- borderRadius: 16,
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 style={styles.container} onPress={handleOpen}>
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: 300,
53
- height: 200,
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 from '../networking/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
  // -----------------------------------------------------------