@ethora/chat-component-rn 26.7.3 → 26.7.4

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 (193) hide show
  1. package/CHANGELOG.md +45 -0
  2. package/README.md +9 -8
  3. package/lib/commonjs/assets/icons.js +7 -2
  4. package/lib/commonjs/assets/icons.js.map +1 -1
  5. package/lib/commonjs/components/Header/HeaderRoomList.js +20 -6
  6. package/lib/commonjs/components/Header/HeaderRoomList.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/ChatRoom.js +26 -4
  8. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/MessageList.js +2 -1
  10. package/lib/commonjs/components/MainComponents/MessageList.js.map +1 -1
  11. package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js +5 -5
  12. package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
  13. package/lib/commonjs/components/MainComponents/RoomList.js +62 -32
  14. package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
  15. package/lib/commonjs/components/Menu/HeaderRoomListMenu.js +208 -70
  16. package/lib/commonjs/components/Menu/HeaderRoomListMenu.js.map +1 -1
  17. package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js +307 -99
  18. package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
  19. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +552 -213
  20. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  21. package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js +226 -0
  22. package/lib/commonjs/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -0
  23. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +6 -2
  24. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  25. package/lib/commonjs/components/Modals/Modal/Modal.js +8 -0
  26. package/lib/commonjs/components/Modals/Modal/Modal.js.map +1 -1
  27. package/lib/commonjs/components/Modals/NewChatModal/NewChatModal.js +202 -104
  28. package/lib/commonjs/components/Modals/NewChatModal/NewChatModal.js.map +1 -1
  29. package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js +556 -0
  30. package/lib/commonjs/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -0
  31. package/lib/commonjs/components/Modals/ProfileHeader/ProfileMenu.js +210 -0
  32. package/lib/commonjs/components/Modals/ProfileHeader/ProfileMenu.js.map +1 -0
  33. package/lib/commonjs/components/Modals/SelectUsersModal/SelectUsersModal.js +4 -2
  34. package/lib/commonjs/components/Modals/SelectUsersModal/SelectUsersModal.js.map +1 -1
  35. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js +507 -244
  36. package/lib/commonjs/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  37. package/lib/commonjs/components/Modals/UserSettingsModal/UserSettingsModal.js +74 -46
  38. package/lib/commonjs/components/Modals/UserSettingsModal/UserSettingsModal.js.map +1 -1
  39. package/lib/commonjs/components/Modals/styledModalComponents.js +9 -2
  40. package/lib/commonjs/components/Modals/styledModalComponents.js.map +1 -1
  41. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +17 -7
  42. package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
  43. package/lib/commonjs/components/styled/SendInput.js +80 -1
  44. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  45. package/lib/commonjs/components/styled/StyledComponents.js +9 -3
  46. package/lib/commonjs/components/styled/StyledComponents.js.map +1 -1
  47. package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js +11 -4
  48. package/lib/commonjs/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
  49. package/lib/commonjs/helpers/getChatBackground.js +23 -0
  50. package/lib/commonjs/helpers/getChatBackground.js.map +1 -0
  51. package/lib/commonjs/helpers/mediaLibraryRuntime.js +31 -0
  52. package/lib/commonjs/helpers/mediaLibraryRuntime.js.map +1 -0
  53. package/lib/commonjs/helpers/sheetGestures.js +32 -0
  54. package/lib/commonjs/helpers/sheetGestures.js.map +1 -0
  55. package/lib/commonjs/i18n/strings.js +121 -1
  56. package/lib/commonjs/i18n/strings.js.map +1 -1
  57. package/lib/commonjs/networking/api-requests/user.api.js +59 -0
  58. package/lib/commonjs/networking/api-requests/user.api.js.map +1 -1
  59. package/lib/commonjs/roomStore/chatSettingsSlice.js +3 -1
  60. package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
  61. package/lib/module/assets/icons.js +6 -1
  62. package/lib/module/assets/icons.js.map +1 -1
  63. package/lib/module/components/Header/HeaderRoomList.js +20 -6
  64. package/lib/module/components/Header/HeaderRoomList.js.map +1 -1
  65. package/lib/module/components/MainComponents/ChatRoom.js +26 -4
  66. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  67. package/lib/module/components/MainComponents/MessageList.js +2 -1
  68. package/lib/module/components/MainComponents/MessageList.js.map +1 -1
  69. package/lib/module/components/MainComponents/ProfileImagePlaceholder.js +5 -5
  70. package/lib/module/components/MainComponents/ProfileImagePlaceholder.js.map +1 -1
  71. package/lib/module/components/MainComponents/RoomList.js +62 -32
  72. package/lib/module/components/MainComponents/RoomList.js.map +1 -1
  73. package/lib/module/components/Menu/HeaderRoomListMenu.js +211 -73
  74. package/lib/module/components/Menu/HeaderRoomListMenu.js.map +1 -1
  75. package/lib/module/components/Modals/AttachSheet/AttachSheet.js +298 -102
  76. package/lib/module/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
  77. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +555 -217
  78. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  79. package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js +223 -0
  80. package/lib/module/components/Modals/ChatProfileModal/ReportChatModal.js.map +1 -0
  81. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +6 -2
  82. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  83. package/lib/module/components/Modals/Modal/Modal.js +9 -1
  84. package/lib/module/components/Modals/Modal/Modal.js.map +1 -1
  85. package/lib/module/components/Modals/NewChatModal/NewChatModal.js +205 -106
  86. package/lib/module/components/Modals/NewChatModal/NewChatModal.js.map +1 -1
  87. package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js +548 -0
  88. package/lib/module/components/Modals/ProfileHeader/ProfileHeader.js.map +1 -0
  89. package/lib/module/components/Modals/ProfileHeader/ProfileMenu.js +205 -0
  90. package/lib/module/components/Modals/ProfileHeader/ProfileMenu.js.map +1 -0
  91. package/lib/module/components/Modals/SelectUsersModal/SelectUsersModal.js +4 -2
  92. package/lib/module/components/Modals/SelectUsersModal/SelectUsersModal.js.map +1 -1
  93. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js +509 -248
  94. package/lib/module/components/Modals/UserProfileModal/UserProfileModal.js.map +1 -1
  95. package/lib/module/components/Modals/UserSettingsModal/UserSettingsModal.js +76 -48
  96. package/lib/module/components/Modals/UserSettingsModal/UserSettingsModal.js.map +1 -1
  97. package/lib/module/components/Modals/styledModalComponents.js +9 -2
  98. package/lib/module/components/Modals/styledModalComponents.js.map +1 -1
  99. package/lib/module/components/RoomComponents/ChatRoomItem.js +17 -7
  100. package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
  101. package/lib/module/components/styled/SendInput.js +80 -1
  102. package/lib/module/components/styled/SendInput.js.map +1 -1
  103. package/lib/module/components/styled/StyledComponents.js +9 -3
  104. package/lib/module/components/styled/StyledComponents.js.map +1 -1
  105. package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js +11 -4
  106. package/lib/module/components/styled/StyledInputComponents/StyledInputComponents.js.map +1 -1
  107. package/lib/module/helpers/getChatBackground.js +18 -0
  108. package/lib/module/helpers/getChatBackground.js.map +1 -0
  109. package/lib/module/helpers/mediaLibraryRuntime.js +25 -0
  110. package/lib/module/helpers/mediaLibraryRuntime.js.map +1 -0
  111. package/lib/module/helpers/sheetGestures.js +26 -0
  112. package/lib/module/helpers/sheetGestures.js.map +1 -0
  113. package/lib/module/i18n/strings.js +121 -1
  114. package/lib/module/i18n/strings.js.map +1 -1
  115. package/lib/module/networking/api-requests/user.api.js +56 -0
  116. package/lib/module/networking/api-requests/user.api.js.map +1 -1
  117. package/lib/module/roomStore/chatSettingsSlice.js +3 -1
  118. package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
  119. package/lib/typescript/assets/icons.d.ts +12 -0
  120. package/lib/typescript/assets/icons.d.ts.map +1 -1
  121. package/lib/typescript/components/Header/HeaderRoomList.d.ts.map +1 -1
  122. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  123. package/lib/typescript/components/MainComponents/MessageList.d.ts.map +1 -1
  124. package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
  125. package/lib/typescript/components/Menu/HeaderRoomListMenu.d.ts.map +1 -1
  126. package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts +9 -0
  127. package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts.map +1 -1
  128. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts +10 -0
  129. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  130. package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts +9 -0
  131. package/lib/typescript/components/Modals/ChatProfileModal/ReportChatModal.d.ts.map +1 -0
  132. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  133. package/lib/typescript/components/Modals/Modal/Modal.d.ts.map +1 -1
  134. package/lib/typescript/components/Modals/NewChatModal/NewChatModal.d.ts +1 -1
  135. package/lib/typescript/components/Modals/NewChatModal/NewChatModal.d.ts.map +1 -1
  136. package/lib/typescript/components/Modals/ProfileHeader/ProfileHeader.d.ts +75 -0
  137. package/lib/typescript/components/Modals/ProfileHeader/ProfileHeader.d.ts.map +1 -0
  138. package/lib/typescript/components/Modals/ProfileHeader/ProfileMenu.d.ts +25 -0
  139. package/lib/typescript/components/Modals/ProfileHeader/ProfileMenu.d.ts.map +1 -0
  140. package/lib/typescript/components/Modals/SelectUsersModal/SelectUsersModal.d.ts +6 -1
  141. package/lib/typescript/components/Modals/SelectUsersModal/SelectUsersModal.d.ts.map +1 -1
  142. package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts +16 -0
  143. package/lib/typescript/components/Modals/UserProfileModal/UserProfileModal.d.ts.map +1 -1
  144. package/lib/typescript/components/Modals/UserSettingsModal/UserSettingsModal.d.ts +1 -0
  145. package/lib/typescript/components/Modals/UserSettingsModal/UserSettingsModal.d.ts.map +1 -1
  146. package/lib/typescript/components/Modals/styledModalComponents.d.ts.map +1 -1
  147. package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
  148. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  149. package/lib/typescript/components/styled/StyledComponents.d.ts.map +1 -1
  150. package/lib/typescript/components/styled/StyledInputComponents/StyledInputComponents.d.ts.map +1 -1
  151. package/lib/typescript/helpers/getChatBackground.d.ts +15 -0
  152. package/lib/typescript/helpers/getChatBackground.d.ts.map +1 -0
  153. package/lib/typescript/helpers/mediaLibraryRuntime.d.ts +3 -0
  154. package/lib/typescript/helpers/mediaLibraryRuntime.d.ts.map +1 -0
  155. package/lib/typescript/helpers/sheetGestures.d.ts +20 -0
  156. package/lib/typescript/helpers/sheetGestures.d.ts.map +1 -0
  157. package/lib/typescript/i18n/strings.d.ts.map +1 -1
  158. package/lib/typescript/networking/api-requests/user.api.d.ts +27 -0
  159. package/lib/typescript/networking/api-requests/user.api.d.ts.map +1 -1
  160. package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
  161. package/lib/typescript/types/types.d.ts +8 -5
  162. package/lib/typescript/types/types.d.ts.map +1 -1
  163. package/package.json +6 -1
  164. package/src/assets/icons.tsx +29 -9
  165. package/src/components/Header/HeaderRoomList.tsx +20 -5
  166. package/src/components/MainComponents/ChatRoom.tsx +29 -7
  167. package/src/components/MainComponents/MessageList.tsx +2 -1
  168. package/src/components/MainComponents/ProfileImagePlaceholder.tsx +5 -5
  169. package/src/components/MainComponents/RoomList.tsx +65 -34
  170. package/src/components/Menu/HeaderRoomListMenu.tsx +209 -64
  171. package/src/components/Modals/AttachSheet/AttachSheet.tsx +318 -86
  172. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +598 -279
  173. package/src/components/Modals/ChatProfileModal/ReportChatModal.tsx +244 -0
  174. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +4 -2
  175. package/src/components/Modals/Modal/Modal.tsx +7 -0
  176. package/src/components/Modals/NewChatModal/NewChatModal.tsx +219 -117
  177. package/src/components/Modals/ProfileHeader/ProfileHeader.tsx +615 -0
  178. package/src/components/Modals/ProfileHeader/ProfileMenu.tsx +217 -0
  179. package/src/components/Modals/SelectUsersModal/SelectUsersModal.tsx +14 -4
  180. package/src/components/Modals/UserProfileModal/UserProfileModal.tsx +617 -307
  181. package/src/components/Modals/UserSettingsModal/UserSettingsModal.tsx +75 -58
  182. package/src/components/Modals/styledModalComponents.tsx +9 -2
  183. package/src/components/RoomComponents/ChatRoomItem.tsx +14 -7
  184. package/src/components/styled/SendInput.tsx +83 -2
  185. package/src/components/styled/StyledComponents.ts +9 -3
  186. package/src/components/styled/StyledInputComponents/StyledInputComponents.tsx +11 -4
  187. package/src/helpers/getChatBackground.ts +18 -0
  188. package/src/helpers/mediaLibraryRuntime.ts +25 -0
  189. package/src/helpers/sheetGestures.ts +25 -0
  190. package/src/i18n/strings.ts +121 -1
  191. package/src/networking/api-requests/user.api.ts +71 -0
  192. package/src/roomStore/chatSettingsSlice.ts +3 -1
  193. package/src/types/types.ts +8 -5
@@ -35,6 +35,14 @@ interface RoomListProps {
35
35
  onRoomClick?: (chat: IRoom) => void;
36
36
  }
37
37
 
38
+ /** Page ground behind the room list, the search field and the header's
39
+ * rounded bottom corners. */
40
+ const LIST_BACKGROUND = '#E8EDF2';
41
+
42
+ /** First-paint fallback until the floating search strip reports its real
43
+ * height; the list's top padding follows the measurement after that. */
44
+ const SEARCH_BAR_ESTIMATE = 60;
45
+
38
46
  const RoomList: React.FC<RoomListProps> = ({
39
47
  chats,
40
48
  burgerMenu = false,
@@ -120,6 +128,10 @@ const RoomList: React.FC<RoomListProps> = ({
120
128
  }
121
129
  }, [burgerMenu]);
122
130
 
131
+ // Measured, so the list's top padding always matches the floating
132
+ // search strip — including when a larger font or a taller field grows it.
133
+ const [searchBarHeight, setSearchBarHeight] = useState(SEARCH_BAR_ESTIMATE);
134
+
123
135
  const toggleDrawer = () => {
124
136
  if (isDrawerOpen) {
125
137
  closeDrawer();
@@ -180,39 +192,50 @@ const RoomList: React.FC<RoomListProps> = ({
180
192
  <>
181
193
  <View style={styles.scrollContainer}>
182
194
  <HeaderRoomList setDrawerOpen={toggleDrawer} />
183
- {/* Search sits ABOVE the list rather than in its
184
- ListHeaderComponent. As a list header it was simply the
185
- first scrolling row, so the moment you scrolled the rooms
186
- it slid up and disappeared behind the opaque header band.
187
- Fixed here it stays reachable, which is the entire point
188
- of a search field over a long list. */}
189
- <View style={styles.searchBar}>
190
- <SearchInput
191
- icon={<SearchIcon height={20} />}
192
- value={searchTerm}
193
- onChangeText={handleSearchChange}
194
- placeholder={t('search.placeholder')}
195
+ {/* The search field floats OVER the list rather than
196
+ sitting above it in the column: its strip is transparent
197
+ and the rooms scroll underneath, which is what the design
198
+ shows. It is still fixed (not a ListHeaderComponent), so
199
+ it stays reachable however far the list is scrolled — as a
200
+ list header it scrolled away behind the opaque header. */}
201
+ <View style={styles.listArea}>
202
+ <FlatList
203
+ data={filteredChats}
204
+ keyExtractor={(item) => item.jid}
205
+ // Clears the floating field on first paint; scrolling
206
+ // then slides the rooms under it.
207
+ contentContainerStyle={{ paddingTop: searchBarHeight }}
208
+ renderItem={({ item }) => (
209
+ <Pressable
210
+ // Stable testID so e2e drivers can target a room
211
+ // row by its jid local-part (e.g. Main chat under
212
+ // app id `..._...759` → testID `room-...759`).
213
+ testID={`room-${(item.jid || '').split('@')[0]}`}
214
+ accessibilityLabel={`room-${item.title || item.name}`}
215
+ onPress={() => performClick(item)}
216
+ onPressIn={handlePressIn}
217
+ onPressOut={handlePressOut}
218
+ >
219
+ <ChatRoomItem chat={item} config={config} />
220
+ </Pressable>
221
+ )}
222
+ style={styles.chatList}
195
223
  />
224
+ <View
225
+ testID="room-list-search"
226
+ style={styles.searchBar}
227
+ onLayout={(e) =>
228
+ setSearchBarHeight(e.nativeEvent.layout.height)
229
+ }
230
+ >
231
+ <SearchInput
232
+ icon={<SearchIcon height={20} />}
233
+ value={searchTerm}
234
+ onChangeText={handleSearchChange}
235
+ placeholder={t('search.placeholder')}
236
+ />
237
+ </View>
196
238
  </View>
197
- <FlatList
198
- data={filteredChats}
199
- keyExtractor={(item) => item.jid}
200
- renderItem={({ item }) => (
201
- <Pressable
202
- // Stable testID so e2e drivers can target a room
203
- // row by its jid local-part (e.g. Main chat under
204
- // app id `..._...759` → testID `room-...759`).
205
- testID={`room-${(item.jid || '').split('@')[0]}`}
206
- accessibilityLabel={`room-${item.title || item.name}`}
207
- onPress={() => performClick(item)}
208
- onPressIn={handlePressIn}
209
- onPressOut={handlePressOut}
210
- >
211
- <ChatRoomItem chat={item} config={config} />
212
- </Pressable>
213
- )}
214
- style={styles.chatList}
215
- />
216
239
 
217
240
  <HeaderRoomListMenu
218
241
  closeDrawer={closeDrawer}
@@ -238,7 +261,7 @@ const styles = StyleSheet.create({
238
261
  width: '100%',
239
262
  height: '100%',
240
263
  flex: 1,
241
- backgroundColor: '#fff',
264
+ backgroundColor: LIST_BACKGROUND,
242
265
  },
243
266
  scrollContainer: {
244
267
  flexGrow: 1,
@@ -249,6 +272,9 @@ const styles = StyleSheet.create({
249
272
  padding: 16,
250
273
  justifyContent: 'space-between',
251
274
  },
275
+ listArea: {
276
+ flex: 1,
277
+ },
252
278
  searchBar: {
253
279
  // `row` matters: SearchInputWrapper is `flex: 1` plus a fixed 44px
254
280
  // height. In a column parent that flex resolves VERTICALLY against a
@@ -256,14 +282,19 @@ const styles = StyleSheet.create({
256
282
  // but its magnifier. As a row it resolves to width, which is what it
257
283
  // meant back when the field was a list-header child.
258
284
  flexDirection: 'row',
285
+ position: 'absolute',
286
+ top: 0,
287
+ left: 0,
288
+ right: 0,
259
289
  paddingTop: 8,
290
+ paddingBottom: 8,
260
291
  paddingHorizontal: 16,
261
- backgroundColor: '#FAFAFA',
292
+ backgroundColor: 'transparent',
262
293
  },
263
294
  chatList: {
264
295
  flex: 1,
265
296
  paddingHorizontal: 16,
266
- backgroundColor: '#FAFAFA',
297
+ backgroundColor: LIST_BACKGROUND,
267
298
  },
268
299
  });
269
300
 
@@ -1,4 +1,4 @@
1
- import React, { FC, useMemo } from 'react';
1
+ import React, { FC, useEffect, useMemo, useRef, useState } from 'react';
2
2
  import {
3
3
  Alert,
4
4
  Animated,
@@ -7,6 +7,8 @@ import {
7
7
  TouchableWithoutFeedback,
8
8
  StyleSheet,
9
9
  Dimensions,
10
+ Modal,
11
+ PanResponder,
10
12
  View,
11
13
  } from 'react-native';
12
14
  import {
@@ -16,10 +18,15 @@ import {
16
18
  SettingIcon,
17
19
  } from '../../assets/icons';
18
20
  import { useDispatch } from 'react-redux';
21
+ import { useSafeAreaInsets } from 'react-native-safe-area-context';
19
22
  import { setActiveModal } from '../../roomStore/chatSettingsSlice';
20
23
  import { MODAL_TYPES } from '../../helpers/constants/MODAL_TYPES';
21
24
  import { useChatSettingState } from '../../hooks/useChatSettingState';
22
25
  import { useLogout } from '../../hooks/useLogout';
26
+ import {
27
+ shouldClaimVerticalDrag,
28
+ shouldDismissOnDrag,
29
+ } from '../../helpers/sheetGestures';
23
30
  import type { IConfig } from '../../types/types';
24
31
 
25
32
  type LogoutConfig = NonNullable<IConfig['logout']>;
@@ -85,7 +92,7 @@ export const runLogoutFlow = async (
85
92
  }
86
93
  };
87
94
 
88
- const { width } = Dimensions.get('window');
95
+ const { height: windowHeight } = Dimensions.get('window');
89
96
 
90
97
  interface HeaderRoomListMenuProps {
91
98
  isDrawerOpen: boolean;
@@ -106,11 +113,102 @@ export const HeaderRoomListMenu: FC<HeaderRoomListMenuProps> = ({
106
113
  const logoutConfig = config?.logout?.enabled ? config.logout : undefined;
107
114
  const primaryColor = config?.colors?.primary ?? '#0052CD';
108
115
 
109
- const drawerTranslateX = drawerAnimation.interpolate({
116
+ const insets = useSafeAreaInsets();
117
+ // Measured, so the sheet is exactly as tall as its rows and starts
118
+ // fully off-screen no matter how many options the host enables.
119
+ const [sheetHeight, setSheetHeight] = useState(windowHeight * 0.4);
120
+ // Finger offset, layered on top of the open/close animation the parent
121
+ // drives. Reset once the sheet is unmounted, so a swipe-dismissed sheet
122
+ // never snaps back into view on its way out.
123
+ const dragY = useRef(new Animated.Value(0)).current;
124
+ const sheetHeightRef = useRef(sheetHeight);
125
+ sheetHeightRef.current = sheetHeight;
126
+
127
+ // closeDrawer is a fresh function each render; the responder is built
128
+ // once, so it reads the current one through a ref rather than the one it
129
+ // closed over on mount.
130
+ const closeDrawerRef = useRef(closeDrawer);
131
+ closeDrawerRef.current = closeDrawer;
132
+
133
+ useEffect(() => {
134
+ if (!isDrawerOpen) {
135
+ dragY.setValue(0);
136
+ return;
137
+ }
138
+ // Hand `dragY` to the native driver up front. The open/close value is
139
+ // already native (the parent animates it that way), and a transform
140
+ // that mixes a native node with a JS-only one drops the JS updates on
141
+ // the floor — which is exactly a drag that moves nothing.
142
+ Animated.timing(dragY, {
143
+ toValue: 0,
144
+ duration: 0,
145
+ useNativeDriver: true,
146
+ }).start();
147
+ }, [isDrawerOpen, dragY]);
148
+
149
+ const openTranslateY = drawerAnimation.interpolate({
110
150
  inputRange: [0, 1],
111
- outputRange: [width, 0],
151
+ outputRange: [sheetHeight, 0],
112
152
  });
113
153
 
154
+ const dismissByDrag = () => {
155
+ Animated.timing(dragY, {
156
+ toValue: sheetHeightRef.current,
157
+ duration: 180,
158
+ useNativeDriver: true,
159
+ }).start();
160
+ closeDrawerRef.current();
161
+ };
162
+
163
+ const makeDragResponder = (claimOnStart: boolean) =>
164
+ PanResponder.create({
165
+ // The grab area claims on touch-DOWN: nothing to negotiate against,
166
+ // so a pull on the handle always drags.
167
+ onStartShouldSetPanResponder: () => claimOnStart,
168
+ // CAPTURE matters on the body: a drag that starts on a row makes
169
+ // that row's Touchable the responder on touch-down, and a plain
170
+ // `onMoveShouldSetPanResponder` is never consulted afterwards.
171
+ // Capturing on the first clearly-vertical movement takes the gesture
172
+ // back, while a tap (dy ≈ 0) still reaches the row.
173
+ onMoveShouldSetPanResponderCapture: (_evt, g) =>
174
+ claimOnStart || shouldClaimVerticalDrag(g.dy, g.dx),
175
+ onMoveShouldSetPanResponder: (_evt, g) =>
176
+ claimOnStart || shouldClaimVerticalDrag(g.dy, g.dx),
177
+ onPanResponderTerminationRequest: () => false,
178
+ onPanResponderMove: (_evt, g) => {
179
+ dragY.setValue(Math.max(0, g.dy));
180
+ },
181
+ onPanResponderRelease: (_evt, g) => {
182
+ if (shouldDismissOnDrag(g.dy, g.vy)) {
183
+ dismissByDrag();
184
+ return;
185
+ }
186
+ Animated.spring(dragY, {
187
+ toValue: 0,
188
+ tension: 90,
189
+ friction: 14,
190
+ useNativeDriver: true,
191
+ }).start();
192
+ },
193
+ onPanResponderTerminate: () => {
194
+ Animated.spring(dragY, {
195
+ toValue: 0,
196
+ tension: 90,
197
+ friction: 14,
198
+ useNativeDriver: true,
199
+ }).start();
200
+ },
201
+ });
202
+
203
+ // Built once — `useRef(expr)` would re-run PanResponder.create every
204
+ // render and throw the result away.
205
+ const bodyPanRef = useRef<ReturnType<typeof makeDragResponder> | null>(null);
206
+ const handlePanRef = useRef<ReturnType<typeof makeDragResponder> | null>(null);
207
+ if (!bodyPanRef.current) {bodyPanRef.current = makeDragResponder(false);}
208
+ if (!handlePanRef.current) {handlePanRef.current = makeDragResponder(true);}
209
+ const pan = bodyPanRef.current;
210
+ const handlePan = handlePanRef.current;
211
+
114
212
  const menuOptions = useMemo(() => {
115
213
  const options: {
116
214
  label: string;
@@ -122,7 +220,7 @@ export const HeaderRoomListMenu: FC<HeaderRoomListMenuProps> = ({
122
220
  }[] = [
123
221
  {
124
222
  label: 'New Chat',
125
- icon: <AddNewIcon color="#8C8C8C" />,
223
+ icon: <AddNewIcon color={primaryColor} />,
126
224
  onClick: () => {
127
225
  dispatch(setActiveModal(MODAL_TYPES.NEW_CHAT));
128
226
  console.log('New chat clicked');
@@ -131,7 +229,7 @@ export const HeaderRoomListMenu: FC<HeaderRoomListMenuProps> = ({
131
229
  },
132
230
  {
133
231
  label: 'Profile',
134
- icon: <ProfileIcon color="#8C8C8C" />,
232
+ icon: <ProfileIcon color={primaryColor} />,
135
233
  onClick: () => {
136
234
  dispatch(setActiveModal(MODAL_TYPES.PROFILE));
137
235
  console.log('Profile clicked');
@@ -139,7 +237,7 @@ export const HeaderRoomListMenu: FC<HeaderRoomListMenuProps> = ({
139
237
  },
140
238
  {
141
239
  label: 'Settings',
142
- icon: <SettingIcon color="#8C8C8C" />,
240
+ icon: <SettingIcon color={primaryColor} />,
143
241
  onClick: () => {
144
242
  dispatch(setActiveModal(MODAL_TYPES.SETTINGS));
145
243
  console.log('Settings clicked');
@@ -162,47 +260,80 @@ export const HeaderRoomListMenu: FC<HeaderRoomListMenuProps> = ({
162
260
  }, [dispatch, logoutConfig, performLogout, primaryColor, closeDrawer]);
163
261
 
164
262
  return (
165
- <>
263
+ // Presented through a real <Modal>: as an in-tree overlay the sheet was
264
+ // bounded by the room list's own container, so it stopped short of the
265
+ // screen's bottom edge (that pale strip under it) and the dim never
266
+ // reached the host's chrome.
267
+ <Modal
268
+ transparent
269
+ visible={isDrawerOpen}
270
+ animationType="none"
271
+ statusBarTranslucent
272
+ onRequestClose={closeDrawer}
273
+ >
274
+ <TouchableWithoutFeedback onPress={closeDrawer}>
275
+ <Animated.View
276
+ style={[styles.overlay, { opacity: overlayAnimation }]}
277
+ />
278
+ </TouchableWithoutFeedback>
279
+ {/* A bottom sheet rather than the old full-height right-hand drawer:
280
+ * three rows never needed the whole screen, and coming up from the
281
+ * bottom keeps them within thumb reach. Inset by 2pt all round so a
282
+ * sliver of the dimmed screen shows around it. */}
166
283
  <Animated.View
284
+ testID="header-menu-sheet"
285
+ onLayout={(e) => {
286
+ const measured = e.nativeEvent.layout.height;
287
+ if (measured > 0) {setSheetHeight(measured);}
288
+ }}
167
289
  style={[
168
- styles.drawer,
169
- { transform: [{ translateX: drawerTranslateX }] },
290
+ styles.sheet,
291
+ {
292
+ paddingBottom: insets.bottom + 20,
293
+ // Two entries rather than Animated.add: the parent's value is
294
+ // driven natively, and stacking the transforms keeps each node
295
+ // on its own driver instead of mixing them in one expression.
296
+ transform: [{ translateY: openTranslateY }, { translateY: dragY }],
297
+ },
170
298
  ]}
299
+ {...pan.panHandlers}
171
300
  >
172
- {menuOptions.map((option, index) => (
173
- <View key={index} style={styles.menuItemWrapper}>
174
- <TouchableOpacity
175
- style={styles.menuItem}
176
- testID={`header-menu-${option.label}`}
177
- onPress={() => {
178
- option.onClick();
179
- if (!option.closesDrawer) {
180
- closeDrawer();
181
- }
182
- }}
183
- >
184
- <View
185
- style={{
186
- width: 22,
187
- alignItems: 'center',
301
+ {/* Full-width grab area, not just the 40pt pill: it claims the
302
+ * gesture the moment it is touched. */}
303
+ <View
304
+ testID="header-menu-grabber"
305
+ style={styles.grabArea}
306
+ {...handlePan.panHandlers}
307
+ >
308
+ <View style={styles.grabber} />
309
+ </View>
310
+ <View style={styles.card}>
311
+ {menuOptions.map((option, index) => (
312
+ <View key={index} style={styles.menuItemWrapper}>
313
+ <TouchableOpacity
314
+ style={styles.menuItem}
315
+ testID={`header-menu-${option.label}`}
316
+ activeOpacity={0.6}
317
+ onPress={() => {
318
+ option.onClick();
319
+ if (!option.closesDrawer) {
320
+ closeDrawer();
321
+ }
188
322
  }}
189
323
  >
190
- {option.icon}
191
- </View>
192
- <Text style={[styles.label, option?.styles]}>{option.label}</Text>
193
- </TouchableOpacity>
194
- {index < menuOptions.length - 1 && <View style={styles.divider} />}
195
- </View>
196
- ))}
324
+ <View style={styles.iconSlot}>{option.icon}</View>
325
+ <Text style={[styles.label, option?.styles]}>
326
+ {option.label}
327
+ </Text>
328
+ </TouchableOpacity>
329
+ {index < menuOptions.length - 1 && (
330
+ <View style={styles.divider} />
331
+ )}
332
+ </View>
333
+ ))}
334
+ </View>
197
335
  </Animated.View>
198
- {isDrawerOpen && (
199
- <TouchableWithoutFeedback onPress={closeDrawer}>
200
- <Animated.View
201
- style={[styles.overlay, { opacity: overlayAnimation }]}
202
- />
203
- </TouchableWithoutFeedback>
204
- )}
205
- </>
336
+ </Modal>
206
337
  );
207
338
  };
208
339
 
@@ -214,40 +345,54 @@ const styles = StyleSheet.create({
214
345
  menuItem: {
215
346
  flexDirection: 'row',
216
347
  alignItems: 'center',
217
- padding: 16,
348
+ paddingVertical: 18,
349
+ paddingHorizontal: 16,
350
+ },
351
+ iconSlot: {
352
+ width: 26,
353
+ alignItems: 'center',
218
354
  },
219
355
  label: {
220
356
  fontSize: 16,
221
- marginLeft: 10,
357
+ fontWeight: '500',
358
+ color: '#141414',
359
+ marginLeft: 12,
222
360
  },
223
361
  divider: {
224
- height: 1,
225
- width: '100%',
226
- backgroundColor: '#0052cd0d',
362
+ height: StyleSheet.hairlineWidth,
363
+ marginLeft: 54,
364
+ backgroundColor: '#E4E4E9',
227
365
  },
228
- drawer: {
366
+ sheet: {
229
367
  position: 'absolute',
230
- top: 0,
231
- bottom: 0,
232
- right: 0,
233
- width: width * 0.7,
234
- backgroundColor: '#fff',
235
- padding: 20,
368
+ // Inset rather than flush: the dimmed screen stays visible as a thin
369
+ // line around the sheet.
370
+ left: 3,
371
+ right: 3,
372
+ bottom: 3,
373
+ paddingTop: 8,
374
+ backgroundColor: '#FFFFFF',
375
+ borderRadius: 20,
236
376
  shadowColor: '#000',
237
- shadowOpacity: 0.3,
238
- shadowOffset: { width: -2, height: 0 },
239
- shadowRadius: 5,
240
- elevation: 5,
377
+ shadowOpacity: 0.12,
378
+ shadowOffset: { width: 0, height: -3 },
379
+ shadowRadius: 12,
380
+ elevation: 12,
241
381
  zIndex: 999,
242
382
  },
243
- drawerTitle: {
244
- fontSize: 20,
245
- fontWeight: 'bold',
246
- marginBottom: 20,
383
+ grabArea: {
384
+ alignItems: 'center',
385
+ paddingTop: 6,
386
+ paddingBottom: 12,
387
+ },
388
+ grabber: {
389
+ width: 40,
390
+ height: 4,
391
+ borderRadius: 2,
392
+ backgroundColor: '#C9CBD1',
247
393
  },
248
- drawerItem: {
249
- fontSize: 18,
250
- marginBottom: 10,
394
+ card: {
395
+ overflow: 'hidden',
251
396
  },
252
397
  overlay: {
253
398
  position: 'absolute',