Skip to content

Commit

Permalink
Merge pull request #7831 from Expensify/Rory-ArchiveUI
Browse files Browse the repository at this point in the history
Implement Archived Workspace Chat UI
  • Loading branch information
Amal Nazeem authored Mar 24, 2022
2 parents 9c6c2a9 + 811379b commit eb3b18e
Show file tree
Hide file tree
Showing 21 changed files with 312 additions and 70 deletions.
17 changes: 16 additions & 1 deletion src/CONST.js
Original file line number Diff line number Diff line change
Expand Up @@ -215,12 +215,20 @@ const CONST = {
ACTIONS: {
LIMIT: 50,
TYPE: {
IOU: 'IOU',
ADDCOMMENT: 'ADDCOMMENT',
CLOSED: 'CLOSED',
CREATED: 'CREATED',
IOU: 'IOU',
RENAMED: 'RENAMED',
},
},
ARCHIVE_REASON: {
DEFAULT: 'default',
ACCOUNT_CLOSED: 'accountClosed',
ACCOUNT_MERGED: 'accountMerged',
REMOVED_FROM_POLICY: 'removedFromPolicy',
POLICY_DELETED: 'policyDeleted',
},
ERROR: {
INACCESSIBLE_REPORT: 'Report not found',
},
Expand All @@ -245,6 +253,13 @@ const CONST = {
PROCESSING: 1,
SUBMITTED: 2,
},
STATUS: {
OPEN: 0,
SUBMITTED: 1,
CLOSED: 2,
APPROVED: 3,
REIMBURSED: 4,
},
NOTIFICATION_PREFERENCE: {
MUTE: 'mute',
DAILY: 'daily',
Expand Down
93 changes: 93 additions & 0 deletions src/components/ArchivedReportFooter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import lodashGet from 'lodash/get';
import React from 'react';
import PropTypes from 'prop-types';
import {withOnyx} from 'react-native-onyx';
import CONST from '../CONST';
import Banner from './Banner';
import withLocalize, {withLocalizePropTypes} from './withLocalize';
import compose from '../libs/compose';
import personalDetailsPropType from '../pages/personalDetailsPropType';
import ONYXKEYS from '../ONYXKEYS';

const propTypes = {
/** The reason this report was archived */
reportClosedAction: PropTypes.shape({
/** Message attached to the report closed action */
originalMessage: PropTypes.shape({
/** The reason the report was closed */
reason: PropTypes.string.isRequired,

/** (For accountMerged reason only), the email of the previous owner of this report. */
oldLogin: PropTypes.string,

/** (For accountMerged reason only), the email of the account the previous owner was merged into */
newLogin: PropTypes.string,
}).isRequired,
}),

/** The archived report */
report: PropTypes.shape({
/** The policy this report is attached to */
policyID: PropTypes.string,
}).isRequired,

/** Personal details of all users */
personalDetails: PropTypes.objectOf(personalDetailsPropType).isRequired,

/** The list of policies the user has access to. */
policies: PropTypes.objectOf(PropTypes.shape({
/** The name of the policy */
name: PropTypes.string,
})).isRequired,

...withLocalizePropTypes,
};

const defaultProps = {
reportClosedAction: {
originalMessage: {
reason: CONST.REPORT.ARCHIVE_REASON.DEFAULT,
},
},
};

const ArchivedReportFooter = (props) => {
const archiveReason = lodashGet(props.reportClosedAction, 'originalMessage.reason', CONST.REPORT.ARCHIVE_REASON.DEFAULT);
const policyName = lodashGet(props.policies, `${ONYXKEYS.COLLECTION.POLICY}${props.report.policyID}.name`);
let displayName = lodashGet(props.personalDetails, `${props.report.ownerEmail}.displayName`, props.report.ownerEmail);

let oldDisplayName;
if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) {
const newLogin = props.reportClosedAction.originalMessage.newLogin;
const oldLogin = props.reportClosedAction.originalMessage.oldLogin;
displayName = lodashGet(props.personalDetails, `${newLogin}.displayName`, newLogin);
oldDisplayName = lodashGet(props.personalDetails, `${oldLogin}.displayName`, oldLogin);
}

return (
<Banner
text={props.translate(`reportArchiveReasons.${archiveReason}`, {
displayName: `<strong>${displayName}</strong>`,
oldDisplayName: `<strong>${oldDisplayName}</strong>`,
policyName: `<strong>${policyName}</strong>`,
})}
shouldRenderHTML={archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT}
/>
);
};

ArchivedReportFooter.propTypes = propTypes;
ArchivedReportFooter.defaultProps = defaultProps;
ArchivedReportFooter.displayName = 'ArchivedReportFooter';

export default compose(
withLocalize,
withOnyx({
personalDetails: {
key: ONYXKEYS.PERSONAL_DETAILS,
},
policies: {
key: ONYXKEYS.COLLECTION.POLICY,
},
}),
)(ArchivedReportFooter);
56 changes: 56 additions & 0 deletions src/components/Banner.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import React, {memo} from 'react';
import PropTypes from 'prop-types';
import {View} from 'react-native';
import Hoverable from './Hoverable';
import Icon from './Icon';
import * as Expensicons from './Icon/Expensicons';
import RenderHTML from './RenderHTML';
import Text from './Text';
import styles from '../styles/styles';
import * as StyleUtils from '../styles/StyleUtils';
import getButtonState from '../libs/getButtonState';

const propTypes = {
/** Text to display in the banner. */
text: PropTypes.string.isRequired,

/** Should this component render the text as HTML? */
shouldRenderHTML: PropTypes.bool,
};

const defaultProps = {
shouldRenderHTML: false,
};

const Banner = props => (
<Hoverable>
{isHovered => (
<View style={[
styles.flexRow,
styles.alignItemsCenter,
styles.p5,
styles.borderRadiusNormal,
isHovered ? styles.activeComponentBG : styles.hoveredComponentBG,
]}
>
<View style={[styles.mr3]}>
<Icon
src={Expensicons.Exclamation}
fill={StyleUtils.getIconFillColor(getButtonState(isHovered))}
/>
</View>
{
props.shouldRenderHTML
? <RenderHTML html={props.text} />
: <Text>{props.text}</Text>
}
</View>
)}
</Hoverable>
);

Banner.propTypes = propTypes;
Banner.defaultProps = defaultProps;
Banner.displayName = 'Banner';

export default memo(Banner);
2 changes: 1 addition & 1 deletion src/components/GrowlNotification/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -97,10 +97,10 @@ class GrowlNotification extends Component {
<GrowlNotificationContainer translateY={this.state.translateY}>
<TouchableWithoutFeedback onPress={this.fling}>
<View style={styles.growlNotificationBox}>
<Icon src={types[this.state.type].icon} fill={types[this.state.type].iconColor} />
<Text style={styles.growlNotificationText}>
{this.state.bodyText}
</Text>
<Icon src={types[this.state.type].icon} fill={types[this.state.type].iconColor} />
</View>
</TouchableWithoutFeedback>
</GrowlNotificationContainer>
Expand Down
10 changes: 9 additions & 1 deletion src/languages/en.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import CONST from '../CONST';

/* eslint-disable max-len */
export default {
common: {
Expand Down Expand Up @@ -156,7 +158,6 @@ export default {
blockedFromConcierge: 'Communication is barred',
youAppearToBeOffline: 'You appear to be offline.',
fileUploadFailed: 'Upload failed. File is not supported.',
roomIsArchived: 'This chat room has been deleted',
localTime: ({user, time}) => `It's ${time} for ${user}`,
edited: '(edited)',
emoji: 'Emoji',
Expand Down Expand Up @@ -190,6 +191,13 @@ export default {
areTyping: 'are typing...',
multipleUsers: 'Multiple users',
},
reportArchiveReasons: {
[CONST.REPORT.ARCHIVE_REASON.DEFAULT]: 'This chat room has been archived.',
[CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({displayName}) => `This workspace chat is no longer active because ${displayName} closed their account.`,
[CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED]: ({displayName, oldDisplayName}) => `This workspace chat is no longer active because ${oldDisplayName} has merged their account with ${displayName}.`,
[CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY]: ({displayName, policyName}) => `This workspace chat is no longer active because ${displayName} is no longer a member of the ${policyName} workspace.`,
[CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}) => `This workspace chat is no longer active because ${policyName} is no longer an active workspace.`,
},
sidebarScreen: {
fabAction: 'New chat',
newChat: 'New chat',
Expand Down
10 changes: 9 additions & 1 deletion src/languages/es.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import CONST from '../CONST';

/* eslint-disable max-len */
export default {
common: {
Expand Down Expand Up @@ -156,7 +158,6 @@ export default {
blockedFromConcierge: 'Comunicación no permitida',
youAppearToBeOffline: 'Parece que estás desconectado.',
fileUploadFailed: 'Subida fallida. El archivo no es compatible.',
roomIsArchived: 'Esta sala de chat ha sido eliminada',
localTime: ({user, time}) => `Son las ${time} para ${user}`,
edited: '(editado)',
emoji: 'Emoji',
Expand Down Expand Up @@ -190,6 +191,13 @@ export default {
areTyping: 'están escribiendo...',
multipleUsers: 'Varios usuarios',
},
reportArchiveReasons: {
[CONST.REPORT.ARCHIVE_REASON.DEFAULT]: 'Esta sala de chat ha sido eliminada.',
[CONST.REPORT.ARCHIVE_REASON.ACCOUNT_CLOSED]: ({displayName}) => `Este chat de espacio de trabajo esta desactivado porque ${displayName} ha cerrado su cuenta.`,
[CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED]: ({displayName, oldDisplayName}) => `Este chat de espacio de trabajo esta desactivado porque ${oldDisplayName} ha combinado su cuenta con ${displayName}.`,
[CONST.REPORT.ARCHIVE_REASON.REMOVED_FROM_POLICY]: ({displayName, policyName}) => `Este chat de espacio de trabajo esta desactivado porque ${displayName} ha dejado de ser miembro del espacio de trabajo ${policyName}.`,
[CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED]: ({policyName}) => `Este chat de espacio de trabajo esta desactivado porque el espacio de trabajo ${policyName} se ha eliminado.`,
},
sidebarScreen: {
fabAction: 'Nuevo chat',
newChat: 'Nuevo chat',
Expand Down
13 changes: 12 additions & 1 deletion src/libs/LoginUtil.js → src/libs/LoginUtils.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import CONST from '../CONST';

/**
* Remove the MERGED_0@ prefix from merged account emails.
*
* @param {String} email
* @returns {String}
*/
function getEmailWithoutMergedAccountPrefix(email) {
return email.replace(/^MERGED_0@/, '');
}

/**
* Remove the special chars from the phone number
* @param {String} phone
Expand All @@ -9,6 +19,7 @@ function getPhoneNumberWithoutSpecialChars(phone) {
return phone.replace(CONST.REGEX.SPECIAL_CHARS_WITHOUT_NEWLINE, '');
}

export default {
export {
getEmailWithoutMergedAccountPrefix,
getPhoneNumberWithoutSpecialChars,
};
4 changes: 2 additions & 2 deletions src/libs/ValidationUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import moment from 'moment';
import _ from 'underscore';
import CONST from '../CONST';
import * as CardUtils from './CardUtils';
import LoginUtil from './LoginUtil';
import * as LoginUtils from './LoginUtils';

/**
* Implements the Luhn Algorithm, a checksum formula used to validate credit card
Expand Down Expand Up @@ -291,7 +291,7 @@ function isPositiveInteger(input) {
* @returns {Boolean}
*/
function isNumericWithSpecialChars(input) {
return /^\+?\d*$/.test(LoginUtil.getPhoneNumberWithoutSpecialChars(input));
return /^\+?\d*$/.test(LoginUtils.getPhoneNumberWithoutSpecialChars(input));
}

/**
Expand Down
25 changes: 13 additions & 12 deletions src/libs/actions/PersonalDetails.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import CONST from '../../CONST';
import NetworkConnection from '../NetworkConnection';
import * as API from '../API';
import NameValuePair from './NameValuePair';
import * as LoginUtils from '../LoginUtils';
import * as ReportUtils from '../reportUtils';
import * as OptionsListUtils from '../OptionsListUtils';
import Growl from '../Growl';
Expand Down Expand Up @@ -87,20 +88,20 @@ function formatPersonalDetails(personalDetailsList) {

// This method needs to be SUPER PERFORMANT because it can be called with a massive list of logins depending on the policies that someone belongs to
// eslint-disable-next-line rulesdir/prefer-underscore-method
Object.keys(personalDetailsList).forEach((login) => {
const personalDetailsResponse = personalDetailsList[login];
Object.entries(personalDetailsList).forEach(([login, details]) => {
const sanitizedLogin = LoginUtils.getEmailWithoutMergedAccountPrefix(login);

// Form the details into something that has all the data in an easy to use format.
const avatar = getAvatar(personalDetailsResponse, login);
const displayName = getDisplayName(login, personalDetailsResponse);
const pronouns = personalDetailsResponse.pronouns || '';
const timezone = personalDetailsResponse.timeZone || CONST.DEFAULT_TIME_ZONE;
const firstName = personalDetailsResponse.firstName || '';
const lastName = personalDetailsResponse.lastName || '';
const payPalMeAddress = personalDetailsResponse.expensify_payPalMeAddress || '';
const phoneNumber = personalDetailsResponse.phoneNumber || '';
formattedResult[login] = {
login,
const avatar = getAvatar(details, sanitizedLogin);
const displayName = getDisplayName(sanitizedLogin, details);
const pronouns = details.pronouns || '';
const timezone = details.timeZone || CONST.DEFAULT_TIME_ZONE;
const firstName = details.firstName || '';
const lastName = details.lastName || '';
const payPalMeAddress = details.expensify_payPalMeAddress || '';
const phoneNumber = details.phoneNumber || '';
formattedResult[sanitizedLogin] = {
login: sanitizedLogin,
avatar,
displayName,
firstName,
Expand Down
5 changes: 3 additions & 2 deletions src/libs/actions/Report.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import Timing from './Timing';
import * as API from '../API';
import CONST from '../../CONST';
import Log from '../Log';
import * as LoginUtils from '../LoginUtils';
import * as ReportUtils from '../reportUtils';
import * as OptionsListUtils from '../OptionsListUtils';
import Timers from '../Timers';
Expand Down Expand Up @@ -156,7 +157,7 @@ function getChatReportName(fullReport, chatType) {

// For a basic policy room or a Policy Expense chat, return its original name
if (ReportUtils.isUserCreatedPolicyRoom({chatType}) || ReportUtils.isPolicyExpenseChat({chatType})) {
return fullReport.reportName;
return LoginUtils.getEmailWithoutMergedAccountPrefix(fullReport.reportName);
}

const {sharedReportList} = fullReport;
Expand Down Expand Up @@ -214,7 +215,7 @@ function getSimplifiedReportObject(report) {
reportID: report.reportID,
reportName,
chatType,
ownerEmail: lodashGet(report, ['ownerEmail'], ''),
ownerEmail: LoginUtils.getEmailWithoutMergedAccountPrefix(lodashGet(report, ['ownerEmail'], '')),
policyID: lodashGet(report, ['reportNameValuePairs', 'expensify_policyID'], ''),
unreadActionCount: getUnreadActionCount(report),
maxSequenceNumber: lodashGet(report, 'reportActionCount', 0),
Expand Down
5 changes: 3 additions & 2 deletions src/libs/reportUtils.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,16 +162,17 @@ function findLastAccessedReport(reports, ignoreDefaultRooms) {
/**
* Whether the provided report is an archived room
* @param {Object} report
* @param {String} report.chatType
* @param {Number} report.stateNum
* @param {Number} report.statusNum
* @returns {Boolean}
*/
function isArchivedRoom(report) {
if (!isDefaultRoom(report)) {
if (!isChatRoom(report) && !isPolicyExpenseChat(report)) {
return false;
}

return report.statusNum === 2 && report.stateNum === 2;
return report.statusNum === CONST.REPORT.STATUS.CLOSED && report.stateNum === CONST.REPORT.STATE_NUM.SUBMITTED;
}

/**
Expand Down
Loading

0 comments on commit eb3b18e

Please sign in to comment.