diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 9f100d0e1efb..8fa7290323a2 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -256,6 +256,7 @@ type ProfileNavigatorParamList = { [SCREENS.PROFILE_ROOT]: { accountID: string; reportID: string; + backTo: Routes; }; }; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.tsx similarity index 63% rename from src/pages/ProfilePage.js rename to src/pages/ProfilePage.tsx index cc533dbc3a08..a9c350102424 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.tsx @@ -1,10 +1,9 @@ +import type {StackScreenProps} from '@react-navigation/stack'; import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import AutoUpdateTime from '@components/AutoUpdateTime'; import Avatar from '@components/Avatar'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; @@ -20,124 +19,110 @@ import ScreenWrapper from '@components/ScreenWrapper'; import ScrollView from '@components/ScrollView'; import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; -import * as PersonalDetails from '@userActions/PersonalDetails'; -import * as Report from '@userActions/Report'; -import * as Session from '@userActions/Session'; +import type {ProfileNavigatorParamList} from '@navigation/types'; +import * as PersonalDetailsActions from '@userActions/PersonalDetails'; +import * as ReportActions from '@userActions/Report'; +import * as SessionActions from '@userActions/Session'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import personalDetailsPropType from './personalDetailsPropType'; +import type SCREENS from '@src/SCREENS'; +import type {PersonalDetails, PersonalDetailsList, Report, Session} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; -const matchType = PropTypes.shape({ - params: PropTypes.shape({ - /** accountID passed via route /a/:accountID */ - accountID: PropTypes.string, +type ProfilePageOnyxProps = { + /** The personal details of the person who is logged in */ + personalDetails: OnyxEntry; - /** report ID passed */ - reportID: PropTypes.string, - }), -}); + /** The report currently being looked at */ + report: OnyxEntry; -const propTypes = { - /* Onyx Props */ - - /** The personal details of all users */ - personalDetails: PropTypes.objectOf(personalDetailsPropType), - - /** Route params */ - route: matchType.isRequired, + /** The list of all reports + * ONYXKEYS.COLLECTION.REPORT is needed for report key function + */ + // eslint-disable-next-line react/no-unused-prop-types + reports: OnyxCollection; /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), - - ...withLocalizePropTypes, + session: OnyxEntry; }; -const defaultProps = { - // When opening someone else's profile (via deep link) before login, this is empty - personalDetails: {}, - session: { - accountID: 0, - }, -}; +type ProfilePageProps = ProfilePageOnyxProps & StackScreenProps; /** * Gets the phone number to display for SMS logins - * - * @param {Object} details - * @param {String} details.login - * @param {String} details.displayName - * @returns {String} */ -const getPhoneNumber = (details) => { - // If the user hasn't set a displayName, it is set to their phone number, so use that - const displayName = lodashGet(details, 'displayName', ''); +const getPhoneNumber = ({login = '', displayName = ''}: PersonalDetails | EmptyObject): string | undefined => { + // If the user hasn't set a displayName, it is set to their phone number const parsedPhoneNumber = parsePhoneNumber(displayName); + if (parsedPhoneNumber.possible) { - return parsedPhoneNumber.number.e164; + return parsedPhoneNumber?.number?.e164; } // If the user has set a displayName, get the phone number from the SMS login - return details.login ? Str.removeSMSDomain(details.login) : ''; + return login ? Str.removeSMSDomain(login) : ''; }; -function ProfilePage(props) { +function ProfilePage({personalDetails, route, session, report}: ProfilePageProps) { const styles = useThemeStyles(); - const accountID = Number(lodashGet(props.route.params, 'accountID', 0)); - const isCurrentUser = props.session.accountID === accountID; + const {translate, formatPhoneNumber} = useLocalize(); + const accountID = Number(route.params?.accountID ?? 0); + const isCurrentUser = session?.accountID === accountID; + const details: PersonalDetails | EmptyObject = personalDetails?.[accountID] ?? (ValidationUtils.isValidAccountRoute(accountID) ? {} : {isLoading: false, accountID: 0, avatar: ''}); - const details = lodashGet(props.personalDetails, accountID, ValidationUtils.isValidAccountRoute(accountID) ? {} : {isloading: false}); const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details, undefined, undefined, isCurrentUser); - const avatar = lodashGet(details, 'avatar', UserUtils.getDefaultAvatar()); - const fallbackIcon = lodashGet(details, 'fallbackIcon', ''); - const login = lodashGet(details, 'login', ''); - const timezone = lodashGet(details, 'timezone', {}); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const avatar = details?.avatar || UserUtils.getDefaultAvatar(); // we can have an empty string and in this case, we need to show the default avatar + const fallbackIcon = details?.fallbackIcon ?? ''; + const login = details?.login ?? ''; + const timezone = details?.timezone; // If we have a reportID param this means that we // arrived here via the ParticipantsPage and should be allowed to navigate back to it - const shouldShowLocalTime = !ReportUtils.hasAutomatedExpensifyAccountIDs([accountID]) && !_.isEmpty(timezone); - let pronouns = lodashGet(details, 'pronouns', ''); - if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { + const shouldShowLocalTime = !ReportUtils.hasAutomatedExpensifyAccountIDs([accountID]) && !isEmptyObject(timezone); + let pronouns = details?.pronouns ?? ''; + if (pronouns?.startsWith(CONST.PRONOUNS.PREFIX)) { const localeKey = pronouns.replace(CONST.PRONOUNS.PREFIX, ''); - pronouns = props.translate(`pronouns.${localeKey}`); + pronouns = translate(`pronouns.${localeKey}` as TranslationPaths); } const isSMSLogin = Str.isSMSLogin(login); const phoneNumber = getPhoneNumber(details); const phoneOrEmail = isSMSLogin ? getPhoneNumber(details) : login; - const hasMinimumDetails = !_.isEmpty(details.avatar); - const isLoading = lodashGet(details, 'isLoading', false) || _.isEmpty(details); + const hasMinimumDetails = !isEmptyObject(details.avatar); + const isLoading = Boolean(details?.isLoading) || isEmptyObject(details); // If the API returns an error for some reason there won't be any details and isLoading will get set to false, so we want to show a blocking screen const shouldShowBlockingView = !hasMinimumDetails && !isLoading; - const statusEmojiCode = lodashGet(details, 'status.emojiCode', ''); - const statusText = lodashGet(details, 'status.text', ''); + const statusEmojiCode = details?.status?.emojiCode ?? ''; + const statusText = details?.status?.text ?? ''; const hasStatus = !!statusEmojiCode; const statusContent = `${statusEmojiCode} ${statusText}`; - const navigateBackTo = lodashGet(props.route, 'params.backTo'); + const navigateBackTo = route?.params?.backTo; - const shouldShowNotificationPreference = !_.isEmpty(props.report) && !isCurrentUser && props.report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; - const notificationPreference = shouldShowNotificationPreference ? props.translate(`notificationPreferencesPage.notificationPreferences.${props.report.notificationPreference}`) : ''; + const shouldShowNotificationPreference = !isEmptyObject(report) && !isCurrentUser && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN; + const notificationPreference = shouldShowNotificationPreference + ? translate(`notificationPreferencesPage.notificationPreferences.${report.notificationPreference}` as TranslationPaths) + : ''; // eslint-disable-next-line rulesdir/prefer-early-return useEffect(() => { if (ValidationUtils.isValidAccountRoute(accountID)) { - PersonalDetails.openPublicProfilePage(accountID); + PersonalDetailsActions.openPublicProfilePage(accountID); } }, [accountID]); @@ -145,7 +130,7 @@ function ProfilePage(props) { Navigation.goBack(navigateBackTo)} /> @@ -155,10 +140,10 @@ function ProfilePage(props) { Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(String(accountID)))} - accessibilityLabel={props.translate('common.profile')} + accessibilityLabel={translate('common.profile')} accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON} > - + - {props.translate('statusPage.status')} + {translate('statusPage.status')} {statusContent} @@ -194,11 +179,11 @@ function ProfilePage(props) { style={[styles.textLabelSupporting, styles.mb1]} numberOfLines={1} > - {props.translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')} + {translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')} - + - {isSMSLogin ? props.formatPhoneNumber(phoneNumber) : login} + {isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : login} @@ -209,7 +194,7 @@ function ProfilePage(props) { style={[styles.textLabelSupporting, styles.mb1]} numberOfLines={1} > - {props.translate('profilePage.preferredPronouns')} + {translate('profilePage.preferredPronouns')} {pronouns} @@ -220,30 +205,30 @@ function ProfilePage(props) { Navigation.navigate(ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES.getRoute(props.report.reportID))} + description={translate('notificationPreferencesPage.label')} + onPress={() => Navigation.navigate(ROUTES.REPORT_SETTINGS_NOTIFICATION_PREFERENCES.getRoute(report.reportID))} wrapperStyle={[styles.mtn6, styles.mb5]} /> )} - {!isCurrentUser && !Session.isAnonymousUser() && ( + {!isCurrentUser && !SessionActions.isAnonymousUser() && ( Report.navigateToAndOpenReportWithAccountIDs([accountID])} + onPress={() => ReportActions.navigateToAndOpenReportWithAccountIDs([accountID])} wrapperStyle={styles.breakAll} shouldShowRightIcon /> )} - {!_.isEmpty(props.report) && !isCurrentUser && ( + {!isEmptyObject(report) && !isCurrentUser && ( ReportUtils.navigateToPrivateNotes(props.report, props.session)} + onPress={() => ReportUtils.navigateToPrivateNotes(report, session)} wrapperStyle={styles.breakAll} shouldShowRightIcon - brickRoadIndicator={Report.hasErrorInPrivateNotes(props.report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} + brickRoadIndicator={ReportActions.hasErrorInPrivateNotes(report) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined} /> )} @@ -255,18 +240,14 @@ function ProfilePage(props) { ); } -ProfilePage.propTypes = propTypes; -ProfilePage.defaultProps = defaultProps; ProfilePage.displayName = 'ProfilePage'; /** * This function narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - * @param {Object} [report] - * @returns {Object|undefined} */ -const chatReportSelector = (report) => - report && { +const chatReportSelector = (report: OnyxEntry): Report => + (report && { reportID: report.reportID, participantAccountIDs: report.participantAccountIDs, parentReportID: report.parentReportID, @@ -274,30 +255,28 @@ const chatReportSelector = (report) => type: report.type, chatType: report.chatType, isPolicyExpenseChat: report.isPolicyExpenseChat, - }; + }) as Report; -export default compose( - withLocalize, - withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - session: { - key: ONYXKEYS.SESSION, - }, - report: { - key: ({route, session, reports}) => { - const accountID = Number(lodashGet(route.params, 'accountID', 0)); - const reportID = lodashGet(ReportUtils.getChatByParticipants([accountID], reports), 'reportID', ''); - if ((session && Number(session.accountID) === accountID) || Session.isAnonymousUser() || !reportID) { - return null; - } - return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; - }, +export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + selector: chatReportSelector, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + session: { + key: ONYXKEYS.SESSION, + }, + report: { + key: ({route, session, reports}) => { + const accountID = Number(route.params?.accountID ?? 0); + const reportID = ReportUtils.getChatByParticipants([accountID], reports)?.reportID ?? ''; + + if ((Boolean(session) && Number(session?.accountID) === accountID) || SessionActions.isAnonymousUser() || !reportID) { + return `${ONYXKEYS.COLLECTION.REPORT}0`; + } + return `${ONYXKEYS.COLLECTION.REPORT}${reportID}`; }, - }), -)(ProfilePage); + }, +})(ProfilePage);