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,
},