From 68a36ae62a725eb4fc5ae4687397b297c5f3fe36 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Fri, 21 Jul 2023 15:52:39 -0600 Subject: [PATCH 001/236] Add longPress support for TaskPreview Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 17 +++++++++++++++++ src/pages/home/report/ReportActionItem.js | 3 +++ 2 files changed, 20 insertions(+) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 472c71298852..7f18a65e420e 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -22,6 +22,10 @@ import * as ReportUtils from '../../libs/ReportUtils'; import RenderHTML from '../RenderHTML'; import PressableWithoutFeedback from '../Pressable/PressableWithoutFeedback'; import personalDetailsPropType from '../../pages/personalDetailsPropType'; +import {showContextMenuForReport} from '../ShowContextMenuContext'; +import reportPropTypes from '../../pages/reportPropTypes'; +import refPropTypes from '../refPropTypes'; +import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; const propTypes = { /** All personal details asssociated with user */ @@ -49,6 +53,16 @@ const propTypes = { ownerAccountID: PropTypes.number, }), + /* Onyx Props */ + /** chatReport associated with taskReport */ + chatReport: reportPropTypes, + + /** Popover context menu anchor, used for showing context menu */ + contextMenuAnchor: refPropTypes, + + /** Callback for updating context menu active state, used for showing context menu */ + checkIfContextMenuActive: PropTypes.func, + ...withLocalizePropTypes, }; @@ -74,6 +88,9 @@ function TaskPreview(props) { Navigation.navigate(ROUTES.getReportRoute(props.taskReportID))} + onPressIn={() => DeviceCapabilities.canUseTouchScreen() && ControlSelection.block()} + onPressOut={() => ControlSelection.unblock()} + onLongPress={(event) => showContextMenuForReport(event, props.contextMenuAnchor, props.chatReportID, props.action, props.checkIfContextMenuActive)} style={[styles.flexRow, styles.justifyContentBetween]} accessibilityRole={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={props.translate('task.task')} diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 0b67a728ba0e..b15e92e29b62 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -281,8 +281,11 @@ function ReportActionItem(props) { children = ( ); } else { From 0efba40908b3ea408f222edee2d6e9b23d855387 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Fri, 21 Jul 2023 16:27:14 -0600 Subject: [PATCH 002/236] Add ControlSelection to imports Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 7f18a65e420e..9d66d16b9b54 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -26,6 +26,7 @@ import {showContextMenuForReport} from '../ShowContextMenuContext'; import reportPropTypes from '../../pages/reportPropTypes'; import refPropTypes from '../refPropTypes'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; +import ControlSelection from '../../libs/ControlSelection'; const propTypes = { /** All personal details asssociated with user */ From ceaaf3359fdeb3569bf652059dc50f7a9a63ccc4 Mon Sep 17 00:00:00 2001 From: April Bekkala Date: Wed, 27 Sep 2023 22:33:14 -0500 Subject: [PATCH 003/236] Create Copilot Adding the Copilot page info --- .../account-settings/Copilot | 78 +++++++++++++++++++ 1 file changed, 78 insertions(+) create mode 100644 docs/articles/expensify-classic/account-settings/Copilot diff --git a/docs/articles/expensify-classic/account-settings/Copilot b/docs/articles/expensify-classic/account-settings/Copilot new file mode 100644 index 000000000000..dbd26af12d88 --- /dev/null +++ b/docs/articles/expensify-classic/account-settings/Copilot @@ -0,0 +1,78 @@ +--- +title: Copilot +description: Safely delegate tasks without sharing login information. +--- + + +# About + + +# How-to + + +# Deep Dive + + +# FAQ + From 2cde96c0d49f2679704855b635ad0e20f95757bf Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 29 Sep 2023 16:56:37 +0500 Subject: [PATCH 004/236] feat: add new key in onyx for draft report ids --- src/ONYXKEYS.ts | 4 +++ src/libs/actions/DraftReports.ts | 29 ++++++++++++++++++++ src/pages/home/sidebar/SidebarLinksData.js | 32 ++++++++++++++++++---- 3 files changed, 59 insertions(+), 6 deletions(-) create mode 100644 src/libs/actions/DraftReports.ts diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index a1afc4fef2c1..c9b04c4c217a 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -10,6 +10,9 @@ const ONYXKEYS = { /** Holds information about the users account that is logging in */ ACCOUNT: 'account', + /** Holds the reportIDs which are currently in draft */ + DRAFT_REPORT_IDS: 'draftReportIDs', + /** Holds the reportID for the report between the user and their account manager */ ACCOUNT_MANAGER_REPORT_ID: 'accountManagerReportID', @@ -297,6 +300,7 @@ type OnyxKey = DeepValueOf>; type OnyxValues = { [ONYXKEYS.ACCOUNT]: OnyxTypes.Account; + [ONYXKEYS.DRAFT_REPORT_IDS]: Record; [ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID]: string; [ONYXKEYS.NVP_IS_FIRST_TIME_NEW_EXPENSIFY_USER]: boolean; [ONYXKEYS.ACTIVE_CLIENTS]: string[]; diff --git a/src/libs/actions/DraftReports.ts b/src/libs/actions/DraftReports.ts new file mode 100644 index 000000000000..dc1e7a8066f8 --- /dev/null +++ b/src/libs/actions/DraftReports.ts @@ -0,0 +1,29 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../../ONYXKEYS'; +import DraftReportUtils from '../DraftReportUtils'; + +const draftReportUtils = DraftReportUtils.getInstance(); + +/** + * Immediate indication whether the report has a draft. + * + * @param reportID + * @param draft + */ +function setDraftStatusForReportID(reportID: string, draft: boolean) { + const draftReportIDs = {...draftReportUtils.getDraftReportIDs()}; + + if (draftReportIDs[reportID] && draft) { + return; + } + + if (draftReportIDs[reportID] && !draft) { + delete draftReportIDs[reportID]; + Onyx.set(ONYXKEYS.DRAFT_REPORT_IDS, draftReportIDs); + } else { + draftReportIDs[reportID] = draft; + Onyx.merge(ONYXKEYS.DRAFT_REPORT_IDS, {[reportID]: draft}); + } +} + +export default setDraftStatusForReportID; diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 243ba24cdd00..c204dddf872a 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -52,6 +52,10 @@ const propTypes = { /** The policies which the user has access to */ // eslint-disable-next-line react/forbid-prop-types policies: PropTypes.object, + + /** Holds the reportIDs which are in draft */ + // eslint-disable-next-line react/forbid-prop-types + draftReportIDs: PropTypes.object, }; const defaultProps = { @@ -61,15 +65,29 @@ const defaultProps = { priorityMode: CONST.PRIORITY_MODE.DEFAULT, betas: [], policies: [], + draftReportIDs: {}, }; -function SidebarLinksData({isFocused, allReportActions, betas, chatReports, currentReportID, insets, isLoadingReportData, isSmallScreenWidth, onLinkClick, policies, priorityMode}) { +function SidebarLinksData({ + isFocused, + allReportActions, + draftReportIDs, + betas, + chatReports, + currentReportID, + insets, + isLoadingReportData, + isSmallScreenWidth, + onLinkClick, + policies, + priorityMode, +}) { const {translate} = useLocalize(); const reportIDsRef = useRef(null); const isLoading = SessionUtils.didUserLogInDuringSession() && isLoadingReportData; const optionListItems = useMemo(() => { - const reportIDs = SidebarUtils.getOrderedReportIDs(null, chatReports, betas, policies, priorityMode, allReportActions); + const reportIDs = SidebarUtils.getOrderedReportIDs(null, draftReportIDs, chatReports, betas, policies, priorityMode, allReportActions); if (deepEqual(reportIDsRef.current, reportIDs)) { return reportIDsRef.current; } @@ -79,7 +97,7 @@ function SidebarLinksData({isFocused, allReportActions, betas, chatReports, curr reportIDsRef.current = reportIDs; } return reportIDsRef.current || []; - }, [allReportActions, betas, chatReports, policies, priorityMode, isLoading]); + }, [allReportActions, betas, chatReports, draftReportIDs, policies, priorityMode, isLoading]); // We need to make sure the current report is in the list of reports, but we do not want // to have to re-generate the list every time the currentReportID changes. To do that @@ -88,10 +106,10 @@ function SidebarLinksData({isFocused, allReportActions, betas, chatReports, curr // case we re-generate the list a 2nd time with the current report included. const optionListItemsWithCurrentReport = useMemo(() => { if (currentReportID && !_.contains(optionListItems, currentReportID)) { - return SidebarUtils.getOrderedReportIDs(currentReportID, chatReports, betas, policies, priorityMode, allReportActions); + return SidebarUtils.getOrderedReportIDs(currentReportID, draftReportIDs, chatReports, betas, policies, priorityMode, allReportActions); } return optionListItems; - }, [currentReportID, optionListItems, chatReports, betas, policies, priorityMode, allReportActions]); + }, [currentReportID, optionListItems, chatReports, betas, draftReportIDs, policies, priorityMode, allReportActions]); const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; @@ -133,7 +151,6 @@ const chatReportSelector = (report) => reportID: report.reportID, participants: report.participants, participantAccountIDs: report.participantAccountIDs, - hasDraft: report.hasDraft, isPinned: report.isPinned, isHidden: report.isHidden, errorFields: { @@ -202,6 +219,9 @@ export default compose( isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, }, + draftReportIDs: { + key: ONYXKEYS.DRAFT_REPORT_IDS, + }, priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, }, From 877a5f94828aaab64a45d0fe1c9d228afaa223e0 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 29 Sep 2023 17:01:58 +0500 Subject: [PATCH 005/236] feat: add singleton class for draft report utils --- src/libs/DraftReportUtils.ts | 40 ++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 src/libs/DraftReportUtils.ts diff --git a/src/libs/DraftReportUtils.ts b/src/libs/DraftReportUtils.ts new file mode 100644 index 000000000000..71d16a1fce66 --- /dev/null +++ b/src/libs/DraftReportUtils.ts @@ -0,0 +1,40 @@ +import Onyx from 'react-native-onyx'; +import ONYXKEYS from '../ONYXKEYS'; + +class DraftReportUtils { + private static instance: DraftReportUtils; + + private draftReportIDs: Record; + + private constructor() { + DraftReportUtils.instance = this; + + this.draftReportIDs = {}; + + this.subscribeToDraftReportIDs(); + } + + public static getInstance(): DraftReportUtils { + // Ensure singleton instance + return DraftReportUtils.instance ?? new DraftReportUtils(); + } + + private subscribeToDraftReportIDs() { + Onyx.connect({ + key: ONYXKEYS.DRAFT_REPORT_IDS, + callback: (val) => { + if (!val) { + return; + } + + this.draftReportIDs = val; + }, + }); + } + + getDraftReportIDs() { + return this.draftReportIDs; + } +} + +export default DraftReportUtils; From 6903d5bb79e066ba53d32c3c2143ba92ecf43b86 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 29 Sep 2023 17:11:22 +0500 Subject: [PATCH 006/236] feat: confirm to new draft repord id onyx key --- src/components/LHNOptionsList/OptionRowLHN.js | 17 +++++++++-- .../LHNOptionsList/OptionRowLHNData.js | 4 +-- src/libs/ReportActionsUtils.js | 19 +++++++++---- src/libs/ReportUtils.js | 28 +++++++++++++++---- src/libs/actions/Policy.js | 11 +++++--- .../ComposerWithSuggestions.js | 19 ++++++++----- src/types/onyx/Report.ts | 1 - 7 files changed, 72 insertions(+), 27 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 3cfd7c4c4138..dddee8586eb8 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -3,6 +3,7 @@ import React, {useState, useRef} from 'react'; import PropTypes from 'prop-types'; import {View, StyleSheet} from 'react-native'; import lodashGet from 'lodash/get'; +import {withOnyx} from 'react-native-onyx'; import * as optionRowStyles from '../../styles/optionRowStyles'; import styles from '../../styles/styles'; import * as StyleUtils from '../../styles/StyleUtils'; @@ -25,6 +26,7 @@ import * as ReportUtils from '../../libs/ReportUtils'; import useLocalize from '../../hooks/useLocalize'; import Permissions from '../../libs/Permissions'; import Tooltip from '../Tooltip'; +import ONYXKEYS from '../../ONYXKEYS'; const propTypes = { /** Style for hovered state */ @@ -51,6 +53,9 @@ const propTypes = { /** The item that should be rendered */ // eslint-disable-next-line react/forbid-prop-types optionItem: PropTypes.object, + + // eslint-disable-next-line react/forbid-prop-types + draftReportIDs: PropTypes.object, }; const defaultProps = { @@ -61,6 +66,7 @@ const defaultProps = { optionItem: null, isFocused: false, betas: [], + draftReportIDs: {}, }; function OptionRowLHN(props) { @@ -135,6 +141,7 @@ function OptionRowLHN(props) { const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate); const statusContent = formattedDate ? `${statusText} (${formattedDate})` : statusText; const isStatusVisible = Permissions.canUseCustomStatus(props.betas) && !!emojiCode && ReportUtils.isOneOnOneChat(optionItem); + const isDraft = props.draftReportIDs[props.reportID]; return ( )} - {optionItem.hasDraftComment && optionItem.isAllowedToComment && ( + {isDraft && optionItem.isAllowedToComment && ( { if (!key || !report) { return; @@ -47,7 +48,7 @@ Onyx.connect({ * @returns {Boolean} */ function isCreatedAction(reportAction) { - return lodashGet(reportAction, 'actionName') === CONST.REPORT.ACTIONS.TYPE.CREATED; + return reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED; } /** @@ -421,7 +422,10 @@ function getLastVisibleAction(reportID, actionsToMerge = {}) { */ function getLastVisibleMessage(reportID, actionsToMerge = {}) { const lastVisibleAction = getLastVisibleAction(reportID, actionsToMerge); - const message = lodashGet(lastVisibleAction, ['message', 0], {}); + let message = {}; + if (lastVisibleAction.message) { + message = lastVisibleAction.message[0]; + } if (isReportMessageAttachment(message)) { return { @@ -437,9 +441,14 @@ function getLastVisibleMessage(reportID, actionsToMerge = {}) { }; } - const messageText = lodashGet(message, 'text', ''); + let messageText = message.text || ''; + + if (messageText) { + messageText = String(messageText).replace(CONST.REGEX.AFTER_FIRST_LINE_BREAK, '').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(); + } + return { - lastMessageText: String(messageText).replace(CONST.REGEX.AFTER_FIRST_LINE_BREAK, '').substring(0, CONST.REPORT.LAST_MESSAGE_TEXT_MAX_LENGTH).trim(), + lastMessageText: messageText, }; } @@ -638,7 +647,7 @@ function isTaskAction(reportAction) { * @returns {[Object]} */ function getAllReportActions(reportID) { - return lodashGet(allReportActions, reportID, []); + return allReportActions[reportID] || []; } /** diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index f41ad0b75b42..431fb05a1bb7 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -23,6 +23,7 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; import * as CurrencyUtils from './CurrencyUtils'; import * as UserUtils from './UserUtils'; +import DraftReportUtils from './DraftReportUtils'; let currentUserEmail; let currentUserAccountID; @@ -52,13 +53,16 @@ Onyx.connect({ }, }); -let allReports; +let allReports = {}; + Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, callback: (val) => (allReports = val), }); +const draftReportUtils = DraftReportUtils.getInstance(); + let doesDomainHaveApprovedAccountant; Onyx.connect({ key: ONYXKEYS.ACCOUNT, @@ -1201,8 +1205,11 @@ function getDisplayNamesWithTooltips(personalDetailsList, isMultipleParticipantR * @returns {Object} */ function getReport(reportID) { - // Deleted reports are set to null and lodashGet will still return null in that case, so we need to add an extra check - return lodashGet(allReports, `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {}) || {}; + /** + * using typical string concatenation here due to performance issues + * with template literals. + */ + return allReports[ONYXKEYS.COLLECTION.REPORT + reportID] || {}; } /** @@ -1295,14 +1302,23 @@ function getMoneyRequestTotal(report, allReportsDict = null) { * @returns {String} */ function getPolicyExpenseChatName(report, policy = undefined) { - const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || lodashGet(allPersonalDetails, [report.ownerAccountID, 'login']) || report.reportName; + const ownerAccountID = report.ownerAccountID; + const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || allPersonalDetails[ownerAccountID].login || report.reportName; // If the policy expense chat is owned by this user, use the name of the policy as the report name. if (report.isOwnPolicyExpenseChat) { return getPolicyName(report, false, policy); } - const policyExpenseChatRole = lodashGet(allPolicies, [`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`, 'role']) || 'user'; + let policyExpenseChatRole = 'user'; + /** + * using typical string concatenation here due to performance issues + * with template literals. + */ + const policyItem = allPolicies[ONYXKEYS.COLLECTION.POLICY + report.policyID]; + if (policyItem) { + policyExpenseChatRole = policyItem.role || 'user'; + } // If this user is not admin and this policy expense chat has been archived because of account merging, this must be an old workspace chat // of the account which was merged into the current user's account. Use the name of the policy as the name of the report. @@ -2971,7 +2987,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, } // Include reports that are relevant to the user in any view mode. Criteria include having a draft, having an outstanding IOU, or being assigned to an open task. - if (report.hasDraft || isWaitingForIOUActionFromCurrentUser(report) || isWaitingForTaskCompleteFromAssignee(report)) { + if (draftReportUtils.getDraftReportIDs()[report.reportID] || isWaitingForIOUActionFromCurrentUser(report) || isWaitingForTaskCompleteFromAssignee(report)) { return true; } const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(report.reportID); diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index fcce909c5582..b3d68ff4395d 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -13,6 +13,9 @@ import * as ErrorUtils from '../ErrorUtils'; import * as ReportUtils from '../ReportUtils'; import * as PersonalDetailsUtils from '../PersonalDetailsUtils'; import Log from '../Log'; +import DraftReportUtils from '../DraftReportUtils'; + +const draftReportUtils = DraftReportUtils.getInstance(); const allPolicies = {}; Onyx.connect({ @@ -29,12 +32,14 @@ Onyx.connect({ const policyReports = ReportUtils.getAllPolicyReports(policyID); const cleanUpMergeQueries = {}; const cleanUpSetQueries = {}; + const draftReportIDs = {...draftReportUtils.getDraftReportIDs()}; _.each(policyReports, ({reportID}) => { - cleanUpMergeQueries[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] = {hasDraft: false}; cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; + delete draftReportIDs[reportID]; }); Onyx.mergeCollection(ONYXKEYS.COLLECTION.REPORT, cleanUpMergeQueries); Onyx.multiSet(cleanUpSetQueries); + Onyx.set(ONYXKEYS.DRAFT_REPORT_IDS, draftReportIDs); delete allPolicies[key]; return; } @@ -96,7 +101,6 @@ function deleteWorkspace(policyID, reports, policyName) { value: { stateNum: CONST.REPORT.STATE_NUM.SUBMITTED, statusNum: CONST.REPORT.STATUS.CLOSED, - hasDraft: false, oldPolicyName: allPolicies[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`].name, }, })), @@ -121,13 +125,12 @@ function deleteWorkspace(policyID, reports, policyName) { // Restore the old report stateNum and statusNum const failureData = [ - ..._.map(reports, ({reportID, stateNum, statusNum, hasDraft, oldPolicyName}) => ({ + ..._.map(reports, ({reportID, stateNum, statusNum, oldPolicyName}) => ({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: { stateNum, statusNum, - hasDraft, oldPolicyName, }, })), diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index d04983dc2f75..1d7d1fbcb001 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -34,6 +34,7 @@ import withKeyboardState from '../../../../components/withKeyboardState'; import {propTypes, defaultProps} from './composerWithSuggestionsProps'; import focusWithDelay from '../../../../libs/focusWithDelay'; import useDebounce from '../../../../hooks/useDebounce'; +import setDraftStatusForReportID from '../../../../libs/actions/DraftReports'; const {RNTextInputReset} = NativeModules; @@ -203,7 +204,13 @@ function ComposerWithSuggestions({ debouncedUpdateFrequentlyUsedEmojis(); } - setIsCommentEmpty(!!newComment.match(/^(\s)*$/)); + const isNewCommentEmpty = !!newComment.match(/^(\s)*$/); + const isPrevCommentEmpty = !!commentRef.current.match(/^(\s)*$/); + + /** Only update isCommentEmpty state if it's different from previous one */ + if (isNewCommentEmpty !== isPrevCommentEmpty) { + setIsCommentEmpty(isNewCommentEmpty); + } setValue(newComment); if (commentValue !== newComment) { // Ensure emoji suggestions are hidden even when the selection is not changed (so calculateEmojiSuggestion would not be called). @@ -220,12 +227,11 @@ function ComposerWithSuggestions({ // Indicate that draft has been created. if (commentRef.current.length === 0 && newComment.length !== 0) { - Report.setReportWithDraft(reportID, true); + setDraftStatusForReportID(reportID, true); } - // The draft has been deleted. - if (newComment.length === 0) { - Report.setReportWithDraft(reportID, false); + else if (newComment.length === 0) { + setDraftStatusForReportID(reportID, false); } commentRef.current = newComment; @@ -469,8 +475,7 @@ function ComposerWithSuggestions({ if (value.length === 0) { return; } - Report.setReportWithDraft(reportID, true); - + setDraftStatusForReportID(reportID, true); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 46e51fe41238..1e24a5805084 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -66,7 +66,6 @@ type Report = { parentReportID?: string; parentReportActionID?: string; isOptimisticReport?: boolean; - hasDraft?: boolean; managerID?: number; lastVisibleActionLastModified?: string; displayName?: string; From 64b095fd30844b5fb7d2c8951a07289e9277787f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 29 Sep 2023 17:11:46 +0500 Subject: [PATCH 007/236] feat: add waitForCollectionCallback --- src/libs/OptionsListUtils.js | 2 +- src/libs/actions/Report.js | 14 ++------------ src/libs/actions/Welcome.js | 1 + 3 files changed, 4 insertions(+), 13 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e0f334ca36af..6382def58648 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -83,6 +83,7 @@ Onyx.connect({ const policyExpenseReports = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, callback: (report, key) => { if (!ReportUtils.isPolicyExpenseChat(report)) { return; @@ -486,7 +487,6 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { result.ownerAccountID = report.ownerAccountID; result.reportID = report.reportID; result.isUnread = ReportUtils.isUnread(report); - result.hasDraftComment = report.hasDraft; result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 66008ae5ae2a..941d7dce7b49 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -64,6 +64,7 @@ Onyx.connect({ const currentReportData = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, callback: (data, key) => { if (!key || !data) { return; @@ -919,17 +920,6 @@ function saveReportCommentNumberOfLines(reportID, numberOfLines) { Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, numberOfLines); } -/** - * Immediate indication whether the report has a draft comment. - * - * @param {String} reportID - * @param {Boolean} hasDraft - * @returns {Promise} - */ -function setReportWithDraft(reportID, hasDraft) { - return Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {hasDraft}); -} - /** * Broadcasts whether or not a user is typing on a report over the report's private pusher channel. * @@ -994,6 +984,7 @@ function handleReportChanged(report) { Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, callback: handleReportChanged, }); @@ -2189,7 +2180,6 @@ export { saveReportActionDraftNumberOfLines, deleteReportComment, navigateToConciergeChat, - setReportWithDraft, addPolicyReport, deleteReport, navigateToConciergeChatAndDeleteReport, diff --git a/src/libs/actions/Welcome.js b/src/libs/actions/Welcome.js index 8e1832edb9a7..fac2d031fe1e 100644 --- a/src/libs/actions/Welcome.js +++ b/src/libs/actions/Welcome.js @@ -58,6 +58,7 @@ Onyx.connect({ const allReports = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, initWithStoredValues: false, callback: (val, key) => { if (!val || !key) { From 268e57af777a3a61d9b0bb10a46596952e8d3f60 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 29 Sep 2023 17:12:27 +0500 Subject: [PATCH 008/236] perf: call updateUnread method if count is changed --- src/libs/UnreadIndicatorUpdater/index.js | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/src/libs/UnreadIndicatorUpdater/index.js b/src/libs/UnreadIndicatorUpdater/index.js index 09fa82612314..dcfedce83d75 100644 --- a/src/libs/UnreadIndicatorUpdater/index.js +++ b/src/libs/UnreadIndicatorUpdater/index.js @@ -1,14 +1,26 @@ import _ from 'underscore'; import Onyx from 'react-native-onyx'; +import {InteractionManager} from 'react-native'; import ONYXKEYS from '../../ONYXKEYS'; import updateUnread from './updateUnread/index'; import * as ReportUtils from '../ReportUtils'; +let previousUnreadCount = 0; + Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, waitForCollectionCallback: true, callback: (reportsFromOnyx) => { - const unreadReports = _.filter(reportsFromOnyx, ReportUtils.isUnread); - updateUnread(_.size(unreadReports)); + if (!reportsFromOnyx) { + return; + } + + InteractionManager.runAfterInteractions(() => { + const unreadReportsCount = _.filter(reportsFromOnyx, ReportUtils.isUnread).length || 0; + if (previousUnreadCount !== unreadReportsCount) { + previousUnreadCount = unreadReportsCount; + updateUnread(unreadReportsCount); + } + }); }, }); From 740700d7386a2489b88f03f95b20ca2798f6ad15 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 29 Sep 2023 17:13:24 +0500 Subject: [PATCH 009/236] perf: use dict keys instead of whole dict and refactor the foreach loop --- src/libs/SidebarUtils.js | 33 +++++++++++++++++---------------- 1 file changed, 17 insertions(+), 16 deletions(-) diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 7a32db660021..75f1782670b2 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -103,6 +103,7 @@ let hasInitialReportActions = false; /** * @param {String} currentReportId + * @param {Object} draftReportIDs * @param {Object} allReportsDict * @param {Object} betas * @param {String[]} policies @@ -110,11 +111,12 @@ let hasInitialReportActions = false; * @param {Object} allReportActions * @returns {String[]} An array of reportIDs sorted in the proper order */ -function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions) { +function getOrderedReportIDs(currentReportId, draftReportIDs, allReportsDict, betas, policies, priorityMode, allReportActions) { + const allReportsDictKeys = Object.keys(allReportsDict); // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( // eslint-disable-next-line es/no-optional-chaining - [currentReportId, allReportsDict, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], + [currentReportId, allReportsDictKeys, betas, draftReportIDs, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], (key, value) => { /** * Exclude 'participantAccountIDs', 'participants' and 'lastMessageText' not to overwhelm a cached key value with huge data, @@ -149,18 +151,6 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p } } - // There are a few properties that need to be calculated for the report which are used when sorting reports. - reportsToDisplay.forEach((report) => { - // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params. - // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add - // the reportDisplayName property to the report object directly. - // eslint-disable-next-line no-param-reassign - report.displayName = ReportUtils.getReportName(report); - - // eslint-disable-next-line no-param-reassign - report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReportsDict); - }); - // The LHN is split into five distinct groups, and each group is sorted a little differently. The groups will ALWAYS be in this order: // 1. Pinned - Always sorted by reportDisplayName // 2. Outstanding IOUs - Always sorted by iouReportAmount with the largest amounts at the top of the group @@ -171,17 +161,29 @@ function getOrderedReportIDs(currentReportId, allReportsDict, betas, policies, p // 5. Archived reports // - Sorted by lastVisibleActionCreated in default (most recent) view mode // - Sorted by reportDisplayName in GSD (focus) view mode + const pinnedReports = []; const outstandingIOUReports = []; const draftReports = []; const nonArchivedReports = []; const archivedReports = []; + + // There are a few properties that need to be calculated for the report which are used when sorting reports. reportsToDisplay.forEach((report) => { + // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params. + // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add + // the reportDisplayName property to the report object directly. + // eslint-disable-next-line no-param-reassign + report.displayName = ReportUtils.getReportName(report); + + // eslint-disable-next-line no-param-reassign + report.iouReportAmount = ReportUtils.getMoneyRequestTotal(report, allReportsDict); + if (report.isPinned) { pinnedReports.push(report); } else if (ReportUtils.isWaitingForIOUActionFromCurrentUser(report)) { outstandingIOUReports.push(report); - } else if (report.hasDraft) { + } else if (draftReportIDs[report.reportID]) { draftReports.push(report); } else if (ReportUtils.isArchivedRoom(report)) { archivedReports.push(report); @@ -296,7 +298,6 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, result.statusNum = report.statusNum; result.isUnread = ReportUtils.isUnread(report); result.isUnreadWithMention = ReportUtils.isUnreadWithMention(report); - result.hasDraftComment = report.hasDraft; result.isPinned = report.isPinned; result.iouReportID = report.iouReportID; result.keyForList = String(report.reportID); From cd0346e0e9dffecf7d11fa0d13586a2c7e8824b3 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 2 Oct 2023 11:04:17 +0500 Subject: [PATCH 010/236] fix: prettier fixes --- src/libs/ReportUtils.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 431fb05a1bb7..a4554197c584 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1208,7 +1208,7 @@ function getReport(reportID) { /** * using typical string concatenation here due to performance issues * with template literals. - */ + */ return allReports[ONYXKEYS.COLLECTION.REPORT + reportID] || {}; } @@ -1314,7 +1314,7 @@ function getPolicyExpenseChatName(report, policy = undefined) { /** * using typical string concatenation here due to performance issues * with template literals. - */ + */ const policyItem = allPolicies[ONYXKEYS.COLLECTION.POLICY + report.policyID]; if (policyItem) { policyExpenseChatRole = policyItem.role || 'user'; From e48bd2f7a97333dbe779d49fa5f97d24a1fea76f Mon Sep 17 00:00:00 2001 From: April Bekkala Date: Mon, 2 Oct 2023 14:13:44 -0500 Subject: [PATCH 011/236] Rename Copilot to Copilot.md needed to add .md to the title --- .../expensify-classic/account-settings/{Copilot => Copilot.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename docs/articles/expensify-classic/account-settings/{Copilot => Copilot.md} (100%) diff --git a/docs/articles/expensify-classic/account-settings/Copilot b/docs/articles/expensify-classic/account-settings/Copilot.md similarity index 100% rename from docs/articles/expensify-classic/account-settings/Copilot rename to docs/articles/expensify-classic/account-settings/Copilot.md From 03b62212e1ae0f97c92019f8585ffca275a67999 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 3 Oct 2023 10:40:54 +0500 Subject: [PATCH 012/236] test: fix unit tests --- src/components/LHNOptionsList/OptionRowLHN.js | 17 ++++------------- .../LHNOptionsList/OptionRowLHNData.js | 8 +++++++- src/components/optionPropTypes.js | 3 --- src/libs/OptionsListUtils.js | 1 - src/libs/ReportUtils.js | 4 ++++ tests/unit/OptionsListUtilsTest.js | 1 - tests/utils/LHNTestUtils.js | 4 +--- 7 files changed, 16 insertions(+), 22 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index dddee8586eb8..0f0b196d715a 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -3,7 +3,6 @@ import React, {useState, useRef} from 'react'; import PropTypes from 'prop-types'; import {View, StyleSheet} from 'react-native'; import lodashGet from 'lodash/get'; -import {withOnyx} from 'react-native-onyx'; import * as optionRowStyles from '../../styles/optionRowStyles'; import styles from '../../styles/styles'; import * as StyleUtils from '../../styles/StyleUtils'; @@ -26,7 +25,6 @@ import * as ReportUtils from '../../libs/ReportUtils'; import useLocalize from '../../hooks/useLocalize'; import Permissions from '../../libs/Permissions'; import Tooltip from '../Tooltip'; -import ONYXKEYS from '../../ONYXKEYS'; const propTypes = { /** Style for hovered state */ @@ -54,8 +52,7 @@ const propTypes = { // eslint-disable-next-line react/forbid-prop-types optionItem: PropTypes.object, - // eslint-disable-next-line react/forbid-prop-types - draftReportIDs: PropTypes.object, + hasDraft: PropTypes.bool, }; const defaultProps = { @@ -66,7 +63,7 @@ const defaultProps = { optionItem: null, isFocused: false, betas: [], - draftReportIDs: {}, + hasDraft: false, }; function OptionRowLHN(props) { @@ -141,7 +138,7 @@ function OptionRowLHN(props) { const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate); const statusContent = formattedDate ? `${statusText} (${formattedDate})` : statusText; const isStatusVisible = Permissions.canUseCustomStatus(props.betas) && !!emojiCode && ReportUtils.isOneOnOneChat(optionItem); - const isDraft = props.draftReportIDs[props.reportID]; + const isDraft = props.hasDraft; return ( { - if (!optionItem || optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { + if (!optionItem || hasDraft || !comment || comment.length <= 0 || isFocused) { return; } setDraftStatusForReportID(reportID, true); @@ -131,6 +133,7 @@ function OptionRowLHNData({ {...propsToForward} isFocused={isFocused} optionItem={optionItem} + hasDraft={hasDraft} /> ); } @@ -181,6 +184,9 @@ export default React.memo( }, }), withOnyx({ + draftReportIDs: { + key: ONYXKEYS.DRAFT_REPORT_IDS, + }, fullReport: { key: (props) => ONYXKEYS.COLLECTION.REPORT + props.reportID, }, diff --git a/src/components/optionPropTypes.js b/src/components/optionPropTypes.js index 709298036f07..6f84fff24a52 100644 --- a/src/components/optionPropTypes.js +++ b/src/components/optionPropTypes.js @@ -25,9 +25,6 @@ export default PropTypes.shape({ // reportID (only present when there is a matching report) reportID: PropTypes.string, - // Whether the report has a draft comment or not - hasDraftComment: PropTypes.bool, - // Key used internally by React keyForList: PropTypes.string, diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 6382def58648..eacbb1648122 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -444,7 +444,6 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { login: null, reportID: null, phoneNumber: null, - hasDraftComment: false, keyForList: null, searchText: null, isDefaultRoom: false, diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index a4554197c584..f25bb0ef0fa5 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1209,6 +1209,10 @@ function getReport(reportID) { * using typical string concatenation here due to performance issues * with template literals. */ + if (!allReports) { + return {}; + } + return allReports[ONYXKEYS.COLLECTION.REPORT + reportID] || {}; } diff --git a/tests/unit/OptionsListUtilsTest.js b/tests/unit/OptionsListUtilsTest.js index 6f20e48835fd..79e2cb79077a 100644 --- a/tests/unit/OptionsListUtilsTest.js +++ b/tests/unit/OptionsListUtilsTest.js @@ -16,7 +16,6 @@ describe('OptionsListUtils', () => { reportID: 1, participantAccountIDs: [2, 1], reportName: 'Iron Man, Mister Fantastic', - hasDraft: true, }, 2: { lastReadTime: '2021-01-14 11:25:39.296', diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 7cb69b23a578..2dbc390cb283 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -149,10 +149,9 @@ function getFakeReportAction(actor = 'email1@test.com', millisecondsInThePast = * @param {boolean} hasAddWorkspaceError * @param {boolean} isUnread * @param {boolean} isPinned - * @param {boolean} hasDraft * @returns {Object} */ -function getAdvancedFakeReport(isArchived, isUserCreatedPolicyRoom, hasAddWorkspaceError, isUnread, isPinned, hasDraft) { +function getAdvancedFakeReport(isArchived, isUserCreatedPolicyRoom, hasAddWorkspaceError, isUnread, isPinned) { return { ...getFakeReport([1, 2], 0, isUnread), type: CONST.REPORT.TYPE.CHAT, @@ -161,7 +160,6 @@ function getAdvancedFakeReport(isArchived, isUserCreatedPolicyRoom, hasAddWorksp stateNum: isArchived ? CONST.REPORT.STATE_NUM.SUBMITTED : 0, errorFields: hasAddWorkspaceError ? {addWorkspaceRoom: 'blah'} : null, isPinned, - hasDraft, }; } From dbeef9830a3b7bfdfee9f63a63000d77deceb549 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 3 Oct 2023 11:43:52 +0500 Subject: [PATCH 013/236] test: fix Sidebar tests --- src/libs/ReportUtils.js | 4 +- tests/unit/SidebarFilterTest.js | 21 ++++++----- tests/unit/SidebarOrderTest.js | 66 ++++++++++++++------------------- 3 files changed, 41 insertions(+), 50 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index f25bb0ef0fa5..529bee905924 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -1307,7 +1307,9 @@ function getMoneyRequestTotal(report, allReportsDict = null) { */ function getPolicyExpenseChatName(report, policy = undefined) { const ownerAccountID = report.ownerAccountID; - const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || allPersonalDetails[ownerAccountID].login || report.reportName; + const personalDetails = allPersonalDetails[ownerAccountID]; + const login = personalDetails ? personalDetails.login : null; + const reportOwnerDisplayName = getDisplayNameForParticipant(report.ownerAccountID) || login || report.reportName; // If the policy expense chat is owned by this user, use the name of the policy as the report name. if (report.isOwnPolicyExpenseChat) { diff --git a/tests/unit/SidebarFilterTest.js b/tests/unit/SidebarFilterTest.js index 18e499d89293..982393b3e191 100644 --- a/tests/unit/SidebarFilterTest.js +++ b/tests/unit/SidebarFilterTest.js @@ -23,6 +23,7 @@ const ONYXKEYS = { POLICY: 'policy_', }, NETWORK: 'network', + DRAFT_REPORT_IDS: 'draftReportIDs', }; describe('Sidebar', () => { @@ -100,12 +101,7 @@ describe('Sidebar', () => { it('includes an empty chat report if it has a draft', () => { LHNTestUtils.getDefaultRenderedSidebarLinks(); - // Given a new report with a draft text - const report = { - ...LHNTestUtils.getFakeReport([1, 2], 0), - hasDraft: true, - }; - + const report = LHNTestUtils.getFakeReport([1, 2], 0); return ( waitForBatchedUpdates() // When Onyx is updated to contain that report @@ -114,6 +110,8 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + // Set the draft status for the given reportID + [ONYXKEYS.DRAFT_REPORT_IDS]: {[report.reportID]: true}, }), ) @@ -341,6 +339,8 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, + // Set the draft status for the given reportID + [ONYXKEYS.DRAFT_REPORT_IDS]: {[report2.reportID]: boolArr[boolArr.length - 1]}, }), ) // Then depending on the outcome, either one or two reports are visible @@ -435,10 +435,7 @@ describe('Sidebar', () => { it('always shows pinned and draft chats', () => { // Given a draft report and a pinned report - const draftReport = { - ...LHNTestUtils.getFakeReport([1, 2]), - hasDraft: true, - }; + const draftReport = LHNTestUtils.getFakeReport([1, 2]); const pinnedReport = { ...LHNTestUtils.getFakeReport([3, 4]), isPinned: true, @@ -455,6 +452,8 @@ describe('Sidebar', () => { [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, [`${ONYXKEYS.COLLECTION.REPORT}${draftReport.reportID}`]: draftReport, [`${ONYXKEYS.COLLECTION.REPORT}${pinnedReport.reportID}`]: pinnedReport, + // Set the draft status for the given reportID + [ONYXKEYS.DRAFT_REPORT_IDS]: {[draftReport.reportID]: true}, }), ) @@ -666,6 +665,8 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, + // Set the draft status for the given reportID + [ONYXKEYS.DRAFT_REPORT_IDS]: {[report2.reportID]: boolArr[boolArr.length - 1]}, }), ) diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index 4a693d679b86..0503d8f33dff 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -24,6 +24,7 @@ const ONYXKEYS = { REPORT_ACTIONS: 'reportActions_', }, NETWORK: 'network', + DRAFT_REPORT_IDS: 'draftReportIDs', }; describe('Sidebar', () => { @@ -148,12 +149,8 @@ describe('Sidebar', () => { it('changes the order when adding a draft to the active report', () => { // Given three reports in the recently updated order of 3, 2, 1 - // And the first report has a draft // And the currently viewed report is the first report - const report1 = { - ...LHNTestUtils.getFakeReport([1, 2], 3), - hasDraft: true, - }; + const report1 = LHNTestUtils.getFakeReport([1, 2], 3); const report2 = LHNTestUtils.getFakeReport([3, 4], 2); const report3 = LHNTestUtils.getFakeReport([5, 6], 1); @@ -176,6 +173,8 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, + // Setting the draft status for first report + [ONYXKEYS.DRAFT_REPORT_IDS]: {[report1.reportID]: true}, }), ) @@ -188,7 +187,7 @@ describe('Sidebar', () => { const hintText = Localize.translateLocal('accessibilityHints.chatUserDisplayNames'); const displayNames = screen.queryAllByLabelText(hintText); expect(displayNames).toHaveLength(3); - expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('One, Two'); // this has `hasDraft` flag enabled so it will be on top + expect(lodashGet(displayNames, [0, 'props', 'children'])).toBe('One, Two'); // this has a `reportID` in `draftReportID` so it will be on top expect(lodashGet(displayNames, [1, 'props', 'children'])).toBe('Five, Six'); expect(lodashGet(displayNames, [2, 'props', 'children'])).toBe('Three, Four'); }) @@ -244,13 +243,9 @@ describe('Sidebar', () => { it('reorders the reports to keep draft reports on top', () => { // Given three reports in the recently updated order of 3, 2, 1 - // And the second report has a draft // And the currently viewed report is the second report const report1 = LHNTestUtils.getFakeReport([1, 2], 3); - const report2 = { - ...LHNTestUtils.getFakeReport([3, 4], 2), - hasDraft: true, - }; + const report2 = LHNTestUtils.getFakeReport([3, 4], 2); const report3 = LHNTestUtils.getFakeReport([5, 6], 1); // Each report has at least one ADDCOMMENT action so should be rendered in the LNH @@ -272,6 +267,8 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, + // Setting the draft status for second report + [ONYXKEYS.DRAFT_REPORT_IDS]: {[report2.reportID]: true}, }), ) @@ -300,11 +297,7 @@ describe('Sidebar', () => { LHNTestUtils.getDefaultRenderedSidebarLinks(); // Given a single report - // And the report has a draft - const report = { - ...LHNTestUtils.getFakeReport([1, 2]), - hasDraft: true, - }; + const report = LHNTestUtils.getFakeReport([1, 2]); return ( waitForBatchedUpdates() @@ -315,6 +308,8 @@ describe('Sidebar', () => { [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + // Setting the draft status for the report + [ONYXKEYS.DRAFT_REPORT_IDS]: {[report.reportID]: true}, }), ) @@ -324,7 +319,7 @@ describe('Sidebar', () => { }) // When the draft is removed - .then(() => Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, {hasDraft: null})) + .then(() => Onyx.set(ONYXKEYS.DRAFT_REPORT_IDS, {})) // Then the pencil icon goes away .then(() => { @@ -373,16 +368,13 @@ describe('Sidebar', () => { it('sorts chats by pinned > IOU > draft', () => { // Given three reports in the recently updated order of 3, 2, 1 // with the current user set to email9@ (someone not participating in any of the chats) - // with a report that has a draft, a report that is pinned, and + // with a report that is pinned, and // an outstanding IOU report that doesn't belong to the current user const report1 = { ...LHNTestUtils.getFakeReport([1, 2], 3), isPinned: true, }; - const report2 = { - ...LHNTestUtils.getFakeReport([3, 4], 2), - hasDraft: true, - }; + const report2 = LHNTestUtils.getFakeReport([3, 4], 2); const report3 = { ...LHNTestUtils.getFakeReport([5, 6], 1), hasOutstandingIOU: false, @@ -418,6 +410,8 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, + // Setting the draft status for second report + [ONYXKEYS.DRAFT_REPORT_IDS]: {[report2.reportID]: true}, }), ) @@ -499,23 +493,10 @@ describe('Sidebar', () => { it('alphabetizes all the chats that have drafts', () => { // Given three reports in the recently updated order of 3, 2, 1 - // and they all have drafts - const report1 = { - ...LHNTestUtils.getFakeReport([1, 2], 3), - hasDraft: true, - }; - const report2 = { - ...LHNTestUtils.getFakeReport([3, 4], 2), - hasDraft: true, - }; - const report3 = { - ...LHNTestUtils.getFakeReport([5, 6], 1), - hasDraft: true, - }; - const report4 = { - ...LHNTestUtils.getFakeReport([7, 8], 0), - hasDraft: true, - }; + const report1 = LHNTestUtils.getFakeReport([1, 2], 3); + const report2 = LHNTestUtils.getFakeReport([3, 4], 2); + const report3 = LHNTestUtils.getFakeReport([5, 6], 1); + const report4 = LHNTestUtils.getFakeReport([7, 8], 0); LHNTestUtils.getDefaultRenderedSidebarLinks('0'); return ( waitForBatchedUpdates() @@ -528,6 +509,13 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, + // Setting the draft status for all reports + [ONYXKEYS.DRAFT_REPORT_IDS]: { + [report1.reportID]: true, + [report2.reportID]: true, + [report3.reportID]: true, + [report4.reportID]: true, + }, }), ) From fcad1ec9cb4fccd0807bba81f820dc165b27eb0f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 3 Oct 2023 11:45:00 +0500 Subject: [PATCH 014/236] fix: use allReportsDict in getordeeredReportIDs --- src/libs/SidebarUtils.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/libs/SidebarUtils.js b/src/libs/SidebarUtils.js index 75f1782670b2..78839060ee5e 100644 --- a/src/libs/SidebarUtils.js +++ b/src/libs/SidebarUtils.js @@ -112,11 +112,10 @@ let hasInitialReportActions = false; * @returns {String[]} An array of reportIDs sorted in the proper order */ function getOrderedReportIDs(currentReportId, draftReportIDs, allReportsDict, betas, policies, priorityMode, allReportActions) { - const allReportsDictKeys = Object.keys(allReportsDict); // Generate a unique cache key based on the function arguments const cachedReportsKey = JSON.stringify( // eslint-disable-next-line es/no-optional-chaining - [currentReportId, allReportsDictKeys, betas, draftReportIDs, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], + [currentReportId, draftReportIDs, allReportsDict, betas, policies, priorityMode, allReportActions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]?.length || 1], (key, value) => { /** * Exclude 'participantAccountIDs', 'participants' and 'lastMessageText' not to overwhelm a cached key value with huge data, @@ -254,7 +253,6 @@ function getOptionData(report, reportActions, personalDetails, preferredLocale, phoneNumber: null, isUnread: null, isUnreadWithMention: null, - hasDraftComment: false, keyForList: null, searchText: null, isPinned: false, From 8fbd7a91fddf5b8de0d93a7ab77865a8b256a254 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 3 Oct 2023 12:21:44 +0500 Subject: [PATCH 015/236] test: fix unit test --- src/libs/actions/Report.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 941d7dce7b49..1ae0b70fed24 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -984,7 +984,6 @@ function handleReportChanged(report) { Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, callback: handleReportChanged, }); From 713a35cde701f3c631100fce1e24272b0dfb47c6 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 3 Oct 2023 12:57:05 +0500 Subject: [PATCH 016/236] test: add test for draft report utils --- tests/unit/DraftReportUtilsTest.js | 59 ++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 tests/unit/DraftReportUtilsTest.js diff --git a/tests/unit/DraftReportUtilsTest.js b/tests/unit/DraftReportUtilsTest.js new file mode 100644 index 000000000000..0ac8a77dc907 --- /dev/null +++ b/tests/unit/DraftReportUtilsTest.js @@ -0,0 +1,59 @@ +import Onyx from 'react-native-onyx'; +import {cleanup} from '@testing-library/react-native'; +import DraftReportUtils from '../../src/libs/DraftReportUtils'; + +const ONYXKEYS = { + DRAFT_REPORT_IDS: 'draftReportIDs', +}; + +const reportID = 1; + +describe('DraftReportUtils', () => { + beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + }), + ); + + // Clear out Onyx after each test so that each test starts with a clean slate + afterEach(() => { + cleanup(); + Onyx.clear(); + }); + + describe('Singleton', () => { + it('should return the same instance when called multiple times', () => { + // Call getInstance multiple times + const instance1 = DraftReportUtils.getInstance(); + const instance2 = DraftReportUtils.getInstance(); + const instance3 = DraftReportUtils.getInstance(); + + // Ensure that all instances are the same + expect(instance1).toBe(instance2); + expect(instance2).toBe(instance3); + }); + }); + + it('should return an empty object when there are no draft reports', () => { + const draftReportIDs = DraftReportUtils.getInstance().getDraftReportIDs(); + expect(draftReportIDs).toEqual({}); + }); + + it('should return an object of draft report IDs when draft is set through onyx', async () => { + await Onyx.merge(ONYXKEYS.DRAFT_REPORT_IDS, {[reportID]: true}); + const draftReportIDs = DraftReportUtils.getInstance().getDraftReportIDs(); + expect(draftReportIDs).toEqual({[`${reportID}`]: true}); + }); + + it('should return an empty object of draft report IDs when draft is unset through onyx', async () => { + const draftReportUtils = DraftReportUtils.getInstance(); + + await Onyx.merge(ONYXKEYS.DRAFT_REPORT_IDS, {[reportID]: true}); + let draftReportIDs = draftReportUtils.getDraftReportIDs(); + expect(draftReportIDs).toEqual({[`${reportID}`]: true}); + + await Onyx.set(ONYXKEYS.DRAFT_REPORT_IDS, {}); + draftReportIDs = draftReportUtils.getDraftReportIDs(); + expect(draftReportIDs).toEqual({}); + }); +}); From d21ae8d45a1a5563c6e959872633a0b961cbfac7 Mon Sep 17 00:00:00 2001 From: Youssef Lourayad Date: Tue, 3 Oct 2023 17:49:39 +0800 Subject: [PATCH 017/236] Add accessibility translation for floating action button --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 81e1c22b0ccc..7b6cfc372c48 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -458,7 +458,7 @@ export default { buttonSearch: 'Buscar', buttonMySettings: 'Mi configuración', fabNewChat: 'Enviar mensaje', - fabNewChatExplained: 'Enviar mensaje', + fabNewChatExplained: 'Enviar mensaje (Acción flotante)', chatPinned: 'Chat fijado', draftedMessage: 'Mensaje borrador', listOfChatMessages: 'Lista de mensajes del chat', From f7e50d653862e67ddd1bcfc1c222869d500b3458 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 3 Oct 2023 15:33:42 +0500 Subject: [PATCH 018/236] fix: linting --- tests/unit/DraftReportUtilsTest.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/DraftReportUtilsTest.js b/tests/unit/DraftReportUtilsTest.js index 0ac8a77dc907..ec13b9c69de7 100644 --- a/tests/unit/DraftReportUtilsTest.js +++ b/tests/unit/DraftReportUtilsTest.js @@ -27,7 +27,7 @@ describe('DraftReportUtils', () => { const instance1 = DraftReportUtils.getInstance(); const instance2 = DraftReportUtils.getInstance(); const instance3 = DraftReportUtils.getInstance(); - + // Ensure that all instances are the same expect(instance1).toBe(instance2); expect(instance2).toBe(instance3); @@ -47,7 +47,7 @@ describe('DraftReportUtils', () => { it('should return an empty object of draft report IDs when draft is unset through onyx', async () => { const draftReportUtils = DraftReportUtils.getInstance(); - + await Onyx.merge(ONYXKEYS.DRAFT_REPORT_IDS, {[reportID]: true}); let draftReportIDs = draftReportUtils.getDraftReportIDs(); expect(draftReportIDs).toEqual({[`${reportID}`]: true}); From 359a003b771c55a5f50e09bb85dfd713c8e17f7b Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 3 Oct 2023 15:34:06 +0500 Subject: [PATCH 019/236] fix: replace lodashGet --- src/libs/ReportActionsUtils.js | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportActionsUtils.js b/src/libs/ReportActionsUtils.js index 18bc28e19d9a..51cfa35ea120 100644 --- a/src/libs/ReportActionsUtils.js +++ b/src/libs/ReportActionsUtils.js @@ -57,8 +57,16 @@ function isCreatedAction(reportAction) { */ function isDeletedAction(reportAction) { // A deleted comment has either an empty array or an object with html field with empty string as value - const message = lodashGet(reportAction, 'message', []); - return message.length === 0 || lodashGet(message, [0, 'html']) === ''; + let message = []; + if (reportAction.message) { + message = reportAction.message; + } + + if (message.length === 0) { + return true; + } + + return message[0].html === ''; } /** @@ -66,7 +74,10 @@ function isDeletedAction(reportAction) { * @returns {Boolean} */ function isDeletedParentAction(reportAction) { - return lodashGet(reportAction, ['message', 0, 'isDeletedParentAction'], false) && lodashGet(reportAction, 'childVisibleActionCount', 0) > 0; + const isDeleted = (reportAction && reportAction.message && reportAction.message[0] && reportAction.message[0].isDeletedParentAction) || false; + const childVisibleActionCount = (reportAction && reportAction.childVisibleActionCount) || 0; + + return isDeleted && childVisibleActionCount > 0; } /** @@ -74,7 +85,7 @@ function isDeletedParentAction(reportAction) { * @returns {Boolean} */ function isPendingRemove(reportAction) { - return lodashGet(reportAction, 'message[0].moderationDecision.decision') === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; + return reportAction['message[0].moderationDecision.decision'] === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE; } /** From fb569663c9ff5f3772c849410f308ddae984bd54 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 3 Oct 2023 16:46:24 +0500 Subject: [PATCH 020/236] docs: add js doc to DraftReportUtil --- src/libs/DraftReportUtils.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libs/DraftReportUtils.ts b/src/libs/DraftReportUtils.ts index 71d16a1fce66..214b92404e1b 100644 --- a/src/libs/DraftReportUtils.ts +++ b/src/libs/DraftReportUtils.ts @@ -1,6 +1,11 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../ONYXKEYS'; +/** + * A singleton class to manage the draft report IDs + * @class DraftReportUtils + * @singleton + */ class DraftReportUtils { private static instance: DraftReportUtils; @@ -14,11 +19,17 @@ class DraftReportUtils { this.subscribeToDraftReportIDs(); } + /** + * @returns The singleton instance + */ public static getInstance(): DraftReportUtils { // Ensure singleton instance return DraftReportUtils.instance ?? new DraftReportUtils(); } + /** + * Subscribe to the draft report IDs + */ private subscribeToDraftReportIDs() { Onyx.connect({ key: ONYXKEYS.DRAFT_REPORT_IDS, @@ -32,6 +43,9 @@ class DraftReportUtils { }); } + /** + * @returns The draft report IDs + */ getDraftReportIDs() { return this.draftReportIDs; } From fbfe6d2e8405fa6b19492b60a6ae599c61cddd0d Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 4 Oct 2023 19:31:29 +0500 Subject: [PATCH 021/236] fix: revert changes due to unit test fixes --- src/components/LHNOptionsList/OptionRowLHN.js | 17 +++++++++++++---- .../LHNOptionsList/OptionRowLHNData.js | 10 ++++------ 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 15568b99a6f9..c3a847144a55 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -3,6 +3,7 @@ import React, {useState, useRef} from 'react'; import PropTypes from 'prop-types'; import {View, StyleSheet} from 'react-native'; import lodashGet from 'lodash/get'; +import {withOnyx} from 'react-native-onyx'; import * as optionRowStyles from '../../styles/optionRowStyles'; import styles from '../../styles/styles'; import * as StyleUtils from '../../styles/StyleUtils'; @@ -25,6 +26,7 @@ import * as ReportUtils from '../../libs/ReportUtils'; import useLocalize from '../../hooks/useLocalize'; import Permissions from '../../libs/Permissions'; import Tooltip from '../Tooltip'; +import ONYXKEYS from '../../ONYXKEYS'; const propTypes = { /** Style for hovered state */ @@ -52,7 +54,8 @@ const propTypes = { // eslint-disable-next-line react/forbid-prop-types optionItem: PropTypes.object, - hasDraft: PropTypes.bool, + // eslint-disable-next-line react/forbid-prop-types + draftReportIDs: PropTypes.object, }; const defaultProps = { @@ -63,7 +66,7 @@ const defaultProps = { optionItem: null, isFocused: false, betas: [], - hasDraft: false, + draftReportIDs: {}, }; function OptionRowLHN(props) { @@ -143,7 +146,7 @@ function OptionRowLHN(props) { const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate); const statusContent = formattedDate ? `${statusText} (${formattedDate})` : statusText; const isStatusVisible = Permissions.canUseCustomStatus(props.betas) && !!emojiCode && ReportUtils.isOneOnOneChat(optionItem); - const isDraft = props.hasDraft; + const isDraft = props.draftReportIDs[props.reportID]; return ( { + const draftReportIDs = DraftReportUtils.getInstance().getDraftReportIDs(); + const hasDraft = draftReportIDs[reportID]; + if (!optionItem || hasDraft || !comment || comment.length <= 0 || isFocused) { return; } @@ -133,7 +135,6 @@ function OptionRowLHNData({ {...propsToForward} isFocused={isFocused} optionItem={optionItem} - hasDraft={hasDraft} /> ); } @@ -184,9 +185,6 @@ export default React.memo( }, }), withOnyx({ - draftReportIDs: { - key: ONYXKEYS.DRAFT_REPORT_IDS, - }, fullReport: { key: (props) => ONYXKEYS.COLLECTION.REPORT + props.reportID, }, From b0fa108a9ef097ebc5358d8576c6173bc9cc16a0 Mon Sep 17 00:00:00 2001 From: April Bekkala Date: Wed, 4 Oct 2023 11:03:02 -0500 Subject: [PATCH 022/236] Update Copilot.md https://github.com/Expensify/App/pull/28361#pullrequestreview-1656620957 made necessary edit noted in above comment --- .../expensify-classic/account-settings/Copilot.md | 9 --------- 1 file changed, 9 deletions(-) diff --git a/docs/articles/expensify-classic/account-settings/Copilot.md b/docs/articles/expensify-classic/account-settings/Copilot.md index dbd26af12d88..4fac402b7ced 100644 --- a/docs/articles/expensify-classic/account-settings/Copilot.md +++ b/docs/articles/expensify-classic/account-settings/Copilot.md @@ -2,15 +2,11 @@ title: Copilot description: Safely delegate tasks without sharing login information. --- - # About - # How-to - # Deep Dive - # FAQ - From 67125d17037f34d45c35d77862148cfd08995638 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Wed, 4 Oct 2023 10:08:53 -0600 Subject: [PATCH 023/236] The merge was wrong Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 79d016fa297f..6fbe2c175799 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -57,16 +57,17 @@ const propTypes = { ownerAccountID: PropTypes.number, }), - /* Onyx Props */ /** chatReport associated with taskReport */ chatReport: reportPropTypes, - + /** Popover context menu anchor, used for showing context menu */ contextMenuAnchor: refPropTypes, /** Callback for updating context menu active state, used for showing context menu */ checkIfContextMenuActive: PropTypes.func, - ...withLocalizePropTypes, + + /* Onyx Props */ + ...withLocalizePropTypes, ...withCurrentUserPersonalDetailsPropTypes, }; @@ -87,8 +88,7 @@ function TaskPreview(props) { : props.action.childStateNum === CONST.REPORT.STATE_NUM.SUBMITTED && props.action.childStatusNum === CONST.REPORT.STATUS.APPROVED; const taskTitle = props.taskReport.reportName || props.action.childReportName; const taskAssigneeAccountID = Task.getTaskAssigneeAccountID(props.taskReport) || props.action.childManagerAccountID; - const taskAssignee = lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'login'], lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'displayName'], '')); - const htmlForTaskPreview = taskAssignee ? `@${taskAssignee} ${taskTitle}` : `${taskTitle}`; + const assigneeLogin = lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'login'], ''); const assigneeDisplayName = lodashGet(props.personalDetailsList, [taskAssigneeAccountID, 'displayName'], ''); const taskAssignee = assigneeDisplayName || LocalePhoneNumber.formatPhoneNumber(assigneeLogin); const htmlForTaskPreview = From eb4fe936253d86232d0e096ca4c6328471d95e96 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Wed, 4 Oct 2023 11:09:07 -0600 Subject: [PATCH 024/236] We use chatReportID not chatReport Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 6fbe2c175799..3635e8dbb24d 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -57,8 +57,8 @@ const propTypes = { ownerAccountID: PropTypes.number, }), - /** chatReport associated with taskReport */ - chatReport: reportPropTypes, + /** The chat report associated with taskReport */ + chatReportID: PropTypes.string.isRequired, /** Popover context menu anchor, used for showing context menu */ contextMenuAnchor: refPropTypes, From eb985defedccd75ff4d347cff2c7adb433e068d6 Mon Sep 17 00:00:00 2001 From: Pierre Michel Date: Wed, 4 Oct 2023 11:16:09 -0600 Subject: [PATCH 025/236] reportPropTypes is not used Signed-off-by: Pierre Michel --- src/components/ReportActionItem/TaskPreview.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/ReportActionItem/TaskPreview.js b/src/components/ReportActionItem/TaskPreview.js index 3635e8dbb24d..8c757b51790c 100644 --- a/src/components/ReportActionItem/TaskPreview.js +++ b/src/components/ReportActionItem/TaskPreview.js @@ -26,7 +26,6 @@ import personalDetailsPropType from '../../pages/personalDetailsPropType'; import * as Session from '../../libs/actions/Session'; import * as LocalePhoneNumber from '../../libs/LocalePhoneNumber'; import {showContextMenuForReport} from '../ShowContextMenuContext'; -import reportPropTypes from '../../pages/reportPropTypes'; import refPropTypes from '../refPropTypes'; import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; import ControlSelection from '../../libs/ControlSelection'; From 9e54973035bd998503c3bfaad497b1412f729a52 Mon Sep 17 00:00:00 2001 From: Sofie de Vreese <40040992+SofiedeVreese@users.noreply.github.com> Date: Tue, 10 Oct 2023 01:38:34 +0000 Subject: [PATCH 026/236] Update Auto-Reconciliation.md Updating article to add image placeholder per https://stackoverflowteams.com/c/expensify/questions/17454/17455 --- .../expensify-classic/expensify-card/Auto-Reconciliation.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index 9de47d6e5beb..059877a18075 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -44,10 +44,14 @@ Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s **What happens**: When an Expensify Card is used to make purchases, the amount spent is automatically deducted from your company’s 'Settlement Account' (your business checking account). This deduction happens on a daily or monthly basis, depending on your chosen settlement frequency. Don't worry; this settlement account is pre-defined when you apply for the Expensify Card, and you can't accidentally change it. **Accounting treatment**: After your card balance is settled each day, we update your accounting system with a journal entry. This entry credits your bank account (referred to as the GL account) and debits the Expensify Card Clearing Account. To ensure accuracy, please make sure that the 'bank account' in your Expensify Card settings matches your real-life settlement account. You can easily verify this by navigating to **Settings > Account > Payments**, where you'll see 'Settlement Account' next to your business bank account. To keep track of settlement figures by date, use the Company Card Reconciliation Dashboard's Settlements tab: +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/image-name.png){:width="100%"} + ### Submitting, Approving, and Exporting Expenses **What happens**: Users submit their expenses on a report, which might occur after some time has passed since the initial purchase. Once the report is approved, it's then exported to your accounting software. **Accounting treatment**: When the report is exported, we create a journal entry in your accounting system. This entry credits the Clearing Account and debits the Liability Account for the purchase amount. The Liability Account functions as a bank account in your ledger, specifically for Expensify Card expenses: +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/image-name.png){:width="100%"} + # Deep Dive ## QuickBooks Online From b2a9994d3c1b8e146e2d2571a4748ce2f68f6d9e Mon Sep 17 00:00:00 2001 From: Oscar Franco Date: Tue, 10 Oct 2023 12:58:32 +0200 Subject: [PATCH 027/236] Bump onyx --- package-lock.json | 18 +++++++++--------- package.json | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/package-lock.json b/package-lock.json index ddebbe8a3832..3b267a21489f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -94,7 +94,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.98", + "react-native-onyx": "1.0.104", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", @@ -44710,17 +44710,17 @@ } }, "node_modules/react-native-onyx": { - "version": "1.0.98", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.98.tgz", - "integrity": "sha512-2wJNmZVBJs2Y0p1G/es4tQZnplJR8rOyVbHv9KZaq/SXluLUnIovttf1MMhVXidDLT+gcE+u20Mck/Gpb8bY0w==", + "version": "1.0.104", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.104.tgz", + "integrity": "sha512-XUrDUZNP9vJIvcCo9NC1PIjgCckgYlTqNL/ksZV2f7EN5HOXLhHqGnp7U5Dpd5qrNcU1jOyfI9AEci2bklevCA==", "dependencies": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", "underscore": "^1.13.1" }, "engines": { - "node": "16.15.1", - "npm": "8.11.0" + "node": ">=16.15.1 <=18.17.1", + "npm": ">=8.11.0 <=9.6.7" }, "peerDependencies": { "idb-keyval": "^6.2.1", @@ -85286,9 +85286,9 @@ } }, "react-native-onyx": { - "version": "1.0.98", - "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.98.tgz", - "integrity": "sha512-2wJNmZVBJs2Y0p1G/es4tQZnplJR8rOyVbHv9KZaq/SXluLUnIovttf1MMhVXidDLT+gcE+u20Mck/Gpb8bY0w==", + "version": "1.0.104", + "resolved": "https://registry.npmjs.org/react-native-onyx/-/react-native-onyx-1.0.104.tgz", + "integrity": "sha512-XUrDUZNP9vJIvcCo9NC1PIjgCckgYlTqNL/ksZV2f7EN5HOXLhHqGnp7U5Dpd5qrNcU1jOyfI9AEci2bklevCA==", "requires": { "ascii-table": "0.0.9", "fast-equals": "^4.0.3", diff --git a/package.json b/package.json index 9a3b9ed3af86..e3d480962032 100644 --- a/package.json +++ b/package.json @@ -137,7 +137,7 @@ "react-native-linear-gradient": "^2.8.1", "react-native-localize": "^2.2.6", "react-native-modal": "^13.0.0", - "react-native-onyx": "1.0.98", + "react-native-onyx": "1.0.104", "react-native-pager-view": "^6.2.0", "react-native-pdf": "^6.7.1", "react-native-performance": "^5.1.0", From c92ab2ef56ad8e714cdc0f67b73e1cb09829197d Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 11 Oct 2023 09:01:05 +0700 Subject: [PATCH 028/236] fix anonymous user can edit profile --- src/libs/Navigation/NavigationRoot.js | 6 ++++++ src/libs/actions/Session/index.js | 15 +++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index 34a52adfeca9..d8f083b9c59b 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -12,6 +12,7 @@ import StatusBar from '../StatusBar'; import useCurrentReportID from '../../hooks/useCurrentReportID'; import useWindowDimensions from '../../hooks/useWindowDimensions'; import {SidebarNavigationContext} from '../../pages/home/sidebar/SidebarNavigationContext'; +import * as Session from '../actions/Session'; // https://reactnavigation.org/docs/themes const navigationTheme = { @@ -133,6 +134,11 @@ function NavigationRoot(props) { // Update the global navigation to show the correct selected menu items. globalNavigation.updateFromNavigationState(state); + + const route = Navigation.getActiveRoute(); + if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) { + Session.signOutAndRedirectToSignIn(); + } }; return ( diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 117a092c3875..30c5f3320e08 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -787,6 +787,20 @@ function waitForUserSignIn() { }); } +/** + * check if the route can be accessed by anonymous user + * + * @param {string} route + */ + +const canAccessRouteByAnonymousUser = (route) => { + const reportID = ReportUtils.getReportIDFromLink(route); + if (reportID) { + return true; + } + return false; +}; + export { beginSignIn, beginAppleSignIn, @@ -815,4 +829,5 @@ export { toggleTwoFactorAuth, validateTwoFactorAuth, waitForUserSignIn, + canAccessRouteByAnonymousUser, }; From fa0fc433829dc7e5749a7047ede3851cc3315ab9 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 11 Oct 2023 13:25:01 +0700 Subject: [PATCH 029/236] fix update util function --- src/libs/ReportUtils.js | 1 + src/libs/actions/Session/index.js | 13 +++++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index b5fc0bff6ec7..ffb34355845f 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3959,4 +3959,5 @@ export { getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, isReportDraft, + parseReportRouteParams }; diff --git a/src/libs/actions/Session/index.js b/src/libs/actions/Session/index.js index 30c5f3320e08..b52f172f2efa 100644 --- a/src/libs/actions/Session/index.js +++ b/src/libs/actions/Session/index.js @@ -798,6 +798,19 @@ const canAccessRouteByAnonymousUser = (route) => { if (reportID) { return true; } + const parsedReportRouteParams = ReportUtils.parseReportRouteParams(route); + let routeRemovedReportId = route; + if (parsedReportRouteParams.reportID) { + routeRemovedReportId = route.replace(lodashGet(parsedReportRouteParams, 'reportID', ''), ':reportID'); + } + if (route.startsWith('/')) { + routeRemovedReportId = routeRemovedReportId.slice(1); + } + const routesCanAccessByAnonymousUser = [ROUTES.SIGN_IN_MODAL, ROUTES.REPORT_WITH_ID_DETAILS.route, ROUTES.REPORT_WITH_ID_DETAILS_SHARE_CODE.route]; + + if (_.contains(routesCanAccessByAnonymousUser, routeRemovedReportId)) { + return true; + } return false; }; From 78ba2a46d5bfb9f2fce3a22f2c786d9810cd3565 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 11 Oct 2023 14:26:11 +0700 Subject: [PATCH 030/236] fix lint issue --- src/libs/ReportUtils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index ffb34355845f..50df076ba043 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3959,5 +3959,5 @@ export { getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, isReportDraft, - parseReportRouteParams + parseReportRouteParams, }; From 709d48fbab1fce7dfc3936ad3b3a81c8b5a0e1d6 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 13 Oct 2023 13:42:14 +0700 Subject: [PATCH 031/236] fix dissmiss modal login when clicking on back button --- src/pages/signin/SignInModal.js | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pages/signin/SignInModal.js b/src/pages/signin/SignInModal.js index f1ce09def084..0ca8fa6838b2 100644 --- a/src/pages/signin/SignInModal.js +++ b/src/pages/signin/SignInModal.js @@ -24,7 +24,11 @@ function SignInModal() { shouldEnableMaxHeight testID={SignInModal.displayName} > - + { + Navigation.dismissModal(); + }} + /> ); From 0d08b325058d0a96f2bb543150d268f786f1a9e4 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 13 Oct 2023 13:45:17 +0700 Subject: [PATCH 032/236] fix refactor code --- src/pages/signin/SignInModal.js | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pages/signin/SignInModal.js b/src/pages/signin/SignInModal.js index 0ca8fa6838b2..98bd0692298c 100644 --- a/src/pages/signin/SignInModal.js +++ b/src/pages/signin/SignInModal.js @@ -24,11 +24,7 @@ function SignInModal() { shouldEnableMaxHeight testID={SignInModal.displayName} > - { - Navigation.dismissModal(); - }} - /> + ); From 5c1b12b64f05662f499a41d3a193358eb2a70462 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 16 Oct 2023 16:28:24 +0500 Subject: [PATCH 033/236] fix: add initialValue to reduce re-renders --- src/components/LHNOptionsList/OptionRowLHN.js | 1 + src/components/LHNOptionsList/OptionRowLHNData.js | 7 ++++++- src/pages/home/sidebar/SidebarLinksData.js | 6 ++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index e6cac266c10d..fcd13758d442 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -318,6 +318,7 @@ export default React.memo( withOnyx({ draftReportIDs: { key: ONYXKEYS.DRAFT_REPORT_IDS, + initialValue: {}, }, })(OptionRowLHN), ); diff --git a/src/components/LHNOptionsList/OptionRowLHNData.js b/src/components/LHNOptionsList/OptionRowLHNData.js index 883a29fb3117..a510edf9fc59 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.js +++ b/src/components/LHNOptionsList/OptionRowLHNData.js @@ -178,14 +178,17 @@ export default React.memo( withOnyx({ fullReport: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + initialValue: {}, }, reportActions: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, canEvict: false, + initialValue: {}, }, personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, selector: personalDetailsSelector, + initialValue: {}, }, preferredLocale: { key: ONYXKEYS.NVP_PREFERRED_LOCALE, @@ -196,15 +199,17 @@ export default React.memo( parentReportActions: { key: ({fullReport}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${fullReport.parentReportID}`, canEvict: false, + initialValue: {}, }, policy: { key: ({fullReport}) => `${ONYXKEYS.COLLECTION.POLICY}${fullReport.policyID}`, + initialValue: {}, }, // Ideally, we aim to access only the last transaction for the current report by listening to changes in reportActions. // In some scenarios, a transaction might be created after reportActions have been modified. // This can lead to situations where `lastTransaction` doesn't update and retains the previous value. // However, performance overhead of this is minimized by using memos inside the component. - receiptTransactions: {key: ONYXKEYS.COLLECTION.TRANSACTION}, + receiptTransactions: {key: ONYXKEYS.COLLECTION.TRANSACTION, initialValue: {}}, }), // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file withOnyx({ diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index e67c3d4de89f..d96245919b84 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -200,26 +200,32 @@ export default compose( chatReports: { key: ONYXKEYS.COLLECTION.REPORT, selector: chatReportSelector, + initialValue: {}, }, isLoadingReportData: { key: ONYXKEYS.IS_LOADING_REPORT_DATA, }, draftReportIDs: { key: ONYXKEYS.DRAFT_REPORT_IDS, + initialValue: {}, }, priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, + initialValue: CONST.PRIORITY_MODE.DEFAULT, }, betas: { key: ONYXKEYS.BETAS, + initialValue: [], }, allReportActions: { key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, selector: reportActionsSelector, + initialValue: {}, }, policies: { key: ONYXKEYS.COLLECTION.POLICY, selector: policySelector, + initialValue: {}, }, }), )(SidebarLinksData); From 98f89ada062eec261414497e82626cd79bb8f783 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Tue, 17 Oct 2023 14:02:53 +0200 Subject: [PATCH 034/236] [TS migration] Migrate 'SafeAreaConsumer.js' component to TypeScript --- ...feAreaConsumer.js => SafeAreaConsumer.tsx} | 31 +++++++++++-------- 1 file changed, 18 insertions(+), 13 deletions(-) rename src/components/{SafeAreaConsumer.js => SafeAreaConsumer.tsx} (55%) diff --git a/src/components/SafeAreaConsumer.js b/src/components/SafeAreaConsumer.tsx similarity index 55% rename from src/components/SafeAreaConsumer.js rename to src/components/SafeAreaConsumer.tsx index 78d7426ba380..c2439e25ecd3 100644 --- a/src/components/SafeAreaConsumer.js +++ b/src/components/SafeAreaConsumer.tsx @@ -1,29 +1,34 @@ import React from 'react'; -import PropTypes from 'prop-types'; -import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import {EdgeInsets, SafeAreaInsetsContext} from 'react-native-safe-area-context'; +import {DimensionValue} from 'react-native'; import * as StyleUtils from '../styles/StyleUtils'; -const propTypes = { - /** Children to render. */ - children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, +type ChildrenProps = { + paddingTop?: DimensionValue; + paddingBottom?: DimensionValue; + insets?: EdgeInsets; + safeAreaPaddingBottomStyle: { + paddingBottom?: DimensionValue; + }; +}; + +type SafeAreaConsumerProps = { + children: (props: ChildrenProps) => React.ReactNode; }; /** * This component is a light wrapper around the SafeAreaInsetsContext.Consumer. There are several places where we * may need not just the insets, but the computed styles so we save a few lines of code with this. - * - * @param {Object} props - * @returns {React.Component} */ -function SafeAreaConsumer(props) { +function SafeAreaConsumer({children}: SafeAreaConsumerProps) { return ( {(insets) => { - const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets); - return props.children({ + const {paddingTop, paddingBottom} = StyleUtils.getSafeAreaPadding(insets ?? undefined); + return children({ paddingTop, paddingBottom, - insets, + insets: insets ?? undefined, safeAreaPaddingBottomStyle: {paddingBottom}, }); }} @@ -32,5 +37,5 @@ function SafeAreaConsumer(props) { } SafeAreaConsumer.displayName = 'SafeAreaConsumer'; -SafeAreaConsumer.propTypes = propTypes; + export default SafeAreaConsumer; From 872645c276342312772f7c73029e7faa95fd533f Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Wed, 18 Oct 2023 11:55:47 +0800 Subject: [PATCH 035/236] update number of lines when window width changes --- src/components/Composer/index.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/components/Composer/index.js b/src/components/Composer/index.js index ad7a84cc1828..14974f217aab 100755 --- a/src/components/Composer/index.js +++ b/src/components/Composer/index.js @@ -11,7 +11,6 @@ import updateIsFullComposerAvailable from '../../libs/ComposerUtils/updateIsFull import * as ComposerUtils from '../../libs/ComposerUtils'; import * as Browser from '../../libs/Browser'; import * as StyleUtils from '../../styles/StyleUtils'; -import withWindowDimensions, {windowDimensionsPropTypes} from '../withWindowDimensions'; import compose from '../../libs/compose'; import styles from '../../styles/styles'; import Text from '../Text'; @@ -19,6 +18,7 @@ import isEnterWhileComposition from '../../libs/KeyboardShortcut/isEnterWhileCom import CONST from '../../CONST'; import withNavigation from '../withNavigation'; import ReportActionComposeFocusManager from '../../libs/ReportActionComposeFocusManager'; +import useWindowDimensions from '../../hooks/useWindowDimensions'; const propTypes = { /** Maximum number of lines in the text input */ @@ -87,8 +87,6 @@ const propTypes = { isComposerFullSize: PropTypes.bool, ...withLocalizePropTypes, - - ...windowDimensionsPropTypes, }; const defaultProps = { @@ -168,6 +166,7 @@ function Composer({ isComposerFullSize, ...props }) { + const {windowWidth} = useWindowDimensions(); const textRef = useRef(null); const textInput = useRef(null); const initialValue = defaultValue ? `${defaultValue}` : `${value || ''}`; @@ -366,7 +365,7 @@ function Composer({ setNumberOfLines(generalNumberOfLines); textInput.current.style.height = 'auto'; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [value, maxLines, numberOfLinesProp, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable]); + }, [value, maxLines, numberOfLinesProp, onNumberOfLinesChange, isFullComposerAvailable, setIsFullComposerAvailable, windowWidth]); useEffect(() => { updateNumberOfLines(); @@ -491,7 +490,6 @@ Composer.defaultProps = defaultProps; export default compose( withLocalize, - withWindowDimensions, withNavigation, )( React.forwardRef((props, ref) => ( From 991662b0c61c0ca03a24f765eff8beed4b397d76 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 18 Oct 2023 13:03:39 +0200 Subject: [PATCH 036/236] ref: moved PopoverProvider to TS --- .../{index.native.js => index.native.tsx} | 18 ++----- .../PopoverProvider/{index.js => index.tsx} | 47 +++++++------------ src/components/PopoverProvider/types.ts | 18 +++++++ 3 files changed, 40 insertions(+), 43 deletions(-) rename src/components/PopoverProvider/{index.native.js => index.native.tsx} (57%) rename src/components/PopoverProvider/{index.js => index.tsx} (65%) create mode 100644 src/components/PopoverProvider/types.ts diff --git a/src/components/PopoverProvider/index.native.js b/src/components/PopoverProvider/index.native.tsx similarity index 57% rename from src/components/PopoverProvider/index.native.js rename to src/components/PopoverProvider/index.native.tsx index f34abcb1fa62..d4ca6813f408 100644 --- a/src/components/PopoverProvider/index.native.js +++ b/src/components/PopoverProvider/index.native.tsx @@ -1,26 +1,20 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import {PopoverContextProps, PopoverContextValue} from './types'; -const propTypes = { - children: PropTypes.node.isRequired, -}; - -const defaultProps = {}; - -const PopoverContext = React.createContext({ +const PopoverContext = React.createContext({ onOpen: () => {}, - popover: {}, + popover: null, close: () => {}, isOpen: false, }); -function PopoverContextProvider(props) { +function PopoverContextProvider(props: PopoverContextProps) { return ( {}, close: () => {}, - popover: {}, + popover: null, isOpen: false, }} > @@ -29,8 +23,6 @@ function PopoverContextProvider(props) { ); } -PopoverContextProvider.defaultProps = defaultProps; -PopoverContextProvider.propTypes = propTypes; PopoverContextProvider.displayName = 'PopoverContextProvider'; export default PopoverContextProvider; diff --git a/src/components/PopoverProvider/index.js b/src/components/PopoverProvider/index.tsx similarity index 65% rename from src/components/PopoverProvider/index.js rename to src/components/PopoverProvider/index.tsx index efa230d920d5..728f9a207121 100644 --- a/src/components/PopoverProvider/index.js +++ b/src/components/PopoverProvider/index.tsx @@ -1,24 +1,18 @@ import React from 'react'; -import PropTypes from 'prop-types'; +import {AnchorRef, PopoverContextProps, PopoverContextValue} from './types'; -const propTypes = { - children: PropTypes.node.isRequired, -}; - -const defaultProps = {}; - -const PopoverContext = React.createContext({ +const PopoverContext = React.createContext({ onOpen: () => {}, - popover: {}, + popover: null, close: () => {}, isOpen: false, }); -function PopoverContextProvider(props) { +function PopoverContextProvider(props: PopoverContextProps) { const [isOpen, setIsOpen] = React.useState(false); - const activePopoverRef = React.useRef(null); + const activePopoverRef = React.useRef(null); - const closePopover = React.useCallback((anchorRef) => { + const closePopover = React.useCallback((anchorRef?: React.RefObject) => { if (!activePopoverRef.current || (anchorRef && anchorRef !== activePopoverRef.current.anchorRef)) { return; } @@ -28,17 +22,12 @@ function PopoverContextProvider(props) { }, []); React.useEffect(() => { - const listener = (e) => { - if ( - !activePopoverRef.current || - !activePopoverRef.current.ref || - !activePopoverRef.current.ref.current || - activePopoverRef.current.ref.current.contains(e.target) || - (activePopoverRef.current.anchorRef && activePopoverRef.current.anchorRef.current && activePopoverRef.current.anchorRef.current.contains(e.target)) - ) { + const listener = (e: Event) => { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + if (activePopoverRef.current?.ref?.current?.contains(e.target as Node) || activePopoverRef.current?.anchorRef?.current?.contains(e.target as Node)) { return; } - const ref = activePopoverRef.current.anchorRef; + const ref = activePopoverRef.current?.anchorRef; closePopover(ref); }; document.addEventListener('click', listener, true); @@ -48,8 +37,8 @@ function PopoverContextProvider(props) { }, [closePopover]); React.useEffect(() => { - const listener = (e) => { - if (!activePopoverRef.current || !activePopoverRef.current.ref || !activePopoverRef.current.ref.current || activePopoverRef.current.ref.current.contains(e.target)) { + const listener = (e: Event) => { + if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) { return; } closePopover(); @@ -61,7 +50,7 @@ function PopoverContextProvider(props) { }, [closePopover]); React.useEffect(() => { - const listener = (e) => { + const listener = (e: KeyboardEvent) => { if (e.key !== 'Escape') { return; } @@ -87,8 +76,8 @@ function PopoverContextProvider(props) { }, [closePopover]); React.useEffect(() => { - const listener = (e) => { - if (activePopoverRef.current && activePopoverRef.current.ref && activePopoverRef.current.ref.current && activePopoverRef.current.ref.current.contains(e.target)) { + const listener = (e: Event) => { + if (activePopoverRef.current?.ref?.current?.contains(e.target as Node)) { return; } @@ -101,8 +90,8 @@ function PopoverContextProvider(props) { }, [closePopover]); const onOpen = React.useCallback( - (popoverParams) => { - if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams.ref) { + (popoverParams: AnchorRef) => { + if (activePopoverRef.current && activePopoverRef.current.ref !== popoverParams?.ref) { closePopover(activePopoverRef.current.anchorRef); } activePopoverRef.current = popoverParams; @@ -125,8 +114,6 @@ function PopoverContextProvider(props) { ); } -PopoverContextProvider.defaultProps = defaultProps; -PopoverContextProvider.propTypes = propTypes; PopoverContextProvider.displayName = 'PopoverContextProvider'; export default PopoverContextProvider; diff --git a/src/components/PopoverProvider/types.ts b/src/components/PopoverProvider/types.ts new file mode 100644 index 000000000000..733e363bce00 --- /dev/null +++ b/src/components/PopoverProvider/types.ts @@ -0,0 +1,18 @@ +type PopoverContextProps = { + children: React.ReactNode; +}; + +type PopoverContextValue = { + onOpen?: (popoverParams: AnchorRef) => void; + popover?: AnchorRef | null; + close: (anchorRef?: React.RefObject) => void; + isOpen: boolean; +}; + +type AnchorRef = { + ref: React.RefObject; + close: (anchorRef?: React.RefObject) => void; + anchorRef?: React.RefObject; +}; + +export type {PopoverContextProps, PopoverContextValue, AnchorRef}; From eab87d28d3a3747f68f1096e2b68f03583799a64 Mon Sep 17 00:00:00 2001 From: Blazej Kustra Date: Wed, 18 Oct 2023 13:05:00 +0200 Subject: [PATCH 037/236] Adjust after internal review --- src/components/SafeAreaConsumer.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/SafeAreaConsumer.tsx b/src/components/SafeAreaConsumer.tsx index c2439e25ecd3..dec0964b34a9 100644 --- a/src/components/SafeAreaConsumer.tsx +++ b/src/components/SafeAreaConsumer.tsx @@ -1,6 +1,6 @@ import React from 'react'; import {EdgeInsets, SafeAreaInsetsContext} from 'react-native-safe-area-context'; -import {DimensionValue} from 'react-native'; +import type {DimensionValue} from 'react-native'; import * as StyleUtils from '../styles/StyleUtils'; type ChildrenProps = { @@ -13,7 +13,7 @@ type ChildrenProps = { }; type SafeAreaConsumerProps = { - children: (props: ChildrenProps) => React.ReactNode; + children: React.FC; }; /** From 419e7be5f20b363254974576a8032f9be0cbb274 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Wed, 18 Oct 2023 16:23:30 +0200 Subject: [PATCH 038/236] Rename OpacityView --- src/components/{OpacityView.js => OpacityView.tsx} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/components/{OpacityView.js => OpacityView.tsx} (100%) diff --git a/src/components/OpacityView.js b/src/components/OpacityView.tsx similarity index 100% rename from src/components/OpacityView.js rename to src/components/OpacityView.tsx From 03af64861ae3e64fc41cfc4c19274b13f51f68af Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 19 Oct 2023 00:56:31 +0700 Subject: [PATCH 039/236] fix sign in modal appear for a second --- src/libs/Navigation/NavigationRoot.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index a920bfeeca9e..b39e5bbb0d5f 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -13,6 +13,8 @@ import useCurrentReportID from '../../hooks/useCurrentReportID'; import useWindowDimensions from '../../hooks/useWindowDimensions'; import {SidebarNavigationContext} from '../../pages/home/sidebar/SidebarNavigationContext'; import * as Session from '../actions/Session'; +import getCurrentUrl from './currentUrl'; +import ROUTES from '../../ROUTES'; // https://reactnavigation.org/docs/themes const navigationTheme = { @@ -102,7 +104,7 @@ function NavigationRoot(props) { const animateStatusBarBackgroundColor = () => { const currentRoute = navigationRef.getCurrentRoute(); - const currentScreenBackgroundColor = (currentRoute.params && currentRoute.params.backgroundColor) || themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG; + const currentScreenBackgroundColor = themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG; prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current; statusBarBackgroundColor.current = currentScreenBackgroundColor; @@ -136,7 +138,7 @@ function NavigationRoot(props) { globalNavigation.updateFromNavigationState(state); const route = Navigation.getActiveRoute(); - if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) { + if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route) && !getCurrentUrl().includes(ROUTES.SIGN_IN_MODAL)) { Session.signOutAndRedirectToSignIn(); } }; From 0ac3e98f814025b0df91c793c1352e94a218f190 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 19 Oct 2023 01:02:12 +0700 Subject: [PATCH 040/236] fix revert unrelated change --- src/libs/Navigation/NavigationRoot.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index b39e5bbb0d5f..a22b6714a306 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -104,7 +104,7 @@ function NavigationRoot(props) { const animateStatusBarBackgroundColor = () => { const currentRoute = navigationRef.getCurrentRoute(); - const currentScreenBackgroundColor = themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG; + const currentScreenBackgroundColor = (currentRoute.params && currentRoute.params.backgroundColor) || themeColors.PAGE_BACKGROUND_COLORS[currentRoute.name] || themeColors.appBG; prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current; statusBarBackgroundColor.current = currentScreenBackgroundColor; From bd2274c63dd1c23c8bb509949a44ab6e72d7e75f Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 19 Oct 2023 16:20:14 +0200 Subject: [PATCH 041/236] WIP --- src/components/FullscreenLoadingIndicator.js | 33 ------------------- src/components/FullscreenLoadingIndicator.tsx | 22 +++++++++++++ 2 files changed, 22 insertions(+), 33 deletions(-) delete mode 100644 src/components/FullscreenLoadingIndicator.js create mode 100644 src/components/FullscreenLoadingIndicator.tsx diff --git a/src/components/FullscreenLoadingIndicator.js b/src/components/FullscreenLoadingIndicator.js deleted file mode 100644 index 5c212b6dc29e..000000000000 --- a/src/components/FullscreenLoadingIndicator.js +++ /dev/null @@ -1,33 +0,0 @@ -import _ from 'underscore'; -import React from 'react'; -import {ActivityIndicator, StyleSheet, View} from 'react-native'; -import styles from '../styles/styles'; -import themeColors from '../styles/themes/default'; -import stylePropTypes from '../styles/stylePropTypes'; - -const propTypes = { - /** Additional style props */ - style: stylePropTypes, -}; - -const defaultProps = { - style: [], -}; - -function FullScreenLoadingIndicator(props) { - const additionalStyles = _.isArray(props.style) ? props.style : [props.style]; - return ( - - - - ); -} - -FullScreenLoadingIndicator.propTypes = propTypes; -FullScreenLoadingIndicator.defaultProps = defaultProps; -FullScreenLoadingIndicator.displayName = 'FullScreenLoadingIndicator'; - -export default FullScreenLoadingIndicator; diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx new file mode 100644 index 000000000000..3f5f62f533d7 --- /dev/null +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -0,0 +1,22 @@ +import React from 'react'; +import {ActivityIndicator, StyleSheet, View} from 'react-native'; +import styles from '../styles/styles'; +import themeColors from '../styles/themes/default'; + +type FullScreenLoadingIndicatorProps = { + style: Record | Array> | (() => void); +}; + +function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProps) { + const additionalStyles = Array.isArray(style) ? style : [style]; + return ( + + + + ); +} + +export default FullScreenLoadingIndicator; From 9d1cd9d436023ac07fde031abc93a3f2eecb54d3 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Thu, 19 Oct 2023 16:35:10 +0200 Subject: [PATCH 042/236] remove moment from datepicker --- src/CONST.ts | 1 - .../DatePicker/datepickerPropTypes.js | 4 +- src/components/DatePicker/index.android.js | 10 ++--- src/components/DatePicker/index.ios.js | 10 ++--- src/components/DatePicker/index.js | 12 +++--- .../NewDatePicker/CalendarPicker/index.js | 40 +++++++++---------- src/components/NewDatePicker/index.js | 10 ++--- src/libs/DateUtils.ts | 36 +++++++++++++++++ tests/unit/CalendarPickerTest.js | 11 +---- 9 files changed, 81 insertions(+), 53 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index bc74cbe77717..6a52563d238b 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -131,7 +131,6 @@ const CONST = { DESKTOP: `${ACTIVE_EXPENSIFY_URL}NewExpensify.dmg`, }, DATE: { - MOMENT_FORMAT_STRING: 'YYYY-MM-DD', SQL_DATE_TIME: 'YYYY-MM-DD HH:mm:ss', FNS_FORMAT_STRING: 'yyyy-MM-dd', LOCAL_TIME_FORMAT: 'h:mm a', diff --git a/src/components/DatePicker/datepickerPropTypes.js b/src/components/DatePicker/datepickerPropTypes.js index 8bd5d890c42c..f896023d386b 100644 --- a/src/components/DatePicker/datepickerPropTypes.js +++ b/src/components/DatePicker/datepickerPropTypes.js @@ -6,13 +6,13 @@ const propTypes = { ...fieldPropTypes, /** - * The datepicker supports any value that `moment` can parse. + * The datepicker supports any value that `new Date()` can parse. * `onInputChange` would always be called with a Date (or null) */ value: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), /** - * The datepicker supports any defaultValue that `moment` can parse. + * The datepicker supports any defaultValue that `new Date()` can parse. * `onInputChange` would always be called with a Date (or null) */ defaultValue: PropTypes.oneOfType([PropTypes.instanceOf(Date), PropTypes.string]), diff --git a/src/components/DatePicker/index.android.js b/src/components/DatePicker/index.android.js index 5bdda580d357..24faf2b19745 100644 --- a/src/components/DatePicker/index.android.js +++ b/src/components/DatePicker/index.android.js @@ -1,7 +1,7 @@ import React from 'react'; import {Keyboard} from 'react-native'; import RNDatePicker from '@react-native-community/datetimepicker'; -import moment from 'moment'; +import {format} from 'date-fns'; import _ from 'underscore'; import TextInput from '../TextInput'; import CONST from '../../CONST'; @@ -28,8 +28,7 @@ class DatePicker extends React.Component { this.setState({isPickerVisible: false}); if (event.type === 'set') { - const asMoment = moment(selectedDate, true); - this.props.onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); + this.props.onInputChange(format(selectedDate, CONST.DATE.FNS_FORMAT_STRING)); } } @@ -39,7 +38,8 @@ class DatePicker extends React.Component { } render() { - const dateAsText = this.props.value || this.props.defaultValue ? moment(this.props.value || this.props.defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; + const date = this.props.value || this.props.defaultValue; + const dateAsText = date ? format(new Date(date), CONST.DATE.FNS_FORMAT_STRING) : ''; return ( <> @@ -73,7 +73,7 @@ class DatePicker extends React.Component { /> {this.state.isPickerVisible && ( { setIsPickerVisible(false); - const asMoment = moment(selectedDate, true); - onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); + onInputChange(format(selectedDate, CONST.DATE.FNS_FORMAT_STRING)); }; /** @@ -77,7 +77,7 @@ function DatePicker({value, defaultValue, innerRef, onInputChange, preferredLoca setSelectedDate(date); }; - const dateAsText = value || defaultValue ? moment(value || defaultValue).format(CONST.DATE.MOMENT_FORMAT_STRING) : ''; + const dateAsText = dateValue ? format(new Date(dateValue), CONST.DATE.FNS_FORMAT_STRING) : ''; return ( <> diff --git a/src/components/DatePicker/index.js b/src/components/DatePicker/index.js index d14886fd1c59..e0672f847295 100644 --- a/src/components/DatePicker/index.js +++ b/src/components/DatePicker/index.js @@ -1,5 +1,5 @@ import React, {useEffect, useRef} from 'react'; -import moment from 'moment'; +import {format, isValid} from 'date-fns'; import _ from 'underscore'; import TextInput from '../TextInput'; import CONST from '../../CONST'; @@ -13,8 +13,8 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl useEffect(() => { // Adds nice native datepicker on web/desktop. Not possible to set this through props inputRef.current.setAttribute('type', 'date'); - inputRef.current.setAttribute('max', moment(maxDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); - inputRef.current.setAttribute('min', moment(minDate).format(CONST.DATE.MOMENT_FORMAT_STRING)); + inputRef.current.setAttribute('max', format(new Date(maxDate), CONST.DATE.FNS_FORMAT_STRING)); + inputRef.current.setAttribute('min', format(new Date(minDate), CONST.DATE.FNS_FORMAT_STRING)); inputRef.current.classList.add('expensify-datepicker'); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -29,9 +29,9 @@ function DatePicker({maxDate, minDate, onInputChange, innerRef, label, value, pl return; } - const asMoment = moment(text, true); - if (asMoment.isValid()) { - onInputChange(asMoment.format(CONST.DATE.MOMENT_FORMAT_STRING)); + const date = new Date(text); + if (isValid(date)) { + onInputChange(format(date, CONST.DATE.FNS_FORMAT_STRING)); } }; diff --git a/src/components/NewDatePicker/CalendarPicker/index.js b/src/components/NewDatePicker/CalendarPicker/index.js index d03c36997845..67b3ef3aa91a 100644 --- a/src/components/NewDatePicker/CalendarPicker/index.js +++ b/src/components/NewDatePicker/CalendarPicker/index.js @@ -1,7 +1,7 @@ import _ from 'underscore'; import React from 'react'; import {View} from 'react-native'; -import moment from 'moment'; +import {setYear, format, getYear, subMonths, addMonths, startOfDay, endOfMonth, setDate, isSameDay} from 'date-fns'; import PropTypes from 'prop-types'; import Str from 'expensify-common/lib/str'; import Text from '../../Text'; @@ -11,6 +11,7 @@ import styles from '../../../styles/styles'; import generateMonthMatrix from './generateMonthMatrix'; import withLocalize, {withLocalizePropTypes} from '../../withLocalize'; import CONST from '../../../CONST'; +import DateUtils from '../../../libs/DateUtils'; import getButtonState from '../../../libs/getButtonState'; import * as StyleUtils from '../../../styles/StyleUtils'; import PressableWithFeedback from '../../Pressable/PressableWithFeedback'; @@ -34,8 +35,8 @@ const propTypes = { const defaultProps = { value: new Date(), - minDate: moment().year(CONST.CALENDAR_PICKER.MIN_YEAR).toDate(), - maxDate: moment().year(CONST.CALENDAR_PICKER.MAX_YEAR).toDate(), + minDate: setYear(new Date(), CONST.CALENDAR_PICKER.MIN_YEAR), + maxDate: setYear(new Date(), CONST.CALENDAR_PICKER.MAX_YEAR), onSelected: () => {}, }; @@ -46,16 +47,15 @@ class CalendarPicker extends React.PureComponent { if (props.minDate >= props.maxDate) { throw new Error('Minimum date cannot be greater than the maximum date.'); } - - let currentDateView = moment(props.value, CONST.DATE.MOMENT_FORMAT_STRING).toDate(); + let currentDateView = new Date(props.value); if (props.maxDate < currentDateView) { currentDateView = props.maxDate; } else if (props.minDate > currentDateView) { currentDateView = props.minDate; } - const minYear = moment(this.props.minDate).year(); - const maxYear = moment(this.props.maxDate).year(); + const minYear = getYear(new Date(this.props.minDate)); + const maxYear = getYear(new Date(this.props.maxDate)); this.state = { currentDateView, @@ -79,7 +79,7 @@ class CalendarPicker extends React.PureComponent { onYearSelected(year) { this.setState((prev) => { - const newCurrentDateView = moment(prev.currentDateView).set('year', year).toDate(); + const newCurrentDateView = setYear(new Date(prev.currentDateView), year); return { currentDateView: newCurrentDateView, @@ -99,9 +99,9 @@ class CalendarPicker extends React.PureComponent { onDayPressed(day) { this.setState( (prev) => ({ - currentDateView: moment(prev.currentDateView).set('date', day).toDate(), + currentDateView: setDate(new Date(prev.currentDateView), day), }), - () => this.props.onSelected(moment(this.state.currentDateView).format('YYYY-MM-DD')), + () => this.props.onSelected(format(new Date(this.state.currentDateView), CONST.DATE.FNS_FORMAT_STRING)), ); } @@ -109,24 +109,24 @@ class CalendarPicker extends React.PureComponent { * Handles the user pressing the previous month arrow of the calendar picker. */ moveToPrevMonth() { - this.setState((prev) => ({currentDateView: moment(prev.currentDateView).subtract(1, 'months').toDate()})); + this.setState((prev) => ({currentDateView: subMonths(new Date(prev.currentDateView), 1)})); } /** * Handles the user pressing the next month arrow of the calendar picker. */ moveToNextMonth() { - this.setState((prev) => ({currentDateView: moment(prev.currentDateView).add(1, 'months').toDate()})); + this.setState((prev) => ({currentDateView: addMonths(new Date(prev.currentDateView), 1)})); } render() { - const monthNames = _.map(moment.localeData(this.props.preferredLocale).months(), Str.recapitalize); - const daysOfWeek = _.map(moment.localeData(this.props.preferredLocale).weekdays(), (day) => day.toUpperCase()); + const monthNames = _.map(DateUtils.getMonthNames(this.props.preferredLocale), Str.recapitalize); + const daysOfWeek = _.map(DateUtils.getDaysOfWeek(this.props.preferredLocale), (day) => day.toUpperCase()); const currentMonthView = this.state.currentDateView.getMonth(); const currentYearView = this.state.currentDateView.getFullYear(); const calendarDaysMatrix = generateMonthMatrix(currentYearView, currentMonthView); - const hasAvailableDatesNextMonth = moment(this.props.maxDate).endOf('month').endOf('day') >= moment(this.state.currentDateView).add(1, 'months'); - const hasAvailableDatesPrevMonth = moment(this.props.minDate).startOf('month').startOf('day') <= moment(this.state.currentDateView).subtract(1, 'months'); + const hasAvailableDatesNextMonth = startOfDay(endOfMonth(new Date(this.props.maxDate))) > addMonths(new Date(this.state.currentDateView), 1); + const hasAvailableDatesPrevMonth = startOfDay(new Date(this.props.minDate)) < endOfMonth(subMonths(new Date(this.state.currentDateView), 1)); return ( @@ -201,11 +201,11 @@ class CalendarPicker extends React.PureComponent { style={styles.flexRow} > {_.map(week, (day, index) => { - const currentDate = moment([currentYearView, currentMonthView, day]); - const isBeforeMinDate = currentDate < moment(this.props.minDate).startOf('day'); - const isAfterMaxDate = currentDate > moment(this.props.maxDate).startOf('day'); + const currentDate = new Date(currentYearView, currentMonthView, day); + const isBeforeMinDate = currentDate < startOfDay(new Date(this.props.minDate)); + const isAfterMaxDate = currentDate > startOfDay(new Date(this.props.maxDate)); const isDisabled = !day || isBeforeMinDate || isAfterMaxDate; - const isSelected = moment(this.props.value).isSame(moment([currentYearView, currentMonthView, day]), 'day'); + const isSelected = isSameDay(new Date(this.props.value), new Date(currentYearView, currentMonthView, day)); return ( { return timezone; } +/** + * @returns [January, Fabruary, March, April, May, June, July, August, ...] + */ +function getMonthNames(preferredLocale: string): string[] { + if (preferredLocale) { + setLocale(preferredLocale); + } + const fullYear = new Date().getFullYear(); + const monthsArray = eachMonthOfInterval({ + start: new Date(fullYear, 0, 1), // January 1st of the current year + end: new Date(fullYear, 11, 31), // December 31st of the current year + }); + + // eslint-disable-next-line rulesdir/prefer-underscore-method + return monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT)); +} + +/** + * @returns [Monday, Thuesday, Wednesday, ...] + */ +function getDaysOfWeek(preferredLocale: string): string[] { + if (preferredLocale) { + setLocale(preferredLocale); + } + const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week + const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week + const daysOfWeek = eachDayOfInterval({start: startOfCurrentWeek, end: endOfCurrentWeek}); + + // eslint-disable-next-line rulesdir/prefer-underscore-method + return daysOfWeek.map((date) => format(date, 'eeee')); +} + // Used to throttle updates to the timezone when necessary let lastUpdatedTimezoneTime = new Date(); @@ -357,6 +391,8 @@ const DateUtils = { isToday, isTomorrow, isYesterday, + getMonthNames, + getDaysOfWeek, }; export default DateUtils; diff --git a/tests/unit/CalendarPickerTest.js b/tests/unit/CalendarPickerTest.js index 512a86a25e19..235dff45f631 100644 --- a/tests/unit/CalendarPickerTest.js +++ b/tests/unit/CalendarPickerTest.js @@ -1,17 +1,10 @@ import {render, fireEvent, within} from '@testing-library/react-native'; -import {format, eachMonthOfInterval, subYears, addYears} from 'date-fns'; +import {subYears, addYears} from 'date-fns'; import DateUtils from '../../src/libs/DateUtils'; import CalendarPicker from '../../src/components/NewDatePicker/CalendarPicker'; import CONST from '../../src/CONST'; -DateUtils.setLocale(CONST.LOCALES.EN); -const fullYear = new Date().getFullYear(); -const monthsArray = eachMonthOfInterval({ - start: new Date(fullYear, 0, 1), // January 1st of the current year - end: new Date(fullYear, 11, 31), // December 31st of the current year -}); -// eslint-disable-next-line rulesdir/prefer-underscore-method -const monthNames = monthsArray.map((monthDate) => format(monthDate, CONST.DATE.MONTH_FORMAT)); +const monthNames = DateUtils.getMonthNames(CONST.LOCALES.EN); jest.mock('@react-navigation/native', () => ({ useNavigation: () => ({navigate: jest.fn()}), From 8d4678fa56bb60d5c5cf9e4371ffe56f1e2d924b Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 19 Oct 2023 16:37:39 +0200 Subject: [PATCH 043/236] Drop func from possible types --- src/components/FullscreenLoadingIndicator.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 3f5f62f533d7..92e59d0e2d1c 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -4,7 +4,7 @@ import styles from '../styles/styles'; import themeColors from '../styles/themes/default'; type FullScreenLoadingIndicatorProps = { - style: Record | Array> | (() => void); + style: Record | Array>; }; function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProps) { From ba7bd1714b873326c7bda6a75e29c33473100e40 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Thu, 19 Oct 2023 15:45:01 -0400 Subject: [PATCH 044/236] Allow requesting money when expensify owns workspace --- src/libs/ReportUtils.js | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 1b03c3a1bdb2..3e0b8962bf37 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -3515,15 +3515,18 @@ function getMoneyRequestOptions(report, reportParticipants) { const participants = _.filter(reportParticipants, (accountID) => currentUserPersonalDetails.accountID !== accountID); - // Verify if there is any of the expensify accounts amongst the participants in which case user cannot take IOU actions on such report - const hasExcludedIOUAccountIDs = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; - const hasSingleParticipantInReport = participants.length === 1; - const hasMultipleParticipants = participants.length > 1; - - if (hasExcludedIOUAccountIDs) { + // We don't allow IOU actions if an Expensify account is a participant of the report, unless the policy that the report is on is owned by an Expensify account + const doParticipantsIncludeExpensifyAccounts = lodashIntersection(reportParticipants, CONST.EXPENSIFY_ACCOUNT_IDS).length > 0; + const policyID = lodashGet(report, 'policyID', ''); + const policyOwnerAccountID = lodashGet(allPolicies, `${ONYXKEYS.COLLECTION.POLICY}${policyID}.ownerAccountID`, 0); + const doExpensifyAccountsOwnPolicy = CONST.EXPENSIFY_ACCOUNT_IDS.includes(policyOwnerAccountID); + if (doParticipantsIncludeExpensifyAccounts && !doExpensifyAccountsOwnPolicy) { return []; } + const hasSingleParticipantInReport = participants.length === 1; + const hasMultipleParticipants = participants.length > 1; + // User created policy rooms and default rooms like #admins or #announce will always have the Split Bill option // unless there are no participants at all (e.g. #admins room for a policy with only 1 admin) // DM chats will have the Split Bill option only when there are at least 3 people in the chat. From 30a906496814f4023fbb11bdaff3a69c789b7772 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 20 Oct 2023 12:14:24 +0500 Subject: [PATCH 045/236] fix: PR feedbacks --- src/components/LHNOptionsList/OptionRowLHN.js | 20 ++++++- .../LHNOptionsList/OptionRowLHNData.js | 18 +----- src/libs/DraftReportUtils.ts | 54 ----------------- src/libs/ReportUtils.js | 21 +++++-- src/libs/UnreadIndicatorUpdater/index.js | 10 ++++ src/libs/actions/DraftReports.ts | 17 +----- src/libs/actions/Policy.js | 14 ++++- .../ComposerWithSuggestions.js | 3 +- tests/unit/DraftReportUtilsTest.js | 59 ------------------- tests/unit/SidebarFilterTest.js | 4 ++ tests/unit/SidebarOrderTest.js | 4 ++ 11 files changed, 66 insertions(+), 158 deletions(-) delete mode 100644 src/libs/DraftReportUtils.ts delete mode 100644 tests/unit/DraftReportUtilsTest.js diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index 6e886ba6e360..0a2a5d26f10d 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -1,5 +1,5 @@ import _ from 'underscore'; -import React, {useState, useRef, useCallback} from 'react'; +import React, {useState, useRef, useCallback, useEffect} from 'react'; import PropTypes from 'prop-types'; import {View, StyleSheet} from 'react-native'; import lodashGet from 'lodash/get'; @@ -31,6 +31,7 @@ import ONYXKEYS from '../../ONYXKEYS'; import DomUtils from '../../libs/DomUtils'; import useWindowDimensions from '../../hooks/useWindowDimensions'; import ReportActionComposeFocusManager from '../../libs/ReportActionComposeFocusManager'; +import setDraftStatusForReportID from '../../libs/actions/DraftReports'; const propTypes = { /** Style for hovered state */ @@ -60,6 +61,8 @@ const propTypes = { // eslint-disable-next-line react/forbid-prop-types draftReportIDs: PropTypes.object, + + shouldUpdateDraftStatus: PropTypes.bool, }; const defaultProps = { @@ -71,6 +74,7 @@ const defaultProps = { isFocused: false, betas: [], draftReportIDs: {}, + shouldUpdateDraftStatus: false, }; function OptionRowLHN(props) { @@ -83,6 +87,17 @@ function OptionRowLHN(props) { const optionItem = props.optionItem; const [isContextMenuActive, setIsContextMenuActive] = useState(false); + const hasDraft = props.draftReportIDs[props.reportID]; + + useEffect(() => { + if (props.shouldUpdateDraftStatus || hasDraft) { + return; + } + + setDraftStatusForReportID(props.reportID, true); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useFocusEffect( useCallback(() => { isFocusedRef.current = true; @@ -159,7 +174,6 @@ function OptionRowLHN(props) { const formattedDate = DateUtils.getStatusUntilDate(statusClearAfterDate); const statusContent = formattedDate ? `${statusText} (${formattedDate})` : statusText; const isStatusVisible = Permissions.canUseCustomStatus(props.betas) && !!emojiCode && ReportUtils.isOneOnOneChat(optionItem); - const isDraft = props.draftReportIDs[props.reportID]; return ( )} - {isDraft && optionItem.isAllowedToComment && ( + {hasDraft && optionItem.isAllowedToComment && ( { - const draftReportIDs = DraftReportUtils.getInstance().getDraftReportIDs(); - const hasDraft = draftReportIDs[reportID]; - - if (!optionItem || hasDraft || !comment || comment.length <= 0 || isFocused) { - return; - } - setDraftStatusForReportID(reportID, true); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); - return ( ); } diff --git a/src/libs/DraftReportUtils.ts b/src/libs/DraftReportUtils.ts deleted file mode 100644 index 214b92404e1b..000000000000 --- a/src/libs/DraftReportUtils.ts +++ /dev/null @@ -1,54 +0,0 @@ -import Onyx from 'react-native-onyx'; -import ONYXKEYS from '../ONYXKEYS'; - -/** - * A singleton class to manage the draft report IDs - * @class DraftReportUtils - * @singleton - */ -class DraftReportUtils { - private static instance: DraftReportUtils; - - private draftReportIDs: Record; - - private constructor() { - DraftReportUtils.instance = this; - - this.draftReportIDs = {}; - - this.subscribeToDraftReportIDs(); - } - - /** - * @returns The singleton instance - */ - public static getInstance(): DraftReportUtils { - // Ensure singleton instance - return DraftReportUtils.instance ?? new DraftReportUtils(); - } - - /** - * Subscribe to the draft report IDs - */ - private subscribeToDraftReportIDs() { - Onyx.connect({ - key: ONYXKEYS.DRAFT_REPORT_IDS, - callback: (val) => { - if (!val) { - return; - } - - this.draftReportIDs = val; - }, - }); - } - - /** - * @returns The draft report IDs - */ - getDraftReportIDs() { - return this.draftReportIDs; - } -} - -export default DraftReportUtils; diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index a12b4a48f393..30c375c2e574 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -23,7 +23,6 @@ import isReportMessageAttachment from './isReportMessageAttachment'; import * as defaultWorkspaceAvatars from '../components/Icon/WorkspaceDefaultAvatars'; import * as CurrencyUtils from './CurrencyUtils'; import * as UserUtils from './UserUtils'; -import DraftReportUtils from './DraftReportUtils'; let currentUserEmail; let currentUserAccountID; @@ -53,7 +52,7 @@ Onyx.connect({ }, }); -let allReports = {}; +let allReports; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT, @@ -61,7 +60,17 @@ Onyx.connect({ callback: (val) => (allReports = val), }); -const draftReportUtils = DraftReportUtils.getInstance(); +let draftReportIDs = {}; +Onyx.connect({ + key: ONYXKEYS.DRAFT_REPORT_IDS, + callback: (val) => { + if (!val) { + return; + } + + draftReportIDs = val; + }, +}); let doesDomainHaveApprovedAccountant; Onyx.connect({ @@ -767,7 +776,7 @@ function isMoneyRequestReport(reportOrID) { */ function getReport(reportID) { /** - * using typical string concatenation here due to performance issues + * Using typical string concatenation here due to performance issues * with template literals. */ if (!allReports) { @@ -1414,7 +1423,7 @@ function getPolicyExpenseChatName(report, policy = undefined) { let policyExpenseChatRole = 'user'; /** - * using typical string concatenation here due to performance issues + * Using typical string concatenation here due to performance issues * with template literals. */ const policyItem = allPolicies[ONYXKEYS.COLLECTION.POLICY + report.policyID]; @@ -3198,7 +3207,7 @@ function shouldReportBeInOptionList(report, currentReportId, isInGSDMode, betas, } // Include reports that are relevant to the user in any view mode. Criteria include having a draft, having an outstanding IOU, or being assigned to an open task. - if (draftReportUtils.getDraftReportIDs()[report.reportID] || isWaitingForIOUActionFromCurrentUser(report) || isWaitingForTaskCompleteFromAssignee(report)) { + if (draftReportIDs[report.reportID] || isWaitingForIOUActionFromCurrentUser(report) || isWaitingForTaskCompleteFromAssignee(report)) { return true; } const lastVisibleMessage = ReportActionsUtils.getLastVisibleMessage(report.reportID); diff --git a/src/libs/UnreadIndicatorUpdater/index.js b/src/libs/UnreadIndicatorUpdater/index.js index dcfedce83d75..e67b5b1feb06 100644 --- a/src/libs/UnreadIndicatorUpdater/index.js +++ b/src/libs/UnreadIndicatorUpdater/index.js @@ -15,6 +15,16 @@ Onyx.connect({ return; } + /** + * We need to wait until after interactions have finished to update the unread count because otherwise + * the unread count will be updated while the interactions/animations are in progress and we don't want + * to put more work on the main thread. + * + * For eg. On web we are manipulating DOM and it makes it a better candidate to wait until after interactions + * have finished. + * + * More info: https://reactnative.dev/docs/interactionmanager + */ InteractionManager.runAfterInteractions(() => { const unreadReportsCount = _.filter(reportsFromOnyx, ReportUtils.isUnread).length || 0; if (previousUnreadCount !== unreadReportsCount) { diff --git a/src/libs/actions/DraftReports.ts b/src/libs/actions/DraftReports.ts index dc1e7a8066f8..97e5073030ca 100644 --- a/src/libs/actions/DraftReports.ts +++ b/src/libs/actions/DraftReports.ts @@ -1,8 +1,5 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '../../ONYXKEYS'; -import DraftReportUtils from '../DraftReportUtils'; - -const draftReportUtils = DraftReportUtils.getInstance(); /** * Immediate indication whether the report has a draft. @@ -11,19 +8,7 @@ const draftReportUtils = DraftReportUtils.getInstance(); * @param draft */ function setDraftStatusForReportID(reportID: string, draft: boolean) { - const draftReportIDs = {...draftReportUtils.getDraftReportIDs()}; - - if (draftReportIDs[reportID] && draft) { - return; - } - - if (draftReportIDs[reportID] && !draft) { - delete draftReportIDs[reportID]; - Onyx.set(ONYXKEYS.DRAFT_REPORT_IDS, draftReportIDs); - } else { - draftReportIDs[reportID] = draft; - Onyx.merge(ONYXKEYS.DRAFT_REPORT_IDS, {[reportID]: draft}); - } + Onyx.merge(ONYXKEYS.DRAFT_REPORT_IDS, {[reportID]: draft}); } export default setDraftStatusForReportID; diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index fffe71a5c931..1fef24a92b11 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -14,9 +14,18 @@ import * as ErrorUtils from '../ErrorUtils'; import * as ReportUtils from '../ReportUtils'; import * as PersonalDetailsUtils from '../PersonalDetailsUtils'; import Log from '../Log'; -import DraftReportUtils from '../DraftReportUtils'; -const draftReportUtils = DraftReportUtils.getInstance(); +let draftReportIDs = {}; +Onyx.connect({ + key: ONYXKEYS.DRAFT_REPORT_IDS, + callback: (val) => { + if (!val) { + return; + } + + draftReportIDs = val; + }, +}); const allPolicies = {}; Onyx.connect({ @@ -33,7 +42,6 @@ Onyx.connect({ const policyReports = ReportUtils.getAllPolicyReports(policyID); const cleanUpMergeQueries = {}; const cleanUpSetQueries = {}; - const draftReportIDs = {...draftReportUtils.getDraftReportIDs()}; _.each(policyReports, ({reportID}) => { cleanUpSetQueries[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] = null; delete draftReportIDs[reportID]; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js index 91471d8fe145..b470528eede5 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions.js @@ -247,8 +247,9 @@ function ComposerWithSuggestions({ if (commentRef.current.length === 0 && newComment.length !== 0) { setDraftStatusForReportID(reportID, true); } + // The draft has been deleted. - else if (newComment.length === 0) { + if (newComment.length === 0) { setDraftStatusForReportID(reportID, false); } diff --git a/tests/unit/DraftReportUtilsTest.js b/tests/unit/DraftReportUtilsTest.js deleted file mode 100644 index ec13b9c69de7..000000000000 --- a/tests/unit/DraftReportUtilsTest.js +++ /dev/null @@ -1,59 +0,0 @@ -import Onyx from 'react-native-onyx'; -import {cleanup} from '@testing-library/react-native'; -import DraftReportUtils from '../../src/libs/DraftReportUtils'; - -const ONYXKEYS = { - DRAFT_REPORT_IDS: 'draftReportIDs', -}; - -const reportID = 1; - -describe('DraftReportUtils', () => { - beforeAll(() => - Onyx.init({ - keys: ONYXKEYS, - }), - ); - - // Clear out Onyx after each test so that each test starts with a clean slate - afterEach(() => { - cleanup(); - Onyx.clear(); - }); - - describe('Singleton', () => { - it('should return the same instance when called multiple times', () => { - // Call getInstance multiple times - const instance1 = DraftReportUtils.getInstance(); - const instance2 = DraftReportUtils.getInstance(); - const instance3 = DraftReportUtils.getInstance(); - - // Ensure that all instances are the same - expect(instance1).toBe(instance2); - expect(instance2).toBe(instance3); - }); - }); - - it('should return an empty object when there are no draft reports', () => { - const draftReportIDs = DraftReportUtils.getInstance().getDraftReportIDs(); - expect(draftReportIDs).toEqual({}); - }); - - it('should return an object of draft report IDs when draft is set through onyx', async () => { - await Onyx.merge(ONYXKEYS.DRAFT_REPORT_IDS, {[reportID]: true}); - const draftReportIDs = DraftReportUtils.getInstance().getDraftReportIDs(); - expect(draftReportIDs).toEqual({[`${reportID}`]: true}); - }); - - it('should return an empty object of draft report IDs when draft is unset through onyx', async () => { - const draftReportUtils = DraftReportUtils.getInstance(); - - await Onyx.merge(ONYXKEYS.DRAFT_REPORT_IDS, {[reportID]: true}); - let draftReportIDs = draftReportUtils.getDraftReportIDs(); - expect(draftReportIDs).toEqual({[`${reportID}`]: true}); - - await Onyx.set(ONYXKEYS.DRAFT_REPORT_IDS, {}); - draftReportIDs = draftReportUtils.getDraftReportIDs(); - expect(draftReportIDs).toEqual({}); - }); -}); diff --git a/tests/unit/SidebarFilterTest.js b/tests/unit/SidebarFilterTest.js index 982393b3e191..bf839b0b36d8 100644 --- a/tests/unit/SidebarFilterTest.js +++ b/tests/unit/SidebarFilterTest.js @@ -110,6 +110,7 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + // Set the draft status for the given reportID [ONYXKEYS.DRAFT_REPORT_IDS]: {[report.reportID]: true}, }), @@ -339,6 +340,7 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, + // Set the draft status for the given reportID [ONYXKEYS.DRAFT_REPORT_IDS]: {[report2.reportID]: boolArr[boolArr.length - 1]}, }), @@ -452,6 +454,7 @@ describe('Sidebar', () => { [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, [`${ONYXKEYS.COLLECTION.REPORT}${draftReport.reportID}`]: draftReport, [`${ONYXKEYS.COLLECTION.REPORT}${pinnedReport.reportID}`]: pinnedReport, + // Set the draft status for the given reportID [ONYXKEYS.DRAFT_REPORT_IDS]: {[draftReport.reportID]: true}, }), @@ -665,6 +668,7 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.POLICY}${policy.policyID}`]: policy, + // Set the draft status for the given reportID [ONYXKEYS.DRAFT_REPORT_IDS]: {[report2.reportID]: boolArr[boolArr.length - 1]}, }), diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index 0503d8f33dff..1b8f231d7b0e 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -267,6 +267,7 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, + // Setting the draft status for second report [ONYXKEYS.DRAFT_REPORT_IDS]: {[report2.reportID]: true}, }), @@ -308,6 +309,7 @@ describe('Sidebar', () => { [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, [`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`]: report, + // Setting the draft status for the report [ONYXKEYS.DRAFT_REPORT_IDS]: {[report.reportID]: true}, }), @@ -410,6 +412,7 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, [`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`]: iouReport, + // Setting the draft status for second report [ONYXKEYS.DRAFT_REPORT_IDS]: {[report2.reportID]: true}, }), @@ -509,6 +512,7 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, + // Setting the draft status for all reports [ONYXKEYS.DRAFT_REPORT_IDS]: { [report1.reportID]: true, From c846ff11670c8b9d10c890f4e0319a73e4f062c8 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 20 Oct 2023 12:18:00 +0500 Subject: [PATCH 046/236] fix: PR feedbacks --- tests/unit/SidebarOrderTest.js | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/SidebarOrderTest.js b/tests/unit/SidebarOrderTest.js index 1b8f231d7b0e..6b89b8a94179 100644 --- a/tests/unit/SidebarOrderTest.js +++ b/tests/unit/SidebarOrderTest.js @@ -173,6 +173,7 @@ describe('Sidebar', () => { [`${ONYXKEYS.COLLECTION.REPORT}${report1.reportID}`]: report1, [`${ONYXKEYS.COLLECTION.REPORT}${report2.reportID}`]: report2, [`${ONYXKEYS.COLLECTION.REPORT}${report3.reportID}`]: report3, + // Setting the draft status for first report [ONYXKEYS.DRAFT_REPORT_IDS]: {[report1.reportID]: true}, }), From ce653159b75e12465402d199712bd2e02e9f4a37 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 20 Oct 2023 14:30:33 +0700 Subject: [PATCH 047/236] fix error when open sign in modal --- .../HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js | 5 +++++ src/libs/Navigation/NavigationRoot.js | 8 -------- src/libs/actions/Report.js | 8 +++++++- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js index 92a313cf1e0a..5b15d7014d1f 100644 --- a/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js +++ b/src/components/HTMLEngineProvider/HTMLRenderers/AnchorRenderer.js @@ -15,6 +15,7 @@ import * as Url from '../../../libs/Url'; import ROUTES from '../../../ROUTES'; import tryResolveUrlFromApiRoot from '../../../libs/tryResolveUrlFromApiRoot'; import useEnvironment from '../../../hooks/useEnvironment'; +import * as Session from '../../../libs/actions/Session'; function AnchorRenderer(props) { const htmlAttribs = props.tnode.attributes; @@ -52,6 +53,10 @@ function AnchorRenderer(props) { // If we are handling a New Expensify link then we will assume this should be opened by the app internally. This ensures that the links are opened internally via react-navigation // instead of in a new tab or with a page refresh (which is the default behavior of an anchor tag) if (internalNewExpensifyPath && hasSameOrigin) { + if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(internalNewExpensifyPath)) { + Session.signOutAndRedirectToSignIn(); + return; + } Navigation.navigate(internalNewExpensifyPath); return; } diff --git a/src/libs/Navigation/NavigationRoot.js b/src/libs/Navigation/NavigationRoot.js index a22b6714a306..c7a3b14e4fb0 100644 --- a/src/libs/Navigation/NavigationRoot.js +++ b/src/libs/Navigation/NavigationRoot.js @@ -12,9 +12,6 @@ import StatusBar from '../StatusBar'; import useCurrentReportID from '../../hooks/useCurrentReportID'; import useWindowDimensions from '../../hooks/useWindowDimensions'; import {SidebarNavigationContext} from '../../pages/home/sidebar/SidebarNavigationContext'; -import * as Session from '../actions/Session'; -import getCurrentUrl from './currentUrl'; -import ROUTES from '../../ROUTES'; // https://reactnavigation.org/docs/themes const navigationTheme = { @@ -136,11 +133,6 @@ function NavigationRoot(props) { // Update the global navigation to show the correct selected menu items. globalNavigation.updateFromNavigationState(state); - - const route = Navigation.getActiveRoute(); - if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route) && !getCurrentUrl().includes(ROUTES.SIGN_IN_MODAL)) { - Session.signOutAndRedirectToSignIn(); - } }; return ( diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index af1b4a0ac1dd..d3e2f9c749d2 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1909,7 +1909,13 @@ function openReportFromDeepLink(url, isAuthenticated) { InteractionManager.runAfterInteractions(() => { Session.waitForUserSignIn().then(() => { if (route === ROUTES.CONCIERGE) { - navigateToConciergeChat(true); + navigateToConciergeChat(); + return; + } + if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) { + Navigation.isNavigationReady().then(() => { + Session.signOutAndRedirectToSignIn(); + }); return; } Navigation.navigate(route, CONST.NAVIGATION.TYPE.PUSH); From ced932a1d2c8af33881b72772bec64293b6a014a Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 20 Oct 2023 14:36:09 +0700 Subject: [PATCH 048/236] fix revert not related changes --- src/libs/actions/Report.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index d3e2f9c749d2..51dcdc49847d 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -1909,7 +1909,7 @@ function openReportFromDeepLink(url, isAuthenticated) { InteractionManager.runAfterInteractions(() => { Session.waitForUserSignIn().then(() => { if (route === ROUTES.CONCIERGE) { - navigateToConciergeChat(); + navigateToConciergeChat(true); return; } if (Session.isAnonymousUser() && !Session.canAccessRouteByAnonymousUser(route)) { From dfddd3300c375d738e491fe71db1f2cb4d6afadb Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 20 Oct 2023 14:40:30 +0700 Subject: [PATCH 049/236] fix remove not related change --- src/libs/ReportUtils.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/ReportUtils.js b/src/libs/ReportUtils.js index 88261a0061ea..011907c2c88b 100644 --- a/src/libs/ReportUtils.js +++ b/src/libs/ReportUtils.js @@ -4065,6 +4065,5 @@ export { getIOUReportActionDisplayMessage, isWaitingForTaskCompleteFromAssignee, isReportDraft, - parseReportRouteParams, shouldUseFullTitleToDisplay, }; From 7ca29f856c41305baebae957112b9e7ce7089d02 Mon Sep 17 00:00:00 2001 From: sarious Date: Fri, 20 Oct 2023 13:07:45 +0400 Subject: [PATCH 050/236] Fix 17866: After pressing the back arrow to "Send/Request money" screen, the keyboard flashes --- src/components/ScreenWrapper/index.js | 6 ++- src/hooks/useInitialWindowDimensions/index.js | 50 +++++++++++++++++++ .../index.native.js | 50 +++++++++++++++++++ src/pages/iou/MoneyRequestSelectorPage.js | 2 + 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useInitialWindowDimensions/index.js create mode 100644 src/hooks/useInitialWindowDimensions/index.native.js diff --git a/src/components/ScreenWrapper/index.js b/src/components/ScreenWrapper/index.js index e2af40589a8a..ecb83c28759e 100644 --- a/src/components/ScreenWrapper/index.js +++ b/src/components/ScreenWrapper/index.js @@ -16,12 +16,14 @@ import toggleTestToolsModal from '../../libs/actions/TestTool'; import CustomDevMenu from '../CustomDevMenu'; import * as Browser from '../../libs/Browser'; import useWindowDimensions from '../../hooks/useWindowDimensions'; +import useInitialDimensions from '../../hooks/useInitialWindowDimensions'; import useKeyboardState from '../../hooks/useKeyboardState'; import useEnvironment from '../../hooks/useEnvironment'; import useNetwork from '../../hooks/useNetwork'; function ScreenWrapper({ shouldEnableMaxHeight, + shouldEnableMinHeight, includePaddingTop, keyboardAvoidingViewBehavior, includeSafeAreaPaddingBottom, @@ -37,12 +39,14 @@ function ScreenWrapper({ testID, }) { const {windowHeight, isSmallScreenWidth} = useWindowDimensions(); + const {initialHeight} = useInitialDimensions(); const keyboardState = useKeyboardState(); const {isDevelopment} = useEnvironment(); const {isOffline} = useNetwork(); const navigation = useNavigation(); const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); const maxHeight = shouldEnableMaxHeight ? windowHeight : undefined; + const minHeight = shouldEnableMinHeight ? initialHeight : undefined; const isKeyboardShown = lodashGet(keyboardState, 'isKeyboardShown', false); const panResponder = useRef( @@ -125,7 +129,7 @@ function ScreenWrapper({ {...keyboardDissmissPanResponder.panHandlers} > diff --git a/src/hooks/useInitialWindowDimensions/index.js b/src/hooks/useInitialWindowDimensions/index.js new file mode 100644 index 000000000000..ae498f45377f --- /dev/null +++ b/src/hooks/useInitialWindowDimensions/index.js @@ -0,0 +1,50 @@ +// eslint-disable-next-line no-restricted-imports +import {useState, useEffect} from 'react'; +import {Dimensions} from 'react-native'; + +export default function () { + const [dimensions, setDimensions] = useState(() => { + const window = Dimensions.get('window'); + const screen = Dimensions.get('screen'); + + return { + screenHeight: screen.height, + screenWidth: screen.width, + initialHeight: window.height, + initialWidth: window.width, + }; + }); + + useEffect(() => { + const onDimensionChange = (newDimensions) => { + const {window, screen} = newDimensions; + + setDimensions((oldState) => { + if (screen.width !== oldState.screenWidth || screen.height !== oldState.screenHeight || window.height > oldState.initialHeight) { + return { + initialHeight: window.height, + initialWidth: window.width, + screenHeight: screen.height, + screenWidth: screen.width, + }; + } + + return oldState; + }); + }; + + const dimensionsEventListener = Dimensions.addEventListener('change', onDimensionChange); + + return () => { + if (!dimensionsEventListener) { + return; + } + dimensionsEventListener.remove(); + }; + }, []); + + return { + initialWidth: dimensions.initialWidth, + initialHeight: dimensions.initialHeight, + }; +} diff --git a/src/hooks/useInitialWindowDimensions/index.native.js b/src/hooks/useInitialWindowDimensions/index.native.js new file mode 100644 index 000000000000..ae498f45377f --- /dev/null +++ b/src/hooks/useInitialWindowDimensions/index.native.js @@ -0,0 +1,50 @@ +// eslint-disable-next-line no-restricted-imports +import {useState, useEffect} from 'react'; +import {Dimensions} from 'react-native'; + +export default function () { + const [dimensions, setDimensions] = useState(() => { + const window = Dimensions.get('window'); + const screen = Dimensions.get('screen'); + + return { + screenHeight: screen.height, + screenWidth: screen.width, + initialHeight: window.height, + initialWidth: window.width, + }; + }); + + useEffect(() => { + const onDimensionChange = (newDimensions) => { + const {window, screen} = newDimensions; + + setDimensions((oldState) => { + if (screen.width !== oldState.screenWidth || screen.height !== oldState.screenHeight || window.height > oldState.initialHeight) { + return { + initialHeight: window.height, + initialWidth: window.width, + screenHeight: screen.height, + screenWidth: screen.width, + }; + } + + return oldState; + }); + }; + + const dimensionsEventListener = Dimensions.addEventListener('change', onDimensionChange); + + return () => { + if (!dimensionsEventListener) { + return; + } + dimensionsEventListener.remove(); + }; + }, []); + + return { + initialWidth: dimensions.initialWidth, + initialHeight: dimensions.initialHeight, + }; +} diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index 0786faa3841b..bcf75bd64056 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -22,6 +22,7 @@ import NewRequestAmountPage from './steps/NewRequestAmountPage'; import reportPropTypes from '../reportPropTypes'; import * as ReportUtils from '../../libs/ReportUtils'; import usePrevious from '../../hooks/usePrevious'; +import * as DeviceCapabilities from '../../libs/DeviceCapabilities'; const propTypes = { /** React Navigation route */ @@ -85,6 +86,7 @@ function MoneyRequestSelectorPage(props) { From 722043e9115259eb26153b817a114ad84ee5d165 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Fri, 20 Oct 2023 14:38:38 +0200 Subject: [PATCH 051/236] Resolve problem with styles --- src/components/OpacityView.tsx | 36 +++++++++++++--------------------- 1 file changed, 14 insertions(+), 22 deletions(-) diff --git a/src/components/OpacityView.tsx b/src/components/OpacityView.tsx index daef93cdc09b..74ed2a0c92ba 100644 --- a/src/components/OpacityView.tsx +++ b/src/components/OpacityView.tsx @@ -1,69 +1,61 @@ import React from 'react'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; -import PropTypes from 'prop-types'; +import {ViewStyle} from 'react-native'; import variables from '../styles/variables'; import * as StyleUtils from '../styles/StyleUtils'; import shouldRenderOffscreen from '../libs/shouldRenderOffscreen'; -const propTypes = { +type OpacityViewProps = { /** * Should we dim the view */ - shouldDim: PropTypes.bool.isRequired, + shouldDim: boolean; /** * Content to render */ - children: PropTypes.node.isRequired, + children: React.ReactNode; /** * Array of style objects * @default [] */ // eslint-disable-next-line react/forbid-prop-types - style: PropTypes.oneOfType([PropTypes.object, PropTypes.array]), + style: ViewStyle | ViewStyle[]; /** * The value to use for the opacity when the view is dimmed * @default 0.5 */ - dimmingValue: PropTypes.number, + dimmingValue?: number; /** Whether the view needs to be rendered offscreen (for Android only) */ - needsOffscreenAlphaCompositing: PropTypes.bool, + needsOffscreenAlphaCompositing?: boolean; }; -const defaultProps = { - style: [], - dimmingValue: variables.hoverDimValue, - needsOffscreenAlphaCompositing: false, -}; - -function OpacityView(props) { +function OpacityView({shouldDim, children, style = [], dimmingValue = variables.hoverDimValue, needsOffscreenAlphaCompositing = false}: OpacityViewProps) { const opacity = useSharedValue(1); const opacityStyle = useAnimatedStyle(() => ({ opacity: opacity.value, })); React.useEffect(() => { - if (props.shouldDim) { - opacity.value = withTiming(props.dimmingValue, {duration: 50}); + if (shouldDim) { + opacity.value = withTiming(dimmingValue, {duration: 50}); } else { opacity.value = withTiming(1, {duration: 50}); } - }, [props.shouldDim, props.dimmingValue, opacity]); + }, [shouldDim, dimmingValue, opacity]); return ( - {props.children} + {children} ); } OpacityView.displayName = 'OpacityView'; -OpacityView.propTypes = propTypes; -OpacityView.defaultProps = defaultProps; export default OpacityView; From bd92d7e6503bdf413bedfc8e216d74a814e92b68 Mon Sep 17 00:00:00 2001 From: Puneet Lath Date: Fri, 20 Oct 2023 12:12:55 -0400 Subject: [PATCH 052/236] Allow assigning task in workspace chat where expensify is owner --- .../report/ReportActionCompose/AttachmentPickerWithMenuItems.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js index 6522bedc825a..d3be3f47bf2f 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js @@ -152,7 +152,7 @@ function AttachmentPickerWithMenuItems({ */ const taskOption = useMemo(() => { // We only prevent the task option from showing if it's a DM and the other user is an Expensify default email - if (!Permissions.canUseTasks(betas) || ReportUtils.isExpensifyOnlyParticipantInReport(report)) { + if (!Permissions.canUseTasks(betas) || (!ReportUtils.isPolicyExpenseChat(report) && ReportUtils.isExpensifyOnlyParticipantInReport(report))) { return []; } From 4117624cbce608726c21a203278098a3fb370f88 Mon Sep 17 00:00:00 2001 From: Sofie de Vreese <40040992+SofiedeVreese@users.noreply.github.com> Date: Sun, 22 Oct 2023 23:11:08 +0000 Subject: [PATCH 053/236] Update Auto-Reconciliation.md Updating image placeholders with actual image URLs --- .../expensify-card/Auto-Reconciliation.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md index 059877a18075..824f01f688b3 100644 --- a/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md +++ b/docs/articles/expensify-classic/expensify-card/Auto-Reconciliation.md @@ -35,6 +35,8 @@ To set up your auto-reconciliation account with the Expensify Card, follow these 5. Head to the "Settings" tab. 6. Select the account in your accounting solution that you want to use for reconciliation. Make sure this account matches the settlement business bank account. +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/Auto-Reconciliaton_Image1.png){:width="100%"} + That's it! You've successfully set up your auto-reconciliation account. ## How does Auto-Reconciliation work @@ -44,13 +46,11 @@ Once Auto-Reconciliation is enabled, there are a few things that happen. Let’s **What happens**: When an Expensify Card is used to make purchases, the amount spent is automatically deducted from your company’s 'Settlement Account' (your business checking account). This deduction happens on a daily or monthly basis, depending on your chosen settlement frequency. Don't worry; this settlement account is pre-defined when you apply for the Expensify Card, and you can't accidentally change it. **Accounting treatment**: After your card balance is settled each day, we update your accounting system with a journal entry. This entry credits your bank account (referred to as the GL account) and debits the Expensify Card Clearing Account. To ensure accuracy, please make sure that the 'bank account' in your Expensify Card settings matches your real-life settlement account. You can easily verify this by navigating to **Settings > Account > Payments**, where you'll see 'Settlement Account' next to your business bank account. To keep track of settlement figures by date, use the Company Card Reconciliation Dashboard's Settlements tab: -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/image-name.png){:width="100%"} +![Insert alt text for accessibility here](https://help.expensify.com/assets/images/Auto-Reconciliation_Image2.png){:width="100%"} ### Submitting, Approving, and Exporting Expenses **What happens**: Users submit their expenses on a report, which might occur after some time has passed since the initial purchase. Once the report is approved, it's then exported to your accounting software. -**Accounting treatment**: When the report is exported, we create a journal entry in your accounting system. This entry credits the Clearing Account and debits the Liability Account for the purchase amount. The Liability Account functions as a bank account in your ledger, specifically for Expensify Card expenses: - -![Insert alt text for accessibility here](https://help.expensify.com/assets/images/image-name.png){:width="100%"} +**Accounting treatment**: When the report is exported, we create a journal entry in your accounting system. This entry credits the Clearing Account and debits the Liability Account for the purchase amount. The Liability Account functions as a bank account in your ledger, specifically for Expensify Card expenses. # Deep Dive ## QuickBooks Online From 512c4d5b97d6689074000c42a80794c0b25611a8 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 23 Oct 2023 19:49:21 +0200 Subject: [PATCH 054/236] Restore displayName --- src/components/FullscreenLoadingIndicator.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 92e59d0e2d1c..4cd8002cf2c7 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -19,4 +19,6 @@ function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProp ); } +FullScreenLoadingIndicator.displayName = 'FullScreenLoadingIndicator'; + export default FullScreenLoadingIndicator; From 7044bbb83da3685e1eb19a4fc4b2ba4c9f2841de Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 23 Oct 2023 19:54:07 +0200 Subject: [PATCH 055/236] Use ViewStyle --- src/components/FullscreenLoadingIndicator.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 4cd8002cf2c7..43a7c4180284 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -1,10 +1,10 @@ import React from 'react'; -import {ActivityIndicator, StyleSheet, View} from 'react-native'; +import {ActivityIndicator, StyleProp, StyleSheet, View, ViewStyle} from 'react-native'; import styles from '../styles/styles'; import themeColors from '../styles/themes/default'; type FullScreenLoadingIndicatorProps = { - style: Record | Array>; + style: StyleProp; }; function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProps) { From 1088ada601544648d36bbdbbce33b409d8d1a6d2 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Mon, 23 Oct 2023 21:45:59 +0200 Subject: [PATCH 056/236] Drop not needed casting --- src/components/FullscreenLoadingIndicator.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/FullscreenLoadingIndicator.tsx b/src/components/FullscreenLoadingIndicator.tsx index 43a7c4180284..dc2a08e3ac5e 100644 --- a/src/components/FullscreenLoadingIndicator.tsx +++ b/src/components/FullscreenLoadingIndicator.tsx @@ -8,9 +8,8 @@ type FullScreenLoadingIndicatorProps = { }; function FullScreenLoadingIndicator({style = []}: FullScreenLoadingIndicatorProps) { - const additionalStyles = Array.isArray(style) ? style : [style]; return ( - + Date: Tue, 24 Oct 2023 10:55:59 +0700 Subject: [PATCH 057/236] fix: regression 29888 --- src/components/ReportActionItem/MoneyRequestPreview.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestPreview.js b/src/components/ReportActionItem/MoneyRequestPreview.js index 43500c731728..06e374306ae8 100644 --- a/src/components/ReportActionItem/MoneyRequestPreview.js +++ b/src/components/ReportActionItem/MoneyRequestPreview.js @@ -263,7 +263,9 @@ function MoneyRequestPreview(props) { ) : ( - {getPreviewHeaderText() + (isSettled ? ` • ${getSettledMessage()}` : '')} + + {getPreviewHeaderText() + (isSettled ? ` • ${getSettledMessage()}` : '')} + {hasFieldErrors && ( Date: Tue, 24 Oct 2023 04:32:10 -0700 Subject: [PATCH 058/236] Create Fringe-Benefits.md --- .../Fringe-Benefits.md | 43 +++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md diff --git a/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md b/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md new file mode 100644 index 000000000000..d76b6e413840 --- /dev/null +++ b/docs/articles/expensify-classic/insights-and-custom-reporting/Fringe-Benefits.md @@ -0,0 +1,43 @@ +--- +title: Fringe Benefits +description: How to track your Fringe Benefits +--- +# Overview +If you’re looking to track and report expense data to calculate Fringe Benefits Tax (FBT), you can use Expensify’s special workflow that allows you to capture extra information and use a template to export to a spreadsheet. + +# How to set up Fringe Benefit Tax + +## Add Attendee Count Tags +First, you’ll need to add these exact two tags to your Workspace: +1) Number of Internal Attendees +2) Number of External Attendees + +These tags must be named exactly as written above, ensuring there are no extra spaces at the beginning or at the end. You’ll need to set the tags to be numbers 00 - 10 or whatever number you wish to go up to (up to the maximum number of attendees you would expect at any one time), one tag per number i.e. “01”, “02”, “03” etc. These tags can be added in addition to those that are pulled in from your accounting solution. Follow these [instructions](https://help.expensify.com/articles/expensify-classic/workspace-and-domain-settings/Tags#gsc.tab=0) to add tags. + +## Add Payroll Code +Go to **Settings > Workspaces > Group > _Workspace Name_ > Categories** and within the categories you wish to track FBT against, select **Edit Category** and add the code “TAG”: + +## Enable Workflow +Once you’ve added the two tags (Internal Attendees and External Attendees) and added the payroll code “TAG” to FBT categories, you can now contact Expensify at concierge@expensify.com and send through a request to enable the FBT workflow. Please send through the following request: +>“Can you please add the custom workflow/DEW named FRINGE_BENEFIT_TAX to my company workspace named ?” +Once the FBT workflow is enabled, it will require anything with the code “TAG” to include the two attendee count tags in order to be submitted. + + +# For Users +Once these steps are completed, users who create expenses coded with any category that has the payroll code “TAG” (e.g. Entertainment Expenses) but don’t add the internal and external attendee counts, will not be able to submit their expenses. +# For Admins +You are now able to create and run a report, which shows all expenses under these categories and also shows the number of internal and external attendees. Because we don’t presume to know all of the data points you wish to capture, you’ll need to create a Custom CSV export. +Here’s an example of the Excel formulas to use to report on attendees: +- `{expense:tag:ntag-1}` outputs the first tag the user chooses. +- `{expense:tag:ntag-3}` outputs the third tag the user chooses. + +Your expenses may have multiple levels of coding, i.e.: +- GL Code (Category) +- Department (Tag 1) +- Location (Tag 2) +- Number of Internal Attendees (Tag 3) +- Number of External Attendees (Tag 4) + +In the above case, you’ll want to use `{expense:tag:ntag-3}` and `{expense:tag:ntag-4}` as formulas to report on the number of internal and external attendees. + +Our article on [Custom Templates](https://help.expensify.com/articles/expensify-classic/insights-and-custom-reporting/Custom-Templates#gsc.tab=0) shows you how to create a custom CSV. From d8772efae9355f1bc6a89adebc3d74446ef8d530 Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 24 Oct 2023 14:24:01 +0200 Subject: [PATCH 059/236] Fix styles --- src/components/OpacityView.tsx | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/components/OpacityView.tsx b/src/components/OpacityView.tsx index 74ed2a0c92ba..87e2976c4d0b 100644 --- a/src/components/OpacityView.tsx +++ b/src/components/OpacityView.tsx @@ -1,9 +1,9 @@ import React from 'react'; -import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; -import {ViewStyle} from 'react-native'; +import Animated, {AnimatedStyle, useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; +import {StyleProp, ViewStyle} from 'react-native'; import variables from '../styles/variables'; -import * as StyleUtils from '../styles/StyleUtils'; import shouldRenderOffscreen from '../libs/shouldRenderOffscreen'; +import styles from '../styles/styles'; type OpacityViewProps = { /** @@ -20,8 +20,7 @@ type OpacityViewProps = { * Array of style objects * @default [] */ - // eslint-disable-next-line react/forbid-prop-types - style: ViewStyle | ViewStyle[]; + style: StyleProp>>; /** * The value to use for the opacity when the view is dimmed @@ -49,7 +48,7 @@ function OpacityView({shouldDim, children, style = [], dimmingValue = variables. return ( {children} From f862334f9dc17289d38aecac23510ea33aa6382e Mon Sep 17 00:00:00 2001 From: Bartosz Grajdek Date: Tue, 24 Oct 2023 15:22:47 +0200 Subject: [PATCH 060/236] Lint fixes --- src/components/OpacityView.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/OpacityView.tsx b/src/components/OpacityView.tsx index 87e2976c4d0b..581b083bff86 100644 --- a/src/components/OpacityView.tsx +++ b/src/components/OpacityView.tsx @@ -3,7 +3,6 @@ import Animated, {AnimatedStyle, useAnimatedStyle, useSharedValue, withTiming} f import {StyleProp, ViewStyle} from 'react-native'; import variables from '../styles/variables'; import shouldRenderOffscreen from '../libs/shouldRenderOffscreen'; -import styles from '../styles/styles'; type OpacityViewProps = { /** From 87f60149c8b3540a06d9b51f67e6f77110b42b2f Mon Sep 17 00:00:00 2001 From: Adam Grzybowski Date: Tue, 24 Oct 2023 18:25:05 +0200 Subject: [PATCH 061/236] limit report routes in customStackNavigator --- .../createCustomStackNavigator/index.js | 36 ++++++++++- .../index.native.js | 60 +++++++++++++++++++ 2 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js index 58be3d2af3da..bfc56cf171c0 100644 --- a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.js @@ -1,9 +1,10 @@ -import React, {useRef} from 'react'; +import React, {useRef, useMemo} from 'react'; import PropTypes from 'prop-types'; import {useNavigationBuilder, createNavigatorFactory} from '@react-navigation/native'; import {StackView} from '@react-navigation/stack'; import CustomRouter from './CustomRouter'; import useWindowDimensions from '../../../../hooks/useWindowDimensions'; +import NAVIGATORS from '../../../../NAVIGATORS'; const propTypes = { /* Determines if the navigator should render the StackView (narrow) or ThreePaneView (wide) */ @@ -25,6 +26,24 @@ const defaultProps = { screenOptions: undefined, }; +function splitRoutes(routes) { + const reportRoutes = []; + const rhpRoutes = []; + const otherRoutes = []; + + routes.forEach((route) => { + if (route.name === NAVIGATORS.CENTRAL_PANE_NAVIGATOR) { + reportRoutes.push(route); + } else if (route.name === NAVIGATORS.RIGHT_MODAL_NAVIGATOR) { + rhpRoutes.push(route); + } else { + otherRoutes.push(route); + } + }); + + return {reportRoutes, rhpRoutes, otherRoutes}; +} + function ResponsiveStackNavigator(props) { const {isSmallScreenWidth} = useWindowDimensions(); @@ -40,12 +59,25 @@ function ResponsiveStackNavigator(props) { getIsSmallScreenWidth: () => isSmallScreenWidthRef.current, }); + const stateToRender = useMemo(() => { + const {reportRoutes, rhpRoutes, otherRoutes} = splitRoutes(state.routes); + + // Remove all report routes except the last 3. This will improve performance. + const limitedReportRoutes = reportRoutes.slice(-3); + + return { + ...state, + index: otherRoutes.length + limitedReportRoutes.length + rhpRoutes.length - 1, + routes: [...otherRoutes, ...limitedReportRoutes, ...rhpRoutes], + }; + }, [state]); + return ( diff --git a/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js new file mode 100644 index 000000000000..58be3d2af3da --- /dev/null +++ b/src/libs/Navigation/AppNavigator/createCustomStackNavigator/index.native.js @@ -0,0 +1,60 @@ +import React, {useRef} from 'react'; +import PropTypes from 'prop-types'; +import {useNavigationBuilder, createNavigatorFactory} from '@react-navigation/native'; +import {StackView} from '@react-navigation/stack'; +import CustomRouter from './CustomRouter'; +import useWindowDimensions from '../../../../hooks/useWindowDimensions'; + +const propTypes = { + /* Determines if the navigator should render the StackView (narrow) or ThreePaneView (wide) */ + isSmallScreenWidth: PropTypes.bool.isRequired, + + /* Children for the useNavigationBuilder hook */ + children: PropTypes.oneOfType([PropTypes.func, PropTypes.node]).isRequired, + + /* initialRouteName for this navigator */ + initialRouteName: PropTypes.oneOf([PropTypes.string, PropTypes.undefined]), + + /* Screen options defined for this navigator */ + // eslint-disable-next-line react/forbid-prop-types + screenOptions: PropTypes.object, +}; + +const defaultProps = { + initialRouteName: undefined, + screenOptions: undefined, +}; + +function ResponsiveStackNavigator(props) { + const {isSmallScreenWidth} = useWindowDimensions(); + + const isSmallScreenWidthRef = useRef(isSmallScreenWidth); + + isSmallScreenWidthRef.current = isSmallScreenWidth; + + const {navigation, state, descriptors, NavigationContent} = useNavigationBuilder(CustomRouter, { + children: props.children, + screenOptions: props.screenOptions, + initialRouteName: props.initialRouteName, + // Options for useNavigationBuilder won't update on prop change, so we need to pass a getter for the router to have the current state of isSmallScreenWidth. + getIsSmallScreenWidth: () => isSmallScreenWidthRef.current, + }); + + return ( + + + + ); +} + +ResponsiveStackNavigator.defaultProps = defaultProps; +ResponsiveStackNavigator.propTypes = propTypes; +ResponsiveStackNavigator.displayName = 'ResponsiveStackNavigator'; + +export default createNavigatorFactory(ResponsiveStackNavigator); From f7d19008dfa0ca6f68986396776ed57a533c5fba Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Oct 2023 19:15:19 +0200 Subject: [PATCH 062/236] update options on transition end --- src/pages/SearchPage.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pages/SearchPage.js b/src/pages/SearchPage.js index 272fb30de858..f0796ffb7000 100755 --- a/src/pages/SearchPage.js +++ b/src/pages/SearchPage.js @@ -65,15 +65,13 @@ class SearchPage extends Component { this.searchRendered = this.searchRendered.bind(this); this.selectReport = this.selectReport.bind(this); this.onChangeText = this.onChangeText.bind(this); + this.updateOptions = this.updateOptions.bind(this); this.debouncedUpdateOptions = _.debounce(this.updateOptions.bind(this), 75); - - const {recentReports, personalDetails, userToInvite} = OptionsListUtils.getSearchOptions(props.reports, props.personalDetails, '', props.betas); - this.state = { searchValue: '', - recentReports, - personalDetails, - userToInvite, + recentReports: {}, + personalDetails: {}, + userToInvite: {}, }; } @@ -186,6 +184,7 @@ class SearchPage extends Component { {({didScreenTransitionEnd, safeAreaPaddingBottomStyle}) => ( <> From 0e4411cff508bdad6b54f855e843156699d98981 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Tue, 24 Oct 2023 19:15:20 +0200 Subject: [PATCH 063/236] remove timeout on search focus --- src/components/OptionsSelector/BaseOptionsSelector.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 0125fc8e178e..5e37db057db5 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -75,9 +75,7 @@ class BaseOptionsSelector extends Component { this.subscribeToKeyboardShortcut(); if (this.props.isFocused && this.props.autoFocus && this.textInput) { - this.focusTimeout = setTimeout(() => { - this.textInput.focus(); - }, CONST.ANIMATED_TRANSITION); + this.textInput.focus(); } this.scrollToIndex(this.props.selectedOptions.length ? 0 : this.state.focusedIndex, false); @@ -139,10 +137,6 @@ class BaseOptionsSelector extends Component { } componentWillUnmount() { - if (this.focusTimeout) { - clearTimeout(this.focusTimeout); - } - this.unSubscribeFromKeyboardShortcut(); } From 62e087b9eca0d8608f01b3a0cecb1aea8d06ff4d Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 25 Oct 2023 05:54:20 +0700 Subject: [PATCH 064/236] Always show confirmation button --- .../MoneyRequestParticipantsSelector.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index 7e88ebe7db48..bc6e5f02ff5e 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -248,7 +248,6 @@ function MoneyRequestParticipantsSelector({ // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); - const shouldShowConfirmButton = !(participants.length > 1 && hasPolicyExpenseChatParticipant); const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; return ( @@ -266,7 +265,6 @@ function MoneyRequestParticipantsSelector({ ref={forwardedRef} headerMessage={headerMessage} boldStyle - shouldShowConfirmButton={shouldShowConfirmButton && isAllowedToSplit} confirmButtonText={translate('iou.addToSplit')} onConfirmSelection={navigateToSplit} textInputLabel={translate('optionsSelector.nameEmailOrPhoneNumber')} @@ -274,6 +272,7 @@ function MoneyRequestParticipantsSelector({ shouldShowOptions={isOptionsDataReady} shouldPreventDefaultFocusOnSelectRow={!Browser.isMobile()} shouldDelayFocus + footerContent /> ); From 40b36e0a15029bab343047d0c5e0a0752ead876e Mon Sep 17 00:00:00 2001 From: VH Date: Wed, 25 Oct 2023 06:23:18 +0700 Subject: [PATCH 065/236] Display warning message when split bill with multiple participants including a workspace --- .../MoneyRequestParticipantsSelector.js | 25 +++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index bc6e5f02ff5e..62548fb2854c 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -16,6 +16,8 @@ import CONST from '../../../../CONST'; import personalDetailsPropType from '../../../personalDetailsPropType'; import reportPropTypes from '../../../reportPropTypes'; import refPropTypes from '../../../../components/refPropTypes'; +import Button from '../../../../components/Button'; +import FormHelpMessage from '../../../../components/FormHelpMessage'; const propTypes = { /** Beta features list */ @@ -248,8 +250,27 @@ function MoneyRequestParticipantsSelector({ // This is getting properly fixed in https://github.com/Expensify/App/issues/27508, but as a stop-gap to prevent // the app from crashing on native when you try to do this, we'll going to hide the button if you have a workspace and other participants const hasPolicyExpenseChatParticipant = _.some(participants, (participant) => participant.isPolicyExpenseChat); + const shouldShowErrorMessage = participants.length > 1 && hasPolicyExpenseChatParticipant; const isAllowedToSplit = !isDistanceRequest && iouType !== CONST.IOU.TYPE.SEND; + const footerContent = + {shouldShowErrorMessage && ( + + )} +