@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,48 +1,98 @@
1
- import React, { useCallback, useEffect, useMemo, useState } from 'react';
1
+ /** @format */
2
+
3
+ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
4
+ import {
5
+ ActivityIndicator,
6
+ Animated,
7
+ Image,
8
+ Share,
9
+ StyleSheet,
10
+ Text,
11
+ TouchableOpacity,
12
+ View,
13
+ } from 'react-native';
14
+ import { ModalContainerFullScreen } from '../styledModalComponents';
2
15
  import {
3
- CenterContainer,
4
- UserInfo,
5
- UserName,
6
- UserStatus,
7
- ModalContainerFullScreen,
8
- ActionButton,
9
- Label,
10
- BorderedContainer,
11
- LabelData,
12
- ModalBackground,
13
- ModalContainer,
14
- CloseButton,
15
- ModalTitle,
16
- GroupContainer,
17
- } from '../styledModalComponents';
18
- import { ChatIcon, DeleteIcon, DownloadIcon, EditIcon, IconDoc, LeaveIcon, MoreIcon } from '../../../assets/icons';
19
- import ModalHeaderComponent from '../ModalHeaderComponent';
20
- import { useSelector } from 'react-redux';
21
- import { RootState } from '../../../roomStore';
22
- import { ProfileImagePlaceholder } from '../../MainComponents/ProfileImagePlaceholder';
23
- import Button from '../../styled/Button';
24
- import DropdownMenu from '../../DropdownMenu/DropdownMenu';
16
+ ChatIcon,
17
+ EditIcon,
18
+ IconDoc,
19
+ LeaveIcon,
20
+ LogoutIcon,
21
+ ProfileIcon,
22
+ ShareIcon,
23
+ } from '../../../assets/icons';
24
+ import { useAppDispatch } from '../../../hooks/hooks';
25
+ import { useChatSettingState } from '../../../hooks/useChatSettingState';
26
+ import { useXmppClient } from '../../../context/xmppProvider';
27
+ import { useToast } from '../../../context/ToastContext';
28
+ import { useT } from '../../../i18n/useT';
29
+ import { useFileToken } from '../../../hooks/useFileToken';
30
+ import { appendFileToken } from '../../../helpers/secureFileUrl';
31
+ import { getIconColor } from '../../../helpers/getIconColor';
32
+ import { chatTextStyle } from '../../../helpers/typography';
33
+ import { getElementFont } from '../../../helpers/getElementFont';
34
+ import { LANGUAGE_OPTIONS } from '../../../helpers/constants/LANGUAGE_OPTIONS';
35
+ import { MODAL_TYPES } from '../../../helpers/constants/MODAL_TYPES';
25
36
  import {
26
- logout,
37
+ setActiveFile,
27
38
  setActiveModal,
28
39
  setLangSource,
29
40
  setSelectedUser,
30
41
  } from '../../../roomStore/chatSettingsSlice';
31
- import { addRoomViaApi, setCurrentRoom, setLogoutState } from '../../../roomStore/roomsSlice';
32
- import EditUserModal from './EditUserModal';
33
- import { walletToUsername } from '../../../helpers/walletUsername';
34
- import { useXmppClient } from '../../../context/xmppProvider';
35
- import Loader from '../../styled/Loader';
36
- import { IRoom, Iso639_1Codes } from '../../../types/types';
37
- import Select from '../../MainComponents/Select';
38
- import { useAppDispatch, useAppSelector } from '../../../hooks/hooks';
39
- import { ScrollView, Text, View } from 'react-native';
42
+ import { addRoomViaApi, setCurrentRoom } from '../../../roomStore/roomsSlice';
43
+ import { runLogoutFlow } from '../../Menu/HeaderRoomListMenu';
44
+ import { useLogout } from '../../../hooks/useLogout';
40
45
  import { ApiRoom, postPrivateRoom } from '../../../networking/api-requests/rooms.api';
46
+ import {
47
+ getUserFiles,
48
+ isMediaFile,
49
+ UserFile,
50
+ } from '../../../networking/api-requests/user.api';
41
51
  import { createRoomFromApi } from '../../../helpers/createRoomFromApi';
42
- import { useToast } from '../../../context/ToastContext';
43
- import { LANGUAGE_OPTIONS } from '../../../helpers/constants/LANGUAGE_OPTIONS';
44
- import { deleteDocument, getDocuments } from '../../../networking/api-requests/user.api';
45
- import { useChatSettingState } from '../../../hooks/useChatSettingState';
52
+ import { walletToUsername } from '../../../helpers/walletUsername';
53
+ import { Iso639_1Codes } from '../../../types/types';
54
+ import {
55
+ ProfileHero,
56
+ ProfileTopBar,
57
+ HeroAction,
58
+ useHeaderMetrics,
59
+ } from '../ProfileHeader/ProfileHeader';
60
+ import ProfileMenu, { ProfileMenuItem } from '../ProfileHeader/ProfileMenu';
61
+ import EditUserModal from './EditUserModal';
62
+
63
+ const PREFIX = 'user-profile';
64
+
65
+ type TabKey = 'language' | 'media' | 'documents';
66
+
67
+ /** Two uppercase letters for a person without a picture. */
68
+ export const userInitials = (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
+ };
76
+
77
+ /**
78
+ * Which tabs a profile shows. Only the signed-in user has tabs at all:
79
+ * `GET /v2/files/` returns the JWT owner's files, so there is nothing to
80
+ * put under Media/Documents for anybody else, and the language picker is
81
+ * a personal setting.
82
+ */
83
+ export const profileTabs = (opts: {
84
+ isOwnProfile: boolean;
85
+ translatesEnabled: boolean;
86
+ hasMedia: boolean;
87
+ hasDocuments: boolean;
88
+ }): TabKey[] => {
89
+ if (!opts.isOwnProfile) {return [];}
90
+ const tabs: TabKey[] = [];
91
+ if (opts.translatesEnabled) {tabs.push('language');}
92
+ if (opts.hasMedia) {tabs.push('media');}
93
+ if (opts.hasDocuments) {tabs.push('documents');}
94
+ return tabs;
95
+ };
46
96
 
47
97
  interface UserProfileModalProps {
48
98
  handleCloseModal: any;
@@ -51,89 +101,111 @@ interface UserProfileModalProps {
51
101
  const UserProfileModal: React.FC<UserProfileModalProps> = ({
52
102
  handleCloseModal,
53
103
  }) => {
104
+ const t = useT();
54
105
  const dispatch = useAppDispatch();
55
-
56
106
  const { client } = useXmppClient();
57
107
  const { showToast } = useToast();
58
-
59
108
  const { config, user, selectedUser, langSource } = useChatSettingState();
109
+ const fileToken = useFileToken();
110
+
111
+ const [isEditing, setIsEditing] = useState(false);
112
+ const [files, setFiles] = useState<UserFile[]>([]);
113
+ const [loadingFiles, setLoadingFiles] = useState(false);
114
+ const [activeTab, setActiveTab] = useState<TabKey | null>(null);
115
+
116
+ const scrollY = useRef(new Animated.Value(0)).current;
117
+ const scrollRef = useRef<any>(null);
118
+ const { collapseDistance } = useHeaderMetrics();
119
+ const [viewportHeight, setViewportHeight] = useState(0);
120
+ // `scrollY` is driven natively, so its value can't be read back with
121
+ // addListener; the Animated.event's JS `listener` still fires, which is
122
+ // enough to know which state the header is in.
123
+ const [isCollapsed, setIsCollapsed] = useState(false);
124
+
125
+ const isOwnProfile = !selectedUser;
126
+ const profileUser: any = selectedUser ?? user;
127
+
128
+ const media = useMemo(() => files.filter(isMediaFile), [files]);
129
+ const documents = useMemo(
130
+ () => files.filter((file) => !isMediaFile(file)),
131
+ [files]
132
+ );
60
133
 
61
- const [isEditing, setIsEditing] = useState<boolean>(false);
62
- const [documents, setDocuments] = useState<any[]>([]);
63
- const [deleteDocumentId, setDeleteDocumentId] = useState<string>('');
64
- const [showDelete, setShowDelete] = useState<boolean>(false);
65
-
66
- const handleDeleteDocument = async () => {
67
- try {
68
- await deleteDocument(deleteDocumentId);
69
- setDocuments(documents.filter((doc) => doc._id !== deleteDocumentId));
70
- showToast({
71
- id: Date.now().toString(),
72
- title: 'Success',
73
- message: 'Document deleted successfully',
74
- type: 'success',
75
- });
76
- // handleGetDocs();
77
- } catch (error) {
78
- console.error('Error deleting document', error);
79
- showToast({
80
- id: Date.now().toString(),
81
- title: 'Error',
82
- message: 'Failed to delete document',
83
- type: 'error',
84
- });
85
- } finally {
86
- setShowDelete(false);
87
- }
88
- };
89
-
90
- const handleGetDocs = async () => {
91
- try {
92
- const { data } = await getDocuments(user?.defaultWallet?.walletAddress);
93
- const items = data.results.filter((el: {locations: unknown[]}) => el.locations[0]);
134
+ const tabs = useMemo(
135
+ () =>
136
+ profileTabs({
137
+ isOwnProfile,
138
+ translatesEnabled: !!config?.translates?.enabled,
139
+ hasMedia: media.length > 0,
140
+ hasDocuments: documents.length > 0,
141
+ }),
142
+ [isOwnProfile, config?.translates?.enabled, media.length, documents.length]
143
+ );
94
144
 
95
- setDocuments(items);
96
- } catch (error) {
97
- console.error('Error getting docs', error);
145
+ // Keep the selected tab valid as the lists load in.
146
+ useEffect(() => {
147
+ if (tabs.length === 0) {
148
+ setActiveTab(null);
149
+ return;
98
150
  }
99
- };
100
-
151
+ setActiveTab((current) =>
152
+ current && tabs.includes(current) ? current : tabs[0]
153
+ );
154
+ }, [tabs]);
155
+
156
+ // One request per visit, and only for one's own profile — the endpoint
157
+ // is scoped to the JWT owner, so it would return *our* files while
158
+ // looking at somebody else's page.
101
159
  useEffect(() => {
102
- handleGetDocs();
103
- }, []);
160
+ if (!isOwnProfile) {
161
+ setFiles([]);
162
+ return;
163
+ }
164
+ let cancelled = false;
165
+ setLoadingFiles(true);
166
+ getUserFiles()
167
+ .then((rows) => {
168
+ if (!cancelled) {setFiles(rows);}
169
+ })
170
+ .catch((error) => {
171
+ console.warn('Could not load user files:', error);
172
+ if (!cancelled) {setFiles([]);}
173
+ })
174
+ .finally(() => {
175
+ if (!cancelled) {setLoadingFiles(false);}
176
+ });
177
+ return () => {
178
+ cancelled = true;
179
+ };
180
+ }, [isOwnProfile, user?.token]);
104
181
 
105
182
  const handleBackClick = useCallback(() => {
106
183
  dispatch(setSelectedUser(undefined));
107
184
  handleCloseModal();
108
- }, []);
185
+ }, [dispatch, handleCloseModal]);
109
186
 
187
+ // Same teardown the room-list menu runs (XMPP, redux, persisted slices,
188
+ // AsyncStorage), including the host's confirm copy and callbacks.
189
+ const performLogout = useLogout();
110
190
  const handleLogout = useCallback(() => {
111
- dispatch(logout());
112
- dispatch(setLogoutState());
113
- }, []);
114
-
115
- const menuOptions = useMemo(
116
- () => [
117
- {
118
- label: 'Log Out',
119
- icon: <LeaveIcon />,
120
- onClick: () => {
121
- handleLogout();
122
- },
123
- styles: { color: 'red' },
124
- },
125
- ],
126
- []
127
- );
128
-
129
- const handleSelect = (selected: { name: string; id: Iso639_1Codes }) => {
130
- dispatch(setLangSource(selected.id));
131
- };
132
-
133
- const EditClick = useCallback(() => {
134
- setIsEditing(true);
135
- }, []);
136
-
191
+ runLogoutFlow(config?.logout ?? { enabled: true }, performLogout).catch(() => {});
192
+ }, [config?.logout, performLogout]);
193
+
194
+ const handleShare = useCallback(async () => {
195
+ const name =
196
+ profileUser?.name ||
197
+ `${profileUser?.firstName ?? ''} ${profileUser?.lastName ?? ''}`.trim();
198
+ const id =
199
+ profileUser?.userJID ||
200
+ profileUser?.defaultWallet?.walletAddress ||
201
+ profileUser?.id ||
202
+ '';
203
+ try {
204
+ await Share.share({ message: id ? `${name}\n${id}` : name });
205
+ } catch (error) {
206
+ console.warn('Could not share the profile:', error);
207
+ }
208
+ }, [profileUser]);
137
209
 
138
210
  const handleRoomCreation = async (
139
211
  newChat: ApiRoom,
@@ -146,15 +218,9 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
146
218
  usersArrayLength
147
219
  );
148
220
 
149
- if (!normalizedChat || !client) return;
150
-
151
- dispatch(
152
- addRoomViaApi({
153
- room: normalizedChat,
154
- xmpp: client,
155
- })
156
- );
221
+ if (!normalizedChat || !client) {return;}
157
222
 
223
+ dispatch(addRoomViaApi({ room: normalizedChat, xmpp: client }));
158
224
  dispatch(setCurrentRoom({ roomJID: normalizedChat?.jid || '' }));
159
225
 
160
226
  showToast({
@@ -177,22 +243,19 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
177
243
  type: 'info',
178
244
  duration: 3000,
179
245
  });
180
- let newRoomJid = '';
181
246
  if (config?.newArch) {
182
247
  const newRoom = await postPrivateRoom(
183
248
  selectedUser?.userJID ?? (selectedUser?.id || '')
184
249
  );
185
250
  handleRoomCreation(newRoom, 2);
186
- newRoomJid = newRoom.name;
187
251
  } else {
188
252
  const selectedUserUsername = walletToUsername(selectedUser?.id || '');
189
253
  const myUsername = walletToUsername(user.defaultWallet.walletAddress);
190
254
 
191
- const combinedWalletAddress = [myUsername, selectedUserUsername]
255
+ const roomJid = [myUsername, selectedUserUsername]
192
256
  .sort()
193
- .join('.');
194
-
195
- const roomJid = combinedWalletAddress.toLowerCase();
257
+ .join('.')
258
+ .toLowerCase();
196
259
 
197
260
  const combinedUsersName = [
198
261
  user.firstName,
@@ -201,11 +264,12 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
201
264
  .sort()
202
265
  .join(' and ');
203
266
 
204
- newRoomJid = (await client?.createPrivateRoomStanza(
205
- combinedUsersName,
206
- `Private chat ${combinedUsersName}`,
207
- roomJid
208
- )) || '';
267
+ const newRoomJid =
268
+ (await client?.createPrivateRoomStanza(
269
+ combinedUsersName,
270
+ `Private chat ${combinedUsersName}`,
271
+ roomJid
272
+ )) || '';
209
273
 
210
274
  if (newRoomJid) {
211
275
  await client?.inviteRoomRequestStanza(selectedUserUsername, newRoomJid);
@@ -214,202 +278,448 @@ const UserProfileModal: React.FC<UserProfileModalProps> = ({
214
278
  }
215
279
 
216
280
  dispatch(setActiveModal(undefined));
217
- }, [selectedUser]);
281
+ }, [selectedUser]); // eslint-disable-line react-hooks/exhaustive-deps
282
+
283
+ const openFile = (file: UserFile) => {
284
+ dispatch(
285
+ setActiveFile({
286
+ fileName: file.name,
287
+ fileURL: file.url,
288
+ mimetype: file.mimetype,
289
+ })
290
+ );
291
+ dispatch(setActiveModal(MODAL_TYPES.FILE_PREVIEW));
292
+ };
293
+
294
+ const heroActions: HeroAction[] = useMemo(
295
+ () =>
296
+ isOwnProfile
297
+ ? [
298
+ {
299
+ key: 'account',
300
+ label: t('action.account'),
301
+ icon: (color: string) => <ProfileIcon color={color} />,
302
+ onPress: () => dispatch(setActiveModal(MODAL_TYPES.SETTINGS)),
303
+ },
304
+ {
305
+ key: 'share',
306
+ label: t('action.share'),
307
+ icon: (color: string) => <ShareIcon color={color} />,
308
+ onPress: handleShare,
309
+ },
310
+ {
311
+ key: 'edit',
312
+ label: t('action.edit'),
313
+ icon: (color: string) => <EditIcon color={color} />,
314
+ onPress: () => setIsEditing(true),
315
+ },
316
+ {
317
+ key: 'logout',
318
+ label: t('action.logOut'),
319
+ icon: (color: string) => <LogoutIcon color={color} />,
320
+ onPress: handleLogout,
321
+ },
322
+ ]
323
+ : [
324
+ ...(config?.hideMemberSendMessageAction
325
+ ? []
326
+ : [
327
+ {
328
+ key: 'message',
329
+ label: t('action.message'),
330
+ icon: (color: string) => <ChatIcon color={color} />,
331
+ onPress: handlePrivateMessage,
332
+ },
333
+ ]),
334
+ {
335
+ key: 'share',
336
+ label: t('action.share'),
337
+ icon: (color: string) => <ShareIcon color={color} />,
338
+ onPress: handleShare,
339
+ },
340
+ ],
341
+ [isOwnProfile, t, handleShare, handlePrivateMessage, handleLogout] // eslint-disable-line react-hooks/exhaustive-deps
342
+ );
218
343
 
219
- const modalUser: any = selectedUser ?? user;
344
+ // Collapsed, the hero's round buttons are gone — the "…" menu carries
345
+ // exactly the same actions so nothing becomes unreachable.
346
+ const menuItems: ProfileMenuItem[] = useMemo(
347
+ () =>
348
+ isCollapsed
349
+ ? heroActions.map((action) => ({
350
+ key: action.key,
351
+ label: action.label,
352
+ icon: action.icon('#141414'),
353
+ destructive: action.key === 'logout',
354
+ onPress: action.onPress,
355
+ }))
356
+ : [],
357
+ [isCollapsed, heroActions]
358
+ );
220
359
 
221
- const findLanguage = () => {
222
- if(!langSource) {return null;}
360
+ if (isEditing) {
361
+ return (
362
+ <ModalContainerFullScreen>
363
+ <EditUserModal
364
+ setIsEditing={setIsEditing}
365
+ modalUser={profileUser}
366
+ config={config}
367
+ />
368
+ </ModalContainerFullScreen>
369
+ );
370
+ }
371
+
372
+ const displayName =
373
+ profileUser?.name ||
374
+ `${profileUser?.firstName ?? ''} ${profileUser?.lastName ?? ''}`.trim() ||
375
+ t('modal.profile.title');
376
+ const heroImage =
377
+ appendFileToken(profileUser?.profileImage, fileToken) || null;
378
+ const heroColor = config?.colors?.avatar || getIconColor(config);
379
+ const description = profileUser?.description;
380
+
381
+ const renderTabBody = () => {
382
+ if (activeTab === 'language') {
383
+ return (
384
+ <View>
385
+ {LANGUAGE_OPTIONS.map((option) => {
386
+ const selected = langSource === option.id;
387
+ return (
388
+ <TouchableOpacity
389
+ key={option.id}
390
+ testID={`${PREFIX}-language-${option.id}`}
391
+ activeOpacity={0.7}
392
+ style={styles.languageRow}
393
+ onPress={() =>
394
+ dispatch(setLangSource(option.id as Iso639_1Codes))
395
+ }
396
+ >
397
+ <Text style={styles.languageLabel}>{option.name}</Text>
398
+ {selected && (
399
+ <Text style={[styles.check, { color: getIconColor(config) }]}>
400
+ ✓
401
+ </Text>
402
+ )}
403
+ </TouchableOpacity>
404
+ );
405
+ })}
406
+ </View>
407
+ );
408
+ }
223
409
 
224
- const language = LANGUAGE_OPTIONS.find((lang) => lang.id === langSource);
225
- return language || null;
226
- };
410
+ if (activeTab === 'media') {
411
+ return (
412
+ <View style={styles.mediaGrid}>
413
+ {media.map((file) => (
414
+ <TouchableOpacity
415
+ key={file.id}
416
+ testID={`${PREFIX}-media-${file.id}`}
417
+ activeOpacity={0.8}
418
+ style={styles.mediaTile}
419
+ onPress={() => openFile(file)}
420
+ >
421
+ <Image
422
+ source={{ uri: appendFileToken(file.url, fileToken) || file.url }}
423
+ style={styles.mediaImage}
424
+ resizeMode="cover"
425
+ />
426
+ </TouchableOpacity>
427
+ ))}
428
+ </View>
429
+ );
430
+ }
227
431
 
228
- const showDeleteModal = (docId: string) => {
229
- setDeleteDocumentId(docId);
230
- setShowDelete(true);
432
+ return (
433
+ <View>
434
+ {documents.map((file) => (
435
+ <TouchableOpacity
436
+ key={file.id}
437
+ testID={`${PREFIX}-document-${file.id}`}
438
+ activeOpacity={0.7}
439
+ style={styles.documentRow}
440
+ onPress={() => openFile(file)}
441
+ >
442
+ <IconDoc />
443
+ <View style={styles.documentText}>
444
+ <Text numberOfLines={1} style={styles.documentName}>
445
+ {file.name}
446
+ </Text>
447
+ {!!file.createdAt && (
448
+ <Text style={styles.documentDate}>
449
+ {new Date(file.createdAt).toLocaleDateString()}
450
+ </Text>
451
+ )}
452
+ </View>
453
+ </TouchableOpacity>
454
+ ))}
455
+ </View>
456
+ );
231
457
  };
232
458
 
233
- const DefaultBody = useMemo(
234
- () => (
235
- <>
236
- <ModalHeaderComponent
237
- handleCloseModal={handleBackClick}
238
- headerTitle={'Profile'}
239
- rightMenu={
240
- !selectedUser && (
241
- <>
242
- <Button onPress={EditClick}>
243
- <EditIcon color="#8C8C8C" />
244
- </Button>
245
- <DropdownMenu
246
- options={menuOptions}
247
- position="right"
248
- menuIcon={<MoreIcon />}
249
- />
250
- </>
251
- )
459
+ return (
460
+ <ModalContainerFullScreen style={styles.screen}>
461
+ <Animated.ScrollView
462
+ ref={scrollRef}
463
+ style={styles.scroll}
464
+ contentContainerStyle={[
465
+ styles.scrollContent,
466
+ viewportHeight > 0 && {
467
+ minHeight: viewportHeight + collapseDistance,
468
+ },
469
+ ]}
470
+ onLayout={(e) => setViewportHeight(e.nativeEvent.layout.height)}
471
+ showsVerticalScrollIndicator={false}
472
+ scrollEventThrottle={16}
473
+ onScroll={Animated.event(
474
+ [{ nativeEvent: { contentOffset: { y: scrollY } } }],
475
+ {
476
+ useNativeDriver: true,
477
+ listener: (e: any) => {
478
+ const y = e?.nativeEvent?.contentOffset?.y ?? 0;
479
+ setIsCollapsed(y >= collapseDistance - 8);
480
+ },
481
+ }
482
+ )}
483
+ >
484
+ <ProfileHero
485
+ testIDPrefix={PREFIX}
486
+ title={displayName}
487
+ imageUri={heroImage}
488
+ fallbackColor={heroColor}
489
+ initials={userInitials(displayName)}
490
+ scrollY={scrollY}
491
+ actions={heroActions}
492
+ titleStyle={chatTextStyle(config?.typography?.profile?.title)}
493
+ titleAccessory={
494
+ isOwnProfile ? (
495
+ <TouchableOpacity
496
+ testID={`${PREFIX}-logout`}
497
+ activeOpacity={0.7}
498
+ style={styles.leaveButton}
499
+ onPress={handleLogout}
500
+ >
501
+ <LeaveIcon color="#FFFFFF" width={18} height={18} />
502
+ <Text style={styles.leaveLabel}>{t('action.leave')}</Text>
503
+ </TouchableOpacity>
504
+ ) : undefined
252
505
  }
253
506
  />
254
- <CenterContainer>
255
- <ProfileImagePlaceholder
256
- icon={modalUser?.profileImage ?? null}
257
- name={modalUser?.name ?? modalUser?.firstName}
258
- size={120}
259
- />
260
- <UserInfo>
261
- <UserName>
262
- {modalUser?.name
263
- ? `${modalUser?.name}`
264
- : `${modalUser?.firstName} ${modalUser?.lastName}`}
265
- </UserName>
266
- {/* <UserStatus>Status</UserStatus> */}
267
- </UserInfo>
268
- {!selectedUser && config?.translates?.enabled && (
269
- <BorderedContainer>
270
- <Select
271
- options={LANGUAGE_OPTIONS}
272
- placeholder={'Select your language'}
273
- onSelect={handleSelect}
274
- accentColor={config?.colors?.primary}
275
- selectedValue={findLanguage()}
276
- />
277
- </BorderedContainer>
278
- )}
279
- <BorderedContainer>
280
- <Label>About</Label>
281
- <LabelData>
282
- {modalUser?.description && modalUser?.description?.length > 4
283
- ? modalUser.description
284
- : 'No description'}
285
- </LabelData>
286
- </BorderedContainer>
287
-
288
- {selectedUser && !config?.disableMemberProfileActions && (
289
- <>
290
- {!config?.hideMemberSendMessageAction && (
291
- <ActionButton
292
- StartIcon={<ChatIcon />}
293
- onPress={handlePrivateMessage}
294
- variant="filled"
295
- >
296
- <Text style={{ color: '#ffffff' }}>Message</Text>
297
- </ActionButton>
298
- )}
299
- {!config?.hideMemberCopyIdAction && (
300
- <ActionButton
301
- onPress={() => {}}
302
- // onPress={() => handleCopyClick(selectedUser.id)}
303
- variant="filled"
304
- >
305
- <Text style={{ color: '#ffffff' }}>Copy User Id</Text>
306
- </ActionButton>
307
- )}
308
- </>
309
- )}
310
507
 
311
- <BorderedContainer>
312
- <ScrollView
313
- style={{ maxHeight: 400 }}
314
- showsVerticalScrollIndicator={true}
315
- nestedScrollEnabled={true}
508
+ <View style={styles.body}>
509
+ <View style={styles.card}>
510
+ <Text
511
+ style={[
512
+ styles.cardLabel,
513
+ getElementFont(config, 'profileSectionLabel'),
514
+ ]}
316
515
  >
317
- {documents.map((doc) => (
318
- <View
319
- key={doc._id}
320
- style={{
321
- flexDirection: 'row',
322
- alignItems: 'center',
323
- justifyContent: 'space-between',
324
- gap: 24,
325
- backgroundColor: '#F3F6FC',
326
- marginBottom: 8,
327
- paddingVertical: 8,
328
- paddingHorizontal: 16,
329
- borderRadius: 8,
330
- }}
331
- >
332
- <View style={{
333
- flexDirection: 'row',
334
- alignItems: 'center',
335
- gap: 24,
336
- }}>
337
- <View><IconDoc/></View>
338
- <View>
339
- <Label style={{ paddingBottom: 4 }}>{doc.documentName}</Label>
340
- <LabelData>
341
- {new Date(doc.createdAt).toLocaleDateString('en-GB', { day: '2-digit', month: 'short', year: 'numeric' }) + ' ' + new Date(doc.createdAt).toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' })}
342
- </LabelData>
343
- </View>
344
- </View>
345
- <Button onPress={() => showDeleteModal(doc._id)}>
346
- <DeleteIcon/>
347
- </Button>
516
+ {t('modal.profile.about')}
517
+ </Text>
518
+ <Text style={styles.cardValue}>
519
+ {description && description.length > 4
520
+ ? description
521
+ : t('modal.profile.noDescription')}
522
+ </Text>
523
+ </View>
524
+
525
+ {tabs.length > 0 && (
526
+ <View style={styles.card}>
527
+ <View style={styles.tabsRow}>
528
+ {tabs.map((tab) => {
529
+ const active = tab === activeTab;
530
+ const count =
531
+ tab === 'media'
532
+ ? media.length
533
+ : tab === 'documents'
534
+ ? documents.length
535
+ : 0;
536
+ return (
537
+ <TouchableOpacity
538
+ key={tab}
539
+ testID={`${PREFIX}-tab-${tab}`}
540
+ activeOpacity={0.7}
541
+ style={[
542
+ styles.tab,
543
+ active && { borderBottomColor: getIconColor(config) },
544
+ ]}
545
+ onPress={() => setActiveTab(tab)}
546
+ >
547
+ <Text
548
+ style={[
549
+ styles.tabLabel,
550
+ active && { color: getIconColor(config) },
551
+ ]}
552
+ >
553
+ {t(`modal.profile.${tab}`)}
554
+ </Text>
555
+ {count > 0 && (
556
+ <View
557
+ style={[
558
+ styles.tabBadge,
559
+ { backgroundColor: getIconColor(config) },
560
+ ]}
561
+ >
562
+ <Text style={styles.tabBadgeText}>{count}</Text>
563
+ </View>
564
+ )}
565
+ </TouchableOpacity>
566
+ );
567
+ })}
348
568
  </View>
349
- ))}
350
- </ScrollView>
351
- </BorderedContainer>
352
-
353
-
354
- {/* <EmptySection /> */}
355
- </CenterContainer>
356
- </>
357
- ),
358
- [modalUser, documents]
359
- );
569
+ {renderTabBody()}
570
+ </View>
571
+ )}
360
572
 
361
- const EditingBody = useMemo(
362
- () => (
363
- <EditUserModal
364
- setIsEditing={setIsEditing}
365
- modalUser={modalUser}
366
- config={config}
573
+ {isOwnProfile && loadingFiles && files.length === 0 && (
574
+ <ActivityIndicator testID={`${PREFIX}-files-loading`} />
575
+ )}
576
+ </View>
577
+ </Animated.ScrollView>
578
+
579
+ <ProfileTopBar
580
+ testIDPrefix={PREFIX}
581
+ title={displayName}
582
+ imageUri={heroImage}
583
+ fallbackColor={heroColor}
584
+ initials={userInitials(displayName)}
585
+ scrollY={scrollY}
586
+ onBack={handleBackClick}
587
+ titleStyle={chatTextStyle(config?.typography?.profile?.screenTitle)}
588
+ menu={<ProfileMenu testIDPrefix={PREFIX} items={menuItems} />}
367
589
  />
368
- ),
369
- [modalUser]
370
- );
371
-
372
- return (
373
- <>
374
- <ModalContainerFullScreen>
375
- {!isEditing ? DefaultBody : EditingBody}
376
- </ModalContainerFullScreen>
377
-
378
- {showDelete && (
379
- <ModalBackground style={{ position: 'absolute', zIndex: 9999 }}>
380
- <ModalContainer>
381
- <CloseButton onPress={() => setShowDelete(false)}>
382
- <Text style={{ fontSize: 24 }}>&times;</Text>
383
- </CloseButton>
384
- <ModalTitle>Delete this document?</ModalTitle>
385
-
386
- <GroupContainer>
387
- <Button
388
- onPress={() => setShowDelete(false)}
389
- text={'Cancel'}
390
- style={{ width: '100%' }}
391
- unstyled
392
- variant="filled"
393
- color="white"
394
- />
395
- <Button
396
- onPress={handleDeleteDocument}
397
- text={'Delete'}
398
- style={{
399
- width: '100%',
400
- borderWidth: 1,
401
- borderColor: 'red',
402
- }}
403
- color="red"
404
- unstyled
405
- variant="outlined"
406
- />
407
- </GroupContainer>
408
- </ModalContainer>
409
- </ModalBackground>
410
- )}
411
- </>
590
+ </ModalContainerFullScreen>
412
591
  );
413
592
  };
414
593
 
594
+ const styles = StyleSheet.create({
595
+ screen: {
596
+ position: 'relative',
597
+ backgroundColor: '#F2F3F5',
598
+ },
599
+ scroll: {
600
+ width: '100%',
601
+ flex: 1,
602
+ },
603
+ scrollContent: {
604
+ paddingBottom: 32,
605
+ },
606
+ body: {
607
+ padding: 12,
608
+ gap: 12,
609
+ },
610
+ card: {
611
+ backgroundColor: '#FFFFFF',
612
+ borderRadius: 14,
613
+ padding: 16,
614
+ shadowColor: '#000',
615
+ shadowOffset: { width: 0, height: 1 },
616
+ shadowOpacity: 0.05,
617
+ shadowRadius: 4,
618
+ elevation: 1,
619
+ },
620
+ cardLabel: {
621
+ color: '#8C8C8C',
622
+ fontSize: 14,
623
+ },
624
+ cardValue: {
625
+ color: '#141414',
626
+ fontSize: 16,
627
+ marginTop: 4,
628
+ },
629
+ leaveButton: {
630
+ flexDirection: 'row',
631
+ alignItems: 'center',
632
+ gap: 6,
633
+ paddingHorizontal: 12,
634
+ paddingVertical: 7,
635
+ borderRadius: 18,
636
+ backgroundColor: 'rgba(0,0,0,0.35)',
637
+ },
638
+ leaveLabel: {
639
+ color: '#FFFFFF',
640
+ fontSize: 14,
641
+ fontWeight: '500',
642
+ },
643
+ tabsRow: {
644
+ flexDirection: 'row',
645
+ gap: 20,
646
+ borderBottomWidth: StyleSheet.hairlineWidth,
647
+ borderBottomColor: '#EFEFF2',
648
+ marginBottom: 12,
649
+ },
650
+ tab: {
651
+ flexDirection: 'row',
652
+ alignItems: 'center',
653
+ gap: 6,
654
+ paddingBottom: 10,
655
+ borderBottomWidth: 2,
656
+ borderBottomColor: 'transparent',
657
+ },
658
+ tabLabel: {
659
+ fontSize: 15,
660
+ fontWeight: '500',
661
+ color: '#8C8C8C',
662
+ },
663
+ tabBadge: {
664
+ minWidth: 20,
665
+ paddingHorizontal: 5,
666
+ paddingVertical: 1,
667
+ borderRadius: 10,
668
+ alignItems: 'center',
669
+ },
670
+ tabBadgeText: {
671
+ color: '#FFFFFF',
672
+ fontSize: 11,
673
+ fontWeight: '600',
674
+ },
675
+ languageRow: {
676
+ flexDirection: 'row',
677
+ alignItems: 'center',
678
+ justifyContent: 'space-between',
679
+ paddingVertical: 12,
680
+ },
681
+ languageLabel: {
682
+ fontSize: 16,
683
+ color: '#141414',
684
+ },
685
+ check: {
686
+ fontSize: 16,
687
+ fontWeight: '700',
688
+ },
689
+ mediaGrid: {
690
+ flexDirection: 'row',
691
+ flexWrap: 'wrap',
692
+ gap: 6,
693
+ },
694
+ mediaTile: {
695
+ width: '31.8%',
696
+ aspectRatio: 1,
697
+ borderRadius: 10,
698
+ overflow: 'hidden',
699
+ backgroundColor: '#EFEFF2',
700
+ },
701
+ mediaImage: {
702
+ width: '100%',
703
+ height: '100%',
704
+ },
705
+ documentRow: {
706
+ flexDirection: 'row',
707
+ alignItems: 'center',
708
+ gap: 14,
709
+ paddingVertical: 12,
710
+ },
711
+ documentText: {
712
+ flex: 1,
713
+ },
714
+ documentName: {
715
+ fontSize: 16,
716
+ color: '#141414',
717
+ },
718
+ documentDate: {
719
+ fontSize: 13,
720
+ color: '#8C8C8C',
721
+ marginTop: 2,
722
+ },
723
+ });
724
+
415
725
  export default UserProfileModal;