diff --git a/assets/emojis.js b/assets/emojis.js index 9a5a2f28968..6fe6ddf0f06 100644 --- a/assets/emojis.js +++ b/assets/emojis.js @@ -2259,6 +2259,13 @@ const emojis = [ 'meeting', 'shake', ], + types: [ + '🤝🏿', + '🤝🏾', + '🤝🏽', + '🤝🏼', + '🤝🏻', + ], }, { name: 'pray', diff --git a/package-lock.json b/package-lock.json index a75657407e6..7ce33483263 100644 --- a/package-lock.json +++ b/package-lock.json @@ -68,7 +68,7 @@ "react-native-image-picker": "^4.10.2", "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.32", + "react-native-onyx": "1.0.35", "react-native-pdf": "^6.6.2", "react-native-performance": "^4.0.0", "react-native-permissions": "^3.0.1", @@ -35507,9 +35507,9 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.32.tgz", - "integrity": "sha512-mhmCrxYfNlLM8bpP2M5g1u90115VqbJ1Lt2PjyrQsJihHRTdc7yqbiWwWlEQ1KyAYVR79JCSesKelgkdRAZIag==", + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.35.tgz", + "integrity": "sha512-HQDSM0c2ADb54NoSQdxqeJOhViICB9HwE8aMEB62AdHkRw6crCoIX7iSIF0ewbZ2A/hbX3frewWq8AUh3AyMvA==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", @@ -69938,9 +69938,9 @@ } }, "react-native-onyx": { - "version": "1.0.32", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.32.tgz", - "integrity": "sha512-mhmCrxYfNlLM8bpP2M5g1u90115VqbJ1Lt2PjyrQsJihHRTdc7yqbiWwWlEQ1KyAYVR79JCSesKelgkdRAZIag==", + "version": "1.0.35", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.35.tgz", + "integrity": "sha512-HQDSM0c2ADb54NoSQdxqeJOhViICB9HwE8aMEB62AdHkRw6crCoIX7iSIF0ewbZ2A/hbX3frewWq8AUh3AyMvA==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index d893766bbcd..25c960d96a1 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "react-native-image-picker": "^4.10.2", "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.32", + "react-native-onyx": "1.0.35", "react-native-pdf": "^6.6.2", "react-native-performance": "^4.0.0", "react-native-permissions": "^3.0.1", diff --git a/src/components/AddPlaidBankAccount.js b/src/components/AddPlaidBankAccount.js index 29d76102b4b..386eea4c325 100644 --- a/src/components/AddPlaidBankAccount.js +++ b/src/components/AddPlaidBankAccount.js @@ -119,8 +119,7 @@ class AddPlaidBankAccount extends React.Component { if (!plaidBankAccounts.length) { return ( - {(!token || this.props.plaidData.isLoading) - && ( + {this.props.plaidData.isLoading && ( diff --git a/src/components/RoomHeaderAvatars.js b/src/components/RoomHeaderAvatars.js index 7d8b5f13400..0bde27133d2 100644 --- a/src/components/RoomHeaderAvatars.js +++ b/src/components/RoomHeaderAvatars.js @@ -76,7 +76,7 @@ const RoomHeaderAvatars = (props) => { <> diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 12c1d4adb64..f87fd13a89f 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1,5 +1,4 @@ import {Linking} from 'react-native'; -import moment from 'moment'; import _ from 'underscore'; import lodashGet from 'lodash/get'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; @@ -1110,6 +1109,10 @@ function subscribeToNewActionEvent(reportID, callback) { * @param {Object} action */ function showReportActionNotification(reportID, action) { + if (ReportActionsUtils.isDeletedAction(action)) { + return; + } + if (!ActiveClientManager.isClientTheLeader()) { Log.info('[LOCAL_NOTIFICATION] Skipping notification because this client is not the leader'); return; @@ -1153,6 +1156,12 @@ function showReportActionNotification(reportID, action) { Navigation.navigate(ROUTES.getReportRoute(reportID)); }, }); + + // Notify the ReportActionsView that a new comment has arrived + if (reportID === newActionSubscriber.reportID) { + const isFromCurrentUser = action.actorAccountID === currentUserAccountID; + newActionSubscriber.callback(isFromCurrentUser, action.reportActionID); + } } /** @@ -1164,51 +1173,6 @@ function clearIOUError(reportID) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {errorFields: {iou: null}}); } -// We are using this map to ensure actions are only handled once -const handledReportActions = {}; -Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - initWithStoredValues: false, - callback: (actions, key) => { - // reportID can be derived from the Onyx key - const reportID = key.split('_')[1]; - if (!reportID) { - return; - } - - _.each(actions, (action) => { - if (lodashGet(handledReportActions, [reportID, action.sequenceNumber])) { - return; - } - - if (ReportActionsUtils.isDeletedAction(action)) { - return; - } - - if (!action.created) { - return; - } - - // If we are past the deadline to notify for this comment don't do it - if (moment.utc(moment(action.created).unix() * 1000).isBefore(moment.utc().subtract(10, 'seconds'))) { - handledReportActions[reportID] = handledReportActions[reportID] || {}; - handledReportActions[reportID][action.sequenceNumber] = true; - return; - } - - // Notify the ReportActionsView that a new comment has arrived - if (reportID === newActionSubscriber.reportID) { - const isFromCurrentUser = action.actorAccountID === currentUserAccountID; - newActionSubscriber.callback(isFromCurrentUser, action.reportActionID); - } - - showReportActionNotification(reportID, action); - handledReportActions[reportID] = handledReportActions[reportID] || {}; - handledReportActions[reportID][action.sequenceNumber] = true; - }); - }, -}); - export { addComment, addAttachment, @@ -1240,4 +1204,5 @@ export { clearIOUError, getMaxSequenceNumber, subscribeToNewActionEvent, + showReportActionNotification, }; diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index ddd53f65e92..52e0eb0e28a 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -55,18 +55,6 @@ Onyx.connect({ function signOut() { Log.info('Flushing logs before signing out', true, {}, true); - const optimisticData = [ - { - onyxMethod: CONST.ONYX.METHOD.SET, - key: ONYXKEYS.SESSION, - value: null, - }, - { - onyxMethod: CONST.ONYX.METHOD.SET, - key: ONYXKEYS.CREDENTIALS, - value: {}, - }, - ]; API.write('LogOut', { // Send current authToken because we will immediately clear it once triggering this command authToken: NetworkStore.getAuthToken(), @@ -74,7 +62,7 @@ function signOut() { partnerName: CONFIG.EXPENSIFY.PARTNER_NAME, partnerPassword: CONFIG.EXPENSIFY.PARTNER_PASSWORD, shouldRetry: false, - }, {optimisticData}); + }); Timing.clearData(); } diff --git a/src/libs/actions/SignInRedirect.js b/src/libs/actions/SignInRedirect.js index e66e50cab26..c7b1079c04f 100644 --- a/src/libs/actions/SignInRedirect.js +++ b/src/libs/actions/SignInRedirect.js @@ -6,20 +6,6 @@ import * as Localize from '../Localize'; import * as PersistedRequests from './PersistedRequests'; import NetworkConnection from '../NetworkConnection'; -let currentActiveClients; -Onyx.connect({ - key: ONYXKEYS.ACTIVE_CLIENTS, - callback: (val) => { - currentActiveClients = !val ? [] : val; - }, -}); - -let currentPreferredLocale; -Onyx.connect({ - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - callback: val => currentPreferredLocale = val, -}); - let currentIsOffline; let currentShouldForceOffline; Onyx.connect({ @@ -37,31 +23,27 @@ Onyx.connect({ * @param {String} errorMessage */ function clearStorageAndRedirect(errorMessage) { - const activeClients = currentActiveClients; - const preferredLocale = currentPreferredLocale; - const isOffline = currentIsOffline; - const shouldForceOffline = currentShouldForceOffline; + // Under certain conditions, there are key-values we'd like to keep in storage even when a user is logged out. + // We pass these into the clear() method in order to avoid having to reset them on a delayed tick and getting + // flashes of unwanted default state. + const keysToPreserve = []; + keysToPreserve.push(ONYXKEYS.NVP_PREFERRED_LOCALE); + keysToPreserve.push(ONYXKEYS.ACTIVE_CLIENTS); - // Clearing storage discards the authToken. This causes a redirect to the SignIn screen - Onyx.clear() - .then(() => { - if (preferredLocale) { - Onyx.set(ONYXKEYS.NVP_PREFERRED_LOCALE, preferredLocale); - } - if (activeClients && activeClients.length > 0) { - Onyx.set(ONYXKEYS.ACTIVE_CLIENTS, activeClients); - } + // After signing out, set ourselves as offline if we were offline before logging out and we are not forcing it. + // If we are forcing offline, ignore it while signed out, otherwise it would require a refresh because there's no way to toggle the switch to go back online while signed out. + if (currentIsOffline && !currentShouldForceOffline) { + keysToPreserve.push(ONYXKEYS.NETWORK); + } - // After signing out, set ourselves as offline if we were offline before logging out and we are not forcing it. - // If we are forcing offline, ignore it while signed out, otherwise it would require a refresh because there's no way to toggle the switch to go back online while signed out. - if (isOffline && !shouldForceOffline) { - Onyx.set(ONYXKEYS.NETWORK, {isOffline}); + Onyx.clear(keysToPreserve) + .then(() => { + if (!errorMessage) { + return; } - // `Onyx.clear` reinitialize the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` - if (errorMessage) { - Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}}); - } + // `Onyx.clear` reinitializes the Onyx instance with initial values so use `Onyx.merge` instead of `Onyx.set` + Onyx.merge(ONYXKEYS.SESSION, {errors: {[DateUtils.getMicroseconds()]: Localize.translateLocal(errorMessage)}}); }); } diff --git a/src/libs/actions/User.js b/src/libs/actions/User.js index 532e7bb04c6..784a23a5132 100644 --- a/src/libs/actions/User.js +++ b/src/libs/actions/User.js @@ -17,6 +17,7 @@ import * as Localize from '../Localize'; import * as Link from './Link'; import * as SequentialQueue from '../Network/SequentialQueue'; import PusherUtils from '../PusherUtils'; +import * as Report from './Report'; let currentUserAccountID = ''; Onyx.connect({ @@ -256,6 +257,22 @@ function deletePaypalMeAddress() { Growl.show(Localize.translateLocal('paymentsPage.deletePayPalSuccess'), CONST.GROWL.SUCCESS, 3000); } +function triggerNotifications(onyxUpdates) { + _.each(onyxUpdates, (update) => { + if (!update.shouldNotify) { + return; + } + + const reportID = update.key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); + const reportAction = _.chain(update.value) + .values() + .compact() + .first() + .value(); + Report.showReportActionNotification(reportID, reportAction); + }); +} + /** * Initialize our pusher subscription to listen for user changes */ @@ -271,6 +288,7 @@ function subscribeToUserEvents() { PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.ONYX_API_UPDATE, currentUserAccountID, (pushJSON) => { SequentialQueue.getCurrentRequest().then(() => { Onyx.update(pushJSON); + triggerNotifications(pushJSON); }); }); diff --git a/src/pages/GetAssistancePage.js b/src/pages/GetAssistancePage.js index fea935c176c..2f849c9a998 100644 --- a/src/pages/GetAssistancePage.js +++ b/src/pages/GetAssistancePage.js @@ -59,7 +59,6 @@ const GetAssistancePage = props => ( onPress: () => Link.openExternalLink(CONST.NEWHELP_URL), icon: Expensicons.QuestionMark, shouldShowRightIcon: true, - iconFill: themeColors.success, wrapperStyle: [styles.cardMenuItem], }, ]} diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index b8b2812b23e..9ca95796f59 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -87,6 +87,7 @@ class WorkspaceInvitePage extends React.Component { selectedOptions: [], userToInvite, welcomeNote: this.getWelcomeNote(), + shouldDisableButton: false, }; } @@ -108,8 +109,13 @@ class WorkspaceInvitePage extends React.Component { } getExcludedUsers() { - const policyMemberList = _.keys(lodashGet(this.props, 'policyMemberList', {})); - return [...CONST.EXPENSIFY_EMAILS, ...policyMemberList]; + const policyMemberList = lodashGet(this.props, 'policyMemberList', {}); + const usersToExclude = _.filter(_.keys(policyMemberList), policyMember => ( + this.props.network.isOffline + || policyMemberList[policyMember].pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE + || !_.isEmpty(policyMemberList[policyMember].errors) + )); + return [...CONST.EXPENSIFY_EMAILS, ...usersToExclude]; } /** @@ -250,10 +256,12 @@ class WorkspaceInvitePage extends React.Component { return; } - const logins = _.map(this.state.selectedOptions, option => option.login); - const filteredLogins = _.uniq(_.compact(_.map(logins, login => login.toLowerCase().trim()))); - Policy.addMembersToWorkspace(filteredLogins, this.state.welcomeNote || this.getWelcomeNote(), this.props.route.params.policyID); - Navigation.goBack(); + this.setState({shouldDisableButton: true}, () => { + const logins = _.map(this.state.selectedOptions, option => option.login); + const filteredLogins = _.uniq(_.compact(_.map(logins, login => login.toLowerCase().trim()))); + Policy.addMembersToWorkspace(filteredLogins, this.state.welcomeNote || this.getWelcomeNote(), this.props.route.params.policyID); + Navigation.goBack(); + }); } /** @@ -331,7 +339,7 @@ class WorkspaceInvitePage extends React.Component { /> { + const originalModule = jest.requireActual('../../src/libs/actions/Report'); + + return { + ...originalModule, + showReportActionNotification: jest.fn(), + }; +}); + describe('actions/Report', () => { beforeAll(() => { // When using the Pusher mock the act of calling Pusher.isSubscribed will create a @@ -470,4 +479,35 @@ describe('actions/Report', () => { expectedOutput = 'Comment www.google.com [www.facebook.com](https://www.facebook.com)'; expect(newCommentMarkdown).toBe(expectedOutput); }); + + it('should show a notification for report action updates with shouldNotify', () => { + const TEST_USER_ACCOUNT_ID = 1; + const REPORT_ID = 1; + const REPORT_ACTION = {}; + + // Setup user and pusher listeners + return TestHelper.signInWithTestUser(TEST_USER_ACCOUNT_ID) + .then(() => { + User.subscribeToUserEvents(); + return waitForPromisesToResolve(); + }) + .then(() => { + // Simulate a Pusher Onyx update with a report action with shouldNotify + const channel = Pusher.getChannel(`${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${TEST_USER_ACCOUNT_ID}${CONFIG.PUSHER.SUFFIX}`); + channel.emit(Pusher.TYPE.ONYX_API_UPDATE, [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${REPORT_ID}`, + value: { + 1: REPORT_ACTION, + }, + shouldNotify: true, + }, + ]); + return waitForPromisesToResolve(); + }).then(() => { + // Ensure we show a notification for this new report action + expect(Report.showReportActionNotification).toBeCalledWith(String(REPORT_ID), REPORT_ACTION); + }); + }); });