diff --git a/src/CONST.js b/src/CONST.js index d169485906e4..4f05bc37e770 100755 --- a/src/CONST.js +++ b/src/CONST.js @@ -552,8 +552,14 @@ const CONST = { AVATAR_SIZE: { LARGE: 'large', DEFAULT: 'default', + SMALL: 'small', + SUBSCRIPT: 'subscript', + SMALL_SUBSCRIPT: 'small-subscript', + }, + OPTION_MODE: { + COMPACT: 'compact', + DEFAULT: 'default', }, - PHONE_MAX_LENGTH: 15, PHONE_MIN_LENGTH: 5, REGEX: { diff --git a/src/components/Avatar.js b/src/components/Avatar.js index 0e6aa6a381e6..fd3571ab4a9e 100644 --- a/src/components/Avatar.js +++ b/src/components/Avatar.js @@ -1,14 +1,17 @@ import React, {PureComponent} from 'react'; import {Image, View} from 'react-native'; import PropTypes from 'prop-types'; -import styles from '../styles/styles'; -import themeColors from '../styles/themes/default'; -import RoomAvatar from './RoomAvatar'; +import _ from 'underscore'; import stylePropTypes from '../styles/stylePropTypes'; +import Icon from './Icon'; +import themeColors from '../styles/themes/default'; +import CONST from '../CONST'; +import * as StyleUtils from '../styles/StyleUtils'; + const propTypes = { - /** Url source for the avatar */ - source: PropTypes.string, + /** Source for the avatar. Can be a URL or an icon. */ + source: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), /** Extra styles to pass to Image */ imageStyles: PropTypes.arrayOf(PropTypes.object), @@ -17,43 +20,39 @@ const propTypes = { containerStyles: stylePropTypes, /** Set the size of Avatar */ - size: PropTypes.oneOf(['default', 'small']), + size: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), - /** Whether this avatar is for a chat room */ - isChatRoom: PropTypes.bool, - - /** Whether this avatar is for an archived default room */ - isArchivedRoom: PropTypes.bool, + /** The fill color for the icon. Can be hex, rgb, rgba, or valid react-native named color such as 'red' or 'blue' */ + fill: PropTypes.string, }; const defaultProps = { - source: '', + source: null, imageStyles: [], containerStyles: [], - size: 'default', - isChatRoom: false, - isArchivedRoom: false, + size: CONST.AVATAR_SIZE.DEFAULT, + fill: themeColors.icon, }; class Avatar extends PureComponent { render() { - if (!this.props.source && !this.props.isChatRoom) { + if (!this.props.source) { return null; } const imageStyle = [ - this.props.size === 'small' ? styles.avatarSmall : styles.avatarNormal, - - // Background color isn't added for room avatar because it changes it's shape to a square - this.props.isChatRoom ? {} : {backgroundColor: themeColors.icon}, + StyleUtils.getAvatarStyle(this.props.size), ...this.props.imageStyles, ]; + const iconSize = StyleUtils.getAvatarSize(this.props.size); return ( - {this.props.isChatRoom - ? - : } + { + _.isFunction(this.props.source) + ? + : + } ); } diff --git a/src/components/Icon/Expensicons.js b/src/components/Icon/Expensicons.js index 340e8e5ad25e..40e0342153b6 100644 --- a/src/components/Icon/Expensicons.js +++ b/src/components/Icon/Expensicons.js @@ -70,8 +70,11 @@ import Users from '../../../assets/images/users.svg'; import Venmo from '../../../assets/images/venmo.svg'; import Wallet from '../../../assets/images/wallet.svg'; import Workspace from '../../../assets/images/workspace-default-avatar.svg'; +import ActiveRoomAvatar from '../../../assets/images/avatars/room.svg'; +import DeletedRoomAvatar from '../../../assets/images/avatars/deleted-room.svg'; export { + ActiveRoomAvatar, Android, Apple, ArrowRight, @@ -92,6 +95,7 @@ export { ClosedSign, Concierge, CreditCard, + DeletedRoomAvatar, DownArrow, Download, Emoji, diff --git a/src/components/MultipleAvatars.js b/src/components/MultipleAvatars.js index 1d65a5469481..993d36123c3a 100644 --- a/src/components/MultipleAvatars.js +++ b/src/components/MultipleAvatars.js @@ -1,62 +1,57 @@ import React, {memo} from 'react'; import PropTypes from 'prop-types'; import {Image, View} from 'react-native'; +import _ from 'underscore'; import styles from '../styles/styles'; import Avatar from './Avatar'; import Tooltip from './Tooltip'; import Text from './Text'; +import themeColors from '../styles/themes/default'; +import * as StyleUtils from '../styles/StyleUtils'; +import CONST from '../CONST'; const propTypes = { - /** Array of avatar URL */ - avatarImageURLs: PropTypes.arrayOf(PropTypes.string), + /** Array of avatar URLs or icons */ + avatarIcons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])), - /** Set the sie of avatars */ - size: PropTypes.oneOf(['default', 'small']), + /** Set the size of avatars */ + size: PropTypes.oneOf(_.values(CONST.AVATAR_SIZE)), /** Style for Second Avatar */ // eslint-disable-next-line react/forbid-prop-types secondAvatarStyle: PropTypes.arrayOf(PropTypes.object), - /** Whether this avatar is for a chat room */ - isChatRoom: PropTypes.bool, - - /** Whether this avatar is for an archived room */ - isArchivedRoom: PropTypes.bool, - /** Tooltip for the Avatar */ avatarTooltips: PropTypes.arrayOf(PropTypes.string), }; const defaultProps = { - avatarImageURLs: [], - size: 'default', - secondAvatarStyle: [styles.secondAvatarHovered], - isChatRoom: false, - isArchivedRoom: false, + avatarIcons: [], + size: CONST.AVATAR_SIZE.DEFAULT, + secondAvatarStyle: [StyleUtils.getBackgroundAndBorderStyle(themeColors.componentBG)], avatarTooltips: [], }; const MultipleAvatars = (props) => { - const avatarContainerStyles = props.size === 'small' ? styles.emptyAvatarSmall : styles.emptyAvatar; - const singleAvatarStyles = props.size === 'small' ? styles.singleAvatarSmall : styles.singleAvatar; + const avatarContainerStyles = props.size === CONST.AVATAR_SIZE.SMALL ? styles.emptyAvatarSmall : styles.emptyAvatar; + const singleAvatarStyles = props.size === CONST.AVATAR_SIZE.SMALL ? styles.singleAvatarSmall : styles.singleAvatar; const secondAvatarStyles = [ - props.size === 'small' ? styles.secondAvatarSmall : styles.secondAvatar, + props.size === CONST.AVATAR_SIZE.SMALL ? styles.secondAvatarSmall : styles.secondAvatar, ...props.secondAvatarStyle, ]; - if (!props.avatarImageURLs.length) { + if (!props.avatarIcons.length) { return null; } - if (props.avatarImageURLs.length === 1) { + if (props.avatarIcons.length === 1) { return ( @@ -70,17 +65,17 @@ const MultipleAvatars = (props) => { > - {props.avatarImageURLs.length === 2 ? ( + {props.avatarIcons.length === 2 ? ( @@ -89,11 +84,11 @@ const MultipleAvatars = (props) => { - - {`+${props.avatarImageURLs.length - 1}`} + {`+${props.avatarIcons.length - 1}`} diff --git a/src/components/OptionsList/optionsListPropTypes.js b/src/components/OptionsList/optionsListPropTypes.js index 4ca0cf3ab4f9..7b2e20c8828c 100644 --- a/src/components/OptionsList/optionsListPropTypes.js +++ b/src/components/OptionsList/optionsListPropTypes.js @@ -1,7 +1,9 @@ import PropTypes from 'prop-types'; +import _ from 'underscore'; import SectionList from '../SectionList'; import styles from '../../styles/styles'; import optionPropTypes from '../optionPropTypes'; +import CONST from '../../CONST'; const propTypes = { /** option Background Color */ @@ -69,7 +71,7 @@ const propTypes = { showTitleTooltip: PropTypes.bool, /** Toggle between compact and default view of the option */ - optionMode: PropTypes.oneOf(['compact', 'default']), + optionMode: PropTypes.oneOf(_.values(CONST.OPTION_MODE)), /** Whether to disable the interactivity of the list's option row(s) */ disableRowInteractivity: PropTypes.bool, diff --git a/src/components/ReportActionItem/IOUPreview.js b/src/components/ReportActionItem/IOUPreview.js index 4e4792c9086c..309a46bff735 100644 --- a/src/components/ReportActionItem/IOUPreview.js +++ b/src/components/ReportActionItem/IOUPreview.js @@ -139,7 +139,7 @@ const IOUPreview = (props) => { diff --git a/src/components/RoomAvatar.js b/src/components/RoomAvatar.js deleted file mode 100644 index f9675418e542..000000000000 --- a/src/components/RoomAvatar.js +++ /dev/null @@ -1,31 +0,0 @@ -import React, {PureComponent} from 'react'; -import {StyleSheet} from 'react-native'; -import PropTypes from 'prop-types'; -import ActiveRoomAvatar from '../../assets/images/avatars/room.svg'; -import DeletedRoomAvatar from '../../assets/images/avatars/deleted-room.svg'; - -const propTypes = { - /** Extra styles to pass to Image */ - avatarStyle: PropTypes.arrayOf(PropTypes.object), - - /** Whether the room this avatar is for is deleted or not */ - isArchived: PropTypes.bool, -}; - -const defaultProps = { - avatarStyle: [], - isArchived: false, -}; - -class RoomAvatar extends PureComponent { - render() { - return (this.props.isArchived - ? - : - ); - } -} - -RoomAvatar.defaultProps = defaultProps; -RoomAvatar.propTypes = propTypes; -export default RoomAvatar; diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index eb50814854b4..f9cf7351ae32 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -6,46 +6,69 @@ import styles from '../styles/styles'; import Text from './Text'; import CONST from '../CONST'; import Avatar from './Avatar'; +import themeColors from '../styles/themes/default'; +import * as StyleUtils from '../styles/StyleUtils'; const propTypes = { - /** Array of avatar URL */ - avatarImageURLs: PropTypes.arrayOf(PropTypes.string), + /** Array of avatar URLs or icons */ + avatarIcons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])), - /** Whether this avatar is for a custom/default room */ - isChatRoom: PropTypes.bool, + /** Whether show large Avatars */ + shouldShowLargeAvatars: PropTypes.bool, }; const defaultProps = { - avatarImageURLs: [], - isChatRoom: false, + avatarIcons: [], + shouldShowLargeAvatars: false, }; const RoomHeaderAvatars = (props) => { - if (!props.avatarImageURLs.length) { + if (!props.avatarIcons.length) { return null; } - if (props.avatarImageURLs.length === 1 || props.isChatRoom) { + if (props.avatarIcons.length === 1) { return ( ); } - // avatarImageURLsToDisplay - const avatarImageURLsToDisplay = props.avatarImageURLs.slice(0, CONST.REPORT.MAX_PREVIEW_AVATARS); + if (props.shouldShowLargeAvatars) { + return ( + + + + + + + + + ); + } + const avatarIconsToDisplay = props.avatarIcons.slice(0, CONST.REPORT.MAX_PREVIEW_AVATARS); return ( - {_.map(avatarImageURLsToDisplay, (val, index) => ( + {_.map(avatarIconsToDisplay, (val, index) => ( - {index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && props.avatarImageURLs.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && ( + {index === CONST.REPORT.MAX_PREVIEW_AVATARS - 1 && props.avatarIcons.length - CONST.REPORT.MAX_PREVIEW_AVATARS !== 0 && ( <> { ]} /> - {`+${props.avatarImageURLs.length - CONST.REPORT.MAX_PREVIEW_AVATARS}`} + {`+${props.avatarIcons.length - CONST.REPORT.MAX_PREVIEW_AVATARS}`} )} diff --git a/src/components/SubscriptAvatar.js b/src/components/SubscriptAvatar.js new file mode 100644 index 000000000000..61c007b00c2d --- /dev/null +++ b/src/components/SubscriptAvatar.js @@ -0,0 +1,61 @@ +import React, {memo} from 'react'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import _ from 'underscore'; +import styles from '../styles/styles'; +import Tooltip from './Tooltip'; +import themeColors from '../styles/themes/default'; +import Avatar from './Avatar'; +import CONST from '../CONST'; +import * as StyleUtils from '../styles/StyleUtils'; + +const propTypes = { + /** Avatar URL or icon */ + mainAvatar: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + + /** Subscript avatar URL or icon */ + secondaryAvatar: PropTypes.oneOfType([PropTypes.string, PropTypes.func]).isRequired, + + /** Tooltip for the main avatar */ + mainTooltip: PropTypes.string, + + /** Tooltip for the subscript avatar */ + secondaryTooltip: PropTypes.string, + + /** Set the size of avatars */ + mode: PropTypes.oneOf(_.values(CONST.OPTION_MODE)), +}; + +const defaultProps = { + mainTooltip: '', + secondaryTooltip: '', + mode: CONST.OPTION_MODE.DEFAULT, +}; + +const SubscriptAvatar = props => ( + + + + + + + + + + +); + +SubscriptAvatar.displayName = 'SubscriptAvatar'; +SubscriptAvatar.propTypes = propTypes; +SubscriptAvatar.defaultProps = defaultProps; +export default memo(SubscriptAvatar); diff --git a/src/components/optionPropTypes.js b/src/components/optionPropTypes.js index ac4e1593d025..1e5440ddf4a9 100644 --- a/src/components/optionPropTypes.js +++ b/src/components/optionPropTypes.js @@ -7,8 +7,8 @@ export default PropTypes.shape({ // Alternate text to display alternateText: PropTypes.string, - // Array of icon urls - icons: PropTypes.arrayOf(PropTypes.string), + // Array of URLs or icons + icons: PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.func])), // Login (only present when there is a single participant) login: PropTypes.string, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index f7a9e3441fbd..419c2ada5712 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -10,6 +10,7 @@ import * as ReportUtils from './reportUtils'; import * as Localize from './Localize'; import Permissions from './Permissions'; import md5 from './md5'; +import * as Expensicons from '../components/Icon/Expensicons'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -164,13 +165,13 @@ function getParticipantNames(personalDetailList) { * * @param {Object} report * @param {Array} personalDetailList - * @param {Boolean} isChatRoom + * @param {Boolean} isChatRoomOrPolicyExpenseChat * @return {String} */ -function getSearchText(report, personalDetailList, isChatRoom) { +function getSearchText(report, personalDetailList, isChatRoomOrPolicyExpenseChat) { const searchTerms = []; - if (!isChatRoom) { + if (!isChatRoomOrPolicyExpenseChat) { _.each(personalDetailList, (personalDetail) => { searchTerms.push(personalDetail.displayName); searchTerms.push(personalDetail.login); @@ -180,7 +181,7 @@ function getSearchText(report, personalDetailList, isChatRoom) { searchTerms.push(...report.reportName); searchTerms.push(..._.map(report.reportName.split(','), name => name.trim())); - if (isChatRoom) { + if (isChatRoomOrPolicyExpenseChat) { const chatRoomSubtitle = ReportUtils.getChatRoomSubtitle(report, policies); searchTerms.push(...chatRoomSubtitle); searchTerms.push(..._.map(chatRoomSubtitle.split(','), name => name.trim())); @@ -204,20 +205,58 @@ function hasReportDraftComment(report) { && lodashGet(reportsWithDraft, `${ONYXKEYS.COLLECTION.REPORTS_WITH_DRAFT}${report.reportID}`, false); } +/** + * @param {Object} report + * @returns {Array} + */ +function getParticipants(report) { + const participants = [...lodashGet(report, ['participants'], [])]; + if (ReportUtils.isPolicyExpenseChat(report) && !_.contains(participants, report.ownerEmail)) { + participants.push(report.ownerEmail); + } + return participants; +} + +/** + * Get the Avatar urls or return the icon according to the chat type + * + * @param {Object} report + * @returns {Array<*>} + */ +function getAvatarSources(report) { + return _.map(lodashGet(report, 'icons', ['']), (source) => { + if (source) { + return source; + } + if (ReportUtils.isArchivedRoom(report)) { + return Expensicons.DeletedRoomAvatar; + } + if (ReportUtils.isChatRoom(report)) { + return Expensicons.ActiveRoomAvatar; + } + if (ReportUtils.isPolicyExpenseChat(report)) { + return Expensicons.Workspace; + } + return Expensicons.Profile; + }); +} + /** * Creates a report list option * * @param {Array} personalDetailList - * @param {Object} [report] - * @param {Boolean} showChatPreviewLine - * @param {Boolean} forcePolicyNamePreview + * @param {Object} report + * @param {Object} options + * @param {Boolean} [options.showChatPreviewLine] + * @param {Boolean} [options.forcePolicyNamePreview] * @returns {Object} */ function createOption(personalDetailList, report, { showChatPreviewLine = false, forcePolicyNamePreview = false, }) { const isChatRoom = ReportUtils.isChatRoom(report); - const hasMultipleParticipants = personalDetailList.length > 1 || isChatRoom; + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); + const hasMultipleParticipants = personalDetailList.length > 1 || isChatRoom || isPolicyExpenseChat; const personalDetail = personalDetailList[0]; const hasDraftComment = hasReportDraftComment(report); const hasOutstandingIOU = lodashGet(report, 'hasOutstandingIOU', false); @@ -234,20 +273,15 @@ function createOption(personalDetailList, report, { : ''; lastMessageText += report ? lastMessageTextFromReport : ''; - const tooltipText = ReportUtils.getReportParticipantsTitle(lodashGet(report, ['participants'], [])); - + const tooltipText = ReportUtils.getReportParticipantsTitle(getParticipants(report)); + const subtitle = ReportUtils.getChatRoomSubtitle(report, policies); let text; let alternateText; - let icons; - if (isChatRoom) { + if (isChatRoom || isPolicyExpenseChat) { text = lodashGet(report, ['reportName'], ''); alternateText = (showChatPreviewLine && !forcePolicyNamePreview && lastMessageText) ? lastMessageText - : ReportUtils.getChatRoomSubtitle(report, policies); - - // Chat rooms do not use icons from their users for the avatar so falling back on personalDetails - // doesn't make sense here - icons = lodashGet(report, 'icons', ['']); + : subtitle; } else { text = hasMultipleParticipants ? _.map(personalDetailList, ({firstName, login}) => firstName || Str.removeSMSDomain(login)) @@ -256,13 +290,14 @@ function createOption(personalDetailList, report, { alternateText = (showChatPreviewLine && lastMessageText) ? lastMessageText : Str.removeSMSDomain(personalDetail.login); - icons = lodashGet(report, 'icons', [personalDetail.avatar]); } return { text, alternateText, - icons, + icons: getAvatarSources(report), tooltipText, + ownerEmail: lodashGet(report, ['ownerEmail']), + subtitle, participantsList: personalDetailList, // It doesn't make sense to provide a login in the case of a report with multiple participants since @@ -274,7 +309,7 @@ function createOption(personalDetailList, report, { isUnread: report ? report.unreadActionCount > 0 : null, hasDraftComment, keyForList: report ? String(report.reportID) : personalDetail.login, - searchText: getSearchText(report, personalDetailList, isChatRoom), + searchText: getSearchText(report, personalDetailList, isChatRoom || isPolicyExpenseChat), isPinned: lodashGet(report, 'isPinned', false), hasOutstandingIOU, iouReportID: lodashGet(report, 'iouReportID'), @@ -282,6 +317,8 @@ function createOption(personalDetailList, report, { iouReportAmount: lodashGet(iouReport, 'total', 0), isChatRoom, isArchivedRoom: ReportUtils.isArchivedRoom(report), + shouldShowSubscript: isPolicyExpenseChat && !report.isOwnPolicyExpenseChat, + isPolicyExpenseChat, }; } @@ -311,7 +348,7 @@ function isSearchStringMatch(searchValue, searchText, participantNames = new Set /** * Returns the given userDetails is currentUser or not. * @param {Object} userDetails - * @returns {Bool} + * @returns {Boolean} */ function isCurrentUser(userDetails) { @@ -393,10 +430,13 @@ function getOptions(reports, personalDetails, activeReportID, { const allReportOptions = []; _.each(orderedReports, (report) => { - const logins = lodashGet(report, ['participants'], []); + const isChatRoom = ReportUtils.isChatRoom(report); + const isDefaultRoom = ReportUtils.isDefaultRoom(report); + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); + const logins = getParticipants(report); // Report data can sometimes be incomplete. If we have no logins or reportID then we will skip this entry. - const shouldFilterNoParticipants = _.isEmpty(logins) && !ReportUtils.isChatRoom(report) && !ReportUtils.isDefaultRoom(report); + const shouldFilterNoParticipants = _.isEmpty(logins) && !isChatRoom && !isDefaultRoom && !isPolicyExpenseChat; if (!report || !report.reportID || shouldFilterNoParticipants) { return; } @@ -407,7 +447,7 @@ function getOptions(reports, personalDetails, activeReportID, { : ''; const reportContainsIOUDebt = iouReportOwner && iouReportOwner !== currentUserLogin; - const shouldFilterReportIfEmpty = !showReportsWithNoComments && report.lastMessageTimestamp === 0 && !ReportUtils.isDefaultRoom(report); + const shouldFilterReportIfEmpty = !showReportsWithNoComments && report.lastMessageTimestamp === 0 && !isDefaultRoom; const shouldFilterReportIfRead = hideReadReports && report.unreadActionCount === 0; const shouldFilterReport = shouldFilterReportIfEmpty || shouldFilterReportIfRead; if (report.reportID !== activeReportID @@ -418,7 +458,11 @@ function getOptions(reports, personalDetails, activeReportID, { return; } - if (ReportUtils.isChatRoom(report) && (!Permissions.canUseDefaultRooms(betas) || excludeDefaultRooms)) { + if (isChatRoom && (!Permissions.canUseDefaultRooms(betas) || excludeDefaultRooms)) { + return; + } + + if (isPolicyExpenseChat && !Permissions.canUsePolicyExpenseChat(betas)) { return; } @@ -433,9 +477,10 @@ function getOptions(reports, personalDetails, activeReportID, { if (logins.length <= 1) { reportMapForLogins[logins[0]] = report; } + const isSearchingSomeonesPolicyExpenseChat = !report.isOwnPolicyExpenseChat && searchValue !== ''; allReportOptions.push(createOption(reportPersonalDetails, report, { showChatPreviewLine, - forcePolicyNamePreview, + forcePolicyNamePreview: isPolicyExpenseChat ? isSearchingSomeonesPolicyExpenseChat : forcePolicyNamePreview, })); }); @@ -775,13 +820,29 @@ function getCurrencyListForSections(currencyOptions, searchValue) { * * @param {Object} report * @param {Object} personalDetails - * @returns {String} + * @returns {Array} */ function getReportIcons(report, personalDetails) { // Default rooms have a specific avatar so we can return any non-empty array if (ReportUtils.isChatRoom(report)) { return ['']; } + + if (ReportUtils.isPolicyExpenseChat(report)) { + const policyExpenseChatAvatarURL = lodashGet(policies, [ + `${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'avatarURL', + ]); + + // Return the workspace avatar if the user is the owner of the policy expense chat + if (report.isOwnPolicyExpenseChat) { + return [policyExpenseChatAvatarURL]; + } + + // If the user is an admin, return avatar url of the other participant of the report + // (their workspace chat) and the avatar url of the workspace + return [lodashGet(personalDetails, [report.ownerEmail, 'avatarThumbnail']), policyExpenseChatAvatarURL]; + } + const sortedParticipants = _.map(report.participants, dmParticipant => ({ firstName: lodashGet(personalDetails, [dmParticipant, 'firstName'], ''), avatar: lodashGet(personalDetails, [dmParticipant, 'avatarThumbnail'], '') @@ -794,6 +855,7 @@ function getReportIcons(report, personalDetails) { export { addSMSDomainIfPhoneNumber, isCurrentUser, + getAvatarSources, getSearchOptions, getNewChatOptions, getSidebarOptions, diff --git a/src/libs/actions/PersonalDetails.js b/src/libs/actions/PersonalDetails.js index f7a82ce8699d..3edb48d15adb 100644 --- a/src/libs/actions/PersonalDetails.js +++ b/src/libs/actions/PersonalDetails.js @@ -185,12 +185,12 @@ function getFromReportParticipants(reports) { // skip over default rooms which aren't named by participants. const reportsToUpdate = {}; _.each(reports, (report) => { - if (report.participants.length <= 0 && !ReportUtils.isChatRoom(report)) { + if (report.participants.length <= 0 && !ReportUtils.isChatRoom(report) && !ReportUtils.isPolicyExpenseChat(report)) { return; } const avatars = OptionsListUtils.getReportIcons(report, details); - const reportName = ReportUtils.isChatRoom(report) + const reportName = (ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report)) ? report.reportName : _.chain(report.participants) .filter(participant => participant !== currentUserEmail) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 6da07c7211fe..61ddc7b3a07a 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -123,7 +123,7 @@ function getUnreadActionCount(report) { */ function getParticipantEmailsFromReport({sharedReportList, reportNameValuePairs}) { const emailArray = _.map(sharedReportList, participant => participant.email); - return ReportUtils.isChatRoom(reportNameValuePairs) ? emailArray : _.without(emailArray, currentUserEmail); + return (ReportUtils.isChatRoom(reportNameValuePairs) || ReportUtils.isPolicyExpenseChat(reportNameValuePairs)) ? emailArray : _.without(emailArray, currentUserEmail); } /** diff --git a/src/libs/reportUtils.js b/src/libs/reportUtils.js index 509275bb420b..6c18c43c14df 100644 --- a/src/libs/reportUtils.js +++ b/src/libs/reportUtils.js @@ -161,7 +161,7 @@ function isArchivedRoom(report) { * @returns {String} */ function getChatRoomSubtitle(report, policiesMap) { - if (!isDefaultRoom(report) && !isUserCreatedPolicyRoom(report)) { + if (!isDefaultRoom(report) && !isUserCreatedPolicyRoom(report) && !isPolicyExpenseChat(report)) { return ''; } if (report.chatType === CONST.REPORT.CHAT_TYPE.DOMAIN_ALL) { diff --git a/src/pages/ReportDetailsPage.js b/src/pages/ReportDetailsPage.js index 90e2061f3cb2..589ac79a345f 100644 --- a/src/pages/ReportDetailsPage.js +++ b/src/pages/ReportDetailsPage.js @@ -132,11 +132,9 @@ class ReportDetailsPage extends Component { style={styles.reportDetailsTitleContainer} > diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 14234980fa89..4b3969903c44 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -14,6 +14,7 @@ import compose from '../../libs/compose'; import * as Report from '../../libs/actions/Report'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../components/withWindowDimensions'; import MultipleAvatars from '../../components/MultipleAvatars'; +import SubscriptAvatar from '../../components/SubscriptAvatar'; import Navigation from '../../libs/Navigation/Navigation'; import ROUTES from '../../ROUTES'; import DisplayNames from '../../components/DisplayNames'; @@ -86,7 +87,8 @@ const HeaderView = (props) => { }, ); const isChatRoom = ReportUtils.isChatRoom(props.report); - const title = isChatRoom + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); + const title = (isChatRoom || isPolicyExpenseChat) ? props.report.reportName : _.map(displayNamesWithTooltips, ({displayName}) => displayName).join(', '); @@ -97,9 +99,9 @@ const HeaderView = (props) => { // We hide the button when we are chatting with an automated Expensify account since it's not possible to contact // these users via alternative means. It is possible to request a call with Concierge so we leave the option for them. const shouldShowCallButton = isConcierge || !isAutomatedExpensifyAccount; - const avatarTooltip = isChatRoom ? undefined : _.pluck(displayNamesWithTooltips, 'tooltip'); - + const shouldShowSubscript = isPolicyExpenseChat && !props.report.isOwnPolicyExpenseChat; + const avatarIcons = OptionsListUtils.getAvatarSources(props.report); return ( @@ -134,13 +136,19 @@ const HeaderView = (props) => { }} style={[styles.flexRow, styles.alignItemsCenter, styles.flex1]} > - + {shouldShowSubscript ? ( + + ) : ( + + )} { tooltipEnabled numberOfLines={1} textStyles={[styles.headerText, styles.textNoWrap]} - shouldUseFullTitle={isChatRoom} + shouldUseFullTitle={isChatRoom || isPolicyExpenseChat} /> {isChatRoom && ( { const isChatRoom = ReportUtils.isChatRoom(props.report); - + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); + const avatarIcons = OptionsListUtils.getAvatarSources(props.report); return ( { > diff --git a/src/pages/home/sidebar/OptionRow.js b/src/pages/home/sidebar/OptionRow.js index 6141fc8d7e29..159f722b6208 100644 --- a/src/pages/home/sidebar/OptionRow.js +++ b/src/pages/home/sidebar/OptionRow.js @@ -21,6 +21,8 @@ import colors from '../../../styles/colors'; import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize'; import Text from '../../../components/Text'; import SelectCircle from '../../../components/SelectCircle'; +import SubscriptAvatar from '../../../components/SubscriptAvatar'; +import CONST from '../../../CONST'; const propTypes = { /** Background Color of the Option Row */ @@ -55,7 +57,7 @@ const propTypes = { showTitleTooltip: PropTypes.bool, /** Toggle between compact and default view */ - mode: PropTypes.oneOf(['compact', 'default']), + mode: PropTypes.oneOf(_.values(CONST.OPTION_MODE)), /** Whether this option should be disabled */ isDisabled: PropTypes.bool, @@ -87,16 +89,16 @@ const OptionRow = (props) => { : styles.sidebarLinkText; const textUnreadStyle = (props.option.isUnread || props.forceTextUnreadStyle) ? [textStyle, styles.sidebarLinkTextUnread] : [textStyle]; - const displayNameStyle = props.mode === 'compact' + const displayNameStyle = props.mode === CONST.OPTION_MODE.COMPACT ? [styles.optionDisplayName, ...textUnreadStyle, styles.optionDisplayNameCompact, styles.mr2] : [styles.optionDisplayName, ...textUnreadStyle]; - const alternateTextStyle = props.mode === 'compact' + const alternateTextStyle = props.mode === CONST.OPTION_MODE.COMPACT ? [textStyle, styles.optionAlternateText, styles.textLabelSupporting, styles.optionAlternateTextCompact] : [textStyle, styles.optionAlternateText, styles.textLabelSupporting]; - const contentContainerStyles = props.mode === 'compact' + const contentContainerStyles = props.mode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, styles.alignItemsCenter] : [styles.flex1]; - const sidebarInnerRowStyle = StyleSheet.flatten(props.mode === 'compact' ? [ + const sidebarInnerRowStyle = StyleSheet.flatten(props.mode === CONST.OPTION_MODE.COMPACT ? [ styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, @@ -163,22 +165,30 @@ const OptionRow = (props) => { { !_.isEmpty(props.option.icons) && ( - + props.option.shouldShowSubscript ? ( + + ) : ( + + ) ) } @@ -188,7 +198,7 @@ const OptionRow = (props) => { tooltipEnabled={props.showTitleTooltip} numberOfLines={1} textStyles={displayNameStyle} - shouldUseFullTitle={props.option.isChatRoom} + shouldUseFullTitle={props.option.isChatRoom || props.option.isPolicyExpenseChat} /> {props.option.alternateText ? ( {