diff --git a/android/app/build.gradle b/android/app/build.gradle index 7ca8652a115e..11b699aae021 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -155,8 +155,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001019504 - versionName "1.1.95-4" + versionCode 1001019600 + versionName "1.1.96-0" buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString() if (isNewArchitectureEnabled()) { diff --git a/contributingGuides/OFFLINE_UX.md b/contributingGuides/OFFLINE_UX.md index 90d4b45d0204..ac40a4346350 100644 --- a/contributingGuides/OFFLINE_UX.md +++ b/contributingGuides/OFFLINE_UX.md @@ -60,7 +60,7 @@ This is the pattern where we queue the request to be sent when the user is onlin - the user should be given instant feedback and - the user does not need to know when the change is done on the server in the background -**How to implement:** Use [`API.write()`](https://github.com/Expensify/App/blob/3493f3ca3a1dc6cdbf9cb8bd342866fcaf45cf1d/src/libs/API.js#L7-L28) to implement this pattern. For this pattern we should only put `optimisticData` in the options. We don't need successData or failureData as we don't care what response comes back at all. +**How to implement:** Use [`API.write()`](https://github.com/Expensify/App/blob/3493f3ca3a1dc6cdbf9cb8bd342866fcaf45cf1d/src/libs/API.js#L7-L28) to implement this pattern. For this pattern we should only put `optimisticData` in the options. We don't need `successData` or `failureData` as we don't care what response comes back at all. **Example:** Pinning a chat. @@ -78,6 +78,10 @@ When the user is offline: - Use API.write() to implement this pattern - Optimistic data should include `pendingAction` ([with these possible values](https://github.com/Expensify/App/blob/15f7fa622805ee2971808d6bc67181c4715f0c62/src/CONST.js#L775-L779)) - To ensure the UI is shown as described above, you should enclose the components that contain the data that was added/updated/deleted with the OfflineWithFeedback component +- Include this data in the action call: + - `optimisticData` - always include this object when using the Pattern B + - `successData` - include this if the action is `update` or `delete`. You do not have to include this if the action is `add` (same data was already passed using the `optimisticData` object) + - `failureData` - always include this object. In case of `add` action, you will want to add some generic error which covers some unexpected failures which were not handled in the backend **Handling errors:** - The [OfflineWithFeedback component](https://github.com/Expensify/App/blob/main/src/components/OfflineWithFeedback.js) already handles showing errors too, as long as you pass the error field in the [errors prop](https://github.com/Expensify/App/blob/128ea378f2e1418140325c02f0b894ee60a8e53f/src/components/OfflineWithFeedback.js#L29-L31) diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 28154cd4944d..c7ec264285fb 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -17,7 +17,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.1.95 + 1.1.96 CFBundleSignature ???? CFBundleURLTypes @@ -30,7 +30,7 @@ CFBundleVersion - 1.1.95.4 + 1.1.96.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index f73aed273080..c3afd6409c08 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.1.95 + 1.1.96 CFBundleSignature ???? CFBundleVersion - 1.1.95.4 + 1.1.96.0 diff --git a/package-lock.json b/package-lock.json index edd7f7b52de6..dd20c9c7d657 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.1.95-4", + "version": "1.1.96-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.1.95-4", + "version": "1.1.96-0", "hasInstallScript": true, "license": "MIT", "dependencies": { @@ -60,7 +60,7 @@ "react-native-collapsible": "^1.6.0", "react-native-config": "^1.4.5", "react-native-document-picker": "^8.0.0", - "react-native-gesture-handler": "2.5.0", + "react-native-gesture-handler": "2.6.0", "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#3bbd17d63e6c38d38d857b50f6037c1c0376ff06", "react-native-haptic-feedback": "^1.13.0", "react-native-image-pan-zoom": "^2.1.12", @@ -77,7 +77,7 @@ "react-native-render-html": "6.3.1", "react-native-safe-area-context": "^3.1.4", "react-native-screens": "^3.10.1", - "react-native-svg": "^12.1.0", + "react-native-svg": "^13.1.0", "react-native-webview": "^11.17.2", "react-pdf": "5.7.2", "react-plaid-link": "3.3.2", @@ -31541,9 +31541,9 @@ } }, "node_modules/react-native-gesture-handler": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.5.0.tgz", - "integrity": "sha512-djZdcprFf08PZC332D+AeG5wcGeAPhzfCJtB3otUgOgTlvjVXmg/SLFdPJSpzLBqkRAmrC77tM79QgKbuLxkfw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.6.0.tgz", + "integrity": "sha512-IwdYdt5FKjjbRSrSqh8hoNctlYZl5DFnqSJ6buKtrl4A4gyzkrtW6WcmOFl5LnCa6Bcw+znSD77O6UiZ8qda7g==", "dependencies": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", @@ -31790,16 +31790,16 @@ } }, "node_modules/react-native-svg": { - "version": "12.4.4", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-12.4.4.tgz", - "integrity": "sha512-LpcNlEVCURexqPAvQ9ne8KrPVfYz0wIDygwud8VMRmXLezysXzyQN/DTsjm1BO9lIfYp55WQsr3u3yW/vk6iiA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.1.0.tgz", + "integrity": "sha512-drYa+0piaQ27xFEp1MxRBSu6eHbR37qQITKTHNOmPv1NhPUyZ5tH4ICWe7aTLlB2u6KEhpSHl63HJi3jpZFtvw==", "dependencies": { "css-select": "^5.1.0", "css-tree": "^1.1.3" }, "peerDependencies": { "react": "*", - "react-native": ">=0.50.0" + "react-native": "*" } }, "node_modules/react-native-svg-transformer": { @@ -64220,9 +64220,9 @@ "requires": {} }, "react-native-gesture-handler": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.5.0.tgz", - "integrity": "sha512-djZdcprFf08PZC332D+AeG5wcGeAPhzfCJtB3otUgOgTlvjVXmg/SLFdPJSpzLBqkRAmrC77tM79QgKbuLxkfw==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/react-native-gesture-handler/-/react-native-gesture-handler-2.6.0.tgz", + "integrity": "sha512-IwdYdt5FKjjbRSrSqh8hoNctlYZl5DFnqSJ6buKtrl4A4gyzkrtW6WcmOFl5LnCa6Bcw+znSD77O6UiZ8qda7g==", "requires": { "@egjs/hammerjs": "^2.0.17", "hoist-non-react-statics": "^3.3.0", @@ -64391,9 +64391,9 @@ } }, "react-native-svg": { - "version": "12.4.4", - "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-12.4.4.tgz", - "integrity": "sha512-LpcNlEVCURexqPAvQ9ne8KrPVfYz0wIDygwud8VMRmXLezysXzyQN/DTsjm1BO9lIfYp55WQsr3u3yW/vk6iiA==", + "version": "13.1.0", + "resolved": "https://registry.npmjs.org/react-native-svg/-/react-native-svg-13.1.0.tgz", + "integrity": "sha512-drYa+0piaQ27xFEp1MxRBSu6eHbR37qQITKTHNOmPv1NhPUyZ5tH4ICWe7aTLlB2u6KEhpSHl63HJi3jpZFtvw==", "requires": { "css-select": "^5.1.0", "css-tree": "^1.1.3" diff --git a/package.json b/package.json index 158459e78c34..7fe2c037842d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.1.95-4", + "version": "1.1.96-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", @@ -87,7 +87,7 @@ "react-native-collapsible": "^1.6.0", "react-native-config": "^1.4.5", "react-native-document-picker": "^8.0.0", - "react-native-gesture-handler": "2.5.0", + "react-native-gesture-handler": "2.6.0", "react-native-google-places-autocomplete": "git+https://github.com/Expensify/react-native-google-places-autocomplete.git#3bbd17d63e6c38d38d857b50f6037c1c0376ff06", "react-native-haptic-feedback": "^1.13.0", "react-native-image-pan-zoom": "^2.1.12", @@ -104,7 +104,7 @@ "react-native-render-html": "6.3.1", "react-native-safe-area-context": "^3.1.4", "react-native-screens": "^3.10.1", - "react-native-svg": "^12.1.0", + "react-native-svg": "^13.1.0", "react-native-webview": "^11.17.2", "react-pdf": "5.7.2", "react-plaid-link": "3.3.2", diff --git a/src/components/DotIndicatorMessage.js b/src/components/DotIndicatorMessage.js new file mode 100644 index 000000000000..2b073627ef51 --- /dev/null +++ b/src/components/DotIndicatorMessage.js @@ -0,0 +1,68 @@ +import React from 'react'; +import _ from 'underscore'; +import PropTypes from 'prop-types'; +import {View} from 'react-native'; +import styles from '../styles/styles'; +import Icon from './Icon'; +import * as Expensicons from './Icon/Expensicons'; +import colors from '../styles/colors'; +import variables from '../styles/variables'; +import Text from './Text'; + +const propTypes = { + /** + * In most cases this should just be errors from onxyData + * if you are not passing that data then this needs to be in a similar shape like + * { + * timestamp: 'message', + * } + */ + messages: PropTypes.objectOf(PropTypes.string), + + // The type of message, 'error' shows a red dot, 'success' shows a green dot + type: PropTypes.oneOf(['error', 'success']).isRequired, + + // Additional styles to apply to the container */ + // eslint-disable-next-line react/forbid-prop-types + style: PropTypes.arrayOf(PropTypes.object), +}; + +const defaultProps = { + messages: {}, + style: [], +}; + +const DotIndicatorMessage = (props) => { + if (_.isEmpty(props.messages)) { + return null; + } + + // To ensure messages are presented in order we are sort of destroying the data we are given + // and rebuilding as an array so we can render the messages in order. We don't really care about + // the microtime timestamps anyways so isn't the end of the world that we sort of lose them here. + // BEWARE: if you decide to refactor this and keep the microtime keys it could cause performance issues + const sortedMessages = _.chain(props.messages) + .keys() + .sortBy() + .map(key => props.messages[key]) + .value(); + + return ( + + + + + + {_.map(sortedMessages, (message, i) => ( + {message} + ))} + + + ); +}; + +DotIndicatorMessage.propTypes = propTypes; +DotIndicatorMessage.defaultProps = defaultProps; + +export default DotIndicatorMessage; + diff --git a/src/components/OfflineWithFeedback.js b/src/components/OfflineWithFeedback.js index bd0eb8a8b6f7..54d6bc0ab9be 100644 --- a/src/components/OfflineWithFeedback.js +++ b/src/components/OfflineWithFeedback.js @@ -7,14 +7,12 @@ import withLocalize, {withLocalizePropTypes} from './withLocalize'; import {withNetwork} from './OnyxProvider'; import networkPropTypes from './networkPropTypes'; import stylePropTypes from '../styles/stylePropTypes'; -import Text from './Text'; import styles from '../styles/styles'; import Tooltip from './Tooltip'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import * as StyleUtils from '../styles/StyleUtils'; -import colors from '../styles/colors'; -import variables from '../styles/variables'; +import DotIndicatorMessage from './DotIndicatorMessage'; /** * This component should be used when we are using the offline pattern B (offline with feedback). @@ -83,11 +81,6 @@ const OfflineWithFeedback = (props) => { const needsStrikeThrough = props.network.isOffline && props.pendingAction === 'delete'; const hideChildren = !props.network.isOffline && props.pendingAction === 'delete' && !hasErrors; let children = props.children; - const sortedErrors = _.chain(props.errors) - .keys() - .sortBy() - .map(key => props.errors[key]) - .value(); // Apply strikethrough to children if needed, but skip it if we are not going to render them if (needsStrikeThrough && !hideChildren) { @@ -102,14 +95,7 @@ const OfflineWithFeedback = (props) => { )} {hasErrors && ( - - - - - {_.map(sortedErrors, (error, i) => ( - {error} - ))} - + `${count} new message${count > 1 ? 's' : ''}`, + newMessages: 'New messages', reportTypingIndicator: { isTyping: 'is typing...', areTyping: 'are typing...', diff --git a/src/languages/es.js b/src/languages/es.js index 82369add2179..3810cb69a7e7 100644 --- a/src/languages/es.js +++ b/src/languages/es.js @@ -202,7 +202,7 @@ export default { beginningOfChatHistoryPolicyExpenseChatPartTwo: ' y ', beginningOfChatHistoryPolicyExpenseChatPartThree: ' empieza aquí! :tada: Este es el lugar donde chatear, pedir dinero y pagar.', }, - newMessageCount: ({count}) => `${count} mensaje${count > 1 ? 's' : ''} nuevo${count > 1 ? 's' : ''}`, + newMessages: 'Mensajes nuevos', reportTypingIndicator: { isTyping: 'está escribiendo...', areTyping: 'están escribiendo...', diff --git a/src/libs/ErrorUtils.js b/src/libs/ErrorUtils.js index 115468ff1ea6..bcc6c59cae2b 100644 --- a/src/libs/ErrorUtils.js +++ b/src/libs/ErrorUtils.js @@ -1,3 +1,4 @@ +import _ from 'underscore'; import CONST from '../CONST'; /** @@ -35,7 +36,23 @@ function getAuthenticateErrorMessage(response) { } } +/** + * @param {Object} onyxData + * @param {Object} onyxData.errors + * @returns {String} + */ +function getLatestErrorMessage(onyxData) { + return _.chain(onyxData.errors || []) + .keys() + .sortBy() + .reverse() + .map(key => onyxData.errors[key]) + .first() + .value(); +} + export { // eslint-disable-next-line import/prefer-default-export getAuthenticateErrorMessage, + getLatestErrorMessage, }; diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.js b/src/libs/Navigation/AppNavigator/AuthScreens.js index cbb6e0af27a3..0d1eceb4d711 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.js +++ b/src/libs/Navigation/AppNavigator/AuthScreens.js @@ -8,7 +8,6 @@ import * as StyleUtils from '../../../styles/StyleUtils'; import withWindowDimensions, {windowDimensionsPropTypes} from '../../../components/withWindowDimensions'; import CONST from '../../../CONST'; import compose from '../../compose'; -import * as Report from '../../actions/Report'; import * as PersonalDetails from '../../actions/PersonalDetails'; import * as Pusher from '../../Pusher/pusher'; import PusherConnectionManager from '../../PusherConnectionManager'; @@ -110,7 +109,6 @@ class AuthScreens extends React.Component { cluster: CONFIG.PUSHER.CLUSTER, authEndpoint: `${CONFIG.EXPENSIFY.URL_API_ROOT}api?command=AuthenticatePusher`, }).then(() => { - Report.subscribeToUserEvents(); User.subscribeToUserEvents(); Policy.subscribeToPolicyEvents(); }); diff --git a/src/libs/NumberUtils.js b/src/libs/NumberUtils.js index 5acff89aca09..a63396d0569c 100644 --- a/src/libs/NumberUtils.js +++ b/src/libs/NumberUtils.js @@ -4,7 +4,6 @@ import CONST from '../CONST'; * Generates a random positive 64 bit numeric string by randomly generating the left, middle, and right parts and concatenating them. Used to generate client-side ids. * @returns {String} string representation of a randomly generated 64 bit signed integer */ -/* eslint-disable no-unused-vars */ function rand64() { // Max 64-bit signed: // 9,223,372,036,854,775,807 diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index d17c4bb4d330..2364ae9640bb 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -222,6 +222,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { const personalDetailMap = getPersonalDetailsForLogins(logins, personalDetails); const personalDetailList = _.values(personalDetailMap); const isArchivedRoom = ReportUtils.isArchivedRoom(report); + const isDefaultRoom = ReportUtils.isDefaultRoom(report); const hasMultipleParticipants = personalDetailList.length > 1 || isChatRoom || isPolicyExpenseChat; const personalDetail = personalDetailList[0]; const hasOutstandingIOU = lodashGet(report, 'hasOutstandingIOU', false); @@ -286,6 +287,7 @@ function createOption(logins, personalDetails, report, reportActions = {}, { iouReportAmount: lodashGet(iouReport, 'total', 0), isChatRoom, isArchivedRoom, + isDefaultRoom, shouldShowSubscript: isPolicyExpenseChat && !report.isOwnPolicyExpenseChat && !isArchivedRoom, isPolicyExpenseChat, }; @@ -428,8 +430,9 @@ function getOptions(reports, personalDetails, activeReportID, { const shouldFilterReportIfRead = hideReadReports && report.unreadActionCount === 0; const shouldFilterReport = shouldFilterReportIfEmpty || shouldFilterReportIfRead; + if (report.reportID !== activeReportID - && !report.isPinned + && (!report.isPinned || isDefaultRoom) && !hasDraftComment && shouldFilterReport && !reportContainsIOUDebt) { @@ -516,8 +519,10 @@ function getOptions(reports, personalDetails, activeReportID, { } // If the report is pinned and we are using the option to display pinned reports on top then we need to - // collect the pinned reports so we can sort them alphabetically once they are collected - if (prioritizePinnedReports && reportOption.isPinned) { + // collect the pinned reports so we can sort them alphabetically once they are collected. We want to skip + // default archived rooms. + if (prioritizePinnedReports && reportOption.isPinned + && !(reportOption.isArchivedRoom && reportOption.isDefaultRoom)) { pinnedReportOptions.push(reportOption); } else if (prioritizeIOUDebts && reportOption.hasOutstandingIOU && !reportOption.isIOUReportOwner) { iouDebtReportOptions.push(reportOption); diff --git a/src/libs/Pusher/EventType.js b/src/libs/Pusher/EventType.js index 97fa8cc9246b..79e7fb0c5137 100644 --- a/src/libs/Pusher/EventType.js +++ b/src/libs/Pusher/EventType.js @@ -4,7 +4,6 @@ */ export default { REPORT_COMMENT: 'reportComment', - REPORT_COMMENT_EDIT: 'reportCommentEdit', PREFERRED_LOCALE: 'preferredLocale', EXPENSIFY_CARD_UPDATE: 'expensifyCardUpdate', SCREEN_SHARE_REQUEST: 'screenshareRequest', diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 5cef373dd1ad..5146eeb8e986 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -99,7 +99,8 @@ function canEditReportAction(reportAction) { return reportAction.actorEmail === sessionEmail && reportAction.reportActionID && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT - && !isReportMessageAttachment(lodashGet(reportAction, ['message', 0], {})); + && !isReportMessageAttachment(lodashGet(reportAction, ['message', 0], {})) + && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } /** @@ -113,7 +114,8 @@ function canEditReportAction(reportAction) { function canDeleteReportAction(reportAction) { return reportAction.actorEmail === sessionEmail && reportAction.reportActionID - && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT; + && reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.ADDCOMMENT + && reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } /** diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 42a98a7e5850..ebc19a252847 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -2,6 +2,7 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; import lodashGet from 'lodash/get'; import {PUBLIC_DOMAINS} from 'expensify-common/lib/CONST'; +import Str from 'expensify-common/lib/str'; import * as DeprecatedAPI from '../deprecatedAPI'; import * as API from '../API'; import ONYXKEYS from '../../ONYXKEYS'; @@ -752,14 +753,6 @@ function clearAddMemberError(policyID, memberEmail) { }); } -/** - * @param {String} value - * @returns {String} - */ -function capitalizeFirstLetter(value) { - return value.charAt(0).toUpperCase() + value.slice(1); -} - /** * Generate a policy name based on an email and policy list. * @returns {String} @@ -774,9 +767,9 @@ function generateDefaultWorkspaceName() { const domain = emailParts[1]; if (_.includes(PUBLIC_DOMAINS, domain.toLowerCase())) { - defaultWorkspaceName = `${capitalizeFirstLetter(username)}'s Workspace`; + defaultWorkspaceName = `${Str.UCFirst(username)}'s Workspace`; } else { - defaultWorkspaceName = `${capitalizeFirstLetter(domain.split('.')[0])}'s Workspace`; + defaultWorkspaceName = `${Str.UCFirst(domain.split('.')[0])}'s Workspace`; } if (`@${domain.toLowerCase()}` === CONST.SMS.DOMAIN) { diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 541bb2a51a72..4e24a66322c2 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -24,7 +24,6 @@ import * as ReportUtils from '../ReportUtils'; import * as ReportActions from './ReportActions'; import Growl from '../Growl'; import * as Localize from '../Localize'; -import PusherUtils from '../PusherUtils'; import DateUtils from '../DateUtils'; import * as ReportActionsUtils from '../ReportActionsUtils'; import * as NumberUtils from '../NumberUtils'; @@ -416,16 +415,6 @@ function fetchIOUReportByID(iouReportID, chatReportID, shouldRedirectIfEmpty = f }); } -/** - * @param {Number} reportID - * @param {Number} sequenceNumber - */ -function setNewMarkerPosition(reportID, sequenceNumber) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - newMarkerSequenceNumber: sequenceNumber, - }); -} - /** * Get the private pusher channel name for a Report. * @@ -436,48 +425,6 @@ function getReportChannelName(reportID) { return `${CONST.PUSHER.PRIVATE_REPORT_CHANNEL_PREFIX}${reportID}${CONFIG.PUSHER.SUFFIX}`; } -/** - * Initialize our pusher subscriptions to listen for new report comments and pin toggles - */ -function subscribeToUserEvents() { - // If we don't have the user's accountID yet we can't subscribe so return early - if (!currentUserAccountID) { - return; - } - - const pusherChannelName = `${CONST.PUSHER.PRIVATE_USER_CHANNEL_PREFIX}${currentUserAccountID}${CONFIG.PUSHER.SUFFIX}`; - if (Pusher.isSubscribed(pusherChannelName) || Pusher.isAlreadySubscribing(pusherChannelName)) { - return; - } - - // Live-update a report's actions when an 'edit comment' event is received. - PusherUtils.subscribeToPrivateUserChannelEvent(Pusher.TYPE.REPORT_COMMENT_EDIT, - currentUserAccountID, - ({reportID, sequenceNumber, message}) => { - // We only want the active client to process these events once otherwise multiple tabs would decrement the 'unreadActionCount' - if (!ActiveClientManager.isClientTheLeader()) { - return; - } - - const actionsToMerge = {}; - actionsToMerge[sequenceNumber] = {message: [message]}; - - // If someone besides the current user deleted an action and the sequenceNumber is greater than our last read we will decrement the unread count - // we skip this for the current user because we should already have decremented the count optimistically when they deleted the comment. - const isFromCurrentUser = ReportActions.isFromCurrentUser(reportID, sequenceNumber, currentUserAccountID, actionsToMerge); - if (!message.html && !isFromCurrentUser && sequenceNumber > getLastReadSequenceNumber(reportID)) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - unreadActionCount: Math.max(getUnreadActionCount(reportID) - 1, 0), - }); - } - - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - lastMessageText: ReportActions.getLastVisibleMessageText(reportID, actionsToMerge), - }); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, actionsToMerge); - }); -} - /** * Setup reportComment push notification callbacks. */ @@ -684,7 +631,7 @@ function createOptimisticChatReport(participantList) { lastActorEmail: '', lastMessageHtml: '', lastMessageText: null, - lastReadSequenceNumber: undefined, + lastReadSequenceNumber: 0, lastMessageTimestamp: 0, lastVisitedTimestamp: 0, maxSequenceNumber: 0, @@ -880,73 +827,6 @@ function addComment(reportID, text) { addActions(reportID, text); } -/** - * Deletes a comment from the report, basically sets it as empty string - * - * @param {Number} reportID - * @param {Object} reportAction - */ -function deleteReportComment(reportID, reportAction) { - // Optimistic Response - const sequenceNumber = reportAction.sequenceNumber; - const reportActionsToMerge = {}; - const oldMessage = {...reportAction.message}; - reportActionsToMerge[sequenceNumber] = { - ...reportAction, - message: [ - { - type: CONST.REPORT.MESSAGE.TYPE.COMMENT, - html: '', - text: '', - }, - ], - }; - - // If the comment we are deleting is more recent than our last read comment we will update the unread count - if (sequenceNumber > getLastReadSequenceNumber(reportID)) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - unreadActionCount: Math.max(getUnreadActionCount(reportID) - 1, 0), - }); - } - - // Optimistically update the report and reportActions - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, reportActionsToMerge); - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - lastMessageText: ReportActions.getLastVisibleMessageText(reportID, reportActionsToMerge), - }); - - // Try to delete the comment by calling the API - DeprecatedAPI.Report_EditComment({ - reportID, - reportActionID: reportAction.reportActionID, - reportComment: '', - sequenceNumber, - }) - .then((response) => { - if (response.jsonCode === 200) { - return; - } - - // Reverse Optimistic Response - reportActionsToMerge[sequenceNumber] = { - ...reportAction, - message: oldMessage, - }; - - if (sequenceNumber > getLastReadSequenceNumber(reportID)) { - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - unreadActionCount: getUnreadActionCount(reportID) + 1, - }); - } - - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, { - lastMessageText: ReportActions.getLastVisibleMessageText(reportID, reportActionsToMerge), - }); - - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, reportActionsToMerge); - }); -} - /** * Gets the latest page of report actions and updates the last read message * @@ -1112,7 +992,6 @@ function markCommentAsUnread(reportID, sequenceNumber) { onyxMethod: CONST.ONYX.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { - newMarkerSequenceNumber: sequenceNumber, lastReadSequenceNumber: newLastReadSequenceNumber, lastVisitedTimestamp: Date.now(), unreadActionCount: calculateUnreadActionCount(reportID, newLastReadSequenceNumber, maxSequenceNumber), @@ -1216,6 +1095,88 @@ Onyx.connect({ callback: handleReportChanged, }); +/** + * Deletes a comment from the report, basically sets it as empty string + * + * @param {Number} reportID + * @param {Object} reportAction + */ +function deleteReportComment(reportID, reportAction) { + const sequenceNumber = reportAction.sequenceNumber; + const optimisticReportActions = { + [sequenceNumber]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, + }, + }; + + // If we are deleting the last visible message, let's find the previous visible one + // and update the lastMessageText in the chat preview. + const optimisticReport = { + lastMessageText: ReportActions.getLastVisibleMessageText(reportID, { + [sequenceNumber]: { + message: [{ + html: '', + text: '', + }], + }, + }), + }; + + // If the API call fails we must show the original message again, so we revert the message content back to how it was + // and and remove the pendingAction so the strike-through clears + const failureData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [sequenceNumber]: { + message: reportAction.message, + pendingAction: null, + }, + }, + }, + ]; + + const successData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [sequenceNumber]: { + pendingAction: null, + }, + }, + }, + ]; + + // If we are deleting an unread message that is greater than our last read we decrease the unreadActionCount + // since the message we are deleting is an unread + if (sequenceNumber > getLastReadSequenceNumber(reportID)) { + const unreadActionCount = getUnreadActionCount(reportID); + optimisticReport.unreadActionCount = Math.max(unreadActionCount - 1, 0); + } + + const optimisticData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: optimisticReportActions, + }, + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: optimisticReport, + }, + ]; + + const parameters = { + reportID, + sequenceNumber, + reportActionID: reportAction.reportActionID, + }; + API.write('DeleteComment', parameters, {optimisticData, successData, failureData}); +} + /** * Saves a new message for a comment. Marks the comment as edited, which will be reflected in the UI. * @@ -1241,32 +1202,59 @@ function editReportComment(reportID, originalReportAction, textForNewComment) { return; } - // Optimistically update the report action with the new message + // Optimistically update the reportAction with the new message const sequenceNumber = originalReportAction.sequenceNumber; - const newReportAction = {...originalReportAction}; - const actionToMerge = {}; - newReportAction.message[0].isEdited = true; - newReportAction.message[0].html = htmlForNewComment; - newReportAction.message[0].text = parser.htmlToText(htmlForNewComment); - actionToMerge[sequenceNumber] = newReportAction; - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, actionToMerge); - - // Persist the updated report comment - DeprecatedAPI.Report_EditComment({ + const optimisticReportActions = { + [sequenceNumber]: { + pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.UPDATE, + message: [{ + isEdited: true, + html: htmlForNewComment, + text: textForNewComment, + }], + }, + }; + + const optimisticData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: optimisticReportActions, + }, + ]; + + const failureData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [sequenceNumber]: { + ...originalReportAction, + pendingAction: null, + }, + }, + }, + ]; + + const successData = [ + { + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + [sequenceNumber]: { + pendingAction: null, + }, + }, + }, + ]; + + const parameters = { reportID, - reportActionID: originalReportAction.reportActionID, - reportComment: htmlForNewComment, sequenceNumber, - }) - .then((response) => { - if (response.jsonCode === 200) { - return; - } - - // If it fails, reset Onyx - actionToMerge[sequenceNumber] = originalReportAction; - Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, actionToMerge); - }); + reportComment: htmlForNewComment, + reportActionID: originalReportAction.reportActionID, + }; + API.write('UpdateComment', parameters, {optimisticData, successData, failureData}); } /** @@ -1514,12 +1502,6 @@ function viewNewReportAction(reportID, action) { return; } - // When a new message comes in, if the New marker is not already set (newMarkerSequenceNumber === 0), set the marker above the incoming message. - const report = lodashGet(allReports, 'reportID', {}); - if (lodashGet(report, 'newMarkerSequenceNumber', 0) === 0 && report.unreadActionCount > 0) { - setNewMarkerPosition(reportID, report.lastReadSequenceNumber + 1); - } - Log.info('[LOCAL_NOTIFICATION] Creating notification'); LocalNotification.showCommentNotification({ reportAction: action, @@ -1580,9 +1562,7 @@ export { addAttachment, reconnect, updateNotificationPreference, - setNewMarkerPosition, subscribeToReportTypingEvents, - subscribeToUserEvents, subscribeToReportCommentPushNotifications, unsubscribeFromReportChannel, saveReportComment, diff --git a/src/libs/actions/ReportActions.js b/src/libs/actions/ReportActions.js index 4cbb0a4a00c6..504e915ef76b 100644 --- a/src/libs/actions/ReportActions.js +++ b/src/libs/actions/ReportActions.js @@ -109,9 +109,22 @@ function deleteOptimisticReportAction(reportID, sequenceNumber) { }); } +/** + * @param {Number} reportID + * @param {String} sequenceNumber + */ +function clearReportActionErrors(reportID, sequenceNumber) { + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + [sequenceNumber]: { + errors: null, + }, + }); +} + export { getDeletedCommentsCount, getLastVisibleMessageText, + clearReportActionErrors, isFromCurrentUser, deleteOptimisticReportAction, }; diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 8e191c164fe6..6342b3d05ebf 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -90,11 +90,33 @@ function signOutAndRedirectToSignIn() { * @param {String} [login] */ function resendValidationLink(login = credentials.login) { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: true}); - DeprecatedAPI.ResendValidateCode({email: login}) - .finally(() => { - Onyx.merge(ONYXKEYS.ACCOUNT, {isLoading: false}); - }); + const optimisticData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: true, + errors: null, + message: null, + }, + }]; + const successData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + message: Localize.translateLocal('resendValidationForm.linkHasBeenResent'), + }, + }]; + const failureData = [{ + onyxMethod: CONST.ONYX.METHOD.MERGE, + key: ONYXKEYS.ACCOUNT, + value: { + isLoading: false, + message: null, + }, + }]; + + API.write('RequestAccountValidationLink', {email: login}, {optimisticData, successData, failureData}); } /** diff --git a/src/libs/deprecatedAPI.js b/src/libs/deprecatedAPI.js index b056f08a2890..26a25ff2c9e7 100644 --- a/src/libs/deprecatedAPI.js +++ b/src/libs/deprecatedAPI.js @@ -260,19 +260,6 @@ function Report_GetHistory(parameters) { return Network.post(commandName, parameters); } -/** - * @param {Object} parameters - * @param {Number} parameters.reportID - * @param {Number} parameters.reportActionID - * @param {String} parameters.reportComment - * @returns {Promise} - */ -function Report_EditComment(parameters) { - const commandName = 'Report_EditComment'; - requireParameters(['reportID', 'reportActionID', 'reportComment'], parameters, commandName); - return Network.post(commandName, parameters); -} - /** * @param {Object} parameters * @param {String} parameters.email @@ -502,18 +489,6 @@ function Policy_Create(parameters) { return Network.post(commandName, parameters); } -/** - * @param {Object} parameters - * @param {String} parameters.policyID - * @param {String} parameters.value - * @returns {Promise} - */ -function Policy_CustomUnit_Update(parameters) { - const commandName = 'Policy_CustomUnit_Update'; - requireParameters(['policyID', 'customUnit'], parameters, commandName); - return Network.post(commandName, parameters); -} - /** * @param {Object} parameters * @param {String} parameters.policyID @@ -650,7 +625,6 @@ export { Policy_Employees_Merge, RejectTransaction, Report_GetHistory, - Report_EditComment, ResendValidateCode, SetNameValuePair, SetPassword, @@ -667,7 +641,6 @@ export { TransferWalletBalance, GetLocalCurrency, Policy_Create, - Policy_CustomUnit_Update, Policy_CustomUnitRate_Update, Policy_Employees_Remove, PreferredLocale_Update, diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index d2c114335e53..717963c3dd49 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -53,9 +53,6 @@ const propTypes = { /** The largest sequenceNumber on this report */ maxSequenceNumber: PropTypes.number, - /** The current position of the new marker */ - newMarkerSequenceNumber: PropTypes.number, - /** Whether there is an outstanding amount in IOU */ hasOutstandingIOU: PropTypes.bool, diff --git a/src/pages/home/report/FloatingMessageCounter/index.js b/src/pages/home/report/FloatingMessageCounter/index.js index fb9dfba7c362..7025d5a67a4f 100644 --- a/src/pages/home/report/FloatingMessageCounter/index.js +++ b/src/pages/home/report/FloatingMessageCounter/index.js @@ -11,25 +11,17 @@ import withLocalize, {withLocalizePropTypes} from '../../../../components/withLo import FloatingMessageCounterContainer from './FloatingMessageCounterContainer'; const propTypes = { - /** Count of new messages to show in the badge */ - count: PropTypes.number, + /** Whether the New Messages indicator is active */ + isActive: PropTypes.bool, - /** Whether the marker is active */ - active: PropTypes.bool, - - /** Callback to be called when user closes the badge */ - onClose: PropTypes.func, - - /** Callback to be called when user clicks the marker */ + /** Callback to be called when user clicks the New Messages indicator */ onClick: PropTypes.func, ...withLocalizePropTypes, }; const defaultProps = { - count: 0, - active: false, - onClose: () => {}, + isActive: false, onClick: () => {}, }; @@ -45,7 +37,7 @@ class FloatingMessageCounter extends PureComponent { } componentDidUpdate() { - if (this.props.active && this.props.count > 0) { + if (this.props.isActive) { this.show(); } else { this.hide(); @@ -93,24 +85,10 @@ class FloatingMessageCounter extends PureComponent { styles.textWhite, ]} > - {this.props.translate( - 'newMessageCount', - {count: this.props.count}, - )} + {this.props.translate('newMessages')} )} - shouldRemoveRightBorderRadius - /> -