Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add new Profile page #20144

Merged
merged 26 commits into from
Jun 8, 2023
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
e1ab57e
Add route and links for new profile page
puneetlath Jun 3, 2023
558b23c
Add ProfilePage
puneetlath Jun 3, 2023
540087d
Use lodashGet for various details
puneetlath Jun 3, 2023
09e6b1f
Show loading indicator when details arent available
puneetlath Jun 3, 2023
e138aee
Use "hidden" as fallback displayName
puneetlath Jun 3, 2023
249369e
Replace getDetailsRoute usage with getProfileRoute
puneetlath Jun 4, 2023
2d9e221
Fetch the personal details from the API when opening profile page
puneetlath Jun 4, 2023
7f5f165
Dont show timezone if we dont have it
puneetlath Jun 4, 2023
f5832a3
Dont show message option if you dont have their login info
puneetlath Jun 4, 2023
b1f7118
Use avatar that we have
puneetlath Jun 4, 2023
05d5979
Use accountID for default avatar if no login
puneetlath Jun 5, 2023
282d9e3
Merge main into branch
puneetlath Jun 5, 2023
516daeb
Use new profile for report participants
puneetlath Jun 5, 2023
b28bf1c
Make component functional
puneetlath Jun 5, 2023
a9631fc
Fix lint error
puneetlath Jun 5, 2023
c1d5958
Add failed to load view
puneetlath Jun 5, 2023
61976f1
Dont make subtitle requied on BlockingView
puneetlath Jun 6, 2023
8b4f566
Fix not found page showing after loading
puneetlath Jun 6, 2023
afb818c
Merge main into branch
puneetlath Jun 7, 2023
922a505
Re-add personalDetailsList onyx key
puneetlath Jun 7, 2023
c27e85c
Update src/libs/actions/PersonalDetails.js
puneetlath Jun 7, 2023
d77d9b5
Update src/libs/actions/PersonalDetails.js
puneetlath Jun 7, 2023
74b5ed2
Update src/libs/actions/PersonalDetails.js
puneetlath Jun 7, 2023
133cd8e
Alex comments
puneetlath Jun 8, 2023
3ddc601
Update src/pages/ProfilePage.js
puneetlath Jun 8, 2023
57beb1d
Prettier fix
puneetlath Jun 8, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions src/ROUTES.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,8 @@ export default {
SET_PASSWORD_WITH_VALIDATE_CODE: 'setpassword/:accountID/:validateCode',
DETAILS: 'details',
getDetailsRoute: (login) => `details?login=${encodeURIComponent(login)}`,
PROFILE: 'a/:accountID',
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋 Just a heads-up that this has caused a regression in #21969
Deep linking wasn't working for the new PROFILE route.

When adding a new route we should also add it to App/well-known
/apple-app-site-association
for iOS

{ "/": "/a/*", "comment": "Profile Page" }

And to AndroidManifest.xml for Android

<data android:scheme="https" android:host="new.expensify.com" android:pathPrefix="/a"/> 

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow. I had no idea. Can we make that obvious somehow? Like with a checklist item or lint rule or something.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@puneetlath, I proposed to add a checklist item in https://expensify.slack.com/archives/C049HHMV9SM/p1690395182200269
Looks like we should add it, I'll do that tomorrow (not sure what the process is just yet, will find it)

getProfileRoute: (accountID) => `a/${accountID}`,
REPORT_PARTICIPANTS: 'r/:reportID/participants',
getReportParticipantsRoute: (reportID) => `r/${reportID}/participants`,
REPORT_PARTICIPANT: 'r/:reportID/participants/details',
Expand Down
3 changes: 2 additions & 1 deletion src/components/ReportWelcomeText.js
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ const ReportWelcomeText = (props) => {
const isChatRoom = ReportUtils.isChatRoom(props.report);
const isDefault = !(isChatRoom || isPolicyExpenseChat);
const participants = lodashGet(props.report, 'participants', []);
const participantAccountIDs = lodashGet(props.report, 'participantAccountIDs', []);
const isMultipleParticipant = participants.length > 1;
const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(OptionsListUtils.getPersonalDetailsForLogins(participants, props.personalDetails), isMultipleParticipant);
const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(props.report);
Expand Down Expand Up @@ -97,7 +98,7 @@ const ReportWelcomeText = (props) => {
<Tooltip text={tooltip}>
<Text
style={[styles.textStrong]}
onPress={() => Navigation.navigate(ROUTES.getDetailsRoute(participants[index]))}
onPress={() => Navigation.navigate(ROUTES.getProfileRoute(participantAccountIDs[index]))}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was overlooked. participantAccountIDs order was different from participants and it caused regression - Web - New Group - Wrong user details page opens on clicking at display names

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

#20583 (comment) for more details about the root cause.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh wow, that makes total sense. Should've considered that. Thanks for the feedback!

>
{displayName}
</Text>
Expand Down
1 change: 1 addition & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ export default {
and: 'and',
details: 'Details',
privacy: 'Privacy',
hidden: 'Hidden',
delete: 'Delete',
archived: 'archived',
contacts: 'Contacts',
Expand Down
1 change: 1 addition & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ export default {
and: 'y',
details: 'Detalles',
privacy: 'Privacidad',
hidden: 'Oculto',
delete: 'Eliminar',
archived: 'archivado',
contacts: 'Contactos',
Expand Down
6 changes: 6 additions & 0 deletions src/libs/Navigation/AppNavigator/AuthScreens.js
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,12 @@ class AuthScreens extends React.Component {
component={ModalStackNavigators.DetailsModalStackNavigator}
listeners={modalScreenListeners}
/>
<RootStack.Screen
name="Profile"
options={modalScreenOptions}
component={ModalStackNavigators.ProfileModalStackNavigator}
listeners={modalScreenListeners}
/>
<RootStack.Screen
name="Report_Details"
options={modalScreenOptions}
Expand Down
11 changes: 11 additions & 0 deletions src/libs/Navigation/AppNavigator/ModalStackNavigators.js
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ const DetailsModalStackNavigator = createModalStackNavigator([
},
]);

const ProfileModalStackNavigator = createModalStackNavigator([
{
getComponent: () => {
const ProfilePage = require('../../../pages/ProfilePage').default;
return ProfilePage;
},
name: 'Profile_Root',
},
]);

const ReportDetailsModalStackNavigator = createModalStackNavigator([
{
getComponent: () => {
Expand Down Expand Up @@ -718,6 +728,7 @@ export {
IOUSendModalStackNavigator,
SplitDetailsModalStackNavigator,
DetailsModalStackNavigator,
ProfileModalStackNavigator,
ReportDetailsModalStackNavigator,
TaskModalStackNavigator,
ReportSettingsModalStackNavigator,
Expand Down
5 changes: 5 additions & 0 deletions src/libs/Navigation/linkingConfig.js
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,11 @@ export default {
Details_Root: ROUTES.DETAILS,
},
},
Profile: {
screens: {
Profile_Root: ROUTES.PROFILE,
},
},
Participants: {
screens: {
ReportParticipants_Root: ROUTES.REPORT_PARTICIPANTS,
Expand Down
6 changes: 3 additions & 3 deletions src/libs/ReportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -1047,14 +1047,14 @@ function getReportName(report) {
* @param {Object} report
*/
function navigateToDetailsPage(report) {
const participants = lodashGet(report, 'participants', []);
const participantAccountIDs = lodashGet(report, 'participantAccountIDs', []);

if (isChatRoom(report) || isPolicyExpenseChat(report) || isThread(report)) {
Navigation.navigate(ROUTES.getReportDetailsRoute(report.reportID));
return;
}
if (participants.length === 1) {
Navigation.navigate(ROUTES.getDetailsRoute(participants[0]));
if (participantAccountIDs.length === 1) {
Navigation.navigate(ROUTES.getProfileRoute(participantAccountIDs[0]));
return;
}
Navigation.navigate(ROUTES.getReportParticipantsRoute(report.reportID));
Expand Down
9 changes: 9 additions & 0 deletions src/libs/actions/PersonalDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,14 @@ function openPersonalDetailsPage() {
API.read('OpenPersonalDetailsPage');
}

/**
* Fetches public profile info about a given user
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
* @param {Number} accountID
*/
function openPublicProfilePage(accountID) {
API.read('OpenPublicProfilePage', {accountID});
}

/**
* Updates the user's avatar image
*
Expand Down Expand Up @@ -427,6 +435,7 @@ export {
deleteAvatar,
openMoneyRequestModalPage,
openPersonalDetailsPage,
openPublicProfilePage,
extractFirstAndLastNameFromAvailableDetails,
updateDisplayName,
updateLegalName,
Expand Down
228 changes: 228 additions & 0 deletions src/pages/ProfilePage.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import React from 'react';
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
import {View, ScrollView} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import {withOnyx} from 'react-native-onyx';
import Str from 'expensify-common/lib/str';
import lodashGet from 'lodash/get';
import {parsePhoneNumber} from 'awesome-phonenumber';
import styles from '../styles/styles';
import Text from '../components/Text';
import ONYXKEYS from '../ONYXKEYS';
import Avatar from '../components/Avatar';
import HeaderWithCloseButton from '../components/HeaderWithCloseButton';
import Navigation from '../libs/Navigation/Navigation';
import ScreenWrapper from '../components/ScreenWrapper';
import personalDetailsPropType from './personalDetailsPropType';
import withLocalize, {withLocalizePropTypes} from '../components/withLocalize';
import compose from '../libs/compose';
import CommunicationsLink from '../components/CommunicationsLink';
import Tooltip from '../components/Tooltip';
import CONST from '../CONST';
import * as ReportUtils from '../libs/ReportUtils';
import * as Expensicons from '../components/Icon/Expensicons';
import MenuItem from '../components/MenuItem';
import AttachmentModal from '../components/AttachmentModal';
import PressableWithoutFocus from '../components/PressableWithoutFocus';
import * as Report from '../libs/actions/Report';
import OfflineWithFeedback from '../components/OfflineWithFeedback';
import AutoUpdateTime from '../components/AutoUpdateTime';
import * as UserUtils from '../libs/UserUtils';
import * as PersonalDetails from '../libs/actions/PersonalDetails';
import FullScreenLoadingIndicator from '../components/FullscreenLoadingIndicator';

const matchType = PropTypes.shape({
params: PropTypes.shape({
/** accountID passed via route /a/:accountID */
accountID: PropTypes.string,

/** report ID passed */
reportID: PropTypes.string,
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
}),
});

const propTypes = {
/* Onyx Props */

/** The personal details of all users */
personalDetails: personalDetailsPropType,
puneetlath marked this conversation as resolved.
Show resolved Hide resolved

/** Route params */
route: matchType.isRequired,

/** Login list for the user that is signed in */
loginList: PropTypes.shape({
/** Date login was validated, used to show info indicator status */
validatedDate: PropTypes.string,

/** Field-specific server side errors keyed by microtime */
errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)),
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
}),

...withLocalizePropTypes,
};

const defaultProps = {
// When opening someone else's profile (via deep link) before login, this is empty
personalDetails: {},
loginList: {},
};

/**
* 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 parsedPhoneNumber = parsePhoneNumber(displayName);
if (parsedPhoneNumber.possible) {
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) : '';
};

class ProfilePage extends React.PureComponent {
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
componentDidMount() {
PersonalDetails.openPublicProfilePage(this.props.route.params.accountID);
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
}

render() {
const accountID = lodashGet(this.props.route.params, 'accountID', 0);
const reportID = lodashGet(this.props.route.params, 'reportID', '');
const details = lodashGet(this.props.personalDetails, accountID, {});
const displayName = details.displayName ? details.displayName : this.props.translate('common.hidden');
const avatar = lodashGet(details, 'avatar', UserUtils.getDefaultAvatar());
const originalFileName = lodashGet(details, 'originalFileName', '');
const login = lodashGet(details, 'login', '');
const timezone = lodashGet(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 shouldShowBackButton = Boolean(reportID);
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
const shouldShowLocalTime = !ReportUtils.hasAutomatedExpensifyEmails([login]) && !_.isEmpty(timezone);

let pronouns = lodashGet(details, 'pronouns', '');
if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) {
const localeKey = pronouns.replace(CONST.PRONOUNS.PREFIX, '');
pronouns = this.props.translate(`pronouns.${localeKey}`);
}

const isSMSLogin = Str.isSMSLogin(login);
const phoneNumber = getPhoneNumber(details);
const phoneOrEmail = isSMSLogin ? getPhoneNumber(details) : login;

const isCurrentUser = _.keys(this.props.loginList).includes(login);

return (
<ScreenWrapper>
<HeaderWithCloseButton
title={this.props.translate('common.profile')}
shouldShowBackButton={shouldShowBackButton}
onBackButtonPress={() => Navigation.goBack()}
onCloseButtonPress={() => Navigation.dismissModal()}
/>
<View
pointerEvents="box-none"
style={[styles.containerWithSpaceBetween]}
>
{_.isEmpty(details) ? (
<FullScreenLoadingIndicator style={styles.flex1} />
) : (
<ScrollView>
<View style={styles.avatarSectionWrapper}>
<AttachmentModal
headerTitle={displayName}
source={UserUtils.getFullSizeAvatar(avatar, login || accountID)}
isAuthTokenRequired
originalFileName={originalFileName}
>
{({show}) => (
<PressableWithoutFocus
style={styles.noOutline}
onPress={show}
>
<OfflineWithFeedback pendingAction={lodashGet(details, 'pendingFields.avatar', null)}>
<Avatar
containerStyles={[styles.avatarLarge, styles.mb3]}
imageStyles={[styles.avatarLarge]}
source={UserUtils.getAvatar(avatar, login || accountID)}
puneetlath marked this conversation as resolved.
Show resolved Hide resolved
size={CONST.AVATAR_SIZE.LARGE}
/>
</OfflineWithFeedback>
</PressableWithoutFocus>
)}
</AttachmentModal>
{Boolean(displayName) && (
<Text
style={[styles.textHeadline, styles.mb6, styles.pre]}
numberOfLines={1}
>
{displayName}
</Text>
)}
{login ? (
<View style={[styles.mb6, styles.detailsPageSectionContainer, styles.w100]}>
<Text
style={[styles.textLabelSupporting, styles.mb1]}
numberOfLines={1}
>
{this.props.translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')}
</Text>
<CommunicationsLink value={phoneOrEmail}>
<Tooltip text={phoneOrEmail}>
<Text numberOfLines={1}>{isSMSLogin ? this.props.formatPhoneNumber(phoneNumber) : login}</Text>
</Tooltip>
</CommunicationsLink>
</View>
) : null}
{pronouns ? (
<View style={[styles.mb6, styles.detailsPageSectionContainer]}>
<Text
style={[styles.textLabelSupporting, styles.mb1]}
numberOfLines={1}
>
{this.props.translate('profilePage.preferredPronouns')}
</Text>
<Text numberOfLines={1}>{pronouns}</Text>
</View>
) : null}
{shouldShowLocalTime && <AutoUpdateTime timezone={timezone} />}
</View>
{!isCurrentUser && Boolean(login) && (
<MenuItem
title={`${this.props.translate('common.message')}${displayName}`}
icon={Expensicons.ChatBubble}
onPress={() => Report.navigateToAndOpenReport([login])}
wrapperStyle={styles.breakAll}
shouldShowRightIcon
/>
)}
</ScrollView>
)}
</View>
</ScreenWrapper>
);
}
}

ProfilePage.propTypes = propTypes;
ProfilePage.defaultProps = defaultProps;

export default compose(
withLocalize,
withOnyx({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
},
loginList: {
key: ONYXKEYS.LOGIN_LIST,
},
}),
)(ProfilePage);
2 changes: 1 addition & 1 deletion src/pages/home/report/ReactionList/BaseReactionList.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ const BaseReactionList = (props) => {
hoverStyle={styles.hoveredComponentBG}
onSelectRow={() => {
props.onClose();
Navigation.navigate(ROUTES.getDetailsRoute(item.login));
Navigation.navigate(ROUTES.getProfileRoute(item.accountID));
}}
option={{
text: Str.removeSMSDomain(item.displayName),
Expand Down
Loading