Skip to content

Commit

Permalink
Merge pull request #34974 from pasyukevich/feature/migrate-Profile
Browse files Browse the repository at this point in the history
[TS migration] Migrate 'Profile' page to TypeScript
  • Loading branch information
MonilBhavsar authored Mar 11, 2024
2 parents 77375e8 + 8a0af88 commit 60a86c9
Show file tree
Hide file tree
Showing 2 changed files with 96 additions and 116 deletions.
1 change: 1 addition & 0 deletions src/libs/Navigation/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -256,6 +256,7 @@ type ProfileNavigatorParamList = {
[SCREENS.PROFILE_ROOT]: {
accountID: string;
reportID: string;
backTo: Routes;
};
};

Expand Down
211 changes: 95 additions & 116 deletions src/pages/ProfilePage.js → src/pages/ProfilePage.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -20,132 +19,118 @@ 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<PersonalDetailsList>;

/** report ID passed */
reportID: PropTypes.string,
}),
});
/** The report currently being looked at */
report: OnyxEntry<Report>;

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<Report>;

/** Session info for the currently logged in user. */
session: PropTypes.shape({
/** Currently logged in user accountID */
accountID: PropTypes.number,
}),

...withLocalizePropTypes,
session: OnyxEntry<Session>;
};

const defaultProps = {
// When opening someone else's profile (via deep link) before login, this is empty
personalDetails: {},
session: {
accountID: 0,
},
};
type ProfilePageProps = ProfilePageOnyxProps & StackScreenProps<ProfileNavigatorParamList, typeof SCREENS.PROFILE_ROOT>;

/**
* 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]);

return (
<ScreenWrapper testID={ProfilePage.displayName}>
<FullPageNotFoundView shouldShow={shouldShowBlockingView || CONST.RESTRICTED_ACCOUNT_IDS.includes(accountID)}>
<HeaderWithBackButton
title={props.translate('common.profile')}
title={translate('common.profile')}
onBackButtonPress={() => Navigation.goBack(navigateBackTo)}
/>
<View style={[styles.containerWithSpaceBetween, styles.pointerEventsBoxNone]}>
Expand All @@ -155,10 +140,10 @@ function ProfilePage(props) {
<PressableWithoutFocus
style={[styles.noOutline]}
onPress={() => Navigation.navigate(ROUTES.PROFILE_AVATAR.getRoute(String(accountID)))}
accessibilityLabel={props.translate('common.profile')}
accessibilityLabel={translate('common.profile')}
accessibilityRole={CONST.ACCESSIBILITY_ROLE.IMAGEBUTTON}
>
<OfflineWithFeedback pendingAction={lodashGet(details, 'pendingFields.avatar', null)}>
<OfflineWithFeedback pendingAction={details?.pendingFields?.avatar}>
<Avatar
containerStyles={[styles.avatarXLarge, styles.mb3]}
imageStyles={[styles.avatarXLarge]}
Expand All @@ -182,7 +167,7 @@ function ProfilePage(props) {
style={[styles.textLabelSupporting, styles.mb1]}
numberOfLines={1}
>
{props.translate('statusPage.status')}
{translate('statusPage.status')}
</Text>
<Text>{statusContent}</Text>
</View>
Expand All @@ -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')}
</Text>
<CommunicationsLink value={phoneOrEmail}>
<CommunicationsLink value={phoneOrEmail ?? ''}>
<UserDetailsTooltip accountID={details.accountID}>
<Text numberOfLines={1}>{isSMSLogin ? props.formatPhoneNumber(phoneNumber) : login}</Text>
<Text numberOfLines={1}>{isSMSLogin ? formatPhoneNumber(phoneNumber ?? '') : login}</Text>
</UserDetailsTooltip>
</CommunicationsLink>
</View>
Expand All @@ -209,7 +194,7 @@ function ProfilePage(props) {
style={[styles.textLabelSupporting, styles.mb1]}
numberOfLines={1}
>
{props.translate('profilePage.preferredPronouns')}
{translate('profilePage.preferredPronouns')}
</Text>
<Text numberOfLines={1}>{pronouns}</Text>
</View>
Expand All @@ -220,30 +205,30 @@ function ProfilePage(props) {
<MenuItemWithTopDescription
shouldShowRightIcon
title={notificationPreference}
description={props.translate('notificationPreferencesPage.label')}
onPress={() => 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() && (
<MenuItem
title={`${props.translate('common.message')}${displayName}`}
title={`${translate('common.message')}${displayName}`}
titleStyle={styles.flex1}
icon={Expensicons.ChatBubble}
onPress={() => Report.navigateToAndOpenReportWithAccountIDs([accountID])}
onPress={() => ReportActions.navigateToAndOpenReportWithAccountIDs([accountID])}
wrapperStyle={styles.breakAll}
shouldShowRightIcon
/>
)}
{!_.isEmpty(props.report) && !isCurrentUser && (
{!isEmptyObject(report) && !isCurrentUser && (
<MenuItem
title={`${props.translate('privateNotes.title')}`}
title={`${translate('privateNotes.title')}`}
titleStyle={styles.flex1}
icon={Expensicons.Pencil}
onPress={() => 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}
/>
)}
</ScrollView>
Expand All @@ -255,49 +240,43 @@ 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 =>
(report && {
reportID: report.reportID,
participantAccountIDs: report.participantAccountIDs,
parentReportID: report.parentReportID,
parentReportActionID: report.parentReportActionID,
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<ProfilePageProps, ProfilePageOnyxProps>({
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);

0 comments on commit 60a86c9

Please sign in to comment.