Skip to content

Commit

Permalink
Merge pull request #18274 from Expensify/stites-threadUI
Browse files Browse the repository at this point in the history
Add `ReportActionItemThread` and Thread Replies UI
  • Loading branch information
chiragsalian authored May 12, 2023
2 parents 5100743 + 30f5eba commit 293f23b
Show file tree
Hide file tree
Showing 8 changed files with 158 additions and 26 deletions.
7 changes: 6 additions & 1 deletion src/components/MultipleAvatars.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,12 @@ const MultipleAvatars = (props) => {
absolute
>
<View style={[singleAvatarStyles, styles.alignItemsCenter, styles.justifyContentCenter]}>
<Text style={props.size === CONST.AVATAR_SIZE.SMALL ? styles.avatarInnerTextSmall : styles.avatarInnerText}>{`+${props.icons.length - 1}`}</Text>
<Text
selectable={false}
style={props.size === CONST.AVATAR_SIZE.SMALL ? styles.avatarInnerTextSmall : styles.avatarInnerText}
>
{`+${props.icons.length - 1}`}
</Text>
</View>
</Tooltip>
)}
Expand Down
5 changes: 5 additions & 0 deletions src/languages/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -1310,4 +1310,9 @@ export default {
parentReportAction: {
deletedMessage: '[Deleted message]',
},
threads: {
lastReply: 'Last Reply',
replies: 'Replies',
reply: 'Reply',
},
};
5 changes: 5 additions & 0 deletions src/languages/es.js
Original file line number Diff line number Diff line change
Expand Up @@ -1775,4 +1775,9 @@ export default {
parentReportAction: {
deletedMessage: '[Mensaje eliminado]',
},
threads: {
lastReply: 'Última respuesta',
replies: 'Respuestas',
reply: 'Respuesta',
},
};
73 changes: 49 additions & 24 deletions src/libs/ReportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -447,6 +447,17 @@ function isThreadParent(reportAction) {
return reportAction && reportAction.childReportID && reportAction.childReportID !== 0;
}

/**
* Returns true if reportAction is the first chat preview of a Thread
*
* @param {Object} reportAction
* @param {String} reportID
* @returns {Boolean}
*/
function isThreadFirstChat(reportAction, reportID) {
return !_.isUndefined(reportAction.childReportID) && reportAction.childReportID.toString() === reportID;
}

/**
* Get either the policyName or domainName the chat is tied to
* @param {Object} report
Expand Down Expand Up @@ -721,6 +732,41 @@ function getSmallSizeAvatar(avatarURL, login) {
return `${source.substring(0, lastPeriodIndex)}_128${source.substring(lastPeriodIndex)}`;
}

/**
* Returns the appropriate icons for the given chat report using the stored personalDetails.
* The Avatar sources can be URLs or Icon components according to the chat type.
*
* @param {Array} participants
* @param {Object} personalDetails
* @returns {Array<*>}
*/
function getIconsForParticipants(participants, personalDetails) {
const participantDetails = [];
const participantsList = participants || [];

for (let i = 0; i < participantsList.length; i++) {
const login = participantsList[i];
const avatarSource = getAvatar(lodashGet(personalDetails, [login, 'avatar'], ''), login);
participantDetails.push([login, lodashGet(personalDetails, [login, 'firstName'], ''), avatarSource]);
}

// Sort all logins by first name (which is the second element in the array)
const sortedParticipantDetails = participantDetails.sort((a, b) => a[1] - b[1]);

// Now that things are sorted, gather only the avatars (third element in the array) and return those
const avatars = [];
for (let i = 0; i < sortedParticipantDetails.length; i++) {
const userIcon = {
source: sortedParticipantDetails[i][2],
type: CONST.ICON_TYPE_AVATAR,
name: sortedParticipantDetails[i][0],
};
avatars.push(userIcon);
}

return avatars;
}

/**
* Returns the appropriate icons for the given chat report using the stored personalDetails.
* The Avatar sources can be URLs or Icon components according to the chat type.
Expand Down Expand Up @@ -825,30 +871,7 @@ function getIcons(report, personalDetails, defaultIcon = null) {
];
}

const participantDetails = [];
const participants = report.participants || [];

for (let i = 0; i < participants.length; i++) {
const login = participants[i];
const avatarSource = getAvatar(lodashGet(personalDetails, [login, 'avatar'], ''), login);
participantDetails.push([login, lodashGet(personalDetails, [login, 'firstName'], ''), avatarSource]);
}

// Sort all logins by first name (which is the second element in the array)
const sortedParticipantDetails = participantDetails.sort((a, b) => a[1] - b[1]);

// Now that things are sorted, gather only the avatars (third element in the array) and return those
const avatars = [];
for (let i = 0; i < sortedParticipantDetails.length; i++) {
const userIcon = {
source: sortedParticipantDetails[i][2],
type: CONST.ICON_TYPE_AVATAR,
name: sortedParticipantDetails[i][0],
};
avatars.push(userIcon);
}

return avatars;
return getIconsForParticipants(report.participants, personalDetails);
}

/**
Expand Down Expand Up @@ -2020,6 +2043,7 @@ export {
chatIncludesConcierge,
isPolicyExpenseChat,
getDefaultAvatar,
getIconsForParticipants,
getIcons,
getRoomWelcomeMessage,
getDisplayNamesWithTooltips,
Expand Down Expand Up @@ -2069,6 +2093,7 @@ export {
getWorkspaceAvatar,
isThread,
isThreadParent,
isThreadFirstChat,
shouldReportShowSubscript,
isSettled,
};
2 changes: 1 addition & 1 deletion src/pages/home/report/ContextMenu/ContextMenuActions.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ export default [
Permissions.canUseThreads(betas) &&
type === CONTEXT_MENU_TYPES.REPORT_ACTION &&
reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT &&
(_.isUndefined(reportAction.childReportID) || reportAction.childReportID.toString() !== reportID),
!ReportUtils.isThreadFirstChat(reportAction, reportID),
onPress: (closePopover, {reportAction, reportID}) => {
Report.navigateToAndOpenChildReport(lodashGet(reportAction, 'childReportID', '0'), reportAction, reportID);
if (closePopover) {
Expand Down
22 changes: 22 additions & 0 deletions src/pages/home/report/ReportActionItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import ReportActionItemMessage from './ReportActionItemMessage';
import UnreadActionIndicator from '../../../components/UnreadActionIndicator';
import ReportActionItemMessageEdit from './ReportActionItemMessageEdit';
import ReportActionItemCreated from './ReportActionItemCreated';
import ReportActionItemThread from './ReportActionItemThread';
import compose from '../../../libs/compose';
import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions';
import ControlSelection from '../../../libs/ControlSelection';
Expand Down Expand Up @@ -48,6 +49,7 @@ import personalDetailsPropType from '../../personalDetailsPropType';
import ReportActionItemDraft from './ReportActionItemDraft';
import TaskPreview from '../../../components/ReportActionItem/TaskPreview';
import * as ReportActionUtils from '../../../libs/ReportActionsUtils';
import Permissions from '../../../libs/Permissions';

const propTypes = {
/** Report for this action */
Expand Down Expand Up @@ -83,6 +85,9 @@ const propTypes = {
/** All of the personalDetails */
personalDetails: PropTypes.objectOf(personalDetailsPropType),

/** List of betas available to current user */
betas: PropTypes.arrayOf(PropTypes.string),

...windowDimensionsPropTypes,
};

Expand All @@ -92,6 +97,7 @@ const defaultProps = {
preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE,
personalDetails: {},
shouldShowSubscriptAvatar: false,
betas: [],
};

class ReportActionItem extends Component {
Expand Down Expand Up @@ -243,6 +249,10 @@ class ReportActionItem extends Component {

const reactions = _.get(this.props, ['action', 'message', 0, 'reactions'], []);
const hasReactions = reactions.length > 0;
const shouldDisplayThreadReplies =
this.props.action.childCommenterCount && Permissions.canUseThreads(this.props.betas) && !ReportUtils.isThreadFirstChat(this.props.action, this.props.report.reportID);
const oldestFourEmails = lodashGet(this.props.action, 'childOldestFourEmails', '').split(',');

return (
<>
{children}
Expand All @@ -254,6 +264,14 @@ class ReportActionItem extends Component {
/>
</View>
)}
{shouldDisplayThreadReplies && (
<ReportActionItemThread
childReportID={`${this.props.action.childReportID}`}
numberOfReplies={this.props.action.childVisibleActionCount || 0}
mostRecentReply={`${this.props.action.childLastVisibleActionCreated}`}
icons={ReportUtils.getIconsForParticipants(oldestFourEmails, this.props.personalDetails)}
/>
)}
</>
);
}
Expand Down Expand Up @@ -371,6 +389,7 @@ class ReportActionItem extends Component {
isVisible={hovered && !this.props.draftMessage && !hasErrors}
draftMessage={this.props.draftMessage}
isChronosReport={ReportUtils.chatIncludesChronos(this.props.report)}
childReportActionID={this.props.action.childReportActionID}
/>
</View>
)}
Expand Down Expand Up @@ -402,5 +421,8 @@ export default compose(
preferredSkinTone: {
key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE,
},
betas: {
key: ONYXKEYS.BETAS,
},
}),
)(ReportActionItem);
66 changes: 66 additions & 0 deletions src/pages/home/report/ReportActionItemThread.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import React from 'react';
import {View, Pressable, Text} from 'react-native';
import PropTypes from 'prop-types';
import _ from 'underscore';
import styles from '../../../styles/styles';
import * as Report from '../../../libs/actions/Report';
import withLocalize, {withLocalizePropTypes} from '../../../components/withLocalize';
import CONST from '../../../CONST';
import avatarPropTypes from '../../../components/avatarPropTypes';
import MultipleAvatars from '../../../components/MultipleAvatars';
import Navigation from '../../../libs/Navigation/Navigation';
import ROUTES from '../../../ROUTES';

const propTypes = {
/** List of participant icons for the thread */
icons: PropTypes.arrayOf(avatarPropTypes).isRequired,

/** Number of comments under the thread */
numberOfReplies: PropTypes.number.isRequired,

/** Time of the most recent reply */
mostRecentReply: PropTypes.string.isRequired,

/** ID of child thread report */
childReportID: PropTypes.string.isRequired,

/** localization props */
...withLocalizePropTypes,
};

const ReportActionItemThread = (props) => (
<View style={[styles.chatItemMessage]}>
<Pressable
onPress={() => {
Report.openReport(props.childReportID);
Navigation.navigate(ROUTES.getReportRoute(props.childReportID));
}}
>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mt2]}>
<MultipleAvatars
size={CONST.AVATAR_SIZE.SMALLER}
icons={props.icons}
shouldStackHorizontally
avatarTooltips={_.map(props.icons, (icon) => icon.name)}
/>
<View style={[styles.flexRow, styles.lh140Percent, styles.alignItemsEnd]}>
<Text
selectable={false}
style={[styles.link, styles.ml2, styles.h4]}
>
{`${props.numberOfReplies} ${props.numberOfReplies === 1 ? props.translate('threads.reply') : props.translate('threads.replies')}`}
</Text>
<Text
selectable={false}
style={[styles.ml2, styles.textMicroSupporting]}
>{`${props.translate('threads.lastReply')} ${props.datetimeToCalendarTime(props.mostRecentReply)}`}</Text>
</View>
</View>
</Pressable>
</View>
);

ReportActionItemThread.propTypes = propTypes;
ReportActionItemThread.displayName = 'ReportActionItemThread';

export default withLocalize(ReportActionItemThread);
4 changes: 4 additions & 0 deletions src/styles/styles.js
Original file line number Diff line number Diff line change
Expand Up @@ -1018,6 +1018,10 @@ const styles = {
lineHeight: 16,
},

lh140Percent: {
lineHeight: '140%',
},

formHelp: {
color: themeColors.textSupporting,
fontSize: variables.fontSizeLabel,
Expand Down

0 comments on commit 293f23b

Please sign in to comment.