@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
@@ -1,43 +1,78 @@
1
- import React, { useEffect, useMemo, useState } from 'react';
1
+ /** @format */
2
+
3
+ import React, { useEffect, useMemo, useRef, useState } from 'react';
4
+ import {
5
+ Alert,
6
+ Animated,
7
+ Keyboard,
8
+ Linking,
9
+ Pressable,
10
+ StyleSheet,
11
+ Text,
12
+ TextInput,
13
+ TouchableOpacity,
14
+ View,
15
+ } from 'react-native';
16
+ import { useDispatch, useSelector } from 'react-redux';
17
+ import * as ImagePicker from 'expo-image-picker';
2
18
  import { useFileToken } from '../../../hooks/useFileToken';
3
19
  import { appendFileToken } from '../../../helpers/secureFileUrl';
4
- import {
5
- CenterContainer,
6
- UserInfo,
7
- UserName,
8
- UserStatus,
9
- ModalContainerFullScreen,
10
- Label,
11
- BorderedContainer,
12
- LabelData,
13
- Viewider,
14
- Divider,
15
- } from '../styledModalComponents';
16
- import { Pressable, View, Text, ScrollView, Alert, Linking } from 'react-native';
17
- import ModalHeaderComponent from '../ModalHeaderComponent';
20
+ import { ModalContainerFullScreen } from '../styledModalComponents';
18
21
  import { ProfileImagePlaceholder } from '../../MainComponents/ProfileImagePlaceholder';
19
- import { useDispatch, useSelector } from 'react-redux';
20
22
  import { RootState, getActiveRoom } from '../../../roomStore';
21
23
  import { uploadFile } from '../../../networking/api-requests/auth.api';
22
24
  import { useXmppClient } from '../../../context/xmppProvider';
23
- import { updateRoom } from '../../../roomStore/roomsSlice';
24
- import * as ImagePicker from 'expo-image-picker';
25
+ import { deleteRoom, updateRoom } from '../../../roomStore/roomsSlice';
25
26
  import Loader from '../../styled/Loader';
26
- import Button from '../../styled/Button';
27
- import { DeleteIcon, MoreIcon, QrIcon } from '../../../assets/icons';
28
- import Switch from '../../MainComponents/Switch';
27
+ import {
28
+ AddNewIcon,
29
+ DeleteIcon,
30
+ EditIcon,
31
+ LeaveIcon,
32
+ ReportIcon,
33
+ SearchIcon,
34
+ } from '../../../assets/icons';
29
35
  import { useChatSettingState } from '../../../hooks/useChatSettingState';
30
36
  import { chatTextStyle } from '../../../helpers/typography';
31
37
  import { getElementFont } from '../../../helpers/getElementFont';
38
+ import { getIconColor } from '../../../helpers/getIconColor';
32
39
  import { deleteRoomMember } from '../../../networking/api-requests/rooms.api';
33
40
  import { RoomMember } from '../../../types/models/room.model';
34
41
  import { setActiveModal, setSelectedUser } from '../../../roomStore/chatSettingsSlice';
35
42
  import { MODAL_TYPES } from '../../../helpers/constants/MODAL_TYPES';
36
- import DropdownMenu, { MenuOption } from '../../DropdownMenu/DropdownMenu';
37
43
  import SelectUsersModal from '../SelectUsersModal/SelectUsersModal';
38
44
  import { useToast } from '../../../context/ToastContext';
45
+ import { useT } from '../../../i18n/useT';
39
46
  import DeleteChatModal from './DeleteChatModal';
47
+ import ReportChatModal from './ReportChatModal';
48
+ import {
49
+ ProfileHero,
50
+ ProfileTopBar,
51
+ HeroAction,
52
+ useHeaderMetrics,
53
+ } from '../ProfileHeader/ProfileHeader';
54
+ import ProfileMenu, { ProfileMenuItem } from '../ProfileHeader/ProfileMenu';
40
55
 
56
+ /**
57
+ * Where the header should sit when search opens: tucked away at the
58
+ * collapsed offset when the picture is still expanded, and left exactly
59
+ * where it is when the user already scrolled it away.
60
+ */
61
+ export const searchScrollTarget = (
62
+ isCollapsed: boolean,
63
+ collapseDistance: number
64
+ ): number | null => (isCollapsed ? null : collapseDistance);
65
+
66
+ /** Two uppercase letters for a chat without a picture, same rule the
67
+ * avatar placeholder uses for people. */
68
+ export const chatInitials = (name?: string | null): string => {
69
+ const words = (name || '').trim().split(/\s+/).filter(Boolean);
70
+ const letters = (words.length >= 2
71
+ ? [words[0][0], words[1][0]]
72
+ : [words[0]?.[0], words[0]?.[1]]
73
+ ).filter((c) => !!c && /[\p{L}\p{N}]/u.test(c));
74
+ return letters.join('').toUpperCase();
75
+ };
41
76
 
42
77
  interface ChatProfileModalProps {
43
78
  handleCloseModal: any;
@@ -46,33 +81,38 @@ interface ChatProfileModalProps {
46
81
  const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
47
82
  handleCloseModal,
48
83
  }) => {
84
+ const t = useT();
49
85
  const [loading, setLoading] = useState<boolean>(false);
50
- const [visible, setVisible] = useState<boolean>(false);
51
- const [isModalOpen, setIsModalOpen] = useState(false);
86
+ const [isDeleteOpen, setIsDeleteOpen] = useState(false);
87
+ const [isReportOpen, setIsReportOpen] = useState(false);
88
+ const [memberQuery, setMemberQuery] = useState('');
89
+ const [isSearchOpen, setIsSearchOpen] = useState(false);
90
+ // Measured, not guessed: this screen renders inside whatever frame the
91
+ // host gives it (the demo app keeps its own tab bar above), so the
92
+ // window height would overshoot.
93
+ const [viewportHeight, setViewportHeight] = useState(0);
52
94
 
53
95
  const { showToast } = useToast();
54
-
55
- const chatMenuOptions = useMemo(
56
- () => [
57
- {
58
- label: 'Delete chat',
59
- icon: <DeleteIcon />,
60
- onClick: () => {
61
- setIsModalOpen(true);
62
- },
63
- styles: { color: 'red' },
64
- },
65
- ],
66
- []
67
- );
68
-
69
96
  const dispatch = useDispatch();
70
-
71
97
  const { client } = useXmppClient();
72
98
  const { user: stateUser, config } = useChatSettingState();
73
99
  const activeRoom = useSelector((state: RootState) => getActiveRoom(state));
74
100
  const fileToken = useFileToken();
75
101
 
102
+ // Drives both the hero's parallax and the collapsed bar's fade-in. One
103
+ // shared value, written natively by the ScrollView.
104
+ const scrollY = useRef(new Animated.Value(0)).current;
105
+ const scrollRef = useRef<any>(null);
106
+ const searchInputRef = useRef<TextInput>(null);
107
+ const { collapseDistance } = useHeaderMetrics();
108
+ // `scrollY` is driven natively, so its value can't be read back with
109
+ // addListener. The Animated.event's `listener` still fires on the JS
110
+ // side, which is enough to know which state the header is in.
111
+ const [isCollapsed, setIsCollapsed] = useState(false);
112
+ // Set when search collapsed the header itself, so only then does
113
+ // dismissing search (or the keyboard) expand it again.
114
+ const collapsedForSearchRef = useRef(false);
115
+
76
116
  // Pull live member list from the MUC when the modal opens — REST
77
117
  // hydration only fills the *count* (item.participants /
78
118
  // item.members.length); the actual roster comes from a `getRoomMembers`
@@ -87,6 +127,12 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
87
127
  }
88
128
  }, [client, activeRoom?.jid]);
89
129
 
130
+ useEffect(() => {
131
+ if (!activeRoom) {
132
+ dispatch(setActiveModal(undefined));
133
+ }
134
+ }, [activeRoom, dispatch]);
135
+
90
136
  const onUpload = async () => {
91
137
  let loadingSet = false;
92
138
  try {
@@ -103,7 +149,7 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
103
149
  'Photo library permission is needed to select images.',
104
150
  [
105
151
  {
106
- text: 'Cancel',
152
+ text: t('action.cancel'),
107
153
  onPress: () => setLoading(false),
108
154
  style: 'cancel',
109
155
  },
@@ -172,7 +218,13 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
172
218
  }
173
219
  };
174
220
 
175
- const handleDeleteUser = async (userId: string) => {
221
+ const onRemoveIcon = () => {
222
+ client?.setRoomImageStanza(activeRoom?.jid || '', '', 'icon', 'none');
223
+ dispatch(updateRoom({ jid: activeRoom?.jid || '', updates: { icon: null } }));
224
+ };
225
+
226
+ const handleDeleteUser = async (user: RoomMember) => {
227
+ const userId = user.xmppUsername;
176
228
  try {
177
229
  await deleteRoomMember({
178
230
  roomId: activeRoom?.jid?.split('@')[0] || '',
@@ -184,7 +236,7 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
184
236
  jid: activeRoom?.jid || '',
185
237
  updates: {
186
238
  roomMembers: activeRoom?.roomMembers?.filter(
187
- (user) => user.xmppUsername !== userId
239
+ (member) => member.xmppUsername !== userId
188
240
  ),
189
241
  },
190
242
  })
@@ -207,9 +259,19 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
207
259
  }
208
260
  };
209
261
 
210
- const onRemoveClick = async () => {
211
- client?.setRoomImageStanza(activeRoom?.jid || '', '', 'icon', 'none');
212
- dispatch(updateRoom({ jid: activeRoom?.jid || '', updates: { icon: null } }));
262
+ const confirmRemoveMember = (user: RoomMember) => {
263
+ Alert.alert(
264
+ t('modal.chatProfile.removeMember'),
265
+ `${user.firstName} ${user.lastName}`.trim(),
266
+ [
267
+ { text: t('action.cancel'), style: 'cancel' },
268
+ {
269
+ text: t('action.delete'),
270
+ style: 'destructive',
271
+ onPress: () => handleDeleteUser(user),
272
+ },
273
+ ]
274
+ );
213
275
  };
214
276
 
215
277
  const handleUserAvatarClick = (user: RoomMember): void => {
@@ -227,270 +289,527 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
227
289
  );
228
290
  };
229
291
 
230
- const menuOptions = useMemo(
231
- () => (userId: string) => [
292
+ // Leaving is XMPP-side (an unavailable presence to the MUC); dropping the
293
+ // room from the store is what actually takes the user out of the chat in
294
+ // the UI, and the `!activeRoom` effect above then closes this modal.
295
+ const handleLeave = () => {
296
+ Alert.alert(t('modal.leaveChat.title'), t('modal.leaveChat.description'), [
297
+ { text: t('action.cancel'), style: 'cancel' },
232
298
  {
233
- label: 'Appoint as an admin',
234
- icon: null,
235
- onClick: () => {
236
- dispatch(setActiveModal(MODAL_TYPES.PROFILE));
237
- console.log('Profile clicked');
299
+ text: t('action.leave'),
300
+ style: 'destructive',
301
+ onPress: () => {
302
+ try {
303
+ client?.leaveTheRoomStanza?.(activeRoom?.jid || '');
304
+ } catch (error) {
305
+ console.error('Failed to leave the room:', error);
306
+ }
307
+ dispatch(deleteRoom({ jid: activeRoom?.jid || '' }));
308
+ handleCloseModal?.();
238
309
  },
239
310
  },
311
+ ]);
312
+ };
313
+
314
+ const isModerator = activeRoom?.role === 'moderator';
315
+ const canEditIcon =
316
+ !config?.disableChatInfo?.disableIconEdit && activeRoom?.role !== 'participant';
317
+ const canAddMembers = isModerator && activeRoom?.type === 'group';
318
+ const canDeleteChat = isModerator && activeRoom?.type !== 'private';
319
+ const hasIcon = !!activeRoom?.icon;
320
+
321
+ const scrollHeaderTo = (y: number) =>
322
+ scrollRef.current?.scrollTo?.({ y, animated: true });
323
+
324
+ // Opening search from an expanded header tucks the picture away so the
325
+ // results and the keyboard have room; from an already-collapsed header
326
+ // it just opens the field. Whatever it collapsed, it puts back.
327
+ const openMemberSearch = () => {
328
+ const target = searchScrollTarget(isCollapsed, collapseDistance);
329
+ if (target !== null) {
330
+ collapsedForSearchRef.current = true;
331
+ scrollHeaderTo(target);
332
+ }
333
+ setIsSearchOpen(true);
334
+ setTimeout(() => searchInputRef.current?.focus(), 50);
335
+ };
336
+
337
+ const restoreHeader = () => {
338
+ if (!collapsedForSearchRef.current) {return;}
339
+ collapsedForSearchRef.current = false;
340
+ scrollHeaderTo(0);
341
+ };
342
+
343
+ const closeMemberSearch = () => {
344
+ setMemberQuery('');
345
+ setIsSearchOpen(false);
346
+ Keyboard.dismiss();
347
+ restoreHeader();
348
+ };
349
+
350
+ // Putting the keyboard away is the other way out of search, so it
351
+ // restores the header too — the field stays, results and all.
352
+ useEffect(() => {
353
+ if (!isSearchOpen) {return undefined;}
354
+ const sub = Keyboard.addListener('keyboardDidHide', restoreHeader);
355
+ return () => sub.remove();
356
+ }, [isSearchOpen]); // eslint-disable-line react-hooks/exhaustive-deps
357
+
358
+ const heroActions: HeroAction[] = useMemo(
359
+ () => [
240
360
  {
241
- label: 'Unban',
242
- icon: null,
243
- onClick: () => {
244
- dispatch(setActiveModal(MODAL_TYPES.SETTINGS));
245
- console.log('Settings clicked');
246
- },
361
+ key: 'search',
362
+ label: t('action.search'),
363
+ icon: (color: string) => <SearchIcon color={color} />,
364
+ onPress: openMemberSearch,
247
365
  },
248
366
  {
249
- label: 'Delete',
250
- icon: null,
251
- onClick: (e: any) => {
252
- e?.preventDefault();
253
- handleDeleteUser(userId);
254
- },
367
+ key: 'leave',
368
+ label: t('action.leave'),
369
+ icon: (color: string) => <LeaveIcon color={color} />,
370
+ onPress: handleLeave,
371
+ },
372
+ {
373
+ key: 'report',
374
+ label: t('action.report'),
375
+ icon: (color: string) => <ReportIcon color={color} />,
376
+ onPress: () => setIsReportOpen(true),
255
377
  },
256
378
  ],
257
- []
379
+ [t, activeRoom?.jid] // eslint-disable-line react-hooks/exhaustive-deps
258
380
  );
259
381
 
260
- useEffect(() => {
261
- if (!activeRoom) {
262
- dispatch(setActiveModal(undefined));
382
+ // "…" menu. Edit is the chat picture — the only chat detail this SDK can
383
+ // actually change (there is no update-chat endpoint for title/description),
384
+ // so it is only offered when the picture is editable.
385
+ const menuItems: ProfileMenuItem[] = useMemo(() => {
386
+ const items: ProfileMenuItem[] = [];
387
+ if (canEditIcon) {
388
+ items.push({
389
+ key: 'edit',
390
+ label: t('action.edit'),
391
+ icon: <EditIcon color="#141414" width={18} height={18} />,
392
+ onPress: onUpload,
393
+ });
394
+ if (hasIcon) {
395
+ items.push({
396
+ key: 'remove-photo',
397
+ label: t('modal.chatProfile.removePhoto'),
398
+ icon: <DeleteIcon width={18} height={18} />,
399
+ onPress: onRemoveIcon,
400
+ });
401
+ }
263
402
  }
264
- }, [activeRoom, dispatch]);
403
+ // The hero's Search button is off-screen once collapsed, so the menu
404
+ // carries it — the design's collapsed menu does the same.
405
+ if (isCollapsed) {
406
+ items.push({
407
+ key: 'search',
408
+ label: t('action.search'),
409
+ icon: <SearchIcon color="#141414" width={18} height={18} />,
410
+ onPress: openMemberSearch,
411
+ });
412
+ }
413
+ items.push({
414
+ key: 'report',
415
+ label: t('action.report'),
416
+ icon: <ReportIcon color="#141414" width={18} height={18} />,
417
+ onPress: () => setIsReportOpen(true),
418
+ });
419
+ if (canDeleteChat) {
420
+ items.push({
421
+ key: 'delete-and-leave',
422
+ label: t('action.deleteAndLeave'),
423
+ icon: <DeleteIcon width={18} height={18} />,
424
+ destructive: true,
425
+ onPress: () => setIsDeleteOpen(true),
426
+ });
427
+ }
428
+ return items;
429
+ }, [canEditIcon, canDeleteChat, hasIcon, isCollapsed, t]); // eslint-disable-line react-hooks/exhaustive-deps
265
430
 
266
431
  if (!activeRoom) {
267
432
  return null;
268
433
  }
269
434
 
435
+ const title = activeRoom.title || activeRoom.name;
436
+ const memberCount = activeRoom.usersCnt ?? 0;
437
+ const subtitle = t(
438
+ memberCount === 1
439
+ ? 'modal.chatProfile.memberCountSingular'
440
+ : 'modal.chatProfile.memberCountPlural',
441
+ { count: memberCount }
442
+ );
443
+ const heroImage = appendFileToken(activeRoom.icon, fileToken) || null;
444
+ // A single flat colour behind the initials — the config's avatar colour
445
+ // when set, otherwise the primary. (The old per-name hash colour is what
446
+ // made the header read as two different greens next to the members' own
447
+ // avatars.)
448
+ const heroColor = config?.colors?.avatar || getIconColor(config);
449
+ const members = activeRoom.roomMembers ?? [];
450
+ const query = memberQuery.trim().toLowerCase();
451
+ const visibleMembers = query
452
+ ? members.filter((user) =>
453
+ `${user.firstName ?? ''} ${user.lastName ?? ''}`
454
+ .toLowerCase()
455
+ .includes(query)
456
+ )
457
+ : members;
458
+
270
459
  return (
271
- <ModalContainerFullScreen style={{ position: 'relative' }}>
272
- <ModalHeaderComponent
273
- handleCloseModal={handleCloseModal}
274
- headerTitle={'Chat Profile'}
275
- titleStyle={chatTextStyle(config?.typography?.profile?.screenTitle)}
276
- rightMenu={
277
- <>
278
- {activeRoom?.type === 'public' && (
279
- <Button EndIcon={<QrIcon />} onPress={() => setVisible(true)} />
280
- )}
281
- {activeRoom.role === 'moderator' &&
282
- activeRoom.type !== 'private' && (
283
- <DropdownMenu
284
- position="right"
285
- options={chatMenuOptions}
286
- openButton={
287
- <Button
288
- style={{ padding: 8, height: 40 }}
289
- EndIcon={<MoreIcon />}
290
- unstyled
291
- />
292
- }
293
- />
294
- )}
295
- </>
296
- }
297
- />
298
- <ScrollView
299
- style={{ width: '100%', flex: 1 }}
300
- contentContainerStyle={{ flexGrow: 1 }}
301
- showsVerticalScrollIndicator={true}
460
+ <ModalContainerFullScreen style={styles.screen}>
461
+ <Animated.ScrollView
462
+ ref={scrollRef}
463
+ style={styles.scroll}
464
+ // The collapsed state has to be reachable even when the member list
465
+ // is short: without this the content was shorter than the viewport,
466
+ // so the scroll had nowhere to go and simply bounced back.
467
+ contentContainerStyle={[
468
+ styles.scrollContent,
469
+ viewportHeight > 0 && {
470
+ minHeight: viewportHeight + collapseDistance,
471
+ },
472
+ ]}
473
+ onLayout={(e) => setViewportHeight(e.nativeEvent.layout.height)}
474
+ showsVerticalScrollIndicator={false}
475
+ keyboardShouldPersistTaps="handled"
476
+ scrollEventThrottle={16}
477
+ onScroll={Animated.event(
478
+ [{ nativeEvent: { contentOffset: { y: scrollY } } }],
479
+ {
480
+ useNativeDriver: true,
481
+ listener: (e: any) => {
482
+ const y = e?.nativeEvent?.contentOffset?.y ?? 0;
483
+ setIsCollapsed(y >= collapseDistance - 8);
484
+ },
485
+ }
486
+ )}
302
487
  >
303
- <CenterContainer>
304
- <ProfileImagePlaceholder
305
- name={activeRoom.title || activeRoom.name}
306
- icon={appendFileToken(activeRoom.icon, fileToken)}
307
- // Two gates control whether the icon is editable:
308
- // 1. role — only non-participants could edit before.
309
- // 2. NEW: `disableChatInfo.disableIconEdit` — hard kill from
310
- // config so a consumer can make the icon read-only for
311
- // everyone (e.g. branded rooms, patient-facing apps).
312
- // Both must allow it for the picker/remove affordances to show.
313
- upload={{
314
- onUpload,
315
- active:
316
- !config?.disableChatInfo?.disableIconEdit &&
317
- activeRoom?.role !== 'participant',
318
- }}
319
- remove={{
320
- enabled: !config?.disableChatInfo?.disableIconEdit,
321
- onRemoveClick,
322
- }}
323
- role={activeRoom?.role}
324
- size={128}
488
+ <ProfileHero
489
+ title={title}
490
+ subtitle={subtitle}
491
+ imageUri={heroImage}
492
+ fallbackColor={heroColor}
493
+ initials={chatInitials(title)}
494
+ scrollY={scrollY}
495
+ actions={heroActions}
496
+ titleStyle={chatTextStyle(config?.typography?.profile?.title)}
497
+ subtitleStyle={getElementFont(config, 'profileStatus')}
325
498
  />
326
- <UserInfo>
327
- <UserName
328
- fontSize={config?.typography?.profile?.title?.fontSize}
329
- fontWeight={config?.typography?.profile?.title?.fontWeight as any}
330
- >
331
- {activeRoom.title || activeRoom.name}
332
- </UserName>
333
- <UserStatus style={getElementFont(config, 'profileStatus')}>
334
- {activeRoom.usersCnt}{' '}
335
- {activeRoom.usersCnt > 1 ? 'members' : 'member'}
336
- </UserStatus>
337
- </UserInfo>
338
- {activeRoom.role === 'moderator' && activeRoom.type === 'group' && (
339
- <>
340
- {/* <AddMembersModal /> */}
341
- <SelectUsersModal />
342
- </>
343
- )}
344
- {!config?.disableChatInfo?.disableDescription && (
345
- <BorderedContainer>
346
- <LabelData style={getElementFont(config, 'profileSectionLabel')}>
347
- Description
348
- </LabelData>
349
- <Label>{activeRoom?.description || '—'}</Label>
350
- </BorderedContainer>
351
- )}
352
- {!config?.disableChatInfo?.disableType && (
353
- <BorderedContainer>
354
- <LabelData style={getElementFont(config, 'profileSectionLabel')}>
355
- Chat type
356
- </LabelData>
357
- <Label>{activeRoom.type || '—'}</Label>
358
- </BorderedContainer>
359
- )}
360
- {/* <BorderedContainer
361
- style={{
362
- justifyContent: 'space-between',
363
- flexDirection: 'row',
364
- alignItems: 'center',
365
- }}
366
- >
367
- <Label>Notifications</Label>
368
- <Label>
369
- <Switch
370
- onToggle={function (isOn: boolean): void {
371
- throw new Error('Function not implemented.');
372
- }}
373
- bgColor={config?.colors?.primary}
374
- />
375
- </Label>
376
- </BorderedContainer> */}
377
- {!config?.disableChatInfo?.hideMembers && (
378
- <BorderedContainer style={{ paddingHorizontal: 16, paddingVertical: 8 }}>
379
- {loading ? (
380
- <Loader />
381
- ) : (activeRoom?.roomMembers?.length ?? 0) === 0 ? (
382
- <Label style={{ opacity: 0.6, textAlign: 'center', paddingVertical: 8 }}>
383
- Member list unavailable
384
- </Label>
385
- ) : (
386
- <View>
387
- {activeRoom?.roomMembers?.map((user, index) => (
388
- <View
389
- key={user.xmppUsername}
390
- style={{
391
- flexDirection: 'column',
392
- alignItems: 'flex-start',
393
- }}
394
- >
395
- <View
396
- style={{
397
- flexDirection: 'row',
398
- justifyContent: 'flex-start',
399
- paddingHorizontal: 8,
400
- paddingVertical: 0,
401
- alignItems: 'center',
402
- width: '100%',
403
- }}
499
+
500
+ <View style={styles.body}>
501
+ {!config?.disableChatInfo?.disableDescription && (
502
+ <View style={styles.card}>
503
+ <Text style={[styles.cardLabel, getElementFont(config, 'profileSectionLabel')]}>
504
+ {t('modal.chatProfile.description')}
505
+ </Text>
506
+ <Text style={styles.cardValue}>{activeRoom.description || '—'}</Text>
507
+ </View>
508
+ )}
509
+
510
+ {!config?.disableChatInfo?.disableType && (
511
+ <View style={styles.card}>
512
+ <Text style={[styles.cardLabel, getElementFont(config, 'profileSectionLabel')]}>
513
+ {t('modal.chatProfile.chatType')}
514
+ </Text>
515
+ <Text style={styles.cardValue}>{activeRoom.type || '—'}</Text>
516
+ </View>
517
+ )}
518
+
519
+ {!config?.disableChatInfo?.hideMembers && (
520
+ <View style={styles.card}>
521
+ {canAddMembers && (
522
+ <SelectUsersModal
523
+ trigger={(open) => (
524
+ <TouchableOpacity
525
+ testID="chat-profile-add-members"
526
+ activeOpacity={0.7}
527
+ style={styles.addMembersRow}
528
+ onPress={open}
529
+ >
530
+ <AddNewIcon color={getIconColor(config)} />
531
+ <Text style={styles.addMembersLabel}>
532
+ {t('modal.chatProfile.addMembers')}
533
+ </Text>
534
+ </TouchableOpacity>
535
+ )}
536
+ />
537
+ )}
538
+
539
+ <Text
540
+ style={[
541
+ styles.cardLabel,
542
+ getElementFont(config, 'profileSectionLabel'),
543
+ ]}
544
+ >
545
+ {t('modal.chatProfile.membersLabel', { count: memberCount })}
546
+ </Text>
547
+
548
+ {isSearchOpen && (
549
+ <View style={styles.searchRow}>
550
+ <SearchIcon color="#8C8C8C" width={18} height={18} />
551
+ <TextInput
552
+ testID="chat-profile-member-search"
553
+ ref={searchInputRef}
554
+ style={styles.searchInput}
555
+ value={memberQuery}
556
+ onChangeText={setMemberQuery}
557
+ placeholder={t('search.members')}
558
+ placeholderTextColor="#8C8C8C"
559
+ autoCorrect={false}
560
+ returnKeyType="search"
561
+ />
562
+ <TouchableOpacity
563
+ testID="chat-profile-search-close"
564
+ activeOpacity={0.7}
565
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
566
+ onPress={closeMemberSearch}
404
567
  >
405
- <Pressable
406
- style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', gap: 16 }}
407
- // `disableChatInfo.disableMemberTap` blocks the
408
- // tap entirely so the user-profile popup never
409
- // opens. `disableMemberProfileActions` only hid
410
- // the action buttons INSIDE that popup, leaving
411
- // the popup itself to open on tap — this gate
412
- // closes the door. Customer-reported #16.
413
- onPress={
414
- config?.disableChatInfo?.disableMemberTap
415
- ? undefined
416
- : () => handleUserAvatarClick(user)
417
- }
418
- disabled={!!config?.disableChatInfo?.disableMemberTap}
568
+ <Text style={styles.searchClose}>×</Text>
569
+ </TouchableOpacity>
570
+ </View>
571
+ )}
572
+
573
+ {loading ? (
574
+ <Loader />
575
+ ) : visibleMembers.length === 0 ? (
576
+ <Text testID="chat-profile-members-empty" style={styles.emptyMembers}>
577
+ {members.length === 0
578
+ ? t('modal.chatProfile.membersUnavailable')
579
+ : t('search.noResults')}
580
+ </Text>
581
+ ) : (
582
+ visibleMembers.map((user, index) => {
583
+ const banned = user.ban_status === 'banned';
584
+ const roleLabel =
585
+ user.role && user.role !== 'none' && user.role !== 'participant'
586
+ ? user.role
587
+ : null;
588
+ return (
589
+ <View
590
+ key={user.xmppUsername}
591
+ style={[styles.memberRow, index > 0 && styles.memberDivider]}
419
592
  >
420
- <ProfileImagePlaceholder
421
- name={`${user.firstName} ${user.lastName}`}
422
- size={40}
423
- />
424
- <View
425
- style={{
426
- flexDirection: 'column',
427
- gap: 2,
428
- alignItems: 'flex-start',
429
- justifyContent: 'center',
430
- }}
593
+ <Pressable
594
+ style={styles.memberMain}
595
+ // `disableChatInfo.disableMemberTap` blocks the tap
596
+ // entirely so the user-profile popup never opens.
597
+ // `disableMemberProfileActions` only hid the action
598
+ // buttons INSIDE that popup, leaving the popup itself
599
+ // to open on tap — this gate closes the door.
600
+ // Customer-reported #16.
601
+ onPress={
602
+ config?.disableChatInfo?.disableMemberTap
603
+ ? undefined
604
+ : () => handleUserAvatarClick(user)
605
+ }
606
+ disabled={!!config?.disableChatInfo?.disableMemberTap}
431
607
  >
432
- <Label
433
- style={[
434
- { fontSize: 16, fontWeight: 600 },
435
- chatTextStyle(config?.typography?.profile?.memberName),
436
- ]}
437
- >
438
- {user.firstName} {user.lastName}
439
- </Label>
440
- {user.last_active && (
441
- <LabelData style={getElementFont(config, 'profileSectionLabel')}>
442
- {new Date(user.last_active * 1000).toLocaleString()}
443
- </LabelData>
444
- )}
445
- </View>
446
- </Pressable>
447
- {user.role && user.role !== 'none' && (
448
- <Text
449
- style={{
450
- backgroundColor:
451
- user.ban_status !== 'banned' ? '#F3F6FC' : '#FFEBEE',
452
- color:
453
- user.ban_status !== 'banned' ? '#0052CD' : '#F44336',
454
- padding: 5,
455
- borderRadius: 16,
456
- fontSize: 12,
457
- }}
458
- >
459
- {user.role}
460
- </Text>
461
- )}
462
- {stateUser.xmppUsername !== user.xmppUsername &&
463
- activeRoom.role === 'moderator' &&
464
- activeRoom.type !== 'private' && (
465
- <DropdownMenu
466
- options={menuOptions(user.xmppUsername) as MenuOption[]}
467
- openButton={
468
- <Button
469
- onPress={() => {}}
470
- >
471
- <Text>More Options</Text>
472
- </Button>
473
- }
474
- onClose={() => console.log('Dropdown closed')}
608
+ <ProfileImagePlaceholder
609
+ name={`${user.firstName} ${user.lastName}`}
610
+ size={40}
475
611
  />
612
+ <View style={styles.memberText}>
613
+ <Text
614
+ numberOfLines={1}
615
+ style={[
616
+ styles.memberName,
617
+ chatTextStyle(config?.typography?.profile?.memberName),
618
+ ]}
619
+ >
620
+ {user.firstName} {user.lastName}
621
+ </Text>
622
+ {!!user.last_active && (
623
+ <Text
624
+ numberOfLines={1}
625
+ style={[
626
+ styles.memberStatus,
627
+ getElementFont(config, 'profileSectionLabel'),
628
+ ]}
629
+ >
630
+ {new Date(user.last_active * 1000).toLocaleString()}
631
+ </Text>
632
+ )}
633
+ </View>
634
+ </Pressable>
635
+
636
+ {(banned || roleLabel) && (
637
+ <Text
638
+ style={[styles.memberRole, banned && styles.memberBanned]}
639
+ >
640
+ {banned ? t('status.banned') : roleLabel}
641
+ </Text>
476
642
  )}
477
- </View>
478
- {index < (activeRoom?.roomMembers?.length || 0) - 1 && <Divider />}
479
- </View>
480
- ))}
643
+
644
+ {isModerator &&
645
+ activeRoom.type !== 'private' &&
646
+ stateUser.xmppUsername !== user.xmppUsername && (
647
+ <TouchableOpacity
648
+ testID={`chat-profile-remove-${user.xmppUsername}`}
649
+ activeOpacity={0.7}
650
+ hitSlop={{ top: 8, bottom: 8, left: 8, right: 8 }}
651
+ style={styles.memberRemove}
652
+ onPress={() => confirmRemoveMember(user)}
653
+ >
654
+ <DeleteIcon width={20} height={20} />
655
+ </TouchableOpacity>
656
+ )}
657
+ </View>
658
+ );
659
+ })
660
+ )}
481
661
  </View>
482
662
  )}
483
- </BorderedContainer>
484
- )}
485
- </CenterContainer>
486
- </ScrollView>
663
+ </View>
664
+ </Animated.ScrollView>
665
+
666
+ <ProfileTopBar
667
+ title={title}
668
+ subtitle={subtitle}
669
+ imageUri={heroImage}
670
+ fallbackColor={heroColor}
671
+ initials={chatInitials(title)}
672
+ scrollY={scrollY}
673
+ onBack={handleCloseModal}
674
+ titleStyle={chatTextStyle(config?.typography?.profile?.screenTitle)}
675
+ menu={<ProfileMenu items={menuItems} />}
676
+ />
487
677
 
488
678
  <DeleteChatModal
489
- isModalOpen={isModalOpen}
490
- setIsModalOpen={setIsModalOpen}
679
+ isModalOpen={isDeleteOpen}
680
+ setIsModalOpen={setIsDeleteOpen}
681
+ />
682
+ <ReportChatModal
683
+ visible={isReportOpen}
684
+ onClose={() => setIsReportOpen(false)}
491
685
  />
492
686
  </ModalContainerFullScreen>
493
687
  );
494
688
  };
495
689
 
690
+ const styles = StyleSheet.create({
691
+ screen: {
692
+ position: 'relative',
693
+ backgroundColor: '#F2F3F5',
694
+ },
695
+ scroll: {
696
+ width: '100%',
697
+ flex: 1,
698
+ },
699
+ scrollContent: {
700
+ paddingBottom: 32,
701
+ },
702
+ body: {
703
+ padding: 12,
704
+ gap: 12,
705
+ },
706
+ card: {
707
+ backgroundColor: '#FFFFFF',
708
+ borderRadius: 14,
709
+ padding: 16,
710
+ shadowColor: '#000',
711
+ shadowOffset: { width: 0, height: 1 },
712
+ shadowOpacity: 0.05,
713
+ shadowRadius: 4,
714
+ elevation: 1,
715
+ },
716
+ cardLabel: {
717
+ color: '#8C8C8C',
718
+ fontSize: 14,
719
+ },
720
+ cardValue: {
721
+ color: '#141414',
722
+ fontSize: 16,
723
+ marginTop: 4,
724
+ },
725
+ addMembersRow: {
726
+ flexDirection: 'row',
727
+ alignItems: 'center',
728
+ gap: 12,
729
+ paddingBottom: 14,
730
+ marginBottom: 10,
731
+ borderBottomWidth: StyleSheet.hairlineWidth,
732
+ borderBottomColor: '#EFEFF2',
733
+ },
734
+ addMembersLabel: {
735
+ fontSize: 16,
736
+ fontWeight: '500',
737
+ color: '#141414',
738
+ },
739
+ searchRow: {
740
+ flexDirection: 'row',
741
+ alignItems: 'center',
742
+ gap: 8,
743
+ marginTop: 10,
744
+ paddingHorizontal: 12,
745
+ height: 40,
746
+ borderRadius: 12,
747
+ backgroundColor: '#F2F3F5',
748
+ },
749
+ searchInput: {
750
+ flex: 1,
751
+ fontSize: 15,
752
+ color: '#141414',
753
+ padding: 0,
754
+ },
755
+ searchClose: {
756
+ fontSize: 22,
757
+ lineHeight: 24,
758
+ color: '#8C8C8C',
759
+ },
760
+ emptyMembers: {
761
+ opacity: 0.6,
762
+ textAlign: 'center',
763
+ paddingVertical: 8,
764
+ color: '#141414',
765
+ },
766
+ memberRow: {
767
+ flexDirection: 'row',
768
+ alignItems: 'center',
769
+ paddingVertical: 10,
770
+ },
771
+ memberDivider: {
772
+ borderTopWidth: StyleSheet.hairlineWidth,
773
+ borderTopColor: '#EFEFF2',
774
+ },
775
+ memberMain: {
776
+ flex: 1,
777
+ flexDirection: 'row',
778
+ alignItems: 'center',
779
+ gap: 12,
780
+ },
781
+ memberText: {
782
+ flex: 1,
783
+ },
784
+ memberName: {
785
+ fontSize: 16,
786
+ fontWeight: '600',
787
+ color: '#141414',
788
+ },
789
+ memberStatus: {
790
+ fontSize: 13,
791
+ color: '#8C8C8C',
792
+ marginTop: 2,
793
+ },
794
+ memberRole: {
795
+ fontSize: 13,
796
+ color: '#8C8C8C',
797
+ marginLeft: 8,
798
+ },
799
+ memberBanned: {
800
+ color: '#E53935',
801
+ },
802
+ memberRemove: {
803
+ marginLeft: 12,
804
+ },
805
+ menuButton: {
806
+ width: 40,
807
+ height: 40,
808
+ borderRadius: 20,
809
+ backgroundColor: 'rgba(0,0,0,0.28)',
810
+ alignItems: 'center',
811
+ justifyContent: 'center',
812
+ },
813
+ });
814
+
496
815
  export default ChatProfileModal;