diff --git a/src/languages/en.js b/src/languages/en.js index 913d4669aac1..3b69d9dd8c5b 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -121,6 +121,7 @@ export default { message: 'Message ', leaveRoom: 'Leave room', you: 'You', + youAfterPreposition: 'you', your: 'your', conciergeHelp: 'Please reach out to Concierge for help.', maxParticipantsReached: ({count}) => `You've selected the maximum number (${count}) of participants.`, @@ -250,6 +251,7 @@ export default { editComment: 'Edit comment', deleteComment: 'Delete comment', deleteConfirmation: 'Are you sure you want to delete this comment?', + onlyVisible: 'Only visible to', }, emojiReactions: { addReactionTooltip: 'Add reaction', diff --git a/src/languages/es.js b/src/languages/es.js index ba6e38232047..27999a1ef8b9 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -120,6 +120,7 @@ export default { message: 'Chatear con ', leaveRoom: 'Salir de la sala de chat', you: 'Tú', + youAfterPreposition: 'ti', your: 'tu', conciergeHelp: 'Por favor, contacta con Concierge para obtener ayuda.', maxParticipantsReached: ({count}) => `Has seleccionado el número máximo (${count}) de participantes.`, @@ -249,6 +250,7 @@ export default { editComment: 'Editar comentario', deleteComment: 'Eliminar comentario', deleteConfirmation: '¿Estás seguro de que quieres eliminar este comentario?', + onlyVisible: 'Visible sólo para', }, emojiReactions: { addReactionTooltip: 'Añadir una reacción', diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index f35f2ceb7784..ad3272accab6 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -860,12 +860,7 @@ function getReportName(report, policies = {}) { const participantsWithoutCurrentUser = _.without(participants, sessionEmail); const isMultipleParticipantReport = participantsWithoutCurrentUser.length > 1; - const displayNames = []; - for (let i = 0; i < participantsWithoutCurrentUser.length; i++) { - const login = participantsWithoutCurrentUser[i]; - displayNames.push(getDisplayNameForParticipant(login, isMultipleParticipantReport)); - } - return displayNames.join(', '); + return _.map(participantsWithoutCurrentUser, login => getDisplayNameForParticipant(login, isMultipleParticipantReport)).join(', '); } /** @@ -1707,6 +1702,32 @@ function canLeaveRoom(report, isPolicyMember) { return true; } +/** + * Returns display names for those that can see the whisper. + * However, it returns "you" if the current user is the only one who can see it besides the person that sent it. + * + * @param {string[]} participants + * @returns {string} + */ +function getWhisperDisplayNames(participants) { + const isWhisperOnlyVisibleToCurrentUSer = this.isCurrentUserTheOnlyParticipant(participants); + + // When the current user is the only participant, the display name needs to be "you" because that's the only person reading it + if (isWhisperOnlyVisibleToCurrentUSer) { + return Localize.translateLocal('common.youAfterPreposition'); + } + + return _.map(participants, login => getDisplayNameForParticipant(login, !isWhisperOnlyVisibleToCurrentUSer)).join(', '); +} + +/** + * @param {string[]} participants + * @returns {Boolean} + */ +function isCurrentUserTheOnlyParticipant(participants) { + return participants && participants.length === 1 && participants[0] === sessionEmail; +} + export { getReportParticipantsTitle, isReportMessageAttachment, @@ -1727,6 +1748,7 @@ export { isPolicyExpenseChatAdmin, isPublicRoom, isConciergeChatReport, + isCurrentUserTheOnlyParticipant, hasAutomatedExpensifyEmails, hasExpensifyGuidesEmails, hasOutstandingIOU, @@ -1775,4 +1797,5 @@ export { getSmallSizeAvatar, getMoneyRequestOptions, canRequestMoney, + getWhisperDisplayNames, }; diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index c4e7d6c5fe69..31bfb49bfbe5 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -24,7 +24,12 @@ import * as DeviceCapabilities from '../../../libs/DeviceCapabilities'; import MiniReportActionContextMenu from './ContextMenu/MiniReportActionContextMenu'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import * as ContextMenuActions from './ContextMenu/ContextMenuActions'; -import {withBlockedFromConcierge, withNetwork, withReportActionsDrafts} from '../../../components/OnyxProvider'; +import { + withBlockedFromConcierge, + withNetwork, + withPersonalDetails, + withReportActionsDrafts, +} from '../../../components/OnyxProvider'; import RenameAction from '../../../components/ReportActionItem/RenameAction'; import InlineSystemMessage from '../../../components/InlineSystemMessage'; import styles from '../../../styles/styles'; @@ -40,6 +45,11 @@ import ChronosOOOListActions from '../../../components/ReportActionItem/ChronosO import ReportActionItemReactions from '../../../components/Reactions/ReportActionItemReactions'; import * as Report from '../../../libs/actions/Report'; import withLocalize from '../../../components/withLocalize'; +import Icon from '../../../components/Icon'; +import * as Expensicons from '../../../components/Icon/Expensicons'; +import Text from '../../../components/Text'; +import DisplayNames from '../../../components/DisplayNames'; +import personalDetailsPropType from '../../personalDetailsPropType'; const propTypes = { /** Report for this action */ @@ -69,6 +79,9 @@ const propTypes = { /** Stores user's preferred skin tone */ preferredSkinTone: PropTypes.oneOfType([PropTypes.number, PropTypes.string]), + /** All of the personalDetails */ + personalDetails: PropTypes.objectOf(personalDetailsPropType), + ...windowDimensionsPropTypes, }; @@ -76,6 +89,7 @@ const defaultProps = { draftMessage: '', hasOutstandingIOU: false, preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, + personalDetails: {}, }; class ReportActionItem extends Component { @@ -100,7 +114,8 @@ class ReportActionItem extends Component { || !_.isEqual(this.props.action, nextProps.action) || this.state.isContextMenuActive !== nextState.isContextMenuActive || lodashGet(this.props.report, 'statusNum') !== lodashGet(nextProps.report, 'statusNum') - || lodashGet(this.props.report, 'stateNum') !== lodashGet(nextProps.report, 'stateNum'); + || lodashGet(this.props.report, 'stateNum') !== lodashGet(nextProps.report, 'stateNum') + || this.props.translate !== nextProps.translate; } componentDidUpdate(prevProps) { @@ -239,6 +254,13 @@ class ReportActionItem extends Component { if (this.props.action.actionName === CONST.REPORT.ACTIONS.TYPE.CHRONOSOOOLIST) { return ; } + + const whisperedTo = lodashGet(this.props.action, 'whisperedTo', []); + const isWhisper = whisperedTo.length > 0; + const isMultipleParticipant = whisperedTo.length > 1; + const isWhisperOnlyVisibleByUser = isWhisper && ReportUtils.isCurrentUserTheOnlyParticipant(whisperedTo); + const whisperedToPersonalDetails = isWhisper ? _.filter(this.props.personalDetails, details => _.includes(whisperedTo, details.login)) : []; + const displayNamesWithTooltips = isWhisper ? ReportUtils.getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant) : []; return ( + {isWhisper && ( + + + + + + {this.props.translate('reportActionContextMenu.onlyVisible')} +   + + + + )} {!this.props.displayAsGroup ? ( - + {this.renderItemContent(hovered || this.state.isContextMenuActive)} ) : ( - + {this.renderItemContent(hovered || this.state.isContextMenuActive)} )} @@ -318,6 +364,7 @@ export default compose( withWindowDimensions, withLocalize, withNetwork(), + withPersonalDetails(), withBlockedFromConcierge({propName: 'blockedFromConcierge'}), withReportActionsDrafts({ propName: 'draftMessage', diff --git a/src/pages/home/report/ReportActionItemGrouped.js b/src/pages/home/report/ReportActionItemGrouped.js index e811144ccaad..9c65afd9b6f3 100644 --- a/src/pages/home/report/ReportActionItemGrouped.js +++ b/src/pages/home/report/ReportActionItemGrouped.js @@ -6,10 +6,18 @@ import styles from '../../../styles/styles'; const propTypes = { /** Children view component for this action item */ children: PropTypes.node.isRequired, + + /** Styles for the outermost View */ + // eslint-disable-next-line react/forbid-prop-types + wrapperStyles: PropTypes.arrayOf(PropTypes.object), +}; + +const defaultProps = { + wrapperStyles: [styles.chatItem], }; const ReportActionItemGrouped = props => ( - + {props.children} @@ -17,5 +25,6 @@ const ReportActionItemGrouped = props => ( ); ReportActionItemGrouped.propTypes = propTypes; +ReportActionItemGrouped.defaultProps = defaultProps; ReportActionItemGrouped.displayName = 'ReportActionItemGrouped'; export default ReportActionItemGrouped; diff --git a/src/pages/home/report/reportActionPropTypes.js b/src/pages/home/report/reportActionPropTypes.js index f5821dfb9323..46cba2b5c365 100644 --- a/src/pages/home/report/reportActionPropTypes.js +++ b/src/pages/home/report/reportActionPropTypes.js @@ -29,4 +29,7 @@ export default { /** Error message that's come back from the server. */ error: PropTypes.string, + + /** Emails of the people to which the whisper was sent to (if any). Returns empty array if it is not a whisper */ + whisperedTo: PropTypes.arrayOf(PropTypes.string), }; diff --git a/src/styles/styles.js b/src/styles/styles.js index e1b67e42b2b1..13b3dff8616e 100644 --- a/src/styles/styles.js +++ b/src/styles/styles.js @@ -3136,6 +3136,10 @@ const styles = { textAlign: 'center', }, + whisper: { + backgroundColor: themeColors.cardBG, + }, + popoverMaxWidth: { maxWidth: 375, }, diff --git a/src/styles/utilities/spacing.js b/src/styles/utilities/spacing.js index 92445523120e..d56344c74bcf 100644 --- a/src/styles/utilities/spacing.js +++ b/src/styles/utilities/spacing.js @@ -370,6 +370,10 @@ export default { paddingLeft: 20, }, + pl6: { + paddingLeft: 24, + }, + pl10: { paddingLeft: 40, },