diff --git a/src/components/DisplayNames/displayNamesPropTypes.js b/src/components/DisplayNames/displayNamesPropTypes.js index 30492b90bfc6..c923b943174f 100644 --- a/src/components/DisplayNames/displayNamesPropTypes.js +++ b/src/components/DisplayNames/displayNamesPropTypes.js @@ -10,8 +10,14 @@ const propTypes = { /** The name to display in bold */ displayName: PropTypes.string, - /** The tooltip to show when the associated name is hovered */ - tooltip: PropTypes.string, + /** The Account ID for the tooltip */ + accountID: PropTypes.string, + + /** The login for the tooltip fallback */ + login: PropTypes.string, + + /** The avatar for the tooltip fallback */ + avatar: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), }), ), diff --git a/src/components/DisplayNames/index.js b/src/components/DisplayNames/index.js index 9eb7d0bc501f..fd1bcaf8a6e0 100644 --- a/src/components/DisplayNames/index.js +++ b/src/components/DisplayNames/index.js @@ -5,6 +5,7 @@ import {propTypes, defaultProps} from './displayNamesPropTypes'; import styles from '../../styles/styles'; import Tooltip from '../Tooltip'; import Text from '../Text'; +import UserDetailsTooltip from '../UserDetailsTooltip'; class DisplayNames extends PureComponent { constructor(props) { @@ -86,11 +87,16 @@ class DisplayNames extends PureComponent { > {this.props.shouldUseFullTitle ? this.props.fullTitle - : _.map(this.props.displayNamesWithTooltips, ({displayName, tooltip}, index) => ( + : _.map(this.props.displayNamesWithTooltips, ({displayName, accountID, avatar, login}, index) => ( - this.getTooltipShiftX(index)} > {/* // We need to get the refs to all the names which will be used to correct @@ -101,7 +107,7 @@ class DisplayNames extends PureComponent { > {displayName} - + {index < this.props.displayNamesWithTooltips.length - 1 && } ))} diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js index fa6330d4703f..c65e6ff8ed9b 100644 --- a/src/components/MultipleAvatars.js +++ b/src/components/MultipleAvatars.js @@ -2,6 +2,7 @@ import React, {memo} from 'react'; import PropTypes from 'prop-types'; import {View} from 'react-native'; import _ from 'underscore'; +import lodashGet from 'lodash/get'; import styles from '../styles/styles'; import Avatar from './Avatar'; import Tooltip from './Tooltip'; @@ -11,6 +12,8 @@ import * as StyleUtils from '../styles/StyleUtils'; import CONST from '../CONST'; import variables from '../styles/variables'; import avatarPropTypes from './avatarPropTypes'; +import UserDetailsTooltip from './UserDetailsTooltip'; +import * as ReportUtils from '../libs/ReportUtils'; const propTypes = { /** Array of avatar URLs or icons */ @@ -74,7 +77,14 @@ const MultipleAvatars = (props) => { if (props.icons.length === 1 && !props.shouldStackHorizontally) { return ( - + { type={props.icons[0].type} /> - + ); } @@ -112,9 +122,9 @@ const MultipleAvatars = (props) => { {props.shouldStackHorizontally ? ( <> {_.map([...props.icons].splice(0, 4), (icon, index) => ( - { type={icon.type} /> - + ))} {props.icons.length > 4 && ( { ) : ( - + {/* View is necessary for tooltip to show for multiple avatars in LHN */} { type={props.icons[0].type} /> - + {props.icons.length === 2 ? ( - + { type={props.icons[1].type} /> - + ) : ( diff --git a/src/components/ReportWelcomeText.js b/src/components/ReportWelcomeText.js index f2a79dd50fe3..52016d285a9b 100644 --- a/src/components/ReportWelcomeText.js +++ b/src/components/ReportWelcomeText.js @@ -4,6 +4,7 @@ import lodashGet from 'lodash/get'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; +import UserDetailsTooltip from './UserDetailsTooltip'; import styles from '../styles/styles'; import Text from './Text'; import withLocalize, {withLocalizePropTypes} from './withLocalize'; @@ -13,7 +14,6 @@ import * as OptionsListUtils from '../libs/OptionsListUtils'; import ONYXKEYS from '../ONYXKEYS'; import Navigation from '../libs/Navigation/Navigation'; import ROUTES from '../ROUTES'; -import Tooltip from './Tooltip'; import reportPropTypes from '../pages/reportPropTypes'; import CONST from '../CONST'; @@ -93,16 +93,16 @@ const ReportWelcomeText = (props) => { {isDefault && ( {props.translate('reportActionsView.beginningOfChatHistory')} - {_.map(displayNamesWithTooltips, ({displayName, pronouns, tooltip}, index) => ( + {_.map(displayNamesWithTooltips, ({displayName, pronouns, accountID}, index) => ( - + Navigation.navigate(ROUTES.getProfileRoute(participantAccountIDs[index]))} > {displayName} - + {!_.isEmpty(pronouns) && {` (${pronouns})`}} {index === displayNamesWithTooltips.length - 1 && .} {index === displayNamesWithTooltips.length - 2 && {` ${props.translate('common.and')} `}} diff --git a/src/components/UserDetailsTooltip/index.js b/src/components/UserDetailsTooltip/index.js new file mode 100644 index 000000000000..493b40a0a5e1 --- /dev/null +++ b/src/components/UserDetailsTooltip/index.js @@ -0,0 +1,52 @@ +import React, {useCallback} from 'react'; +import {View, Text} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import Str from 'expensify-common/lib/str'; +import lodashGet from 'lodash/get'; +import _ from 'underscore'; +import Avatar from '../Avatar'; +import Tooltip from '../Tooltip'; +import {propTypes, defaultProps} from './userDetailsTooltipPropTypes'; +import styles from '../../styles/styles'; +import ONYXKEYS from '../../ONYXKEYS'; + +function UserDetailsTooltip(props) { + const userDetails = lodashGet(props.personalDetailsList, props.accountID, props.fallbackUserDetails); + const renderTooltipContent = useCallback( + () => ( + + + + + + + {String(userDetails.displayName).trim() ? userDetails.displayName : ''} + + + + {String(userDetails.login).trim() && !_.isEqual(userDetails.login, userDetails.displayName) ? Str.removeSMSDomain(userDetails.login) : ''} + + + ), + [userDetails.avatar, userDetails.displayName, userDetails.login], + ); + + if (!userDetails.displayName && !userDetails.login) { + return props.children; + } + + return {props.children}; +} + +UserDetailsTooltip.propTypes = propTypes; +UserDetailsTooltip.defaultProps = defaultProps; +UserDetailsTooltip.displayName = 'UserDetailsTooltip'; + +export default withOnyx({ + personalDetailsList: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, +})(UserDetailsTooltip); diff --git a/src/components/UserDetailsTooltip/userDetailsTooltipPropTypes.js b/src/components/UserDetailsTooltip/userDetailsTooltipPropTypes.js new file mode 100644 index 000000000000..4d34c73ef1ba --- /dev/null +++ b/src/components/UserDetailsTooltip/userDetailsTooltipPropTypes.js @@ -0,0 +1,28 @@ +import PropTypes from 'prop-types'; +import personalDetailsPropType from '../../pages/personalDetailsPropType'; + +const propTypes = { + /** User's Account ID */ + accountID: PropTypes.oneOfType([PropTypes.string, PropTypes.number]).isRequired, + /** Fallback User Details object used if no accountID */ + fallbackUserDetails: PropTypes.shape({ + /** Avatar URL */ + avatar: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + /** Display Name */ + displayName: PropTypes.string, + /** Login */ + login: PropTypes.string, + }), + /** Component that displays the tooltip */ + children: PropTypes.node.isRequired, + /** List of personalDetails (keyed by accountID) */ + personalDetailsList: PropTypes.objectOf(personalDetailsPropType), +}; + +const defaultProps = { + accountID: '', + fallbackUserDetails: {displayName: '', login: '', avatar: ''}, + personalDetailsList: {}, +}; + +export {propTypes, defaultProps}; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index fa8b93741aa3..4fa3d0ed2afc 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -749,6 +749,7 @@ function getIcons(report, personalDetails, defaultIcon = null, isPayer = false) } if (isConciergeChatReport(report)) { result.source = CONST.CONCIERGE_ICON_URL; + result.name = CONST.EMAIL.CONCIERGE; return [result]; } if (isArchivedRoom(report)) { @@ -845,6 +846,16 @@ function getPersonalDetailsForLogin(login) { ); } +/** + * Gets the accountID for a login by looking in the ONYXKEYS.PERSONAL_DETAILS Onyx key (stored in the local variable, allPersonalDetails). If it doesn't exist in Onyx, + * then an empty string is returned. + * @param {String} login + * @returns {String} + */ +function getAccountIDForLogin(login) { + return lodashGet(allPersonalDetails, [login, 'accountID'], ''); +} + /** * Get the displayName for a single report participant. * @@ -873,7 +884,8 @@ function getDisplayNameForParticipant(login, shouldUseShortForm = false) { function getDisplayNamesWithTooltips(participants, isMultipleParticipantReport) { return _.map(participants, (participant) => { const displayName = getDisplayNameForParticipant(participant.login, isMultipleParticipantReport); - const tooltip = participant.login ? Str.removeSMSDomain(participant.login) : ''; + const avatar = UserUtils.getDefaultAvatar(participant.login); + const accountID = participant.accountID; let pronouns = participant.pronouns; if (pronouns && pronouns.startsWith(CONST.PRONOUNS.PREFIX)) { @@ -883,7 +895,9 @@ function getDisplayNamesWithTooltips(participants, isMultipleParticipantReport) return { displayName, - tooltip, + avatar, + login: participant.login, + accountID, pronouns, }; }); @@ -2153,7 +2167,9 @@ function getParentReport(report) { } export { + getAccountIDForLogin, getReportParticipantsTitle, + getPersonalDetailsForLogin, isReportMessageAttachment, findLastAccessedReport, canEditReportAction, diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 909b52da89b3..1cb2c0ef76f0 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -16,7 +16,7 @@ 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 UserDetailsTooltip from '../components/UserDetailsTooltip'; import CONST from '../CONST'; import * as ReportUtils from '../libs/ReportUtils'; import * as Expensicons from '../components/Icon/Expensicons'; @@ -168,9 +168,9 @@ class DetailsPage extends React.PureComponent { {this.props.translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')} - + {isSMSLogin ? this.props.formatPhoneNumber(phoneNumber) : details.login} - + ) : null} diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 932f489be5f4..31d6aeef422e 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -17,7 +17,7 @@ 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 UserDetailsTooltip from '../components/UserDetailsTooltip'; import CONST from '../CONST'; import * as ReportUtils from '../libs/ReportUtils'; import * as Expensicons from '../components/Icon/Expensicons'; @@ -179,9 +179,9 @@ function ProfilePage(props) { {props.translate(isSMSLogin ? 'common.phoneNumber' : 'common.email')} - + {isSMSLogin ? props.formatPhoneNumber(phoneNumber) : login} - + ) : null} diff --git a/src/pages/home/report/ReportActionItemFragment.js b/src/pages/home/report/ReportActionItemFragment.js index e606b0c65f69..07b00590306e 100644 --- a/src/pages/home/report/ReportActionItemFragment.js +++ b/src/pages/home/report/ReportActionItemFragment.js @@ -8,7 +8,6 @@ import variables from '../../../styles/variables'; import themeColors from '../../../styles/themes/default'; import RenderHTML from '../../../components/RenderHTML'; import Text from '../../../components/Text'; -import Tooltip from '../../../components/Tooltip'; import * as EmojiUtils from '../../../libs/EmojiUtils'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; @@ -19,14 +18,15 @@ import {withNetwork} from '../../../components/OnyxProvider'; import CONST from '../../../CONST'; import applyStrikethrough from '../../../components/HTMLEngineProvider/applyStrikethrough'; import editedLabelStyles from '../../../styles/editedLabelStyles'; +import UserDetailsTooltip from '../../../components/UserDetailsTooltip'; const propTypes = { + /** Users accountID */ + accountID: PropTypes.string.isRequired, + /** The message fragment needing to be displayed */ fragment: reportActionFragmentPropTypes.isRequired, - /** Text to be shown for tooltip When Fragment is report Actor */ - tooltipText: PropTypes.string, - /** Is this fragment an attachment? */ isAttachment: PropTypes.bool, @@ -73,7 +73,6 @@ const defaultProps = { }, loading: false, isSingleLine: false, - tooltipText: '', source: '', style: [], }; @@ -146,14 +145,14 @@ const ReportActionItemFragment = (props) => { } case 'TEXT': return ( - + {props.fragment.text} - + ); case 'LINK': return LINK; diff --git a/src/pages/home/report/ReportActionItemMessage.js b/src/pages/home/report/ReportActionItemMessage.js index aa8639ea071c..fef1bb0377c9 100644 --- a/src/pages/home/report/ReportActionItemMessage.js +++ b/src/pages/home/report/ReportActionItemMessage.js @@ -40,6 +40,7 @@ const ReportActionItemMessage = (props) => ( attachmentInfo={props.action.attachmentInfo} pendingAction={props.action.pendingAction} source={lodashGet(props.action, 'originalMessage.source')} + accountID={String(props.action.actorAccountID)} loading={props.action.isLoading} style={props.style} /> diff --git a/src/pages/home/report/ReportActionItemSingle.js b/src/pages/home/report/ReportActionItemSingle.js index eb5f2fa07434..84a89bf7475f 100644 --- a/src/pages/home/report/ReportActionItemSingle.js +++ b/src/pages/home/report/ReportActionItemSingle.js @@ -14,7 +14,6 @@ import withLocalize, {withLocalizePropTypes} from '../../../components/withLocal import Navigation from '../../../libs/Navigation/Navigation'; import ROUTES from '../../../ROUTES'; import {withPersonalDetails} from '../../../components/OnyxProvider'; -import Tooltip from '../../../components/Tooltip'; import ControlSelection from '../../../libs/ControlSelection'; import * as ReportUtils from '../../../libs/ReportUtils'; import OfflineWithFeedback from '../../../components/OfflineWithFeedback'; @@ -22,6 +21,7 @@ import CONST from '../../../CONST'; import SubscriptAvatar from '../../../components/SubscriptAvatar'; import reportPropTypes from '../../reportPropTypes'; import * as UserUtils from '../../../libs/UserUtils'; +import UserDetailsTooltip from '../../../components/UserDetailsTooltip'; const propTypes = { /** All the data of the action */ @@ -100,14 +100,14 @@ const ReportActionItemSingle = (props) => { noMargin /> ) : ( - + - + )} @@ -123,8 +123,8 @@ const ReportActionItemSingle = (props) => { {_.map(personArray, (fragment, index) => ( { expect(ReportUtils.getDisplayNamesWithTooltips(participantsPersonalDetails, false)).toStrictEqual([ { displayName: 'Ragnar Lothbrok', - tooltip: 'ragnar@vikings.net', + login: 'ragnar@vikings.net', + avatar: { + testUri: '../../../assets/images/avatars/user/default-avatar_16.svg', + }, + accountID: 1, pronouns: undefined, }, { displayName: 'floki@vikings.net', - tooltip: 'floki@vikings.net', + avatar: { + testUri: '../../../assets/images/avatars/user/default-avatar_24.svg', + }, + login: 'floki@vikings.net', + accountID: 2, pronouns: undefined, }, { displayName: 'Lagertha Lothbrok', - tooltip: 'lagertha@vikings.net', + avatar: { + testUri: '../../../assets/images/avatars/user/default-avatar_11.svg', + }, + login: 'lagertha@vikings.net', + accountID: 3, pronouns: 'She/her', }, { displayName: '(833) 240-3627', - tooltip: '+18332403627', + avatar: { + testUri: '../../../assets/images/avatars/user/default-avatar_15.svg', + }, + login: '+18332403627@expensify.sms', + accountID: 4, pronouns: undefined, }, ]); @@ -80,22 +100,38 @@ describe('ReportUtils', () => { expect(ReportUtils.getDisplayNamesWithTooltips(participantsPersonalDetails, true)).toStrictEqual([ { displayName: 'Ragnar', - tooltip: 'ragnar@vikings.net', + login: 'ragnar@vikings.net', + avatar: { + testUri: '../../../assets/images/avatars/user/default-avatar_16.svg', + }, + accountID: 1, pronouns: undefined, }, { displayName: 'floki@vikings.net', - tooltip: 'floki@vikings.net', + avatar: { + testUri: '../../../assets/images/avatars/user/default-avatar_24.svg', + }, + login: 'floki@vikings.net', + accountID: 2, pronouns: undefined, }, { displayName: 'Lagertha', - tooltip: 'lagertha@vikings.net', + avatar: { + testUri: '../../../assets/images/avatars/user/default-avatar_11.svg', + }, + login: 'lagertha@vikings.net', + accountID: 3, pronouns: 'She/her', }, { displayName: '(833) 240-3627', - tooltip: '+18332403627', + avatar: { + testUri: '../../../assets/images/avatars/user/default-avatar_15.svg', + }, + login: '+18332403627@expensify.sms', + accountID: 4, pronouns: undefined, }, ]);