Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ReportActionItemThread and Thread Replies UI #18274

Merged
merged 33 commits into from
May 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
982c97b
Add new button for replying in threads
stitesExpensify Apr 27, 2023
e09ee8b
Add size prop
stitesExpensify Apr 27, 2023
1d40aa0
Add imageSize method
stitesExpensify Apr 28, 2023
4b919fd
change all sizes based on prop
stitesExpensify Apr 28, 2023
55aa13b
Add threads component
stitesExpensify Apr 28, 2023
8162704
Create new component to display threads in reportActions
stitesExpensify Apr 28, 2023
e7b3b9f
Add method for creating/opening threads
stitesExpensify May 1, 2023
4db53da
Pass the child reportactionID through the context menu
stitesExpensify May 1, 2023
8b31d03
Update API call to properly create optimistic thread report and repor…
stitesExpensify May 1, 2023
7761696
go back instead of always home
stitesExpensify May 1, 2023
bae93b4
Remove changes to navigation
stitesExpensify May 3, 2023
3b73583
Remove new openChildReport method
stitesExpensify May 3, 2023
10fe957
Extract more code to other PRs
stitesExpensify May 4, 2023
7780871
Merge branch 'main' into stites-threadUI
grgia May 4, 2023
a05a95a
Clean up thread styles, use Multiple Avatars
grgia May 4, 2023
0de7775
Undo changes to RoomHeaderAvatars that are already accomplished by Mu…
grgia May 4, 2023
9afd2f2
Add translations
grgia May 4, 2023
c75840a
Temporarily reference reportAction
grgia May 8, 2023
cd52ecc
Clean up, icon helper function
grgia May 10, 2023
b54a6b0
Allow to be pressed
grgia May 10, 2023
0f336e4
Merge branch 'main' into stites-threadUI
grgia May 10, 2023
00c1532
fix delimiter
grgia May 10, 2023
20bc93d
undo, my testing data was wrong
grgia May 10, 2023
35492e2
Clean up
grgia May 11, 2023
f69af26
Remove unused param docs
grgia May 11, 2023
1168c38
Merge branch 'main' of github.com:Expensify/App into stites-threadUI
stitesExpensify May 11, 2023
3104e3e
Add beta check
stitesExpensify May 11, 2023
f97c600
Add betas to proptypes
stitesExpensify May 11, 2023
779fa63
Rename to getIconsForParticipants and fix param type
grgia May 12, 2023
628de21
Merge branch 'main' into stites-threadUI
grgia May 12, 2023
087862e
Make text not selectable
grgia May 12, 2023
67d6171
Merge branch 'main' into stites-threadUI
grgia May 12, 2023
30f5eba
Don't show on thread preview (first chat in thread)
grgia May 12, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👋This has caused a small visual issue in #19398.
Reply text wasn't aligned with composer when editing, ReportActionItemThread should have the same styles applied as ReportActionItemReactions above: this.props.draftMessage ? styles.chatItemReactionsDraftRight : {}

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}
grgia marked this conversation as resolved.
Show resolved Hide resolved
/>
</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
grgia marked this conversation as resolved.
Show resolved Hide resolved
onPress={() => {
Report.openReport(props.childReportID);
Navigation.navigate(ROUTES.getReportRoute(props.childReportID));
}}
>
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mt2]}>
grgia marked this conversation as resolved.
Show resolved Hide resolved
<MultipleAvatars
grgia marked this conversation as resolved.
Show resolved Hide resolved
size={CONST.AVATAR_SIZE.SMALLER}
icons={props.icons}
shouldStackHorizontally
grgia marked this conversation as resolved.
Show resolved Hide resolved
avatarTooltips={_.map(props.icons, (icon) => icon.name)}
/>
<View style={[styles.flexRow, styles.lh140Percent, styles.alignItemsEnd]}>
grgia marked this conversation as resolved.
Show resolved Hide resolved
<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