@ethora/chat-component-rn 26.5.4 → 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 (238) hide show
  1. package/CHANGELOG.md +66 -0
  2. package/README.md +25 -4
  3. package/lib/commonjs/components/ContextMenu/ContextMenuComponents.js +1 -7
  4. package/lib/commonjs/components/ContextMenu/ContextMenuComponents.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatHeader.js +7 -1
  6. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/ChatRoom.js +8 -2
  8. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/ChatWrapper.js +2 -1
  10. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  11. package/lib/commonjs/components/MainComponents/MediaMessage.js +11 -5
  12. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  13. package/lib/commonjs/components/MainComponents/MessageList.js +10 -2
  14. package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
  15. package/lib/commonjs/components/MessageBubble/Message.js +60 -7
  16. package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
  17. package/lib/commonjs/components/MessageBubble/MessageInteractions.js +1 -38
  18. package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
  19. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +20 -28
  20. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  21. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +35 -16
  22. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  23. package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js +11 -2
  24. package/lib/commonjs/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
  25. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +1 -1
  26. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  27. package/lib/commonjs/components/RoomComponents/styled/LastMessageEmoji.js +9 -6
  28. package/lib/commonjs/components/RoomComponents/styled/LastMessageEmoji.js.map +1 -1
  29. package/lib/commonjs/components/UsersList/Checkbox.js +68 -0
  30. package/lib/commonjs/components/UsersList/Checkbox.js.map +1 -0
  31. package/lib/commonjs/components/UsersList/StyledComponents.js +1 -7
  32. package/lib/commonjs/components/UsersList/StyledComponents.js.map +1 -1
  33. package/lib/commonjs/components/UsersList/UsersList.js +3 -1
  34. package/lib/commonjs/components/UsersList/UsersList.js.map +1 -1
  35. package/lib/commonjs/components/styled/MediaFilePreview.js +96 -5
  36. package/lib/commonjs/components/styled/MediaFilePreview.js.map +1 -1
  37. package/lib/commonjs/components/styled/MessageImage.js +1 -2
  38. package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
  39. package/lib/commonjs/components/styled/SendInput.js +4 -2
  40. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  41. package/lib/commonjs/components/styled/VideoMessage.js +2 -10
  42. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  43. package/lib/commonjs/context/xmppProvider.js +71 -2
  44. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  45. package/lib/commonjs/helpers/mimeToExtension.js +131 -0
  46. package/lib/commonjs/helpers/mimeToExtension.js.map +1 -0
  47. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +73 -1
  48. package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  49. package/lib/commonjs/hooks/useChatWrapperInit.js +22 -10
  50. package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
  51. package/lib/commonjs/hooks/useEventHandlers.js +9 -1
  52. package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
  53. package/lib/commonjs/hooks/useMessageHeapState.js +6 -2
  54. package/lib/commonjs/hooks/useMessageHeapState.js.map +1 -1
  55. package/lib/commonjs/hooks/useSendMessage.js +48 -4
  56. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  57. package/lib/commonjs/networking/api-requests/auth.api.js +7 -1
  58. package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
  59. package/lib/commonjs/networking/xmppClient.js +87 -1
  60. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  61. package/lib/commonjs/roomStore/roomHeapSlice.js +26 -8
  62. package/lib/commonjs/roomStore/roomHeapSlice.js.map +1 -1
  63. package/lib/module/components/ContextMenu/ContextMenuComponents.js +0 -6
  64. package/lib/module/components/ContextMenu/ContextMenuComponents.js.map +1 -1
  65. package/lib/module/components/MainComponents/ChatHeader.js +7 -1
  66. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  67. package/lib/module/components/MainComponents/ChatRoom.js +8 -2
  68. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  69. package/lib/module/components/MainComponents/ChatWrapper.js +2 -1
  70. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  71. package/lib/module/components/MainComponents/MediaMessage.js +11 -5
  72. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  73. package/lib/module/components/MainComponents/MessageList.js +11 -3
  74. package/lib/module/components/MainComponents/MessageList.js.map +1 -1
  75. package/lib/module/components/MessageBubble/Message.js +60 -7
  76. package/lib/module/components/MessageBubble/Message.js.map +1 -1
  77. package/lib/module/components/MessageBubble/MessageInteractions.js +3 -40
  78. package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
  79. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +21 -29
  80. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  81. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -17
  82. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  83. package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js +11 -2
  84. package/lib/module/components/Modals/ModalWrapper/ModalWrapper.js.map +1 -1
  85. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +1 -1
  86. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  87. package/lib/module/components/RoomComponents/styled/LastMessageEmoji.js +9 -6
  88. package/lib/module/components/RoomComponents/styled/LastMessageEmoji.js.map +1 -1
  89. package/lib/module/components/UsersList/Checkbox.js +63 -0
  90. package/lib/module/components/UsersList/Checkbox.js.map +1 -0
  91. package/lib/module/components/UsersList/StyledComponents.js +0 -6
  92. package/lib/module/components/UsersList/StyledComponents.js.map +1 -1
  93. package/lib/module/components/UsersList/UsersList.js +2 -1
  94. package/lib/module/components/UsersList/UsersList.js.map +1 -1
  95. package/lib/module/components/styled/MediaFilePreview.js +97 -6
  96. package/lib/module/components/styled/MediaFilePreview.js.map +1 -1
  97. package/lib/module/components/styled/MessageImage.js +1 -2
  98. package/lib/module/components/styled/MessageImage.js.map +1 -1
  99. package/lib/module/components/styled/SendInput.js +4 -2
  100. package/lib/module/components/styled/SendInput.js.map +1 -1
  101. package/lib/module/components/styled/VideoMessage.js +2 -10
  102. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  103. package/lib/module/context/xmppProvider.js +73 -3
  104. package/lib/module/context/xmppProvider.js.map +1 -1
  105. package/lib/module/helpers/mimeToExtension.js +124 -0
  106. package/lib/module/helpers/mimeToExtension.js.map +1 -0
  107. package/lib/module/helpers/resolveInitBeforeLoadUser.js +71 -0
  108. package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
  109. package/lib/module/hooks/useChatWrapperInit.js +22 -10
  110. package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
  111. package/lib/module/hooks/useEventHandlers.js +9 -1
  112. package/lib/module/hooks/useEventHandlers.js.map +1 -1
  113. package/lib/module/hooks/useMessageHeapState.js +6 -2
  114. package/lib/module/hooks/useMessageHeapState.js.map +1 -1
  115. package/lib/module/hooks/useSendMessage.js +49 -5
  116. package/lib/module/hooks/useSendMessage.js.map +1 -1
  117. package/lib/module/networking/api-requests/auth.api.js +7 -1
  118. package/lib/module/networking/api-requests/auth.api.js.map +1 -1
  119. package/lib/module/networking/xmppClient.js +87 -1
  120. package/lib/module/networking/xmppClient.js.map +1 -1
  121. package/lib/module/roomStore/roomHeapSlice.js +24 -7
  122. package/lib/module/roomStore/roomHeapSlice.js.map +1 -1
  123. package/lib/typescript/components/ContextMenu/ContextMenuComponents.d.ts +0 -1
  124. package/lib/typescript/components/ContextMenu/ContextMenuComponents.d.ts.map +1 -1
  125. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  126. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  127. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  128. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  129. package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
  130. package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
  131. package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
  132. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  133. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  134. package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts +6 -0
  135. package/lib/typescript/components/Modals/ModalWrapper/ModalWrapper.d.ts.map +1 -1
  136. package/lib/typescript/components/RoomComponents/styled/LastMessageEmoji.d.ts +8 -0
  137. package/lib/typescript/components/RoomComponents/styled/LastMessageEmoji.d.ts.map +1 -1
  138. package/lib/typescript/components/UsersList/Checkbox.d.ts +16 -0
  139. package/lib/typescript/components/UsersList/Checkbox.d.ts.map +1 -0
  140. package/lib/typescript/components/UsersList/StyledComponents.d.ts +0 -1
  141. package/lib/typescript/components/UsersList/StyledComponents.d.ts.map +1 -1
  142. package/lib/typescript/components/styled/MediaFilePreview.d.ts.map +1 -1
  143. package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
  144. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  145. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  146. package/lib/typescript/helpers/mimeToExtension.d.ts +37 -0
  147. package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -0
  148. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts +18 -0
  149. package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
  150. package/lib/typescript/hooks/useChatWrapperInit.d.ts.map +1 -1
  151. package/lib/typescript/hooks/useEventHandlers.d.ts +6 -0
  152. package/lib/typescript/hooks/useEventHandlers.d.ts.map +1 -1
  153. package/lib/typescript/hooks/useMessageHeapState.d.ts +2 -0
  154. package/lib/typescript/hooks/useMessageHeapState.d.ts.map +1 -1
  155. package/lib/typescript/hooks/useSendMessage.d.ts +2 -1
  156. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  157. package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
  158. package/lib/typescript/networking/xmppClient.d.ts +25 -1
  159. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  160. package/lib/typescript/roomStore/roomHeapSlice.d.ts +22 -1
  161. package/lib/typescript/roomStore/roomHeapSlice.d.ts.map +1 -1
  162. package/lib/typescript/types/types.d.ts +6 -0
  163. package/lib/typescript/types/types.d.ts.map +1 -1
  164. package/metro.js +17 -57
  165. package/package.json +1 -1
  166. package/src/components/ContextMenu/ContextMenuComponents.tsx +0 -7
  167. package/src/components/MainComponents/ChatHeader.tsx +5 -1
  168. package/src/components/MainComponents/ChatRoom.tsx +6 -1
  169. package/src/components/MainComponents/ChatWrapper.tsx +1 -0
  170. package/src/components/MainComponents/MediaMessage.tsx +15 -8
  171. package/src/components/MainComponents/MessageList.tsx +10 -3
  172. package/src/components/MessageBubble/Message.tsx +64 -10
  173. package/src/components/MessageBubble/MessageInteractions.tsx +2 -105
  174. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +20 -41
  175. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +30 -12
  176. package/src/components/Modals/ModalWrapper/ModalWrapper.tsx +22 -1
  177. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +1 -1
  178. package/src/components/RoomComponents/styled/LastMessageEmoji.tsx +9 -7
  179. package/src/components/UsersList/Checkbox.tsx +68 -0
  180. package/src/components/UsersList/StyledComponents.ts +0 -8
  181. package/src/components/UsersList/UsersList.tsx +1 -1
  182. package/src/components/styled/MediaFilePreview.tsx +91 -5
  183. package/src/components/styled/MessageImage.tsx +1 -2
  184. package/src/components/styled/SendInput.tsx +3 -0
  185. package/src/components/styled/VideoMessage.tsx +3 -10
  186. package/src/context/xmppProvider.tsx +90 -2
  187. package/src/helpers/mimeToExtension.ts +135 -0
  188. package/src/helpers/resolveInitBeforeLoadUser.ts +79 -0
  189. package/src/hooks/useChatWrapperInit.ts +51 -46
  190. package/src/hooks/useEventHandlers.tsx +24 -1
  191. package/src/hooks/useMessageHeapState.tsx +6 -2
  192. package/src/hooks/useSendMessage.tsx +75 -4
  193. package/src/networking/api-requests/auth.api.ts +5 -1
  194. package/src/networking/xmppClient.ts +95 -1
  195. package/src/roomStore/roomHeapSlice.ts +47 -4
  196. package/src/types/types.ts +10 -0
  197. package/lib/commonjs/components/InputComponents/AudioInput.js +0 -23
  198. package/lib/commonjs/components/InputComponents/AudioInput.js.map +0 -1
  199. package/lib/commonjs/components/InputComponents/AudioRecorder.js +0 -101
  200. package/lib/commonjs/components/InputComponents/AudioRecorder.js.map +0 -1
  201. package/lib/commonjs/components/InputComponents/MediaInput.js +0 -98
  202. package/lib/commonjs/components/InputComponents/MediaInput.js.map +0 -1
  203. package/lib/commonjs/components/InputComponents/RecordingIndicator.js +0 -70
  204. package/lib/commonjs/components/InputComponents/RecordingIndicator.js.map +0 -1
  205. package/lib/commonjs/components/Modals/ModalSelectMedia/ModalSelectMedia.js +0 -207
  206. package/lib/commonjs/components/Modals/ModalSelectMedia/ModalSelectMedia.js.map +0 -1
  207. package/lib/commonjs/components/OperationalModal/OperationalModal.js +0 -83
  208. package/lib/commonjs/components/OperationalModal/OperationalModal.js.map +0 -1
  209. package/lib/module/components/InputComponents/AudioInput.js +0 -18
  210. package/lib/module/components/InputComponents/AudioInput.js.map +0 -1
  211. package/lib/module/components/InputComponents/AudioRecorder.js +0 -95
  212. package/lib/module/components/InputComponents/AudioRecorder.js.map +0 -1
  213. package/lib/module/components/InputComponents/MediaInput.js +0 -92
  214. package/lib/module/components/InputComponents/MediaInput.js.map +0 -1
  215. package/lib/module/components/InputComponents/RecordingIndicator.js +0 -65
  216. package/lib/module/components/InputComponents/RecordingIndicator.js.map +0 -1
  217. package/lib/module/components/Modals/ModalSelectMedia/ModalSelectMedia.js +0 -200
  218. package/lib/module/components/Modals/ModalSelectMedia/ModalSelectMedia.js.map +0 -1
  219. package/lib/module/components/OperationalModal/OperationalModal.js +0 -77
  220. package/lib/module/components/OperationalModal/OperationalModal.js.map +0 -1
  221. package/lib/typescript/components/InputComponents/AudioInput.d.ts +0 -9
  222. package/lib/typescript/components/InputComponents/AudioInput.d.ts.map +0 -1
  223. package/lib/typescript/components/InputComponents/AudioRecorder.d.ts +0 -9
  224. package/lib/typescript/components/InputComponents/AudioRecorder.d.ts.map +0 -1
  225. package/lib/typescript/components/InputComponents/MediaInput.d.ts +0 -16
  226. package/lib/typescript/components/InputComponents/MediaInput.d.ts.map +0 -1
  227. package/lib/typescript/components/InputComponents/RecordingIndicator.d.ts +0 -4
  228. package/lib/typescript/components/InputComponents/RecordingIndicator.d.ts.map +0 -1
  229. package/lib/typescript/components/Modals/ModalSelectMedia/ModalSelectMedia.d.ts +0 -9
  230. package/lib/typescript/components/Modals/ModalSelectMedia/ModalSelectMedia.d.ts.map +0 -1
  231. package/lib/typescript/components/OperationalModal/OperationalModal.d.ts +0 -9
  232. package/lib/typescript/components/OperationalModal/OperationalModal.d.ts.map +0 -1
  233. package/src/components/InputComponents/AudioInput.tsx +0 -24
  234. package/src/components/InputComponents/AudioRecorder.tsx +0 -100
  235. package/src/components/InputComponents/MediaInput.tsx +0 -113
  236. package/src/components/InputComponents/RecordingIndicator.tsx +0 -77
  237. package/src/components/Modals/ModalSelectMedia/ModalSelectMedia.tsx +0 -283
  238. package/src/components/OperationalModal/OperationalModal.tsx +0 -78
@@ -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
  // -----------------------------------------------------------
@@ -0,0 +1,135 @@
1
+ // MIME → file extension table for download/preview filename fallbacks.
2
+ // Centralized so FilePreviewModal, MediaMessage, and any future media
3
+ // surface share one source of truth. Keep additions sorted by category.
4
+
5
+ const MIME_TO_EXT: Record<string, string> = {
6
+ // images
7
+ 'image/jpeg': '.jpg',
8
+ 'image/jpg': '.jpg',
9
+ 'image/png': '.png',
10
+ 'image/gif': '.gif',
11
+ 'image/webp': '.webp',
12
+ 'image/heic': '.heic',
13
+ 'image/heif': '.heif',
14
+ 'image/bmp': '.bmp',
15
+ 'image/svg+xml': '.svg',
16
+ 'image/tiff': '.tiff',
17
+
18
+ // video
19
+ 'video/mp4': '.mp4',
20
+ 'video/quicktime': '.mov',
21
+ 'video/x-m4v': '.m4v',
22
+ 'video/webm': '.webm',
23
+ 'video/3gpp': '.3gp',
24
+ 'video/x-matroska': '.mkv',
25
+ 'video/mpeg': '.mpeg',
26
+
27
+ // audio
28
+ 'audio/mpeg': '.mp3',
29
+ 'audio/mp3': '.mp3',
30
+ 'audio/mp4': '.m4a',
31
+ 'audio/x-m4a': '.m4a',
32
+ 'audio/wav': '.wav',
33
+ 'audio/x-wav': '.wav',
34
+ 'audio/aac': '.aac',
35
+ 'audio/ogg': '.ogg',
36
+ 'audio/flac': '.flac',
37
+ 'audio/webm': '.weba',
38
+
39
+ // documents
40
+ 'application/pdf': '.pdf',
41
+ 'application/msword': '.doc',
42
+ 'application/vnd.openxmlformats-officedocument.wordprocessingml.document':
43
+ '.docx',
44
+ 'application/vnd.ms-excel': '.xls',
45
+ 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': '.xlsx',
46
+ 'application/vnd.ms-powerpoint': '.ppt',
47
+ 'application/vnd.openxmlformats-officedocument.presentationml.presentation':
48
+ '.pptx',
49
+ 'text/plain': '.txt',
50
+ 'text/csv': '.csv',
51
+ 'text/html': '.html',
52
+ 'application/json': '.json',
53
+ 'application/xml': '.xml',
54
+ 'text/xml': '.xml',
55
+ 'application/rtf': '.rtf',
56
+
57
+ // archives
58
+ 'application/zip': '.zip',
59
+ 'application/x-rar-compressed': '.rar',
60
+ 'application/x-7z-compressed': '.7z',
61
+ 'application/gzip': '.gz',
62
+ 'application/x-tar': '.tar',
63
+ };
64
+
65
+ const VALID_EXT_RE = /\.[a-z0-9]{1,8}$/i;
66
+
67
+ /**
68
+ * Returns the conventional extension (including the leading dot) for a
69
+ * MIME type, or `.bin` as a last-resort fallback so callers never end up
70
+ * with an extension-less filename.
71
+ */
72
+ export function getExtensionForMime(mime: string | undefined | null): string {
73
+ if (!mime) {return '.bin';}
74
+ const normalized = mime.toLowerCase().split(';')[0]!.trim();
75
+ return MIME_TO_EXT[normalized] || '.bin';
76
+ }
77
+
78
+ /**
79
+ * Parses a URL or file path and returns the last segment, URL-decoded,
80
+ * stripped of query/hash. Returns `''` when nothing usable is present.
81
+ */
82
+ export function filenameFromUrl(url: string | undefined | null): string {
83
+ if (!url) {return '';}
84
+ try {
85
+ // Strip query + hash without depending on a real URL parser (some RN
86
+ // URLs from CDNs do not parse cleanly).
87
+ const noQuery = url.split('?')[0]!.split('#')[0]!;
88
+ const last = noQuery.split('/').pop() || '';
89
+ return decodeURIComponent(last);
90
+ } catch {
91
+ return '';
92
+ }
93
+ }
94
+
95
+ /**
96
+ * Ensures `fileName` ends with a valid extension. Strategy:
97
+ * 1. If the input already has an extension, return it unchanged.
98
+ * 2. Otherwise, derive the extension from `mime` and append it.
99
+ * 3. If `mime` is unknown, append `.bin`.
100
+ *
101
+ * Empty/falsy filenames get a `media_<timestamp>` base before the
102
+ * extension is appended so callers always get a usable filename.
103
+ */
104
+ export function ensureFilenameHasExtension(
105
+ fileName: string | undefined | null,
106
+ mime: string | undefined | null
107
+ ): string {
108
+ const base =
109
+ (fileName && fileName.trim()) || `media_${Date.now()}`;
110
+ if (VALID_EXT_RE.test(base)) {return base;}
111
+ return `${base}${getExtensionForMime(mime)}`;
112
+ }
113
+
114
+ /**
115
+ * Best-effort filename derivation for a message bubble:
116
+ * - server-provided `fileName`
117
+ * - server-provided `originalName`
118
+ * - URL pathname
119
+ * - generic `media_<timestamp>` so we never render an empty label
120
+ *
121
+ * Always returns a string with a valid extension.
122
+ */
123
+ export function deriveDisplayFilename(opts: {
124
+ fileName?: string | null;
125
+ originalName?: string | null;
126
+ url?: string | null;
127
+ mime?: string | null;
128
+ }): string {
129
+ const candidate =
130
+ (opts.fileName && opts.fileName.trim()) ||
131
+ (opts.originalName && opts.originalName.trim()) ||
132
+ filenameFromUrl(opts.url) ||
133
+ '';
134
+ return ensureFilenameHasExtension(candidate, opts.mime);
135
+ }
@@ -237,4 +237,83 @@ export const applyResolvedUserToStore = (user?: User | null) => {
237
237
  store.dispatch(setUser(user));
238
238
  };
239
239
 
240
+ /**
241
+ * Re-mints a User with a fresh xmppPassword for the XMPP credentials-
242
+ * provider path (bug #17). Unlike `resolveInitBeforeLoadUser` this
243
+ * ALWAYS hydrates via the chosen auth path even if the cached user
244
+ * already had xmppCredentials — that's the whole point, the old ones
245
+ * are stale.
246
+ *
247
+ * Selection order:
248
+ * 1. `jwtLogin` — re-exchange the JWT via `/users/client`.
249
+ * 2. Anything else (`userLogin` / `customLogin` / store / persisted)
250
+ * — `tryHydrateViaMy` on the current user, which calls
251
+ * `/users/my` and, on 401, refreshes the access token via
252
+ * `/users/login/refresh` and retries.
253
+ *
254
+ * Returns `null` only when nothing usable is available — the caller
255
+ * (xmppProvider) falls back to the cached creds in that case.
256
+ */
257
+ export const refreshUserCredentialsForXmpp = async (
258
+ config?: IConfig,
259
+ signal?: AbortSignal
260
+ ): Promise<User | null> => {
261
+ if (signal?.aborted) {return null;}
262
+
263
+ // Priority 1: jwtLogin — same path as `resolveInitBeforeLoadUser`
264
+ // priority 3. We always re-exchange because the original JWT may
265
+ // itself have expired (and the consumer is expected to swap
266
+ // `config.jwtLogin.token` for a fresh one before then).
267
+ if (config?.jwtLogin?.enabled && config?.jwtLogin?.token) {
268
+ try {
269
+ const fresh = await loginViaJwt(config.jwtLogin.token);
270
+ const normalized = normalizeUserForXmpp(fresh);
271
+ if (normalized && hasXmppCredentials(normalized)) {return normalized;}
272
+ } catch (error) {
273
+ if (signal?.aborted || isAbortError(error)) {return null;}
274
+ // fall through to /users/my path
275
+ }
276
+ }
277
+
278
+ const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
279
+
280
+ // Priority 2: redux store user (covers userLogin + customLogin +
281
+ // anything previously persisted into chatSettingStore.user). The
282
+ // user must have at least a refreshToken for the refresh chain to
283
+ // produce something useful.
284
+ const currentUser = store.getState().chatSettingStore.user;
285
+ if (currentUser?.token || currentUser?.refreshToken) {
286
+ const hydrated = await tryHydrateViaMy(
287
+ currentUser as User,
288
+ myEndpoint,
289
+ signal
290
+ ).catch(() => null);
291
+ if (hydrated && hasXmppCredentials(hydrated)) {return hydrated;}
292
+ }
293
+
294
+ // Priority 3: persisted user in AsyncStorage — defensive; the store
295
+ // should already mirror this, but if the redux state got cleared
296
+ // mid-session (logout race) this gives us one last shot.
297
+ try {
298
+ const storedUser = await asyncLocalStorage<User>(
299
+ localStorageConstants.ETHORA_USER
300
+ ).get();
301
+ if (
302
+ storedUser &&
303
+ (storedUser.token || storedUser.refreshToken || storedUser.xmppPassword)
304
+ ) {
305
+ const hydrated = await tryHydrateViaMy(
306
+ storedUser,
307
+ myEndpoint,
308
+ signal
309
+ ).catch(() => null);
310
+ if (hydrated && hasXmppCredentials(hydrated)) {return hydrated;}
311
+ }
312
+ } catch {
313
+ // ignore storage errors
314
+ }
315
+
316
+ return null;
317
+ };
318
+
240
319
  export default resolveInitBeforeLoadUser;
@@ -119,10 +119,8 @@ const useChatWrapperInit = ({
119
119
  user.xmppUsername || user?.defaultWallet?.walletAddress,
120
120
  user?.xmppPassword,
121
121
  config?.xmppSettings
122
- ).then((client) => {
123
- console.log(' Client initialized', client?.status);
124
- return client;
125
- });
122
+ );
123
+ console.log(' Client initialized', newClient?.status);
126
124
 
127
125
  if (roomsList && Object.keys(roomsList).length > 0) {
128
126
  console.log(
@@ -156,27 +154,29 @@ const useChatWrapperInit = ({
156
154
  await newClient.getRoomsStanza();
157
155
  }
158
156
  }
159
- await newClient
160
- .getChatsPrivateStoreRequestStanza()
161
- .then(
162
- async (
163
- roomTimestampObject: any
164
- ) => {
165
- console.log(
166
- 'Got chats private store',
167
- roomTimestampObject?.length || 0
168
- );
169
- updatedChatLastTimestamps(roomTimestampObject, dispatch);
170
- if (!hasSyncedHistoryRef.current) {
171
- console.log('Updating messages till last');
172
- await updateMessagesTillLast(rooms, newClient);
173
- hasSyncedHistoryRef.current = true;
174
- }
175
- setClient(newClient);
176
- setConnectionLost(false);
177
- console.log('Initialization complete');
178
- }
157
+ try {
158
+ const roomTimestampObject: any =
159
+ await newClient.getChatsPrivateStoreRequestStanza();
160
+ console.log(
161
+ 'Got chats private store',
162
+ roomTimestampObject?.length || 0
179
163
  );
164
+ if (roomTimestampObject) {
165
+ updatedChatLastTimestamps(roomTimestampObject, dispatch);
166
+ }
167
+ if (!hasSyncedHistoryRef.current) {
168
+ console.log('Updating messages till last');
169
+ await updateMessagesTillLast(rooms, newClient);
170
+ hasSyncedHistoryRef.current = true;
171
+ }
172
+ setClient(newClient);
173
+ setConnectionLost(false);
174
+ console.log('Initialization complete');
175
+ } catch (err) {
176
+ console.warn('privateStore/timestamps failed', err);
177
+ setClient(newClient);
178
+ setConnectionLost(false);
179
+ }
180
180
 
181
181
  {
182
182
  config?.refreshTokens?.enabled && refresh();
@@ -204,29 +204,34 @@ const useChatWrapperInit = ({
204
204
  }
205
205
  setInited(true);
206
206
  console.log(' Getting chats private store for existing client');
207
- await client
208
- .getChatsPrivateStoreRequestStanza()
209
- .then(
210
- async (
211
- roomTimestampObject: any
212
- ) => {
213
- console.log(
214
- 'Got chats private store for existing client',
215
- roomTimestampObject?.length || 0
216
- );
217
- updatedChatLastTimestamps(roomTimestampObject, dispatch);
218
- if (!hasSyncedHistoryRef.current) {
219
- console.log(
220
- 'Updating messages till last for existing client'
221
- );
222
- await updateMessagesTillLast(rooms, client);
223
- hasSyncedHistoryRef.current = true;
224
- }
225
- setClient(client);
226
- setConnectionLost(false);
227
- console.log(' Initialization complete for existing client');
228
- }
207
+ try {
208
+ const roomTimestampObject: any =
209
+ await client.getChatsPrivateStoreRequestStanza();
210
+ console.log(
211
+ 'Got chats private store for existing client',
212
+ roomTimestampObject?.length || 0
229
213
  );
214
+ if (roomTimestampObject) {
215
+ updatedChatLastTimestamps(roomTimestampObject, dispatch);
216
+ }
217
+ if (!hasSyncedHistoryRef.current) {
218
+ console.log(
219
+ 'Updating messages till last for existing client'
220
+ );
221
+ await updateMessagesTillLast(rooms, client);
222
+ hasSyncedHistoryRef.current = true;
223
+ }
224
+ setClient(client);
225
+ setConnectionLost(false);
226
+ console.log(' Initialization complete for existing client');
227
+ } catch (err) {
228
+ console.warn(
229
+ 'privateStore/timestamps failed (existing client)',
230
+ err
231
+ );
232
+ setClient(client);
233
+ setConnectionLost(false);
234
+ }
230
235
  {
231
236
  config?.refreshTokens?.enabled && refresh();
232
237
  }