diff --git a/package-lock.json b/package-lock.json index 589a57e2f85ba..3bb3d01d320c3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -67,7 +67,7 @@ "react-native-image-picker": "^4.8.5", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.15", + "react-native-onyx": "1.0.17", "react-native-pdf": "^6.6.2", "react-native-performance": "^2.0.0", "react-native-permissions": "^3.0.1", @@ -35515,9 +35515,9 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.15.tgz", - "integrity": "sha512-uIJped+agmOppnCoDcs/w3qFertkLhLHyhmEEBXp0OhzNKuCs01wg7ccYFZxOVv+CtzFbtkLVB1VW3Ty/zYogA==", + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.17.tgz", + "integrity": "sha512-ls2GjURfpBcGnIkwVrg2uuLnTBwd0vrEiUvbMo+GF3k81GAp2flCkVTM7ciAbo155Izk50dm0uXHYq1PIjwTxw==", "dependencies": { "ascii-table": "0.0.9", "lodash": "^4.17.21", @@ -70130,9 +70130,9 @@ } }, "react-native-onyx": { - "version": "1.0.15", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.15.tgz", - "integrity": "sha512-uIJped+agmOppnCoDcs/w3qFertkLhLHyhmEEBXp0OhzNKuCs01wg7ccYFZxOVv+CtzFbtkLVB1VW3Ty/zYogA==", + "version": "1.0.17", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.17.tgz", + "integrity": "sha512-ls2GjURfpBcGnIkwVrg2uuLnTBwd0vrEiUvbMo+GF3k81GAp2flCkVTM7ciAbo155Izk50dm0uXHYq1PIjwTxw==", "requires": { "ascii-table": "0.0.9", "lodash": "^4.17.21", diff --git a/package.json b/package.json index 72db7454f40dd..71dc8586f0280 100644 --- a/package.json +++ b/package.json @@ -94,7 +94,7 @@ "react-native-image-picker": "^4.8.5", "react-native-image-size": "git+https://github.com/Expensify/react-native-image-size#6b5ab5110dc3ed554f8eafbc38d7d87c17147972", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.15", + "react-native-onyx": "1.0.17", "react-native-pdf": "^6.6.2", "react-native-performance": "^2.0.0", "react-native-permissions": "^3.0.1", diff --git a/src/components/BlockingViews/FullPageNotFoundView.js b/src/components/BlockingViews/FullPageNotFoundView.js index 8a4447bb0cdc5..6502852afd2a7 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.js +++ b/src/components/BlockingViews/FullPageNotFoundView.js @@ -18,10 +18,30 @@ const propTypes = { /** If true, child components are replaced with a blocking "not found" view */ shouldShow: PropTypes.bool, + + /** The key in the translations file to use for the title */ + titleKey: PropTypes.string, + + /** The key in the translations file to use for the subtitle */ + subtitleKey: PropTypes.string, + + /** Whether we should show a back icon */ + shouldShowBackButton: PropTypes.bool, + + /** Whether we should show a close button */ + shouldShowCloseButton: PropTypes.bool, + + /** Method to trigger when pressing the back button of the header */ + onBackButtonPress: PropTypes.func, }; const defaultProps = { shouldShow: false, + titleKey: 'notFound.notHere', + subtitleKey: 'notFound.pageNotFound', + shouldShowBackButton: true, + shouldShowCloseButton: true, + onBackButtonPress: () => Navigation.dismissModal(), }; // eslint-disable-next-line rulesdir/no-negated-variables @@ -30,15 +50,16 @@ const FullPageNotFoundView = (props) => { return ( <> Navigation.dismissModal()} + shouldShowBackButton={props.shouldShowBackButton} + shouldShowCloseButton={props.shouldShowCloseButton} + onBackButtonPress={props.onBackButtonPress} onCloseButtonPress={() => Navigation.dismissModal()} /> diff --git a/src/languages/en.js b/src/languages/en.js index 1b7e384a92dd4..9ea22843b1f62 100755 --- a/src/languages/en.js +++ b/src/languages/en.js @@ -525,6 +525,7 @@ export default { iouReportNotFound: 'The payment details you are looking for cannot be found.', notHere: "Hmm... it's not here", pageNotFound: 'That page is nowhere to be found.', + noAccess: 'You don\'t have access to this chat', }, setPasswordPage: { enterPassword: 'Enter a password', @@ -822,6 +823,7 @@ export default { error: { genericAdd: 'There was a problem adding this workspace member.', cannotRemove: 'You cannot remove yourself or the workspace owner.', + genericRemove: 'There was a problem removing that workspace member.', }, }, card: { diff --git a/src/languages/es.js b/src/languages/es.js index 9fee9c421a5a1..9676d7b748a2c 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -525,6 +525,7 @@ export default { iouReportNotFound: 'Los detalles del pago que estás buscando no se pudieron encontrar.', notHere: 'Hmm… no está aquí', pageNotFound: 'La página que buscas no existe.', + noAccess: 'No tienes acceso a este chat', }, setPasswordPage: { enterPassword: 'Escribe una contraseña', @@ -824,6 +825,7 @@ export default { error: { genericAdd: 'Ha ocurrido un problema al agregar el miembro al espacio de trabajo', cannotRemove: 'No puedes eliminarte ni a ti mismo ni al dueño del espacio de trabajo.', + genericRemove: 'Ha ocurrido un problema al eliminar al miembro del espacio de trabajo.', }, }, card: { diff --git a/src/libs/ActiveClientManager/index.js b/src/libs/ActiveClientManager/index.js index 7f0d4bf0cd91b..908a500d6b724 100644 --- a/src/libs/ActiveClientManager/index.js +++ b/src/libs/ActiveClientManager/index.js @@ -1,3 +1,9 @@ +/** + * When you have many tabs in one browser, the data of Onyx is shared between all of them. Since we persist write requests in Onyx, we need to ensure that + * only one tab is processing those saved requests or we would be duplicating data (or creating errors). + * This file ensures exactly that by tracking all the clientIDs connected, storing the most recent one last and it considers that last clientID the "leader". + */ + import _ from 'underscore'; import Onyx from 'react-native-onyx'; import Str from 'expensify-common/lib/str'; @@ -6,38 +12,47 @@ import * as ActiveClients from '../actions/ActiveClients'; const clientID = Str.guid(); const maxClients = 20; - -let activeClients; - -let resolveIsReadyPromise; -const isReadyPromise = new Promise((resolve) => { - resolveIsReadyPromise = resolve; +let activeClients = []; +let resolveSavedSelfPromise; +const savedSelfPromise = new Promise((resolve) => { + resolveSavedSelfPromise = resolve; }); /** + * Determines when the client is ready. We need to wait both till we saved our ID in onyx AND the init method was called * @returns {Promise} */ function isReady() { - return isReadyPromise; + return savedSelfPromise; } Onyx.connect({ key: ONYXKEYS.ACTIVE_CLIENTS, callback: (val) => { - activeClients = !val ? [] : val; - if (activeClients.length >= maxClients) { + activeClients = val; + + // Remove from the beginning of the list any clients that are past the limit, to avoid having thousands of them + let removed = false; + while (activeClients.length >= maxClients) { activeClients.shift(); + removed = true; + } + + // Save the clients back to onyx, if they changed + if (removed) { ActiveClients.setActiveClients(activeClients); } }, }); /** - * Add our client ID to the list of active IDs + * Add our client ID to the list of active IDs. + * We want to ensure we have no duplicates and that the activeClient gets added at the end of the array (see isClientTheLeader) */ function init() { - ActiveClients.addClient(clientID) - .then(resolveIsReadyPromise); + activeClients = _.without(activeClients, clientID); + activeClients.push(clientID); + ActiveClients.setActiveClients(activeClients).then(resolveSavedSelfPromise); } /** diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index a402d67761b18..0b9b62c259166 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -20,7 +20,6 @@ import KeyboardShortcut from '../../KeyboardShortcut'; import Navigation from '../Navigation'; import * as User from '../../actions/User'; import * as Modal from '../../actions/Modal'; -import * as Policy from '../../actions/Policy'; import modalCardStyleInterpolator from './modalCardStyleInterpolator'; import createCustomModalStackNavigator from './createCustomModalStackNavigator'; @@ -100,7 +99,6 @@ class AuthScreens extends React.Component { authEndpoint: `${CONFIG.EXPENSIFY.URL_API_ROOT}api?command=AuthenticatePusher`, }).then(() => { User.subscribeToUserEvents(); - Policy.subscribeToPolicyEvents(); }); // Listen for report changes and fetch some data we need on initialization diff --git a/src/libs/actions/ActiveClients.js b/src/libs/actions/ActiveClients.js index 2a3689b2c099c..744944cfef1c5 100644 --- a/src/libs/actions/ActiveClients.js +++ b/src/libs/actions/ActiveClients.js @@ -3,20 +3,13 @@ import ONYXKEYS from '../../ONYXKEYS'; /** * @param {Array} activeClients + * @return {Promise} */ function setActiveClients(activeClients) { - Onyx.set(ONYXKEYS.ACTIVE_CLIENTS, activeClients); -} - -/** - * @param {Number} clientID - * @returns {Promise} - */ -function addClient(clientID) { - return Onyx.merge(ONYXKEYS.ACTIVE_CLIENTS, [clientID]); + return Onyx.set(ONYXKEYS.ACTIVE_CLIENTS, activeClients); } export { + // eslint-disable-next-line import/prefer-default-export setActiveClients, - addClient, }; diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 12efd891f05d5..ffcbfdc7b4438 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -6,15 +6,11 @@ import Str from 'expensify-common/lib/str'; import * as DeprecatedAPI from '../deprecatedAPI'; import * as API from '../API'; import ONYXKEYS from '../../ONYXKEYS'; -import Growl from '../Growl'; -import CONFIG from '../../CONFIG'; import CONST from '../../CONST'; import * as Localize from '../Localize'; import Navigation from '../Navigation/Navigation'; import ROUTES from '../../ROUTES'; import * as OptionsListUtils from '../OptionsListUtils'; -import * as Report from './Report'; -import * as Pusher from '../Pusher/pusher'; import DateUtils from '../DateUtils'; import * as ReportUtils from '../ReportUtils'; @@ -209,29 +205,21 @@ function removeMembers(members, policyID) { if (members.length === 0) { return; } - - const employeeListUpdate = {}; - _.each(members, login => employeeListUpdate[login] = null); - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, employeeListUpdate); - - // Make the API call to remove a login from the policy - DeprecatedAPI.Policy_Employees_Remove({ + const membersListKey = `${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`; + const optimisticData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: membersListKey, + value: _.object(members, Array(members.length).fill({pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE})), + }]; + const failureData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: membersListKey, + value: _.object(members, Array(members.length).fill({errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal('workspace.people.error.genericRemove')}})), + }]; + API.write('DeleteMembersFromWorkspace', { emailList: members.join(','), policyID, - }) - .then((data) => { - if (data.jsonCode === 200) { - return; - } - - // Rollback removal on failure - _.each(members, login => employeeListUpdate[login] = {}); - Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY_MEMBER_LIST}${policyID}`, employeeListUpdate); - - // Show the user feedback that the removal failed - const errorMessage = data.jsonCode === 666 ? data.message : Localize.translateLocal('workspace.people.genericFailureMessage'); - Growl.show(errorMessage, CONST.GROWL.ERROR, 5000); - }); + }, {optimisticData, failureData}); } /** @@ -653,31 +641,6 @@ function updateLastAccessedWorkspace(policyID) { Onyx.set(ONYXKEYS.LAST_ACCESSED_WORKSPACE_POLICY_ID, policyID); } -/** - * Subscribe to public-policyEditor-[policyID] events. - */ -function subscribeToPolicyEvents() { - _.each(allPolicies, (policy) => { - const pusherChannelName = `public-policyEditor-${policy.id}${CONFIG.PUSHER.SUFFIX}`; - Pusher.subscribe(pusherChannelName, 'policyEmployeeRemoved', ({removedEmails, policyExpenseChatIDs, defaultRoomChatIDs}) => { - // Refetch the policy expense chats to update their state and their actions to get the archive reason - if (!_.isEmpty(policyExpenseChatIDs)) { - Report.fetchChatReportsByIDs(policyExpenseChatIDs); - _.each(policyExpenseChatIDs, (reportID) => { - Report.reconnect(reportID); - }); - } - - // Remove the default chats if we are one of the users getting removed - if (removedEmails.includes(sessionEmail) && !_.isEmpty(defaultRoomChatIDs)) { - _.each(defaultRoomChatIDs, (chatID) => { - Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatID}`, null); - }); - } - }); - }); -} - /** * Removes an error after trying to delete a member * @@ -974,7 +937,6 @@ export { updateWorkspaceCustomUnit, updateCustomUnitRate, updateLastAccessedWorkspace, - subscribeToPolicyEvents, clearDeleteMemberError, clearAddMemberError, clearDeleteWorkspaceError, diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 9008c209e53fe..d9ce4cba8cc7f 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -331,14 +331,6 @@ function fetchChatReportsByIDs(chatList, shouldRedirectIfInaccessible = false) { // Fetch the personal details if there are any PersonalDetails.getFromReportParticipants(_.values(simplifiedReports)); return simplifiedReports; - }) - .catch((err) => { - if (err.message !== CONST.REPORT.ERROR.INACCESSIBLE_REPORT) { - return; - } - - // eslint-disable-next-line no-use-before-define - handleInaccessibleReport(); }); } @@ -1086,6 +1078,7 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { isEdited: true, html: htmlForNewComment, text: textForNewComment, + type: originalReportAction.message[0].type, }], }, }; @@ -1212,14 +1205,6 @@ function navigateToConciergeChat() { Navigation.navigate(ROUTES.getReportRoute(conciergeChatReportID)); } -/** - * Handle the navigation when report is inaccessible - */ -function handleInaccessibleReport() { - Growl.error(Localize.translateLocal('notFound.chatYouLookingForCannotBeFound')); - navigateToConciergeChat(); -} - /** * Creates a policy room, fetches it, and navigates to it. * @param {String} policyID @@ -1544,7 +1529,6 @@ export { getSimplifiedIOUReport, syncChatAndIOUReports, navigateToConciergeChat, - handleInaccessibleReport, setReportWithDraft, createPolicyRoom, addPolicyReport, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index b76237b06b3ab..df96205025373 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -33,6 +33,7 @@ import withDrawerState, {withDrawerPropTypes} from '../../components/withDrawerS import Banner from '../../components/Banner'; import withLocalize from '../../components/withLocalize'; import reportPropTypes from '../reportPropTypes'; +import FullPageNotFoundView from '../../components/BlockingViews/FullPageNotFoundView'; const propTypes = { /** Navigation route context info provided by react navigation */ @@ -169,10 +170,6 @@ class ReportScreen extends React.Component { */ storeCurrentlyViewedReport() { const reportIDFromPath = getReportID(this.props.route); - if (_.isNaN(reportIDFromPath)) { - Report.handleInaccessibleReport(); - return; - } // Always reset the state of the composer view when the current reportID changes toggleReportActionComposeView(true); @@ -239,81 +236,91 @@ class ReportScreen extends React.Component { style={[styles.appContent, styles.flex1, {marginTop: this.state.viewportOffsetTop}]} keyboardAvoidingViewBehavior={Platform.OS === 'android' ? '' : 'padding'} > - - Navigation.navigate(ROUTES.HOME)} - /> - - {this.props.accountManagerReportID && ReportUtils.isConciergeChatReport(this.props.report) && this.state.isBannerVisible && ( - - )} - this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} + { + Navigation.navigate(ROUTES.HOME); + }} > - {this.shouldShowLoader() - ? ( - - ) - : ( - - )} - {(isArchivedRoom || hideComposer) && ( - - {isArchivedRoom && ( - + Navigation.navigate(ROUTES.HOME)} + /> + + {this.props.accountManagerReportID && ReportUtils.isConciergeChatReport(this.props.report) && this.state.isBannerVisible && ( + + )} + this.setState({skeletonViewContainerHeight: event.nativeEvent.layout.height})} + > + {this.shouldShowLoader() + ? ( + + ) + : ( + )} - {!this.props.isSmallScreenWidth && ( - - {hideComposer && ( - - )} - - )} - - )} - {(!hideComposer && this.props.session.shouldShowComposeInput) && ( - - - - + {isArchivedRoom && ( + - - - - )} - + )} + {!this.props.isSmallScreenWidth && ( + + {hideComposer && ( + + )} + + )} + + )} + {(!hideComposer && this.props.session.shouldShowComposeInput) && ( + + + + + + + + )} + + ); } diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 1de2d17595652..0b3f125703404 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -283,14 +283,20 @@ class WorkspaceMembersPage extends React.Component { } render() { - const policyMemberList = _.keys(lodashGet(this.props, 'policyMemberList', {})); - const removableMembers = _.without(policyMemberList, this.props.session.email, this.props.policy.owner); - const data = _.chain(policyMemberList) - .map(email => this.props.personalDetails[email]) - .filter() - .sortBy(person => person.displayName.toLowerCase()) - .map(person => ({...person})) // TODO: here we will add the pendingAction and errors prop - .value(); + const policyMemberList = lodashGet(this.props, 'policyMemberList', {}); + const removableMembers = []; + let data = []; + _.each(policyMemberList, (policyMember, email) => { + if (email !== this.props.session.email && email !== this.props.policy.owner) { + removableMembers.push(email); + } + const details = this.props.personalDetails[email] || {displayName: email, login: email}; + data.push({ + ...policyMember, + ...details, + }); + }); + data = _.sortBy(data, value => value.displayName.toLowerCase()); const policyID = lodashGet(this.props.route, 'params.policyID'); const policyName = lodashGet(this.props.policy, 'name'); diff --git a/src/pages/workspace/withPolicy.js b/src/pages/workspace/withPolicy.js index d9c85a5387ae5..a23ffdcbc97a8 100644 --- a/src/pages/workspace/withPolicy.js +++ b/src/pages/workspace/withPolicy.js @@ -8,7 +8,6 @@ import CONST from '../../CONST'; import getComponentDisplayName from '../../libs/getComponentDisplayName'; import * as Policy from '../../libs/actions/Policy'; import ONYXKEYS from '../../ONYXKEYS'; -import policyMemberPropType from '../policyMemberPropType'; /** * @param {Object} route @@ -56,9 +55,6 @@ const policyPropTypes = { */ errorFields: PropTypes.objectOf(PropTypes.objectOf(PropTypes.string)), }), - - /** The policy member list for the current route */ - policyMemberList: PropTypes.objectOf(policyMemberPropType), }; const policyDefaultProps = {