index.tsx 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. import React, { FC, useCallback, useEffect, useState } from 'react';
  2. import { Linking, ScrollView, Text, TouchableOpacity, View, Image, Platform } from 'react-native';
  3. import { CommonActions, NavigationProp, useFocusEffect } from '@react-navigation/native';
  4. import ReactModal from 'react-native-modal';
  5. import ImageView from 'react-native-image-viewing';
  6. import { usePostGetProfileInfoDataQuery, usePostGetProfileUpdatesQuery } from '@api/user';
  7. import {
  8. PageWrapper,
  9. Loading,
  10. AvatarWithInitials,
  11. Header,
  12. WarningModal
  13. } from '../../../components';
  14. import { adaptiveStyle, Colors } from '../../../theme';
  15. import { styles } from './styles';
  16. import { API_HOST } from '../../../constants';
  17. import { NAVIGATION_PAGES } from '../../../types';
  18. import { storage, StoreType } from '../../../storage';
  19. import { getFontSize } from '../../../utils';
  20. import IconFacebook from '../../../../assets/icons/facebook.svg';
  21. import IconInstagram from '../../../../assets/icons/instagram.svg';
  22. import IconTwitter from '../../../../assets/icons/x(twitter).svg';
  23. import IconYouTube from '../../../../assets/icons/youtube.svg';
  24. import IconGlobe from '../../../../assets/icons/bottom-navigation/globe.svg';
  25. import IconLink from '../../../../assets/icons/link.svg';
  26. import GearIcon from '../../../../assets/icons/gear.svg';
  27. import TBTIcon from '../../../../assets/icons/tbt.svg';
  28. import TickIcon from '../../../../assets/icons/tick.svg';
  29. import UNIcon from '../../../../assets/icons/un_icon.svg';
  30. import NMIcon from '../../../../assets/icons/nm_icon.svg';
  31. import UN25Icon from '../../../../assets/icons/un-25.svg';
  32. import UN50Icon from '../../../../assets/icons/un-50.svg';
  33. import UN75Icon from '../../../../assets/icons/un-75.svg';
  34. import UN100Icon from '../../../../assets/icons/un-100.svg';
  35. import UN150Icon from '../../../../assets/icons/un-150.svg';
  36. import ChevronIcon from '../../../../assets/icons/chevron-left.svg';
  37. import ShareIcon from '../../../../assets/icons/share.svg';
  38. import UnverifiedIcon from '../../../../assets/icons/unverified.svg';
  39. import CommentsIcon from '../../../../assets/icons/messages/comments.svg';
  40. import MapSvg from 'assets/icons/travels-screens/map-location.svg';
  41. import InfoIcon from 'assets/icons/info-solid.svg';
  42. import PremiumIcon from 'assets/icons/premium.svg';
  43. import { ProfileStyles, ScoreStyles, TBTStyles } from '../TravellersScreen/Components/styles';
  44. import UnauthenticatedProfileScreen from './UnauthenticatedProfileScreen';
  45. import { PersonalInfo } from './Components/PersonalInfo';
  46. import { usePostUpdateFriendStatusMutation } from '@api/friends';
  47. import Tooltip from 'react-native-walkthrough-tooltip';
  48. import { useAvatarStore } from 'src/stores/avatarVersionStore';
  49. type Props = {
  50. navigation: NavigationProp<any>;
  51. route: any;
  52. };
  53. const ProfileScreen: FC<Props> = ({ navigation, route }) => {
  54. const isPublicView = route.name === NAVIGATION_PAGES.PUBLIC_PROFILE_VIEW;
  55. const token = storage.get('token', StoreType.STRING) as string;
  56. const currentUserId = storage.get('uid', StoreType.STRING) as string;
  57. if (!token) return <UnauthenticatedProfileScreen />;
  58. const { data: userData, isFetching } = usePostGetProfileInfoDataQuery(
  59. token,
  60. isPublicView ? route.params?.userId : +currentUserId,
  61. true
  62. );
  63. const { data: lastUpdates } = usePostGetProfileUpdatesQuery(
  64. token,
  65. isPublicView ? route.params?.userId : +currentUserId,
  66. true
  67. );
  68. const { mutateAsync: updateFriendStatus } = usePostUpdateFriendStatusMutation();
  69. const [isFriend, setIsFriend] = useState<0 | 1>(0);
  70. const [canBeAuthenticated, setCanBeAuthenticated] = useState<0 | 1>(0);
  71. const [shouldOpenWarningModal, setShouldOpenWarningModal] = useState(false);
  72. const [modalState, setModalState] = useState({
  73. isModalVisible: false,
  74. isWarningVisible: false
  75. });
  76. const [tooltipVisible, setTooltipVisible] = useState(false);
  77. const [tooltipTrustVisible, setTooltipTrustVisible] = useState(false);
  78. const [fullSizeImageVisible, setFullSizeImageVisible] = useState(false);
  79. const { avatarVersion } = useAvatarStore();
  80. useFocusEffect(
  81. useCallback(() => {
  82. if (!route.params?.hideTabBar) {
  83. navigation.getParent()?.setOptions({
  84. tabBarStyle: {
  85. display: 'flex',
  86. ...Platform.select({
  87. android: {
  88. // height: 58
  89. }
  90. })
  91. }
  92. });
  93. }
  94. }, [navigation])
  95. );
  96. useEffect(() => {
  97. setIsFriend(userData?.data?.is_friend ?? 0);
  98. setCanBeAuthenticated(userData?.data?.can_be_authenticated ?? 0);
  99. if (userData && userData?.data?.own_profile === 1) {
  100. const userInfo = {
  101. avatar: userData?.data?.user_data.avatar ?? '',
  102. first_name: userData?.data?.user_data.first_name,
  103. last_name: userData?.data?.user_data.last_name,
  104. homebase_flag: userData?.data?.user_data.flag1
  105. };
  106. storage.set('currentUserData', JSON.stringify(userInfo));
  107. }
  108. }, [userData]);
  109. if (!userData?.data || !lastUpdates || isFetching) return <Loading />;
  110. const data = userData.data;
  111. const links = JSON.parse(data.user_data.links_json);
  112. const handleGoToMap = () => {
  113. data.own_profile === 0
  114. ? navigation.navigate(NAVIGATION_PAGES.USERS_MAP, { userId: route.params?.userId, data })
  115. : navigation.dispatch(
  116. CommonActions.reset({
  117. index: 1,
  118. routes: [{ name: NAVIGATION_PAGES.IN_APP_MAP_TAB }]
  119. })
  120. );
  121. };
  122. const closeModal = (modalName: string) => {
  123. setModalState((prevState) => ({ ...prevState, [modalName]: false }));
  124. };
  125. const openModal = (modalName: string) => {
  126. setModalState((prevState) => ({ ...prevState, [modalName]: true }));
  127. };
  128. const TBRanking = () => {
  129. const colors = [
  130. 'rgba(237, 147, 52, 1)',
  131. 'rgba(128, 128, 128, 1)',
  132. 'rgba(211, 211, 211, 1)',
  133. 'rgba(187, 95, 5, 1)',
  134. '#808080'
  135. ];
  136. const Rank = ({ color }: { color: string }) => (
  137. <View style={adaptiveStyle([ProfileStyles.badge, { backgroundColor: color }], {})}>
  138. <TBTIcon />
  139. </View>
  140. );
  141. return (
  142. <TouchableOpacity
  143. style={adaptiveStyle([TBTStyles.badgeRoot, styles.badgeRoot], {})}
  144. disabled={!data.scores.rank_tbt || data.scores.rank_tbt < 1}
  145. >
  146. <View style={adaptiveStyle([TBTStyles.badgeWrapper, { gap: 10 }], {})}>
  147. {data.user_data.badge_tbt && data.scores.rank_tbt ? (
  148. <Rank color={colors[data.scores.rank_tbt - 1]} />
  149. ) : null}
  150. {data.scores.rank_tbt && data.scores.rank_tbt >= 1 ? (
  151. <Text style={adaptiveStyle([ScoreStyles.scoreNameText], {})}>
  152. TBT # {data.scores.rank_tbt}
  153. </Text>
  154. ) : (
  155. <View style={{ height: 11 }} />
  156. )}
  157. </View>
  158. </TouchableOpacity>
  159. );
  160. };
  161. const handleOpenUrl = (url: string | undefined) => {
  162. url && Linking.openURL(url);
  163. };
  164. const hasActiveLinks = () => {
  165. return (
  166. (links?.f?.link && links?.f?.active !== 0) ||
  167. (links?.i?.link && links?.i?.active !== 0) ||
  168. (links?.t?.link && links?.t?.active !== 0) ||
  169. (links?.y?.link && links?.y?.active !== 0) ||
  170. (links?.www?.link && links?.www?.active !== 0) ||
  171. (links?.other?.link && links?.other?.active !== 0)
  172. );
  173. };
  174. const handleUpdateFriendStatus = async () => {
  175. await updateFriendStatus(
  176. { token: token as string, id: data.friend_db_id, status: -1 },
  177. {
  178. onSuccess: () => {
  179. setIsFriend(0);
  180. }
  181. }
  182. );
  183. };
  184. const handleGoToChat = () => {
  185. navigation.dispatch(
  186. CommonActions.reset({
  187. index: 1,
  188. routes: [
  189. {
  190. name: 'DrawerApp',
  191. state: {
  192. routes: [
  193. {
  194. name: NAVIGATION_PAGES.IN_APP_MESSAGES_TAB,
  195. state: {
  196. routes: [
  197. { name: NAVIGATION_PAGES.CHATS_LIST },
  198. {
  199. name: NAVIGATION_PAGES.CHAT,
  200. params: {
  201. id: route.params?.userId,
  202. name: data.user_data.first_name + ' ' + data.user_data.last_name,
  203. avatar: '/img/avatars/' + data.user_data.avatar,
  204. userType: 'normal'
  205. }
  206. }
  207. ]
  208. }
  209. }
  210. ]
  211. }
  212. }
  213. ]
  214. })
  215. );
  216. };
  217. return (
  218. <PageWrapper>
  219. <Header label="Profile" />
  220. <ScrollView
  221. showsVerticalScrollIndicator={false}
  222. contentContainerStyle={{ paddingBottom: 58 }}
  223. >
  224. <TouchableOpacity
  225. style={[styles.usersMap, { backgroundColor: '#EBF2F5' }]}
  226. onPress={handleGoToMap}
  227. >
  228. <Image
  229. source={{
  230. uri: `${API_HOST}/img/single_maps/${isPublicView ? route.params?.userId : currentUserId}.png?random=${Date.now()}`,
  231. cache: 'reload'
  232. }}
  233. style={styles.usersMap}
  234. />
  235. {data.location_sharing && data.location_last_seen_location && data.own_profile !== 1 ? (
  236. <View style={styles.locationWrapper}>
  237. <View style={styles.locationBtn}>
  238. <MapSvg fill={Colors.WHITE} />
  239. </View>
  240. </View>
  241. ) : null}
  242. </TouchableOpacity>
  243. <View style={styles.pageWrapper}>
  244. <View style={{ gap: 8 }}>
  245. <View style={{ position: 'relative' }}>
  246. {data.user_data.avatar ? (
  247. <TouchableOpacity
  248. onPress={() => setFullSizeImageVisible(true)}
  249. disabled={!data.user_data.avatar}
  250. >
  251. <Image
  252. style={styles.avatar}
  253. source={{
  254. uri:
  255. API_HOST + '/img/avatars/' + data.user_data.avatar + '?v=' + avatarVersion
  256. }}
  257. />
  258. </TouchableOpacity>
  259. ) : (
  260. <AvatarWithInitials
  261. text={`${data.user_data.first_name[0] ?? ''}${data.user_data.last_name[0] ?? ''}`}
  262. flag={API_HOST + '/img/flags_new/' + data.user_data.flag1}
  263. size={64}
  264. borderColor={Colors.WHITE}
  265. />
  266. )}
  267. <View
  268. style={{
  269. position: 'absolute',
  270. bottom: 0,
  271. right: 0,
  272. justifyContent: 'center',
  273. alignItems: 'center'
  274. }}
  275. >
  276. {data.user_data.badge_premium ? <PremiumIcon /> : null}
  277. </View>
  278. </View>
  279. {data.scores.rank_tbt && data.scores.rank_tbt >= 1 ? <TBRanking /> : null}
  280. {isFriend === 1 && token && data.own_profile === 0 ? (
  281. <TouchableOpacity style={styles.friend} onPress={() => openModal('isModalVisible')}>
  282. <Text style={styles.friendText}>Friend</Text>
  283. <View style={{ transform: 'rotate(180deg)' }}>
  284. <ChevronIcon fill={Colors.WHITE} height={8} />
  285. </View>
  286. </TouchableOpacity>
  287. ) : null}
  288. </View>
  289. <View style={{ gap: 5, flex: 1 }}>
  290. <View style={{ height: 34 }}></View>
  291. <View style={styles.nameRow}>
  292. <Text style={[styles.headerText, { fontSize: getFontSize(18) }]}>
  293. {data.user_data.first_name} {data.user_data.last_name}
  294. </Text>
  295. </View>
  296. <View style={styles.userInfoContainer}>
  297. <View style={styles.userInfo}>
  298. <Text style={styles.ageText}>Age: {data.user_data.age}</Text>
  299. <Image
  300. source={{ uri: API_HOST + '/img/flags_new/' + data.user_data.flag1 }}
  301. style={styles.countryFlag}
  302. />
  303. {data.user_data.flag2 && data.user_data.flag2 !== data.user_data.flag1 ? (
  304. <Image
  305. source={{ uri: API_HOST + '/img/flags_new/' + data.user_data.flag2 }}
  306. style={[styles.countryFlag, { marginLeft: -15 }]}
  307. />
  308. ) : null}
  309. <View style={adaptiveStyle(ProfileStyles.badgesWrapper, {})}>
  310. {data.user_data.auth ? (
  311. <Tooltip
  312. isVisible={tooltipTrustVisible}
  313. onClose={() => setTooltipTrustVisible(false)}
  314. content={
  315. <TouchableOpacity
  316. onPress={() => {
  317. setTooltipTrustVisible(false);
  318. Linking.openURL('https://nomadmania.com/trust/').catch((err) =>
  319. console.error('Failed to open trust URL:', err)
  320. );
  321. }}
  322. style={{ flexDirection: 'row', alignItems: 'center' }}
  323. >
  324. <Text
  325. style={{
  326. color: Colors.DARK_BLUE
  327. }}
  328. >
  329. This member is trusted{' '}
  330. </Text>
  331. <InfoIcon fill={Colors.DARK_BLUE} width={14} height={14} />
  332. </TouchableOpacity>
  333. }
  334. contentStyle={{ backgroundColor: Colors.FILL_LIGHT }}
  335. backgroundColor="transparent"
  336. placement="top"
  337. >
  338. <TouchableOpacity onPress={() => setTooltipTrustVisible(true)}>
  339. <TickIcon />
  340. </TouchableOpacity>
  341. </Tooltip>
  342. ) : null}
  343. {data.user_data.badge_un ? <UNIcon /> : null}
  344. {data.user_data.badge_nm ? <NMIcon /> : null}
  345. {data.user_data.badge_un_150 ? <UN150Icon /> : null}
  346. {data.user_data.badge_un_100 ? <UN100Icon /> : null}
  347. {data.user_data.badge_un_75 ? <UN75Icon /> : null}
  348. {data.user_data.badge_un_50 ? <UN50Icon /> : null}
  349. {data.user_data.badge_un_25 ? <UN25Icon /> : null}
  350. {data.user_data.badge_ghost ? (
  351. <Tooltip
  352. isVisible={tooltipVisible}
  353. onClose={() => setTooltipVisible(false)}
  354. content={<Text style={{ color: Colors.DARK_BLUE }}>Unverified User</Text>}
  355. contentStyle={{ backgroundColor: Colors.WHITE }}
  356. backgroundColor="transparent"
  357. allowChildInteraction={false}
  358. placement="top"
  359. >
  360. <TouchableOpacity onPress={() => setTooltipVisible(true)}>
  361. <UnverifiedIcon />
  362. </TouchableOpacity>
  363. </Tooltip>
  364. ) : null}
  365. </View>
  366. </View>
  367. {data.own_profile === 1 ? (
  368. <>
  369. <TouchableOpacity
  370. style={[styles.settings, { right: 25 }]}
  371. onPress={() =>
  372. navigation.navigate(NAVIGATION_PAGES.SHARE_PROFILE, {
  373. data: {
  374. avatar: data.user_data.avatar,
  375. first_name: data.user_data.first_name,
  376. last_name: data.user_data.last_name,
  377. flag1: data.user_data.flag1,
  378. flag2: data.user_data.flag2,
  379. id: +currentUserId,
  380. auth: data.user_data.auth,
  381. badge_un: data.user_data.badge_un,
  382. badge_nm: data.user_data.badge_nm,
  383. badge_un_25: data.user_data.badge_un_25,
  384. badge_un_50: data.user_data.badge_un_50,
  385. badge_un_75: data.user_data.badge_un_75,
  386. badge_un_100: data.user_data.badge_un_100,
  387. badge_un_150: data.user_data.badge_un_150,
  388. badge_premium: data.user_data.badge_premium,
  389. scores: data.scores
  390. }
  391. })
  392. }
  393. >
  394. <ShareIcon
  395. width={20}
  396. height={20}
  397. fill={Colors.DARK_BLUE}
  398. style={{ alignSelf: 'center' }}
  399. />
  400. </TouchableOpacity>
  401. <TouchableOpacity
  402. style={styles.settings}
  403. onPress={() => navigation.navigate(NAVIGATION_PAGES.EDIT_PERSONAL_INFO)}
  404. >
  405. <GearIcon
  406. width={20}
  407. height={20}
  408. fill={Colors.DARK_BLUE}
  409. style={{ alignSelf: 'center' }}
  410. />
  411. </TouchableOpacity>
  412. </>
  413. ) : null}
  414. </View>
  415. {hasActiveLinks() && (
  416. <View style={styles.linksBox}>
  417. {links?.f?.link && links?.f?.active !== 0 ? (
  418. <TouchableOpacity onPress={() => handleOpenUrl(links?.f?.link)}>
  419. <IconFacebook fill={Colors.DARK_BLUE} height={16} />
  420. </TouchableOpacity>
  421. ) : null}
  422. {links?.i?.link && links?.i?.active !== 0 ? (
  423. <TouchableOpacity onPress={() => handleOpenUrl(links?.i?.link)}>
  424. <IconInstagram fill={Colors.DARK_BLUE} height={16} />
  425. </TouchableOpacity>
  426. ) : null}
  427. {links?.t?.link && links?.t?.active !== 0 ? (
  428. <TouchableOpacity onPress={() => handleOpenUrl(links?.t?.link)}>
  429. <IconTwitter fill={Colors.DARK_BLUE} height={16} />
  430. </TouchableOpacity>
  431. ) : null}
  432. {links?.y?.link && links?.y?.active !== 0 ? (
  433. <TouchableOpacity onPress={() => handleOpenUrl(links?.y?.link)}>
  434. <IconYouTube fill={Colors.DARK_BLUE} height={16} />
  435. </TouchableOpacity>
  436. ) : null}
  437. {links?.www?.link && links?.www?.active !== 0 ? (
  438. <TouchableOpacity onPress={() => handleOpenUrl(links?.www?.link)}>
  439. <IconGlobe fill={Colors.DARK_BLUE} height={16} />
  440. </TouchableOpacity>
  441. ) : null}
  442. {links?.other?.link && links?.other?.active !== 0 ? (
  443. <TouchableOpacity onPress={() => handleOpenUrl(links?.other?.link)}>
  444. <IconLink fill={Colors.DARK_BLUE} height={16} />
  445. </TouchableOpacity>
  446. ) : null}
  447. </View>
  448. )}
  449. </View>
  450. </View>
  451. <PersonalInfo
  452. data={{
  453. bio: data.user_data.bio,
  454. scores: data.scores,
  455. homebase: data.user_data.homeregion,
  456. series: data.series,
  457. friends: data.friends,
  458. firstName: data.user_data.first_name,
  459. lastName: data.user_data.last_name,
  460. friendRequestSent: data.friend_request_sent,
  461. friendRequestReceived: data.friend_request_received,
  462. isFriend,
  463. canBeAuthenticated,
  464. setCanBeAuthenticated,
  465. goToChat: handleGoToChat,
  466. friendDbId: data.friend_db_id,
  467. ownProfile: data.own_profile,
  468. locationSharing: data.location_sharing,
  469. lastSeenRegion: data.location_last_seen_region,
  470. lastSeenDate: data.location_last_seen_date,
  471. lastSeenFlag: data.location_last_seen_flag
  472. }}
  473. updates={lastUpdates?.data ? lastUpdates.data?.updates : null}
  474. userId={isPublicView ? route.params?.userId : +currentUserId}
  475. navigation={navigation}
  476. isPublicView={isPublicView}
  477. token={token ? token : null}
  478. />
  479. </ScrollView>
  480. <ReactModal
  481. isVisible={modalState.isModalVisible}
  482. onBackdropPress={() => closeModal('isModalVisible')}
  483. style={styles.modal}
  484. statusBarTranslucent={true}
  485. presentationStyle="overFullScreen"
  486. onModalHide={() => {
  487. if (shouldOpenWarningModal) {
  488. openModal('isWarningVisible');
  489. setShouldOpenWarningModal(false);
  490. }
  491. }}
  492. >
  493. <View style={styles.wrapper}>
  494. <TouchableOpacity
  495. style={styles.btnModalEdit}
  496. onPress={() => {
  497. closeModal('isModalVisible');
  498. setShouldOpenWarningModal(true);
  499. }}
  500. >
  501. <Text style={styles.btnModalEditText}>Unfriend</Text>
  502. <View style={{ transform: 'rotate(180deg)' }}>
  503. <ChevronIcon fill={Colors.DARK_BLUE} height={11} />
  504. </View>
  505. </TouchableOpacity>
  506. </View>
  507. </ReactModal>
  508. <WarningModal
  509. type={'confirm'}
  510. isVisible={modalState.isWarningVisible}
  511. message={`Are you sure you want to unfriend ${data.user_data.first_name} ${data.user_data.last_name}?`}
  512. action={handleUpdateFriendStatus}
  513. onClose={() => closeModal('isWarningVisible')}
  514. title=""
  515. />
  516. <ImageView
  517. images={[
  518. { uri: API_HOST + '/img/avatars/' + data.user_data.avatar + '?v=' + avatarVersion }
  519. ]}
  520. keyExtractor={(imageSrc, index) => index.toString()}
  521. imageIndex={0}
  522. visible={fullSizeImageVisible}
  523. onRequestClose={() => setFullSizeImageVisible(false)}
  524. swipeToCloseEnabled={false}
  525. backgroundColor={Colors.DARK_BLUE}
  526. doubleTapToZoomEnabled={true}
  527. />
  528. </PageWrapper>
  529. );
  530. };
  531. export default ProfileScreen;