From 4810696d937d92af414c59fba2160537b801fde0 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 21 Nov 2023 23:58:29 +0700 Subject: [PATCH 001/124] Update all selectors to use new format for selected participant --- src/libs/OptionsListUtils.js | 4 +++- src/pages/RoomInvitePage.js | 19 +++++++++++-------- src/pages/workspace/WorkspaceInvitePage.js | 18 ++++++++++-------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index b97ae6daed11..f1e97151a062 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -1595,15 +1595,17 @@ function formatMemberForList(member, config = {}) { * @param {Array} betas * @param {String} searchValue * @param {Array} excludeLogins + * @param {Boolean} includeSelectedOptions * @returns {Object} */ -function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', excludeLogins = []) { +function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', excludeLogins = [], includeSelectedOptions = false) { return getOptions([], personalDetails, { betas, searchInputValue: searchValue.trim(), includePersonalDetails: true, excludeLogins, sortPersonalDetailsByAlphaAsc: true, + includeSelectedOptions, }); } diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index a1f7d22c3dc3..cacf603f8836 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -84,7 +84,7 @@ function RoomInvitePage(props) { }, []); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails information const detailsMap = {}; @@ -104,13 +104,16 @@ function RoomInvitePage(props) { const sections = []; let indexOffset = 0; - sections.push({ - title: undefined, - data: selectedOptions, - shouldShow: true, - indexOffset, - }); - indexOffset += selectedOptions.length; + // Only show the selected participants if the search is empty + if (searchTerm === '') { + sections.push({ + title: undefined, + data: selectedOptions, + shouldShow: true, + indexOffset, + }); + indexOffset += selectedOptions.length; + } // Filtering out selected users from the search results const selectedLogins = _.map(selectedOptions, ({login}) => login); diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 4bef69c82414..eeffb35b4d23 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -90,7 +90,7 @@ function WorkspaceInvitePage(props) { const newPersonalDetailsDict = {}; const newSelectedOptionsDict = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap = {}; @@ -130,13 +130,15 @@ function WorkspaceInvitePage(props) { const sections = []; let indexOffset = 0; - sections.push({ - title: undefined, - data: selectedOptions, - shouldShow: true, - indexOffset, - }); - indexOffset += selectedOptions.length; + if (searchTerm === '') { + sections.push({ + title: undefined, + data: selectedOptions, + shouldShow: true, + indexOffset, + }); + indexOffset += selectedOptions.length; + } // Filtering out selected users from the search results const selectedLogins = _.map(selectedOptions, ({login}) => login); From 38c2d4e35e44c0fd529750d4d41207160caa4277 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 1 Dec 2023 12:48:51 +0100 Subject: [PATCH 002/124] Use PersonalDetailsUtils.getDisplayNameOrDefault to escape merged account prefix --- src/libs/OptionsListUtils.js | 17 +++++++++++------ src/libs/PersonalDetailsUtils.js | 3 ++- src/libs/ReportUtils.ts | 5 +++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e3b6ec77380e..e80f8e7dd1ba 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -200,7 +200,7 @@ function isPersonalDetailsReady(personalDetails) { function getParticipantsOption(participant, personalDetails) { const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; const login = detail.login || participant.login; - const displayName = detail.displayName || LocalePhoneNumber.formatPhoneNumber(login); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, 'displayName', LocalePhoneNumber.formatPhoneNumber(login)); return { keyForList: String(detail.accountID), login, @@ -245,7 +245,8 @@ function getParticipantNames(personalDetailList) { participantNames.add(participant.lastName.toLowerCase()); } if (participant.displayName) { - participantNames.add(participant.displayName.toLowerCase()); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(participant, 'displayName'); + participantNames.add(displayName.toLowerCase()); } }); return participantNames; @@ -298,7 +299,11 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic // The regex below is used to remove dots only from the local part of the user email (local-part@domain) // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) // More info https://github.com/Expensify/App/issues/8007 - searchTerms = searchTerms.concat([personalDetail.displayName, personalDetail.login, personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '')]); + searchTerms = searchTerms.concat([ + PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), + personalDetail.login, + personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '') + ]); } } } @@ -507,7 +512,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { const lastMessageTextFromReport = getLastMessageTextForReport(report); const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; - let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; + let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName')}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; if (result.isArchivedRoom) { @@ -1429,8 +1434,8 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail, amountText) { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login); return { - text: personalDetail.displayName || formattedLogin, - alternateText: formattedLogin || personalDetail.displayName, + text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName', formattedLogin), + alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), icons: [ { source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 560480dcec9d..30b710e9d15c 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -1,6 +1,7 @@ import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import _ from 'underscore'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -23,7 +24,7 @@ Onyx.connect({ * @returns {String} */ function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue = '') { - const displayName = lodashGet(passedPersonalDetails, pathToDisplayName); + const displayName = lodashGet(passedPersonalDetails, pathToDisplayName, '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); return displayName || defaultValue || Localize.translateLocal('common.hidden'); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f6c3090143f4..b914658fe448 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -31,6 +31,7 @@ import linkingConfig from './Navigation/linkingConfig'; import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; import Permissions from './Permissions'; +import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import {LastVisibleMessage} from './ReportActionsUtils'; @@ -1270,7 +1271,7 @@ function getIcons( const parentReportAction = ReportActionsUtils.getParentReportAction(report); const actorAccountID = parentReportAction.actorAccountID; - const actorDisplayName = allPersonalDetails?.[actorAccountID ?? -1]?.displayName ?? ''; + const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails, [actorAccountID ?? -1, 'displayName']); const actorIcon = { id: actorAccountID, source: UserUtils.getAvatar(personalDetails?.[actorAccountID ?? -1]?.avatar ?? '', actorAccountID ?? -1), @@ -1394,7 +1395,7 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return formattedLogin; } - const longName = personalDetails.displayName ? personalDetails.displayName : formattedLogin; + const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, 'displayName', formattedLogin); const shortName = personalDetails.firstName ? personalDetails.firstName : longName; From cd7f52b1a39a6b60613dac842d0c4fd6d3092667 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Tue, 5 Dec 2023 21:10:45 +0530 Subject: [PATCH 003/124] fix reposition of popover when window resize --- src/CONST.ts | 1 + .../settings/Wallet/PaymentMethodList.js | 5 +++-- .../settings/Wallet/WalletPage/WalletPage.js | 21 ++++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 3d69c83c5c22..b461c702c53f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -689,6 +689,7 @@ const CONST = { TRIE_INITIALIZATION: 'trie_initialization', COMMENT_LENGTH_DEBOUNCE_TIME: 500, SEARCH_FOR_REPORTS_DEBOUNCE_TIME: 300, + RESIZE_DEBOUNCE_TIME: 100, }, PRIORITY_MODE: { GSD: 'gsd', diff --git a/src/pages/settings/Wallet/PaymentMethodList.js b/src/pages/settings/Wallet/PaymentMethodList.js index 5af4129aefbc..5ac623008fd4 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.js +++ b/src/pages/settings/Wallet/PaymentMethodList.js @@ -289,10 +289,11 @@ function PaymentMethodList({ title={translate('walletPage.addBankAccount')} icon={Expensicons.Plus} wrapperStyle={styles.paymentMethod} + ref={buttonRef} /> ), - [onPress, styles.paymentMethod, translate], + [onPress, styles.paymentMethod, translate, buttonRef], ); /** @@ -346,10 +347,10 @@ function PaymentMethodList({ keyExtractor={keyExtractor} ListEmptyComponent={shouldShowEmptyListMessage ? renderListEmptyComponent : null} ListHeaderComponent={listHeaderComponent} - ListFooterComponent={shouldShowAddBankAccount ? renderListFooterComponent : null} onContentSizeChange={onListContentSizeChange} scrollEnabled={shouldEnableScroll} /> + {shouldShowAddBankAccount && renderListFooterComponent()} {shouldShowAddPaymentMethodButton && ( diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index 6f452eed3629..9dc6c878a1bf 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, InteractionManager, ScrollView, View} from 'react-native'; +import {ActivityIndicator, Dimensions, InteractionManager, ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; @@ -283,8 +283,23 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod if (!shouldListenForResize) { return; } - setMenuPosition(); - }, [shouldListenForResize, setMenuPosition]); + const popoverPositionListener = Dimensions.addEventListener('change', () => { + if (!shouldShowAddPaymentMenu && !shouldShowDefaultDeleteMenu) { + return; + } + if (shouldShowAddPaymentMenu) { + _.debounce(setMenuPosition, CONST.TIMING.RESIZE_DEBOUNCE_TIME)(); + return; + } + setMenuPosition(); + }); + return () => { + if (!popoverPositionListener) { + return; + } + popoverPositionListener.remove(); + }; + }, [shouldShowAddPaymentMenu, shouldShowDefaultDeleteMenu, setMenuPosition, shouldListenForResize]); useEffect(() => { if (!shouldShowDefaultDeleteMenu) { From cb7ba8208be15db5589ec7cddd53fe77a367ea48 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:36:38 +0100 Subject: [PATCH 004/124] migrate MoneyRequestHeaderStatusBar to typescript --- ...rStatusBar.js => MoneyRequestHeaderStatusBar.tsx} | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) rename src/components/{MoneyRequestHeaderStatusBar.js => MoneyRequestHeaderStatusBar.tsx} (80%) diff --git a/src/components/MoneyRequestHeaderStatusBar.js b/src/components/MoneyRequestHeaderStatusBar.tsx similarity index 80% rename from src/components/MoneyRequestHeaderStatusBar.js rename to src/components/MoneyRequestHeaderStatusBar.tsx index fc91678cf6fb..09ff6560e14d 100644 --- a/src/components/MoneyRequestHeaderStatusBar.js +++ b/src/components/MoneyRequestHeaderStatusBar.tsx @@ -1,21 +1,20 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import Text from './Text'; -const propTypes = { +type MoneyRequestHeaderStatusBarProps = { /** Title displayed in badge */ - title: PropTypes.string.isRequired, + title: string; /** Banner Description */ - description: PropTypes.string.isRequired, + description: string; /** Whether we show the border bottom */ - shouldShowBorderBottom: PropTypes.bool.isRequired, + shouldShowBorderBottom: boolean; }; -function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom}) { +function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom}: MoneyRequestHeaderStatusBarProps) { const styles = useThemeStyles(); const borderBottomStyle = shouldShowBorderBottom ? styles.borderBottom : {}; return ( @@ -31,6 +30,5 @@ function MoneyRequestHeaderStatusBar({title, description, shouldShowBorderBottom } MoneyRequestHeaderStatusBar.displayName = 'MoneyRequestHeaderStatusBar'; -MoneyRequestHeaderStatusBar.propTypes = propTypes; export default MoneyRequestHeaderStatusBar; From 3741ecf64429d71571f68b8f70373622125ae091 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 8 Dec 2023 14:04:43 +0700 Subject: [PATCH 005/124] fix: inconsistency between autocomplete address and displayed address --- src/components/AddressSearch/index.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 2fed1d153947..3b8727cada8d 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -192,7 +192,7 @@ function AddressSearch({ // amount of data massaging needs to happen for what the parent expects to get from this function. if (_.size(details)) { onPress({ - address: lodashGet(details, 'description'), + address: autocompleteData.description || lodashGet(details, 'formatted_address', ''), lat: lodashGet(details, 'geometry.location.lat', 0), lng: lodashGet(details, 'geometry.location.lng', 0), name: lodashGet(details, 'name'), @@ -261,7 +261,7 @@ function AddressSearch({ lat: lodashGet(details, 'geometry.location.lat', 0), lng: lodashGet(details, 'geometry.location.lng', 0), - address: lodashGet(details, 'formatted_address', ''), + address: autocompleteData.description || lodashGet(details, 'formatted_address', ''), }; // If the address is not in the US, use the full length state name since we're displaying the address's From a4013d8df0a453facfdb512008355da900a1b49e Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 8 Dec 2023 14:09:35 +0700 Subject: [PATCH 006/124] fix typo --- src/components/AddressSearch/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 3b8727cada8d..379804bf67c6 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -192,7 +192,7 @@ function AddressSearch({ // amount of data massaging needs to happen for what the parent expects to get from this function. if (_.size(details)) { onPress({ - address: autocompleteData.description || lodashGet(details, 'formatted_address', ''), + address: autocompleteData.description || lodashGet(details, 'description', ''), lat: lodashGet(details, 'geometry.location.lat', 0), lng: lodashGet(details, 'geometry.location.lng', 0), name: lodashGet(details, 'name'), From 2db612015f48d8a6e4ed57bb3554c45fdaabdabe Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Sun, 10 Dec 2023 14:11:52 +0100 Subject: [PATCH 007/124] Use PersonalDetailsUtils.getDisplayNameOrDefault where possible --- src/components/ArchivedReportFooter.tsx | 6 +++--- src/libs/OptionsListUtils.js | 18 +++++++++--------- src/libs/PersonalDetailsUtils.js | 11 ++++++----- src/libs/ReportUtils.ts | 8 ++++---- src/libs/SidebarUtils.ts | 2 +- src/libs/actions/Task.js | 3 ++- src/pages/DetailsPage.js | 10 ++++++---- src/pages/ProfilePage.js | 3 ++- src/pages/ReportParticipantsPage.js | 2 +- .../ReportActionCompose/SuggestionMention.js | 6 ++++-- src/pages/home/report/ReportActionItem.js | 4 ++-- 11 files changed, 40 insertions(+), 33 deletions(-) diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 3187bf3604e8..0f5956836773 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -30,14 +30,14 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} const originalMessage = reportClosedAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? reportClosedAction.originalMessage : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [report.ownerAccountID, 'displayName']); + let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[report.ownerAccountID ?? -1]); let oldDisplayName: string | undefined; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { const newAccountID = originalMessage?.newAccountID; const oldAccountID = originalMessage?.oldAccountID; - displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [newAccountID, 'displayName']); - oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [oldAccountID, 'displayName']); + displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[newAccountID ?? -1]); + oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[oldAccountID ?? -1]); } const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index f06ca8e12fcc..6276dc0b0dea 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -201,7 +201,7 @@ function isPersonalDetailsReady(personalDetails) { function getParticipantsOption(participant, personalDetails) { const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; const login = detail.login || participant.login; - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, 'displayName', LocalePhoneNumber.formatPhoneNumber(login)); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, LocalePhoneNumber.formatPhoneNumber(login)); return { keyForList: String(detail.accountID), login, @@ -246,8 +246,7 @@ function getParticipantNames(personalDetailList) { participantNames.add(participant.lastName.toLowerCase()); } if (participant.displayName) { - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(participant, 'displayName'); - participantNames.add(displayName.toLowerCase()); + participantNames.add(PersonalDetailsUtils.getDisplayNameOrDefault(participant).toLowerCase()); } }); return participantNames; @@ -301,9 +300,9 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) // More info https://github.com/Expensify/App/issues/8007 searchTerms = searchTerms.concat([ - PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), + PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false), personalDetail.login, - personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '') + personalDetail.login.replace(/\.(?=[^\s@]*@)/g, ''), ]); } } @@ -517,7 +516,8 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { const lastMessageTextFromReport = getLastMessageTextForReport(report); const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; - let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName')}: ` : ''; + let lastMessageText = + hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails)}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; if (result.isArchivedRoom) { @@ -525,7 +525,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) || CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), + displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails), policyName: ReportUtils.getPolicyName(report), }); } @@ -1438,8 +1438,8 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail, amountText) { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login); return { - text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName', formattedLogin), - alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), + text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, formattedLogin), + alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, '', false), icons: [ { source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 30b710e9d15c..0525540d1ee1 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -18,15 +18,16 @@ Onyx.connect({ }); /** - * @param {Object | Null} passedPersonalDetails - * @param {Array | String} pathToDisplayName + * @param {Object | Null | Undefined} passedPersonalDetails * @param {String} [defaultValue] optional default display name value + * @param {Boolean} [shouldFallbackToHidden] whether to fall back to 'hidden' if the display name and default value are empty * @returns {String} */ -function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue = '') { - const displayName = lodashGet(passedPersonalDetails, pathToDisplayName, '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); +function getDisplayNameOrDefault(passedPersonalDetails, defaultValue = '', shouldFallbackToHidden = true) { + const displayName = lodashGet(passedPersonalDetails, 'displayName', '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); + const fallbackValue = shouldFallbackToHidden ? Localize.translateLocal('common.hidden') : ''; - return displayName || defaultValue || Localize.translateLocal('common.hidden'); + return displayName || defaultValue || fallbackValue; } /** diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2329827dd376..3bf8dbeca4aa 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1289,7 +1289,7 @@ function getIcons( const parentReportAction = ReportActionsUtils.getParentReportAction(report); const actorAccountID = parentReportAction.actorAccountID; - const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails, [actorAccountID ?? -1, 'displayName']); + const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails?.[actorAccountID ?? -1]); const actorIcon = { id: actorAccountID, source: UserUtils.getAvatar(personalDetails?.[actorAccountID ?? -1]?.avatar ?? '', actorAccountID ?? -1), @@ -1413,13 +1413,13 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return formattedLogin; } - const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, 'displayName', formattedLogin); - - const shortName = personalDetails.firstName ? personalDetails.firstName : longName; + const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, formattedLogin, false); if (!longName && shouldFallbackToHidden) { return Localize.translateLocal('common.hidden'); } + + const shortName = personalDetails.firstName ? personalDetails.firstName : longName; return shouldUseShortForm ? shortName : longName; } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 1da1469a2687..786846cc3d61 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -346,7 +346,7 @@ function getOptionData( case CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED: { lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { policyName: ReportUtils.getPolicyName(report, false, policy), - displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName'), + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails), }); break; } diff --git a/src/libs/actions/Task.js b/src/libs/actions/Task.js index e5037d250d2e..b81f2a743ee8 100644 --- a/src/libs/actions/Task.js +++ b/src/libs/actions/Task.js @@ -8,6 +8,7 @@ import * as ErrorUtils from '@libs/ErrorUtils'; import * as LocalePhoneNumber from '@libs/LocalePhoneNumber'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; @@ -668,7 +669,7 @@ function getAssignee(assigneeAccountID, personalDetails) { } return { icons: ReportUtils.getIconsForParticipants([details.accountID], personalDetails), - displayName: details.displayName, + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(details), subtitle: details.login, }; } diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 66345107dbb1..bf899ca27d4b 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -21,6 +21,7 @@ import Text from '@components/Text'; import UserDetailsTooltip from '@components/UserDetailsTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import compose from '@libs/compose'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import useThemeStyles from '@styles/useThemeStyles'; @@ -120,6 +121,7 @@ function DetailsPage(props) { const phoneNumber = getPhoneNumber(details); const phoneOrEmail = isSMSLogin ? getPhoneNumber(details) : details.login; + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details, '', false); const isCurrentUser = props.session.accountID === details.accountID; @@ -132,7 +134,7 @@ function DetailsPage(props) { )} - {Boolean(details.displayName) && ( + {Boolean(displayName) && ( - {details.displayName} + {displayName} )} {details.login ? ( @@ -194,7 +196,7 @@ function DetailsPage(props) { {!isCurrentUser && ( Report.navigateToAndOpenReport([login])} diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 97ec3f99da3c..1b9127e15ad2 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -25,6 +25,7 @@ import UserDetailsTooltip from '@components/UserDetailsTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; @@ -103,7 +104,7 @@ function ProfilePage(props) { const accountID = Number(lodashGet(props.route.params, 'accountID', 0)); const details = lodashGet(props.personalDetails, accountID, ValidationUtils.isValidAccountRoute(accountID) ? {} : {isloading: false}); - const displayName = details.displayName ? details.displayName : props.translate('common.hidden'); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(details); const avatar = lodashGet(details, 'avatar', UserUtils.getDefaultAvatar()); const fallbackIcon = lodashGet(details, 'fallbackIcon', ''); const originalFileName = lodashGet(details, 'originalFileName', ''); diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index ceaa53a41a6b..b40bdc1f7de1 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -60,7 +60,7 @@ const getAllParticipants = (report, personalDetails, translate) => .map((accountID, index) => { const userPersonalDetail = lodashGet(personalDetails, accountID, {displayName: personalDetails.displayName || translate('common.hidden'), avatar: ''}); const userLogin = LocalePhoneNumber.formatPhoneNumber(userPersonalDetail.login || '') || translate('common.hidden'); - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(userPersonalDetail, 'displayName'); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(userPersonalDetail); return { alternateText: userLogin, diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index e55b96ad99f5..af3074eec06d 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -7,6 +7,7 @@ import {usePersonalDetails} from '@components/OnyxProvider'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; @@ -149,7 +150,8 @@ function SuggestionMention({ if (!detail.login || detail.isOptimisticPersonalDetail) { return false; } - const displayText = detail.displayName === formatPhoneNumber(detail.login) ? detail.displayName : `${detail.displayName} ${detail.login}`; + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail); + const displayText = displayName === formatPhoneNumber(detail.login) ? displayName : `${displayName} ${detail.login}`; if (searchValue && !displayText.toLowerCase().includes(searchValue.toLowerCase())) { return false; } @@ -159,7 +161,7 @@ function SuggestionMention({ const sortedPersonalDetails = _.sortBy(filteredPersonalDetails, (detail) => detail.displayName || detail.login); _.each(_.first(sortedPersonalDetails, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length), (detail) => { suggestions.push({ - text: detail.displayName, + text: PersonalDetailsUtils.getDisplayNameOrDefault(detail), alternateText: formatPhoneNumber(detail.login), login: detail.login, icons: [ diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f850daaa1ffb..c6484b93e93e 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -373,7 +373,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [props.report.ownerAccountID, 'displayName']); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); @@ -421,7 +421,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, [props.report.ownerAccountID, 'displayName']); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); const amount = CurrencyUtils.convertToDisplayString(props.report.total, props.report.currency); children = ; From 3bfdf189a251a7ea7132a21f48adc0d50dc94728 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 12 Dec 2023 14:51:25 +0700 Subject: [PATCH 008/124] display loading page for workspace page with section --- src/libs/actions/BankAccounts.ts | 34 ++++++++++++++++++- .../workspace/WorkspacePageWithSections.js | 34 +++++++++++++------ 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/src/libs/actions/BankAccounts.ts b/src/libs/actions/BankAccounts.ts index 8d0c59ab8d60..f7b7ec89c670 100644 --- a/src/libs/actions/BankAccounts.ts +++ b/src/libs/actions/BankAccounts.ts @@ -404,7 +404,39 @@ function verifyIdentityForBankAccount(bankAccountID: number, onfidoData: OnfidoD } function openWorkspaceView() { - API.read('OpenWorkspaceView', {}, {}); + API.read( + 'OpenWorkspaceView', + {}, + { + optimisticData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: true, + }, + }, + ], + successData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: false, + }, + }, + ], + failureData: [ + { + onyxMethod: Onyx.METHOD.MERGE, + key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, + value: { + isLoading: false, + }, + }, + ], + }, + ); } function handlePlaidError(bankAccountID: number, error: string, errorDescription: string, plaidRequestID: string) { diff --git a/src/pages/workspace/WorkspacePageWithSections.js b/src/pages/workspace/WorkspacePageWithSections.js index bea94ed3ef4e..2450e2b29fb0 100644 --- a/src/pages/workspace/WorkspacePageWithSections.js +++ b/src/pages/workspace/WorkspacePageWithSections.js @@ -1,10 +1,11 @@ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useEffect} from 'react'; +import React, {useEffect, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView'; +import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import ScrollViewWithContext from '@components/ScrollViewWithContext'; @@ -87,12 +88,19 @@ function WorkspacePageWithSections({backButtonRoute, children, footer, guidesCal const styles = useThemeStyles(); useNetwork({onReconnect: () => fetchData(shouldSkipVBBACall)}); + const isLoading = lodashGet(reimbursementAccount, 'isLoading', true); const achState = lodashGet(reimbursementAccount, 'achData.state', ''); const hasVBA = achState === BankAccount.STATE.OPEN; const isUsingECard = lodashGet(user, 'isUsingExpensifyCard', false); const policyID = lodashGet(route, 'params.policyID'); const policyName = lodashGet(policy, 'name'); const content = children(hasVBA, policyID, isUsingECard); + const firstRender = useRef(true); + + useEffect(() => { + // Because isLoading is false before merging in Onyx, we need firstRender ref to display loading page as well before isLoading is change to true + firstRender.current = false; + }, []); useEffect(() => { fetchData(shouldSkipVBBACall); @@ -117,17 +125,23 @@ function WorkspacePageWithSections({backButtonRoute, children, footer, guidesCal guidesCallTaskID={guidesCallTaskID} onBackButtonPress={() => Navigation.goBack(backButtonRoute || ROUTES.WORKSPACE_INITIAL.getRoute(policyID))} /> - {shouldUseScrollView ? ( - - {content} - + {isLoading || firstRender.current ? ( + ) : ( - content + <> + {shouldUseScrollView ? ( + + {content} + + ) : ( + content + )} + {footer} + )} - {footer} ); From 76cb656e4a593998e9712940e43ff9f462c8836b Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 14 Dec 2023 00:48:36 +0700 Subject: [PATCH 009/124] fix prevent user from replacing receipt when scanning --- src/components/AttachmentModal.js | 2 +- src/libs/ReportUtils.ts | 5 ++++- src/pages/EditRequestPage.js | 4 ++-- 3 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index 79be536945ac..38c4ceef13a4 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -366,7 +366,7 @@ function AttachmentModal(props) { const parentReportAction = props.parentReportActions[props.report.parentReportActionID]; const canEdit = - ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT) && + ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, props.parentReport.reportID, CONST.EDIT_REQUEST_FIELD.RECEIPT, props.transaction) && !TransactionUtils.isDistanceRequest(props.transaction); if (canEdit) { menuItems.push({ diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 63037416c923..20d663bce8ea 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1839,7 +1839,7 @@ function canEditMoneyRequest(reportAction: OnyxEntry, fieldToEdit * Checks if the current user can edit the provided property of a money request * */ -function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf): boolean { +function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf, transaction: Transaction): boolean { // A list of fields that cannot be edited by anyone, once a money request has been settled const nonEditableFieldsWhenSettled: string[] = [ CONST.EDIT_REQUEST_FIELD.AMOUNT, @@ -1853,6 +1853,9 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, repor if (!canEditMoneyRequest(reportAction, fieldToEdit)) { return false; // User doesn't have permission to edit } + if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT && TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { + return false; + } // Checks if the report is settled // Checks if the provided property is a restricted one diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 95313bea142d..b10d312303c3 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -107,7 +107,7 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT // Decides whether to allow or disallow editing a money request useEffect(() => { // Do not dismiss the modal, when a current user can edit this property of the money request. - if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, parentReport.reportID, fieldToEdit)) { + if (ReportUtils.canEditFieldOfMoneyRequest(parentReportAction, parentReport.reportID, fieldToEdit, transaction)) { return; } @@ -115,7 +115,7 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT Navigation.isNavigationReady().then(() => { Navigation.dismissModal(); }); - }, [parentReportAction, parentReport.reportID, fieldToEdit]); + }, [parentReportAction, parentReport.reportID, fieldToEdit, transaction]); // Update the transaction object and close the modal function editMoneyRequest(transactionChanges) { From 1bcec292dc90e3ae49503607486793e6f5201e7e Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 14 Dec 2023 00:24:10 +0100 Subject: [PATCH 010/124] Show the GBR only if the harvesting is disabled --- src/libs/actions/IOU.js | 33 +++++++++++++++++++++++------ src/types/onyx/Policy.ts | 5 ++++- tests/utils/LHNTestUtils.js | 1 + tests/utils/collections/policies.ts | 1 + 4 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 7513989d08c4..638e0e55631f 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -293,6 +293,7 @@ function buildOnyxDataForMoneyRequest( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, + hasOutstandingChildRequest = false, ) { const optimisticData = [ { @@ -304,6 +305,7 @@ function buildOnyxDataForMoneyRequest( lastReadTime: DateUtils.getDBTime(), lastMessageTranslationKey: '', iouReportID: iouReport.reportID, + hasOutstandingChildRequest, ...(isNewChatReport ? {pendingFields: {createChat: CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD}} : {}), }, }, @@ -612,12 +614,21 @@ function getMoneyRequestInformation( const isNewIOUReport = !chatReport.iouReportID || ReportUtils.hasIOUWaitingOnCurrentUserBankAccount(chatReport); let iouReport = isNewIOUReport ? null : allReports[`${ONYXKEYS.COLLECTION.REPORT}${chatReport.iouReportID}`]; - // If the linked expense report on paid policy is not draft, we need to create a new draft expense report - if (isPolicyExpenseChat && iouReport) { - const policyType = ReportUtils.getPolicy(iouReport.policyID).type || ''; - const isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; - if (isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport)) { - iouReport = null; + // Check if the scheduled submit is enabled in case of expense report + let needsToBeManuallySubmitted = false; + let policy = {}; + let isFromPaidPolicy = false; + if (isPolicyExpenseChat) { + policy = ReportUtils.getPolicy(iouReport.policyID); + const policyType = policy.type || ''; + isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; + needsToBeManuallySubmitted = policy.isHarvestingEnabled || false; + + // If the linked expense report on paid policy is not draft, we need to create a new draft expense report + if (iouReport) { + if (isFromPaidPolicy && !ReportUtils.isDraftExpenseReport(iouReport)) { + iouReport = null; + } } } @@ -728,6 +739,10 @@ function getMoneyRequestInformation( }, } : undefined; + + // The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off + // so the employee has to submit to tits manager manually. + const hasOutstandingChildRequest = isPolicyExpenseChat && isFromPaidPolicy && needsToBeManuallySubmitted; // STEP 5: Build Onyx Data const [optimisticData, successData, failureData] = buildOnyxDataForMoneyRequest( @@ -743,6 +758,7 @@ function getMoneyRequestInformation( optimisticPolicyRecentlyUsedTags, isNewChatReport, isNewIOUReport, + hasOutstandingChildRequest, ); return { @@ -2945,6 +2961,7 @@ function approveMoneyRequest(expenseReport) { function submitReport(expenseReport) { const optimisticSubmittedReportAction = ReportUtils.buildOptimisticSubmittedReportAction(expenseReport.total, expenseReport.currency, expenseReport.reportID); const parentReport = ReportUtils.getReport(expenseReport.parentReportID); + const isCurrentUserManager = currentUserPersonalDetails.accountID === expenseReport.managerID; const optimisticData = [ { @@ -2976,7 +2993,9 @@ function submitReport(expenseReport) { key: `${ONYXKEYS.COLLECTION.REPORT}${parentReport.reportID}`, value: { ...parentReport, - hasOutstandingChildRequest: false, + + // In case its a manager who force submitted the report, they are the next user who needs to take an action + hasOutstandingChildRequest: isCurrentUserManager, iouReportID: null, }, }, diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index a55a7c052b01..d1f893c3c855 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -68,12 +68,15 @@ type Policy = { /** Whether policy expense chats can be created and used on this policy. Enabled manually by CQ/JS snippet. Always true for free policies. */ isPolicyExpenseChatEnabled: boolean; - /** Whether the scheduled submit is enabled */ + /** Whether the auto reporting is enabled */ autoReporting: boolean; /** The scheduled submit frequency set up on the this policy */ autoReportingFrequency: ValueOf; + /** Whether the scheduled submit is enabled */ + isHarvestingEnabled: boolean, + /** The employee list of the policy */ employeeList?: []; }; diff --git a/tests/utils/LHNTestUtils.js b/tests/utils/LHNTestUtils.js index 8385b3fd3e4b..91e13046689b 100644 --- a/tests/utils/LHNTestUtils.js +++ b/tests/utils/LHNTestUtils.js @@ -256,6 +256,7 @@ function getFakePolicy(id = 1, name = 'Workspace-Test-001') { lastModified: 1697323926777105, autoReporting: true, autoReportingFrequency: 'immediate', + isHarvestingEnabled: true, defaultBillable: false, disabledFields: {defaultBillable: true, reimbursable: false}, }; diff --git a/tests/utils/collections/policies.ts b/tests/utils/collections/policies.ts index 266c8bba2d72..4f608e074694 100644 --- a/tests/utils/collections/policies.ts +++ b/tests/utils/collections/policies.ts @@ -11,6 +11,7 @@ export default function createRandomPolicy(index: number): Policy { autoReporting: randBoolean(), isPolicyExpenseChatEnabled: randBoolean(), autoReportingFrequency: rand(Object.values(CONST.POLICY.AUTO_REPORTING_FREQUENCIES)), + isHarvestingEnabled: randBoolean(), outputCurrency: randCurrencyCode(), role: rand(Object.values(CONST.POLICY.ROLE)), owner: randEmail(), From d09f2d8890d0b0f22c504c9b84c2c5edd8005ca8 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 14 Dec 2023 00:25:22 +0100 Subject: [PATCH 011/124] Fix style --- src/libs/actions/IOU.js | 2 +- src/types/onyx/Policy.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 638e0e55631f..30da17cf9ce8 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -739,7 +739,7 @@ function getMoneyRequestInformation( }, } : undefined; - + // The policy expense chat should have the GBR only when its a paid policy and the scheduled submit is turned off // so the employee has to submit to tits manager manually. const hasOutstandingChildRequest = isPolicyExpenseChat && isFromPaidPolicy && needsToBeManuallySubmitted; diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index d1f893c3c855..bfed87262d63 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -75,7 +75,7 @@ type Policy = { autoReportingFrequency: ValueOf; /** Whether the scheduled submit is enabled */ - isHarvestingEnabled: boolean, + isHarvestingEnabled: boolean; /** The employee list of the policy */ employeeList?: []; From e4247c3b3a5471c9b81faf4558e645be28eae93a Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Thu, 14 Dec 2023 00:58:19 +0100 Subject: [PATCH 012/124] Get the policyID from the chat report --- src/libs/actions/IOU.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 30da17cf9ce8..6e7e71c6eade 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -619,7 +619,7 @@ function getMoneyRequestInformation( let policy = {}; let isFromPaidPolicy = false; if (isPolicyExpenseChat) { - policy = ReportUtils.getPolicy(iouReport.policyID); + policy = ReportUtils.getPolicy(chatReport.policyID); const policyType = policy.type || ''; isFromPaidPolicy = policyType === CONST.POLICY.TYPE.TEAM || policyType === CONST.POLICY.TYPE.CORPORATE; needsToBeManuallySubmitted = policy.isHarvestingEnabled || false; From 4452a165c0e39392d48b9c42923e59b184499601 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 15 Dec 2023 17:27:17 +0530 Subject: [PATCH 013/124] replace useEffect with useLayoutEffect --- src/pages/settings/Wallet/WalletPage/WalletPage.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index a35d5b5a6982..5eaa3cebeea5 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -1,5 +1,5 @@ import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {useCallback, useEffect, useRef, useState, useLayoutEffect} from 'react'; import {ActivityIndicator, Dimensions, ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -145,8 +145,8 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod setShouldShowDefaultDeleteMenu(false); return; } - - paymentMethodButtonRef.current = nativeEvent.currentTarget; + console.log(nativeEvent.currentTarget) + paymentMethodButtonRef.current = nativeEvent.currentTarget // The delete/default menu if (accountType) { @@ -277,7 +277,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod PaymentMethods.openWalletPage(); }, [network.isOffline]); - useEffect(() => { + useLayoutEffect(() => { if (!shouldListenForResize) { return; } From 90ad8ba3eb9a75856c153b1e7de8b03ae385c9e4 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Fri, 15 Dec 2023 17:36:40 +0530 Subject: [PATCH 014/124] remove console statement --- src/pages/settings/Wallet/WalletPage/WalletPage.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index 5eaa3cebeea5..bf547bc4bd10 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -1,5 +1,5 @@ import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useRef, useState, useLayoutEffect} from 'react'; +import React, {useCallback, useEffect, useLayoutEffect, useRef, useState} from 'react'; import {ActivityIndicator, Dimensions, ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -145,8 +145,7 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod setShouldShowDefaultDeleteMenu(false); return; } - console.log(nativeEvent.currentTarget) - paymentMethodButtonRef.current = nativeEvent.currentTarget + paymentMethodButtonRef.current = nativeEvent.currentTarget; // The delete/default menu if (accountType) { From 872b392716e6a0babcadd055c18c9568412d7fef Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sat, 16 Dec 2023 15:37:50 +0100 Subject: [PATCH 015/124] add back animation --- .../CustomStatusBarAndBackground/index.tsx | 49 +++++++++++++++---- 1 file changed, 40 insertions(+), 9 deletions(-) diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index b84f9c6a6630..34362d571272 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -1,4 +1,5 @@ import React, {useCallback, useContext, useEffect, useRef, useState} from 'react'; +import {interpolateColor, runOnJS, useAnimatedReaction, useSharedValue, withDelay, withTiming} from 'react-native-reanimated'; import useTheme from '@hooks/useTheme'; import {navigationRef} from '@libs/Navigation/Navigation'; import StatusBar from '@libs/StatusBar'; @@ -33,6 +34,23 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack }; }, [disableRootStatusBar, isNested]); + const prevStatusBarBackgroundColor = useRef(theme.appBG); + const statusBarBackgroundColor = useRef(theme.appBG); + const statusBarAnimation = useSharedValue(0); + + useAnimatedReaction( + () => statusBarAnimation.value, + (current, previous) => { + // Do not run if either of the animated value is null + // or previous animated value is greater than or equal to the current one + if (previous === null || current === null || current <= previous) { + return; + } + const backgroundColor = interpolateColor(statusBarAnimation.value, [0, 1], [prevStatusBarBackgroundColor.current, statusBarBackgroundColor.current]); + runOnJS(updateStatusBarAppearance)({backgroundColor}); + }, + ); + const listenerCount = useRef(0); const updateStatusBarStyle = useCallback( (listenerId?: number) => { @@ -49,23 +67,36 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack currentRoute = navigationRef.getCurrentRoute(); } - let currentScreenBackgroundColor = theme.appBG; let newStatusBarStyle = theme.statusBarStyle; + let currentScreenBackgroundColor = theme.appBG; if (currentRoute && 'name' in currentRoute && currentRoute.name in theme.PAGE_THEMES) { - const screenTheme = theme.PAGE_THEMES[currentRoute.name]; - currentScreenBackgroundColor = screenTheme.backgroundColor; - newStatusBarStyle = screenTheme.statusBarStyle; + const pageTheme = theme.PAGE_THEMES[currentRoute.name]; + + newStatusBarStyle = pageTheme.statusBarStyle; + + const backgroundColorFromRoute = + currentRoute?.params && 'backgroundColor' in currentRoute.params && typeof currentRoute.params.backgroundColor === 'string' && currentRoute.params.backgroundColor; + + // It's possible for backgroundColorFromRoute to be empty string, so we must use "||" to fallback to backgroundColorFallback. + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + currentScreenBackgroundColor = backgroundColorFromRoute || pageTheme.backgroundColor; + } + + prevStatusBarBackgroundColor.current = statusBarBackgroundColor.current; + statusBarBackgroundColor.current = currentScreenBackgroundColor; + + if (currentScreenBackgroundColor !== theme.appBG || prevStatusBarBackgroundColor.current !== theme.appBG) { + statusBarAnimation.value = 0; + statusBarAnimation.value = withDelay(300, withTiming(1)); } // Don't update the status bar style if it's the same as the current one, to prevent flashing. - if (newStatusBarStyle === statusBarStyle) { - updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor}); - } else { - updateStatusBarAppearance({backgroundColor: currentScreenBackgroundColor, statusBarStyle: newStatusBarStyle}); + if (newStatusBarStyle !== statusBarStyle) { + updateStatusBarAppearance({statusBarStyle: newStatusBarStyle}); setStatusBarStyle(newStatusBarStyle); } }, - [statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], + [statusBarAnimation, statusBarStyle, theme.PAGE_THEMES, theme.appBG, theme.statusBarStyle], ); // Add navigation state listeners to update the status bar every time the route changes From d31e907bd6070dd73881fe184c594c44c91ff6c3 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Mon, 18 Dec 2023 20:05:24 +0530 Subject: [PATCH 016/124] Pass created time --- src/libs/actions/Report.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 135e616f7691..1f74d4afc87a 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -337,6 +337,7 @@ function addActions(reportID: string, text = '', file?: File) { reportComment?: string; file?: File; timezone?: string; + createdDate?: string; }; const parameters: AddCommentOrAttachementParameters = { @@ -345,6 +346,7 @@ function addActions(reportID: string, text = '', file?: File) { commentReportActionID: file && reportCommentAction ? reportCommentAction.reportActionID : null, reportComment: reportCommentText, file, + createdDate: file ? attachmentAction?.created : reportCommentAction?.created, }; const optimisticData: OnyxUpdate[] = [ From 3f157a4a5d91c6ef4963a47bae68d42dee05a210 Mon Sep 17 00:00:00 2001 From: Nikos Bright Date: Mon, 18 Dec 2023 13:16:42 -0500 Subject: [PATCH 017/124] TS migration for ReportWelcomeText done --- ...rtWelcomeText.js => ReportWelcomeText.tsx} | 105 ++++++------------ 1 file changed, 37 insertions(+), 68 deletions(-) rename src/components/{ReportWelcomeText.js => ReportWelcomeText.tsx} (57%) diff --git a/src/components/ReportWelcomeText.js b/src/components/ReportWelcomeText.tsx similarity index 57% rename from src/components/ReportWelcomeText.js rename to src/components/ReportWelcomeText.tsx index a204d0c59aaf..9f5ab10caa79 100644 --- a/src/components/ReportWelcomeText.js +++ b/src/components/ReportWelcomeText.tsx @@ -1,92 +1,65 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import _ from 'lodash'; import React from 'react'; import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; import Text from './Text'; import UserDetailsTooltip from './UserDetailsTooltip'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; - -const personalDetailsPropTypes = PropTypes.shape({ - /** The login of the person (either email or phone number) */ - login: PropTypes.string, - - /** The URL of the person's avatar (there should already be a default avatar if - the person doesn't have their own avatar uploaded yet, except for anon users) */ - avatar: PropTypes.string, - - /** This is either the user's full name, or their login if full name is an empty string */ - displayName: PropTypes.string, -}); - -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, - - /** The policy object for the current route */ - policy: PropTypes.shape({ - /** The name of the policy */ - name: PropTypes.string, - - /** The URL for the policy avatar */ - avatar: PropTypes.string, - }), - - /* Onyx Props */ +import withLocalize, {WithLocalizeProps} from './withLocalize'; +type ReportWelcomeTextOnyxProps = { /** All of the personal details for everyone */ - personalDetails: PropTypes.objectOf(personalDetailsPropTypes), - - ...withLocalizePropTypes, + personalDetails: OnyxEntry; }; -const defaultProps = { - report: {}, - policy: {}, - personalDetails: {}, -}; +type ReportWelcomeTextProps = WithLocalizeProps & + ReportWelcomeTextOnyxProps & { + /** The report currently being looked at */ + report: Report; + + /** The policy for the current route */ + policy: Policy; + }; -function ReportWelcomeText(props) { +function ReportWelcomeText({report = {} as Report, policy = {} as Policy, personalDetails = {}, translate}: ReportWelcomeTextProps) { const styles = useThemeStyles(); - const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(props.report); - const isChatRoom = ReportUtils.isChatRoom(props.report); + const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); + const isChatRoom = ReportUtils.isChatRoom(report); const isDefault = !(isChatRoom || isPolicyExpenseChat); - const participantAccountIDs = lodashGet(props.report, 'participantAccountIDs', []); + const participantAccountIDs = report?.participantAccountIDs ?? []; const isMultipleParticipant = participantAccountIDs.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, props.personalDetails), + OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails) as PersonalDetailsList, isMultipleParticipant, ); - const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(props.policy); - const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(props.report, isUserPolicyAdmin); - const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(props.report, participantAccountIDs); + const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); + const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); + const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, participantAccountIDs); return ( <> - {isChatRoom ? props.translate('reportActionsView.welcomeToRoom', {roomName: ReportUtils.getReportName(props.report)}) : props.translate('reportActionsView.sayHello')} + {isChatRoom ? translate('reportActionsView.welcomeToRoom', {roomName: ReportUtils.getReportName(report)}) : translate('reportActionsView.sayHello')} {isPolicyExpenseChat && ( <> - {props.translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartOne')} - {ReportUtils.getDisplayNameForParticipant(props.report.ownerAccountID)} - {props.translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartTwo')} - {ReportUtils.getPolicyName(props.report)} - {props.translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartThree')} + {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartOne')} + {ReportUtils.getDisplayNameForParticipant(report.ownerAccountID)} + {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartTwo')} + {ReportUtils.getPolicyName(report)} + {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartThree')} )} {isChatRoom && ( @@ -95,10 +68,10 @@ function ReportWelcomeText(props) { {roomWelcomeMessage.showReportName && ( Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(props.report.reportID))} + onPress={() => Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID))} suppressHighlighting > - {ReportUtils.getReportName(props.report)} + {ReportUtils.getReportName(report)} )} {roomWelcomeMessage.phrase2 !== undefined && {roomWelcomeMessage.phrase2}} @@ -106,9 +79,9 @@ function ReportWelcomeText(props) { )} {isDefault && ( - {props.translate('reportActionsView.beginningOfChatHistory')} - {_.map(displayNamesWithTooltips, ({displayName, pronouns, accountID}, index) => ( - + {translate('reportActionsView.beginningOfChatHistory')} + {displayNamesWithTooltips.map(({displayName, pronouns, accountID}, index) => ( + {ReportUtils.isOptimisticPersonalDetail(accountID) ? ( {displayName} @@ -124,27 +97,23 @@ function ReportWelcomeText(props) { {!_.isEmpty(pronouns) && {` (${pronouns})`}} {index === displayNamesWithTooltips.length - 1 && .} - {index === displayNamesWithTooltips.length - 2 && {` ${props.translate('common.and')} `}} + {index === displayNamesWithTooltips.length - 2 && {` ${translate('common.and')} `}} {index < displayNamesWithTooltips.length - 2 && , } ))} )} - {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)) && ( - {props.translate('reportActionsView.usePlusButton')} - )} + {(moneyRequestOptions.includes(CONST.IOU.TYPE.SEND) || moneyRequestOptions.includes(CONST.IOU.TYPE.REQUEST)) && {translate('reportActionsView.usePlusButton')}} ); } -ReportWelcomeText.defaultProps = defaultProps; -ReportWelcomeText.propTypes = propTypes; ReportWelcomeText.displayName = 'ReportWelcomeText'; export default compose( - withLocalize, - withOnyx({ + withLocalize, + withOnyx({ personalDetails: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, From 9e7f9746003b64ba7705f0869c391e26e3fd81f4 Mon Sep 17 00:00:00 2001 From: Nikos Bright Date: Mon, 18 Dec 2023 14:21:52 -0500 Subject: [PATCH 018/124] use hooks --- src/components/ReportWelcomeText.tsx | 33 +++++++++++++--------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 9f5ab10caa79..67eb50e54dde 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -2,8 +2,8 @@ import _ from 'lodash'; import React from 'react'; import {View} from 'react-native'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; @@ -14,23 +14,22 @@ import ROUTES from '@src/ROUTES'; import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; import Text from './Text'; import UserDetailsTooltip from './UserDetailsTooltip'; -import withLocalize, {WithLocalizeProps} from './withLocalize'; type ReportWelcomeTextOnyxProps = { /** All of the personal details for everyone */ personalDetails: OnyxEntry; }; -type ReportWelcomeTextProps = WithLocalizeProps & - ReportWelcomeTextOnyxProps & { - /** The report currently being looked at */ - report: Report; +type ReportWelcomeTextProps = ReportWelcomeTextOnyxProps & { + /** The report currently being looked at */ + report: Report; - /** The policy for the current route */ - policy: Policy; - }; + /** The policy for the current route */ + policy: Policy; +}; -function ReportWelcomeText({report = {} as Report, policy = {} as Policy, personalDetails = {}, translate}: ReportWelcomeTextProps) { +function ReportWelcomeText({report = {} as Report, policy = {} as Policy, personalDetails = {}}: ReportWelcomeTextProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); const isChatRoom = ReportUtils.isChatRoom(report); @@ -38,6 +37,7 @@ function ReportWelcomeText({report = {} as Report, policy = {} as Policy, person const participantAccountIDs = report?.participantAccountIDs ?? []; const isMultipleParticipant = participantAccountIDs.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( + // TODO: Remove type assertion (`as PersonalDetailsList`) after `src/libs/OptionsListUtils.js` is migrated into ts OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails) as PersonalDetailsList, isMultipleParticipant, ); @@ -111,11 +111,8 @@ function ReportWelcomeText({report = {} as Report, policy = {} as Policy, person ReportWelcomeText.displayName = 'ReportWelcomeText'; -export default compose( - withLocalize, - withOnyx({ - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), -)(ReportWelcomeText); +export default withOnyx({ + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, +})(ReportWelcomeText); From 4dc3b868e550f89434e3c41730a9d80d20cfcac9 Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Tue, 19 Dec 2023 06:11:18 +0530 Subject: [PATCH 019/124] Add isEditing prop --- src/pages/EditRequestAmountPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/EditRequestAmountPage.js b/src/pages/EditRequestAmountPage.js index 57f10ed281d7..d3de3ceb8a22 100644 --- a/src/pages/EditRequestAmountPage.js +++ b/src/pages/EditRequestAmountPage.js @@ -55,6 +55,7 @@ function EditRequestAmountPage({defaultAmount, defaultCurrency, onNavigateToCurr ref={(e) => (textInput.current = e)} onCurrencyButtonPress={onNavigateToCurrency} onSubmitButtonPress={onSubmit} + isEditing /> ); From a88d06981d454509cef9263c7ac59944854859bc Mon Sep 17 00:00:00 2001 From: Someshwar Tripathi Date: Tue, 19 Dec 2023 06:14:59 +0530 Subject: [PATCH 020/124] Remove outdated buttonTranslationText prop --- src/pages/EditRequestAmountPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/EditRequestAmountPage.js b/src/pages/EditRequestAmountPage.js index d3de3ceb8a22..bc77b9ecca75 100644 --- a/src/pages/EditRequestAmountPage.js +++ b/src/pages/EditRequestAmountPage.js @@ -49,7 +49,6 @@ function EditRequestAmountPage({defaultAmount, defaultCurrency, onNavigateToCurr > (textInput.current = e)} From c4d5eae9ce86d1b218f7f6415e7ddb1b73ca6864 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Tue, 19 Dec 2023 13:09:10 +0530 Subject: [PATCH 021/124] draft version with logs --- src/components/AddressSearch/index.js | 10 +- .../request/step/IOURequestStepWaypoint.js | 95 +++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index d9e4ef2c0f6e..277ecc3fd2b8 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -111,6 +111,9 @@ const propTypes = { /** Information about the network */ network: networkPropTypes.isRequired, + /** location bias based on rectangular format */ + locationBias: PropTypes.string, + ...withLocalizePropTypes, }; @@ -138,6 +141,7 @@ const defaultProps = { maxInputLength: undefined, predefinedPlaces: [], resultTypes: 'address', + locationBias: undefined, }; function AddressSearch({ @@ -162,6 +166,7 @@ function AddressSearch({ shouldSaveDraft, translate, value, + locationBias, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -179,11 +184,12 @@ function AddressSearch({ language: preferredLocale, types: resultTypes, components: isLimitedToUSA ? 'country:us' : undefined, + locationbias: locationBias ? locationBias : 'ipbias', }), - [preferredLocale, resultTypes, isLimitedToUSA], + [preferredLocale, resultTypes, isLimitedToUSA, locationBias], ); const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused; - + // console.log("AddressSearch:locationBias["+locationBias+"]"); const saveLocationDetails = (autocompleteData, details) => { const addressComponents = details.address_components; if (!addressComponents) { diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.js index dc5b9f7d6275..be43d9e2ca89 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.js +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.js @@ -1,5 +1,6 @@ import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; +import lodashIsNil from 'lodash/isNil'; import PropTypes from 'prop-types'; import React, {useMemo, useRef, useState} from 'react'; import {View} from 'react-native'; @@ -38,6 +39,15 @@ const propTypes = { /** The optimistic transaction for this request */ transaction: transactionPropTypes, + /* Current location coordinates of the user*/ + userLocation: PropTypes.shape({ + /** Latitude of the location */ + latitude: PropTypes.number, + + /** Longitude of the location */ + longitude: PropTypes.number, + }), + /** Recent waypoints that the user has selected */ recentWaypoints: PropTypes.arrayOf( PropTypes.shape({ @@ -65,6 +75,7 @@ const propTypes = { const defaultProps = { recentWaypoints: [], transaction: {}, + userLocation: {}, }; function IOURequestStepWaypoint({ @@ -73,6 +84,7 @@ function IOURequestStepWaypoint({ params: {iouType, pageIndex, reportID, transactionID}, }, transaction, + userLocation, }) { const styles = useThemeStyles(); const {windowWidth} = useWindowDimensions(); @@ -83,12 +95,16 @@ function IOURequestStepWaypoint({ const {isOffline} = useNetwork(); const textInput = useRef(null); const parsedWaypointIndex = parseInt(pageIndex, 10); + const directionCoordinates = lodashGet(transaction, 'routes.route0.geometry.coordinates', []); const allWaypoints = lodashGet(transaction, 'comment.waypoints', {}); const currentWaypoint = lodashGet(allWaypoints, `waypoint${pageIndex}`, {}); const waypointCount = _.size(allWaypoints); const filledWaypointCount = _.size(_.filter(allWaypoints, (waypoint) => !_.isEmpty(waypoint))); + const directionCoordinatesSize = _.size(directionCoordinates); + console.log('**** IOUREQUESTSTEPWAYPOINT **** ==> [' + filledWaypointCount + '], directionCoordinatesCount[' + directionCoordinatesSize + ']'); + const waypointDescriptionKey = useMemo(() => { switch (parsedWaypointIndex) { case 0: @@ -100,6 +116,81 @@ function IOURequestStepWaypoint({ } }, [parsedWaypointIndex, waypointCount]); + // Construct the rectangular boundary based on user location, waypoints and direction coordinates + const locationBias = useMemo(() => { + // If there are no filled wayPoints and if user's current location cannot be retrieved, + // it is futile to arrive at a biased location. Let's return + if (filledWaypointCount === 0 && _.isEmpty(userLocation)) { + console.log('Use Case 1: There are no filled waypoints. Also, current location of the user cannot be retrieved. The App will behave like it was before this feature.'); + return null; + } + + // Gather the longitudes and latitudes from filled waypoints. + const longitudes = _.filter( + _.map(allWaypoints, (waypoint) => { + if (!waypoint || lodashIsNil(waypoint.lng)) { + return; + } + return waypoint.lng; + }), + (lng) => lng, + ); + const latitudes = _.filter( + _.map(allWaypoints, (waypoint) => { + if (!waypoint || lodashIsNil(waypoint.lat)) { + return; + } + return waypoint.lat; + }), + (lat) => lat, + ); + + // We will get direction coordinates when user is adding a stop after filling the Start and Finish waypoints. + // Include direction coordinates when available. + if (_.size(directionCoordinates) > 0) { + console.log('Use Case 5: Additional stops after Start and Finish waypoints will give us direction coordinates. Include this to arrive at rectangular boundary'); + console.dir(directionCoordinates); + longitudes.push(..._.map(directionCoordinates, (coordinate) => coordinate[0])); + latitudes.push(..._.map(directionCoordinates, (coordinate) => coordinate[1])); + } + + // When no filled waypoints are available but the current location of the user is available, + // let us consider the current user's location to construct a rectangular bound + if (filledWaypointCount === 0 && !_.isEmpty(userLocation)) { + console.log('Use Case 2: There are no filled waypoints. But, we have the current location of the user. Let us bias location around the current location.'); + longitudes.push(userLocation.longitude); + latitudes.push(userLocation.latitude); + } + + if (filledWaypointCount === 1) { + console.log('Use Case 3: There is exactly one waypoint. Let us bias location around this waypoint: lat[' + latitudes[0] + '],lng[' + longitudes[0] + ']'); + } + + // Extend the rectangular bound by 0.5 degree (roughly around 25-30 miles in US) + const minLat = Math.min(...latitudes) - 0.5; + const minLng = Math.min(...longitudes) - 0.5; + const maxLat = Math.max(...latitudes) + 0.5; + const maxLng = Math.max(...longitudes) + 0.5; + + // Ensuring coordinates do not go out of range. + const south = minLat > -90 ? minLat : -90; + const west = minLng > -180 ? minLng : -180; + const north = maxLat < 90 ? maxLat : 90; + const east = maxLng < 180 ? maxLng : 180; + + const rectFormat = `rectangle:${south},${west}|${north},${east}`; + + if (filledWaypointCount === 2) { + console.log('Use Case 4: There two waypoints. Let us bias location around this waypoint'); + console.dir(latitudes); + console.dir(longitudes); + } + console.log('NEW RECTANGULAR BOUNDARY[' + rectFormat + ']'); + + // Format: rectangle:south,west|north,east + return rectFormat; + }, [userLocation, directionCoordinates, filledWaypointCount]); + const waypointAddress = lodashGet(currentWaypoint, 'address', ''); // Hide the menu when there is only start and finish waypoint const shouldShowThreeDotsButton = waypointCount > 2; @@ -219,6 +310,7 @@ function IOURequestStepWaypoint({ (textInput.current = e)} @@ -257,6 +349,9 @@ export default compose( withWritableReportOrNotFound, withFullTransactionOrNotFound, withOnyx({ + userLocation: { + key: ONYXKEYS.USER_LOCATION, + }, recentWaypoints: { key: ONYXKEYS.NVP_RECENT_WAYPOINTS, From c413337de14fc84e03db474fbb95453e359ba98a Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Mon, 18 Dec 2023 14:56:47 +0100 Subject: [PATCH 022/124] Handle copy-paste and removal of full minutes and hours --- src/components/TimePicker/TimePicker.js | 40 +++++++++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 5b49739150cc..53f2e59c424d 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -114,14 +114,30 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { [hours, minute, amPmValue, defaultValue], ); + const resetHours = () => { + setHours('00'); + setSelectionHour({start: 0, end: 0}); + } + + const resetMinutes = () => { + setMinute('00'); + setSelectionMinute({start: 0, end: 0}); + } + // This function receive value from hour input and validate it // The valid format is HH(from 00 to 12). If the user input 9, it will be 09. If user try to change 09 to 19 it would skip the first character const handleHourChange = (text) => { + if (_.isEmpty(text)) { + resetHours(); + return; + } + const isOnlyNumericValue = /^\d+$/.test(text.trim()); // Skip if the user is pasting the text or use non numeric characters. if (selectionHour.start !== selectionHour.end || !isOnlyNumericValue) { return; } + // Remove non-numeric characters. const filteredText = text.replace(/[^0-9]/g, ''); @@ -186,6 +202,12 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { // This function receive value from minute input and validate it // The valid format is MM(from 00 to 59). If the user input 9, it will be 09. If user try to change 09 to 99 it would skip the character const handleMinutesChange = (text) => { + if (_.isEmpty(text)) { + resetMinutes(); + focusHourInputOnLastCharacter(); + return; + } + const isOnlyNumericValue = /^\d+$/.test(text.trim()); // Skip if the user is pasting the text or use non numeric characters. if (selectionMinute.start !== selectionMinute.end || !isOnlyNumericValue) { @@ -262,14 +284,26 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } if (key === '<' || key === 'Backspace') { if (isHourFocused) { + if (selectionHour.start === 0 && selectionHour.end === 2) { + resetHours(); + return; + } + const newHour = replaceWithZeroAtPosition(hours, selectionHour.start); setHours(newHour); setSelectionHour(decreaseBothSelectionByOne(selectionHour)); } else if (isMinuteFocused) { - if (selectionMinute.start === 0) { + if (selectionMinute.start === 0 && selectionMinute.end === 2) { + resetMinutes(); focusHourInputOnLastCharacter(); return; } + + if (selectionMinute.start === 0 && selectionMinute.end === 0) { + focusHourInputOnLastCharacter(); + return; + } + const newMinute = replaceWithZeroAtPosition(minute, selectionMinute.start); setMinute(newMinute); setSelectionMinute(decreaseBothSelectionByOne(selectionMinute)); @@ -322,13 +356,13 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const handleFocusOnBackspace = useCallback( (e) => { - if (selectionMinute.start !== 0 || e.key !== 'Backspace') { + if (selectionMinute.start !== 0 || selectionMinute.end !== 0 || e.key !== 'Backspace') { return; } hourInputRef.current.focus(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [selectionMinute.start], + [selectionMinute.start, selectionMinute.end], ); const {styleForAM, styleForPM} = StyleUtils.getStatusAMandPMButtonStyle(amPmValue); From 3fad7ceb9c9cf49242e2e42556ca5f36c296d56b Mon Sep 17 00:00:00 2001 From: Nikos Bright Date: Tue, 19 Dec 2023 07:50:25 -0500 Subject: [PATCH 023/124] comments addressed --- src/components/ReportWelcomeText.tsx | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index 67eb50e54dde..f3237c0de29f 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -1,4 +1,3 @@ -import _ from 'lodash'; import React from 'react'; import {View} from 'react-native'; import {OnyxEntry, withOnyx} from 'react-native-onyx'; @@ -22,13 +21,13 @@ type ReportWelcomeTextOnyxProps = { type ReportWelcomeTextProps = ReportWelcomeTextOnyxProps & { /** The report currently being looked at */ - report: Report; + report: OnyxEntry; /** The policy for the current route */ - policy: Policy; + policy: OnyxEntry; }; -function ReportWelcomeText({report = {} as Report, policy = {} as Policy, personalDetails = {}}: ReportWelcomeTextProps) { +function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const isPolicyExpenseChat = ReportUtils.isPolicyExpenseChat(report); @@ -37,8 +36,8 @@ function ReportWelcomeText({report = {} as Report, policy = {} as Policy, person const participantAccountIDs = report?.participantAccountIDs ?? []; const isMultipleParticipant = participantAccountIDs.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - // TODO: Remove type assertion (`as PersonalDetailsList`) after `src/libs/OptionsListUtils.js` is migrated into ts - OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails) as PersonalDetailsList, + // @ts-expect-error TODO: Remove this once `src/libs/OptionsListUtils.js` is migrated to TypeScript. + OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant, ); const isUserPolicyAdmin = PolicyUtils.isPolicyAdmin(policy); @@ -56,7 +55,7 @@ function ReportWelcomeText({report = {} as Report, policy = {} as Policy, person {isPolicyExpenseChat && ( <> {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartOne')} - {ReportUtils.getDisplayNameForParticipant(report.ownerAccountID)} + {ReportUtils.getDisplayNameForParticipant(report?.ownerAccountID)} {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartTwo')} {ReportUtils.getPolicyName(report)} {translate('reportActionsView.beginningOfChatHistoryPolicyExpenseChatPartThree')} @@ -65,7 +64,7 @@ function ReportWelcomeText({report = {} as Report, policy = {} as Policy, person {isChatRoom && ( <> {roomWelcomeMessage.phrase1} - {roomWelcomeMessage.showReportName && ( + {roomWelcomeMessage.showReportName && !!report?.reportID && ( Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID))} @@ -95,7 +94,7 @@ function ReportWelcomeText({report = {} as Report, policy = {} as Policy, person )} - {!_.isEmpty(pronouns) && {` (${pronouns})`}} + {!!pronouns && {` (${pronouns})`}} {index === displayNamesWithTooltips.length - 1 && .} {index === displayNamesWithTooltips.length - 2 && {` ${translate('common.and')} `}} {index < displayNamesWithTooltips.length - 2 && , } From e9e32fb838fd88414827deac4fb9690e8bdd0a3f Mon Sep 17 00:00:00 2001 From: Nikos Bright Date: Tue, 19 Dec 2023 07:52:37 -0500 Subject: [PATCH 024/124] comment with issue number for ts-expect-error --- src/components/ReportWelcomeText.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index f3237c0de29f..d61530f1d0a5 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -36,7 +36,7 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const participantAccountIDs = report?.participantAccountIDs ?? []; const isMultipleParticipant = participantAccountIDs.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - // @ts-expect-error TODO: Remove this once `src/libs/OptionsListUtils.js` is migrated to TypeScript. + // @ts-expect-error TODO: Remove this once OptionsListUtils (https://github.com/Expensify/App/issues/24921) is migrated to TypeScript. OptionsListUtils.getPersonalDetailsForAccountIDs(participantAccountIDs, personalDetails), isMultipleParticipant, ); From 73b8b7f3345fda6344a09e58e6fa450eb9346ba0 Mon Sep 17 00:00:00 2001 From: Nikos Bright Date: Tue, 19 Dec 2023 12:21:53 -0500 Subject: [PATCH 025/124] comments addressed --- src/components/ReportWelcomeText.tsx | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/components/ReportWelcomeText.tsx b/src/components/ReportWelcomeText.tsx index d61530f1d0a5..cb52722b418a 100644 --- a/src/components/ReportWelcomeText.tsx +++ b/src/components/ReportWelcomeText.tsx @@ -44,6 +44,14 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP const roomWelcomeMessage = ReportUtils.getRoomWelcomeMessage(report, isUserPolicyAdmin); const moneyRequestOptions = ReportUtils.getMoneyRequestOptions(report, participantAccountIDs); + const navigateToReport = () => { + if (!report?.reportID) { + return; + } + + Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID)); + }; + return ( <> @@ -64,10 +72,10 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP {isChatRoom && ( <> {roomWelcomeMessage.phrase1} - {roomWelcomeMessage.showReportName && !!report?.reportID && ( + {roomWelcomeMessage.showReportName && ( Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID))} + onPress={navigateToReport} suppressHighlighting > {ReportUtils.getReportName(report)} @@ -80,7 +88,8 @@ function ReportWelcomeText({report, policy, personalDetails}: ReportWelcomeTextP {translate('reportActionsView.beginningOfChatHistory')} {displayNamesWithTooltips.map(({displayName, pronouns, accountID}, index) => ( - + // eslint-disable-next-line react/no-array-index-key + {ReportUtils.isOptimisticPersonalDetail(accountID) ? ( {displayName} From cd3637d4c113be880ea18de8f6e86eb827d4656c Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 20 Dec 2023 00:54:36 +0700 Subject: [PATCH 026/124] fix add optional to transaction param --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 20d663bce8ea..a41d180a901c 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1839,7 +1839,7 @@ function canEditMoneyRequest(reportAction: OnyxEntry, fieldToEdit * Checks if the current user can edit the provided property of a money request * */ -function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf, transaction: Transaction): boolean { +function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf, transaction?: Transaction): boolean { // A list of fields that cannot be edited by anyone, once a money request has been settled const nonEditableFieldsWhenSettled: string[] = [ CONST.EDIT_REQUEST_FIELD.AMOUNT, From 6d8eb98c4ef651cabfafca9045ce723f44454be4 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 20 Dec 2023 00:58:16 +0700 Subject: [PATCH 027/124] fix lint issue --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a41d180a901c..e9d01c11e3b6 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1853,7 +1853,7 @@ function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, repor if (!canEditMoneyRequest(reportAction, fieldToEdit)) { return false; // User doesn't have permission to edit } - if (fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT && TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { + if (!isEmpty(transaction) && fieldToEdit === CONST.EDIT_REQUEST_FIELD.RECEIPT && TransactionUtils.hasReceipt(transaction) && TransactionUtils.isReceiptBeingScanned(transaction)) { return false; } From f92050a6558fe0b07f045711a0ae88da69513e7c Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 19 Dec 2023 17:28:47 +0100 Subject: [PATCH 028/124] Fix after TS migration --- src/components/ArchivedReportFooter.tsx | 6 +- src/libs/OptionsListUtils.js | 2 +- src/libs/PersonalDetailsUtils.js | 211 ---------------------- src/libs/PersonalDetailsUtils.ts | 10 +- src/libs/SidebarUtils.ts | 2 +- src/pages/DetailsPage.js | 2 +- src/pages/ProfilePage.js | 2 +- src/pages/ReportParticipantsPage.js | 2 +- src/pages/home/report/ReportActionItem.js | 4 +- 9 files changed, 15 insertions(+), 226 deletions(-) delete mode 100644 src/libs/PersonalDetailsUtils.js diff --git a/src/components/ArchivedReportFooter.tsx b/src/components/ArchivedReportFooter.tsx index 8604d20130c7..2dae84106971 100644 --- a/src/components/ArchivedReportFooter.tsx +++ b/src/components/ArchivedReportFooter.tsx @@ -30,14 +30,14 @@ function ArchivedReportFooter({report, reportClosedAction, personalDetails = {}} const originalMessage = reportClosedAction?.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED ? reportClosedAction.originalMessage : null; const archiveReason = originalMessage?.reason ?? CONST.REPORT.ARCHIVE_REASON.DEFAULT; - let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[report?.ownerAccountID ?? 0]?.displayName); + let displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[report?.ownerAccountID ?? 0]); let oldDisplayName: string | undefined; if (archiveReason === CONST.REPORT.ARCHIVE_REASON.ACCOUNT_MERGED) { const newAccountID = originalMessage?.newAccountID; const oldAccountID = originalMessage?.oldAccountID; - displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[newAccountID ?? 0]?.displayName); - oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[oldAccountID ?? 0]?.displayName); + displayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[newAccountID ?? 0]); + oldDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails?.[oldAccountID ?? 0]); } const shouldRenderHTML = archiveReason !== CONST.REPORT.ARCHIVE_REASON.DEFAULT; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index f6e1bb6a1486..2f3c6b41225e 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -523,7 +523,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { (lastReportActions[report.reportID] && lastReportActions[report.reportID].originalMessage && lastReportActions[report.reportID].originalMessage.reason) || CONST.REPORT.ARCHIVE_REASON.DEFAULT; lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { - displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(lastActorDetails, 'displayName')), + displayName: archiveReason.displayName || PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails), policyName: ReportUtils.getPolicyName(report), }); } diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js deleted file mode 100644 index 0525540d1ee1..000000000000 --- a/src/libs/PersonalDetailsUtils.js +++ /dev/null @@ -1,211 +0,0 @@ -import lodashGet from 'lodash/get'; -import Onyx from 'react-native-onyx'; -import _ from 'underscore'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import * as LocalePhoneNumber from './LocalePhoneNumber'; -import * as Localize from './Localize'; -import * as UserUtils from './UserUtils'; - -let personalDetails = []; -let allPersonalDetails = {}; -Onyx.connect({ - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - callback: (val) => { - personalDetails = _.values(val); - allPersonalDetails = val; - }, -}); - -/** - * @param {Object | Null | Undefined} passedPersonalDetails - * @param {String} [defaultValue] optional default display name value - * @param {Boolean} [shouldFallbackToHidden] whether to fall back to 'hidden' if the display name and default value are empty - * @returns {String} - */ -function getDisplayNameOrDefault(passedPersonalDetails, defaultValue = '', shouldFallbackToHidden = true) { - const displayName = lodashGet(passedPersonalDetails, 'displayName', '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); - const fallbackValue = shouldFallbackToHidden ? Localize.translateLocal('common.hidden') : ''; - - return displayName || defaultValue || fallbackValue; -} - -/** - * Given a list of account IDs (as number) it will return an array of personal details objects. - * @param {Array} accountIDs - Array of accountIDs - * @param {Number} currentUserAccountID - * @param {Boolean} shouldChangeUserDisplayName - It will replace the current user's personal detail object's displayName with 'You'. - * @returns {Array} - Array of personal detail objects - */ -function getPersonalDetailsByIDs(accountIDs, currentUserAccountID, shouldChangeUserDisplayName = false) { - return _.chain(accountIDs) - .filter((accountID) => !!allPersonalDetails[accountID]) - .map((accountID) => { - const detail = allPersonalDetails[accountID]; - - if (shouldChangeUserDisplayName && currentUserAccountID === detail.accountID) { - return { - ...detail, - displayName: Localize.translateLocal('common.you'), - }; - } - - return detail; - }) - .value(); -} - -/** - * Given a list of logins, find the associated personal detail and return related accountIDs. - * - * @param {Array} logins Array of user logins - * @returns {Array} - Array of accountIDs according to passed logins - */ -function getAccountIDsByLogins(logins) { - return _.reduce( - logins, - (foundAccountIDs, login) => { - const currentDetail = _.find(personalDetails, (detail) => detail.login === login); - if (!currentDetail) { - // generate an account ID because in this case the detail is probably new, so we don't have a real accountID yet - foundAccountIDs.push(UserUtils.generateAccountID(login)); - } else { - foundAccountIDs.push(Number(currentDetail.accountID)); - } - return foundAccountIDs; - }, - [], - ); -} - -/** - * Given a list of accountIDs, find the associated personal detail and return related logins. - * - * @param {Array} accountIDs Array of user accountIDs - * @returns {Array} - Array of logins according to passed accountIDs - */ -function getLoginsByAccountIDs(accountIDs) { - return _.reduce( - accountIDs, - (foundLogins, accountID) => { - const currentDetail = _.find(personalDetails, (detail) => Number(detail.accountID) === Number(accountID)) || {}; - if (currentDetail.login) { - foundLogins.push(currentDetail.login); - } - return foundLogins; - }, - [], - ); -} - -/** - * Given a list of logins and accountIDs, return Onyx data for users with no existing personal details stored - * - * @param {Array} logins Array of user logins - * @param {Array} accountIDs Array of user accountIDs - * @returns {Object} - Object with optimisticData, successData and failureData (object of personal details objects) - */ -function getNewPersonalDetailsOnyxData(logins, accountIDs) { - const optimisticData = {}; - const successData = {}; - const failureData = {}; - - _.each(logins, (login, index) => { - const accountID = accountIDs[index]; - - if (_.isEmpty(allPersonalDetails[accountID])) { - optimisticData[accountID] = { - login, - accountID, - avatar: UserUtils.getDefaultAvatarURL(accountID), - displayName: LocalePhoneNumber.formatPhoneNumber(login), - }; - - /** - * Cleanup the optimistic user to ensure it does not permanently persist. - * This is done to prevent duplicate entries (upon success) since the BE will return other personal details with the correct account IDs. - */ - successData[accountID] = null; - } - }); - - return { - optimisticData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: optimisticData, - }, - ], - successData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: successData, - }, - ], - failureData: [ - { - onyxMethod: Onyx.METHOD.MERGE, - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - value: failureData, - }, - ], - }; -} - -/** - * Applies common formatting to each piece of an address - * - * @param {String} piece - address piece to format - * @returns {String} - formatted piece - */ -function formatPiece(piece) { - return piece ? `${piece}, ` : ''; -} - -/** - * - * @param {String} street1 - street line 1 - * @param {String} street2 - street line 2 - * @returns {String} formatted street - */ -function getFormattedStreet(street1 = '', street2 = '') { - return `${street1}\n${street2}`; -} - -/** - * - * @param {*} street - formatted address - * @returns {[string, string]} [street1, street2] - */ -function getStreetLines(street = '') { - const streets = street.split('\n'); - return [streets[0], streets[1]]; -} - -/** - * Formats an address object into an easily readable string - * - * @param {OnyxTypes.PrivatePersonalDetails} privatePersonalDetails - details object - * @returns {String} - formatted address - */ -function getFormattedAddress(privatePersonalDetails) { - const {address} = privatePersonalDetails; - const [street1, street2] = getStreetLines(address.street); - const formattedAddress = formatPiece(street1) + formatPiece(street2) + formatPiece(address.city) + formatPiece(address.state) + formatPiece(address.zip) + formatPiece(address.country); - - // Remove the last comma of the address - return formattedAddress.trim().replace(/,$/, ''); -} - -export { - getDisplayNameOrDefault, - getPersonalDetailsByIDs, - getAccountIDsByLogins, - getLoginsByAccountIDs, - getNewPersonalDetailsOnyxData, - getFormattedAddress, - getFormattedStreet, - getStreetLines, -}; diff --git a/src/libs/PersonalDetailsUtils.ts b/src/libs/PersonalDetailsUtils.ts index 8bb4ac0aea3e..3eb752bac983 100644 --- a/src/libs/PersonalDetailsUtils.ts +++ b/src/libs/PersonalDetailsUtils.ts @@ -1,4 +1,5 @@ import Onyx, {OnyxEntry} from 'react-native-onyx'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import * as OnyxTypes from '@src/types/onyx'; import {PersonalDetails, PersonalDetailsList} from '@src/types/onyx'; @@ -16,11 +17,10 @@ Onyx.connect({ }, }); -/** - * @param [defaultValue] optional default display name value - */ -function getDisplayNameOrDefault(displayName?: string, defaultValue = ''): string { - return displayName ?? defaultValue ?? Localize.translateLocal('common.hidden'); +function getDisplayNameOrDefault(passedPersonalDetails?: Partial | null, defaultValue = '', shouldFallbackToHidden = true): string { + const displayName = passedPersonalDetails?.displayName ? passedPersonalDetails.displayName.replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, '') : ''; + const fallbackValue = shouldFallbackToHidden ? Localize.translateLocal('common.hidden') : ''; + return displayName || defaultValue || fallbackValue; } /** diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 4744426ecfd3..f1c56cf1f63f 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -344,7 +344,7 @@ function getOptionData( case CONST.REPORT.ARCHIVE_REASON.POLICY_DELETED: { lastMessageText = Localize.translate(preferredLocale, `reportArchiveReasons.${archiveReason}`, { policyName: ReportUtils.getPolicyName(report, false, policy), - displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails?.displayName), + displayName: PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails), }); break; } diff --git a/src/pages/DetailsPage.js b/src/pages/DetailsPage.js index 4a611e216de9..f215b4167ab6 100755 --- a/src/pages/DetailsPage.js +++ b/src/pages/DetailsPage.js @@ -21,8 +21,8 @@ import UserDetailsTooltip from '@components/UserDetailsTooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; -import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import * as Report from '@userActions/Report'; diff --git a/src/pages/ProfilePage.js b/src/pages/ProfilePage.js index 7b33c3dc037a..555aa560cc67 100755 --- a/src/pages/ProfilePage.js +++ b/src/pages/ProfilePage.js @@ -25,8 +25,8 @@ import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; -import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import {parsePhoneNumber} from '@libs/PhoneNumber'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; import * as ValidationUtils from '@libs/ValidationUtils'; diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index e04ffbb352fc..fddf5176f815 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -60,7 +60,7 @@ const getAllParticipants = (report, personalDetails, translate) => .map((accountID, index) => { const userPersonalDetail = lodashGet(personalDetails, accountID, {displayName: personalDetails.displayName || translate('common.hidden'), avatar: ''}); const userLogin = LocalePhoneNumber.formatPhoneNumber(userPersonalDetail.login || '') || translate('common.hidden'); - const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(userPersonalDetail.displayName); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(userPersonalDetail); return { alternateText: userLogin, diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 8520ca178e37..8fa47734c2ea 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -374,7 +374,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, [props.report.ownerAccountID, 'displayName'])); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); const paymentType = lodashGet(props.action, 'originalMessage.paymentType', ''); const isSubmitterOfUnsettledReport = ReportUtils.isCurrentUserSubmitter(props.report.reportID) && !ReportUtils.isSettled(props.report.reportID); @@ -422,7 +422,7 @@ function ReportActionItem(props) { ); } else if (props.action.actionName === CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENTDEQUEUED) { - const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, [props.report.ownerAccountID, 'displayName'])); + const submitterDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(lodashGet(personalDetails, props.report.ownerAccountID)); const amount = CurrencyUtils.convertToDisplayString(props.report.total, props.report.currency); children = ; From bb6ccfcb7881f87642285d65e67088fd7eaf86e1 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Wed, 20 Dec 2023 13:33:33 +0530 Subject: [PATCH 029/124] lint fixes --- src/components/AddressSearch/index.js | 3 +-- .../request/step/IOURequestStepWaypoint.js | 25 +++---------------- 2 files changed, 4 insertions(+), 24 deletions(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 277ecc3fd2b8..71ac0edf7958 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -184,12 +184,11 @@ function AddressSearch({ language: preferredLocale, types: resultTypes, components: isLimitedToUSA ? 'country:us' : undefined, - locationbias: locationBias ? locationBias : 'ipbias', + locationbias: locationBias || 'ipbias', }), [preferredLocale, resultTypes, isLimitedToUSA, locationBias], ); const shouldShowCurrentLocationButton = canUseCurrentLocation && searchValue.trim().length === 0 && isFocused; - // console.log("AddressSearch:locationBias["+locationBias+"]"); const saveLocationDetails = (autocompleteData, details) => { const addressComponents = details.address_components; if (!addressComponents) { diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.js index be43d9e2ca89..323021032ecc 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.js +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.js @@ -39,7 +39,7 @@ const propTypes = { /** The optimistic transaction for this request */ transaction: transactionPropTypes, - /* Current location coordinates of the user*/ + /* Current location coordinates of the user */ userLocation: PropTypes.shape({ /** Latitude of the location */ latitude: PropTypes.number, @@ -102,9 +102,6 @@ function IOURequestStepWaypoint({ const waypointCount = _.size(allWaypoints); const filledWaypointCount = _.size(_.filter(allWaypoints, (waypoint) => !_.isEmpty(waypoint))); - const directionCoordinatesSize = _.size(directionCoordinates); - console.log('**** IOUREQUESTSTEPWAYPOINT **** ==> [' + filledWaypointCount + '], directionCoordinatesCount[' + directionCoordinatesSize + ']'); - const waypointDescriptionKey = useMemo(() => { switch (parsedWaypointIndex) { case 0: @@ -121,7 +118,6 @@ function IOURequestStepWaypoint({ // If there are no filled wayPoints and if user's current location cannot be retrieved, // it is futile to arrive at a biased location. Let's return if (filledWaypointCount === 0 && _.isEmpty(userLocation)) { - console.log('Use Case 1: There are no filled waypoints. Also, current location of the user cannot be retrieved. The App will behave like it was before this feature.'); return null; } @@ -148,8 +144,6 @@ function IOURequestStepWaypoint({ // We will get direction coordinates when user is adding a stop after filling the Start and Finish waypoints. // Include direction coordinates when available. if (_.size(directionCoordinates) > 0) { - console.log('Use Case 5: Additional stops after Start and Finish waypoints will give us direction coordinates. Include this to arrive at rectangular boundary'); - console.dir(directionCoordinates); longitudes.push(..._.map(directionCoordinates, (coordinate) => coordinate[0])); latitudes.push(..._.map(directionCoordinates, (coordinate) => coordinate[1])); } @@ -157,15 +151,10 @@ function IOURequestStepWaypoint({ // When no filled waypoints are available but the current location of the user is available, // let us consider the current user's location to construct a rectangular bound if (filledWaypointCount === 0 && !_.isEmpty(userLocation)) { - console.log('Use Case 2: There are no filled waypoints. But, we have the current location of the user. Let us bias location around the current location.'); longitudes.push(userLocation.longitude); latitudes.push(userLocation.latitude); } - if (filledWaypointCount === 1) { - console.log('Use Case 3: There is exactly one waypoint. Let us bias location around this waypoint: lat[' + latitudes[0] + '],lng[' + longitudes[0] + ']'); - } - // Extend the rectangular bound by 0.5 degree (roughly around 25-30 miles in US) const minLat = Math.min(...latitudes) - 0.5; const minLng = Math.min(...longitudes) - 0.5; @@ -178,18 +167,10 @@ function IOURequestStepWaypoint({ const north = maxLat < 90 ? maxLat : 90; const east = maxLng < 180 ? maxLng : 180; - const rectFormat = `rectangle:${south},${west}|${north},${east}`; - - if (filledWaypointCount === 2) { - console.log('Use Case 4: There two waypoints. Let us bias location around this waypoint'); - console.dir(latitudes); - console.dir(longitudes); - } - console.log('NEW RECTANGULAR BOUNDARY[' + rectFormat + ']'); - // Format: rectangle:south,west|north,east + const rectFormat = `rectangle:${south},${west}|${north},${east}`; return rectFormat; - }, [userLocation, directionCoordinates, filledWaypointCount]); + }, [userLocation, directionCoordinates, filledWaypointCount, allWaypoints]); const waypointAddress = lodashGet(currentWaypoint, 'address', ''); // Hide the menu when there is only start and finish waypoint From d2cac37ce493194c13d7aeced2ac99625f47a5c4 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Wed, 20 Dec 2023 12:58:14 +0100 Subject: [PATCH 030/124] Support full selection replacement --- src/components/TimePicker/TimePicker.js | 75 +++++++++++++------------ 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 53f2e59c424d..038382562dda 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -117,12 +117,13 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const resetHours = () => { setHours('00'); setSelectionHour({start: 0, end: 0}); - } + }; const resetMinutes = () => { setMinute('00'); setSelectionMinute({start: 0, end: 0}); - } + focusHourInputOnLastCharacter(); + }; // This function receive value from hour input and validate it // The valid format is HH(from 00 to 12). If the user input 9, it will be 09. If user try to change 09 to 19 it would skip the first character @@ -133,8 +134,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } const isOnlyNumericValue = /^\d+$/.test(text.trim()); - // Skip if the user is pasting the text or use non numeric characters. - if (selectionHour.start !== selectionHour.end || !isOnlyNumericValue) { + if (!isOnlyNumericValue) { return; } @@ -144,13 +144,11 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { let newHour = hours; let newSelection = selectionHour.start; - // Case when the cursor is at the start. - if (selectionHour.start === 0) { - // Handle cases where the hour would be > 12. - - // when you entering text the filteredText would consist of three numbers + if (selectionHour.start === 0 && selectionHour.end === 0) { + // When the user is entering text, the filteredText consists of three numbers const formattedText = `${filteredText[0]}${filteredText[2] || 0}`; if (formattedText > 12 && formattedText <= 24) { + // The hour is between 12 and 24 – switch AM to PM. newHour = String(formattedText - 12).padStart(2, '0'); newSelection = 2; setAmPmValue(CONST.TIME_PERIOD.PM); @@ -161,8 +159,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newHour = `${formattedText[0]}${formattedText[1]}`; newSelection = 1; } - } else if (selectionHour.start === 1) { - // Case when the cursor is at the second position. + } else if (selectionHour.start === 1 && selectionHour.end === 1) { + // The cursor is at the second position. const formattedText = `${filteredText[0]}${filteredText[1]}`; if (filteredText.length < 2) { @@ -183,13 +181,25 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newSelection = 2; } } else if (selectionHour.start === 2 && selectionHour.end === 2) { - // Case when the cursor is at the end and no text is selected. + // The cursor is at the end and no text is selected. if (filteredText.length < 2) { newHour = `${text}0`; newSelection = 1; } else { newSelection = 2; } + } else if (selectionHour.start === 0 && selectionHour.end === 2) { + if (filteredText <= 1) { + newHour = `${filteredText}0`; + newSelection = 1; + } else if (filteredText > 12 && filteredText <= 24) { + newHour = String(filteredText - 12).padStart(2, '0'); + newSelection = 2; + setAmPmValue(CONST.TIME_PERIOD.PM); + } else { + newHour = String(Math.min(filteredText, 12)).padStart(2, '0'); + newSelection = 2; + } } setHours(newHour); @@ -204,13 +214,11 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const handleMinutesChange = (text) => { if (_.isEmpty(text)) { resetMinutes(); - focusHourInputOnLastCharacter(); return; } const isOnlyNumericValue = /^\d+$/.test(text.trim()); - // Skip if the user is pasting the text or use non numeric characters. - if (selectionMinute.start !== selectionMinute.end || !isOnlyNumericValue) { + if (!isOnlyNumericValue) { return; } @@ -219,22 +227,9 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { let newMinute = minute; let newSelection = selectionMinute.start; - // Case when user selects and replaces the text. - if (selectionMinute.start !== selectionMinute.end) { - // If the first digit is > 5, prepend 0. - if (filteredText.length === 1 && filteredText > 5) { - newMinute = `0${filteredText}`; - newSelection = 2; - // If the first digit is <= 5, append 0 at the end. - } else if (filteredText.length === 1 && filteredText <= 5) { - newMinute = `${filteredText}0`; - newSelection = 1; - } else { - newMinute = `${filteredText.slice(0, 2)}`; - newSelection = 2; - } - } else if (selectionMinute.start === 0) { - // Case when the cursor is at the start. + + if (selectionMinute.start === 0 && selectionMinute.end === 0) { + // The cursor is at the start. const formattedText = `${filteredText[0]}${filteredText[2] || 0}`; if (text[0] >= 6) { newMinute = `0${formattedText[1]}`; @@ -243,8 +238,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newMinute = `${formattedText[0]}${formattedText[1]}`; newSelection = 1; } - } else if (selectionMinute.start === 1) { - // Case when the cursor is at the second position. + } else if (selectionMinute.start === 1 && selectionMinute.end === 1) { + // The cursor is at the second position. // If we remove a value, prepend 0. if (filteredText.length < 2) { newMinute = `0${text}`; @@ -255,10 +250,19 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newMinute = `${text[0]}${text[1]}`; newSelection = 2; } - } else if (filteredText.length < 2) { - // Case when the cursor is at the end and no text is selected. + } else if (selectionMinute.start === 2 && selectionMinute.end === 2 && filteredText.length < 2) { + // The cursor is at the end and no text is selected newMinute = `${text}0`; newSelection = 1; + } else if (selectionMinute.start === 0 && selectionMinute.end === 2) { + // The user selects and replaces the text + if (filteredText <= 5) { + newMinute = `${filteredText}0`; + newSelection = 1; + } else { + newMinute = String(Math.min(filteredText, 59)).padStart(2, '0'); + newSelection = 2; + } } setMinute(newMinute); @@ -295,7 +299,6 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } else if (isMinuteFocused) { if (selectionMinute.start === 0 && selectionMinute.end === 2) { resetMinutes(); - focusHourInputOnLastCharacter(); return; } From 61857e71b9914e286838a7daa29549a9209979d1 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 20 Dec 2023 22:38:55 +0700 Subject: [PATCH 031/124] Add logic in get active route --- src/libs/Navigation/Navigation.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index a3e89a983f98..5bbfba140cb8 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -96,6 +96,10 @@ function getActiveRoute(): string { if (!currentRoute?.name) { return ''; } + + if(currentRoute?.path){ + return currentRoute.path; + } const routeFromState = getPathFromState(navigationRef.getRootState(), linkingConfig.config); From 75292ecedce99666743f05ecf1c5ae2a304d6f63 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Wed, 20 Dec 2023 20:57:24 +0100 Subject: [PATCH 032/124] Support full selection replacement --- src/components/TimePicker/TimePicker.js | 64 ++++++++++--------------- 1 file changed, 24 insertions(+), 40 deletions(-) diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 038382562dda..2ae364de4c26 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -165,7 +165,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { if (filteredText.length < 2) { // If we remove a value, prepend 0. - newHour = `0${text}`; + newHour = `0${filteredText}`; newSelection = 0; // If the second digit is > 2, replace the hour with 0 and the second digit. } else if (formattedText > 12 && formattedText <= 24) { @@ -173,33 +173,23 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newSelection = 2; setAmPmValue(CONST.TIME_PERIOD.PM); } else if (formattedText > 24) { - newHour = `0${text[1]}`; + newHour = `0${filteredText[1]}`; newSelection = 2; } else { - newHour = `${text[0]}${text[1]}`; + newHour = `${filteredText[0]}${filteredText[1]}`; setHours(newHour); newSelection = 2; } - } else if (selectionHour.start === 2 && selectionHour.end === 2) { - // The cursor is at the end and no text is selected. - if (filteredText.length < 2) { - newHour = `${text}0`; - newSelection = 1; - } else { - newSelection = 2; - } - } else if (selectionHour.start === 0 && selectionHour.end === 2) { - if (filteredText <= 1) { - newHour = `${filteredText}0`; - newSelection = 1; - } else if (filteredText > 12 && filteredText <= 24) { - newHour = String(filteredText - 12).padStart(2, '0'); - newSelection = 2; - setAmPmValue(CONST.TIME_PERIOD.PM); - } else { - newHour = String(Math.min(filteredText, 12)).padStart(2, '0'); - newSelection = 2; - } + } else if (filteredText.length <= 1 && filteredText < 2) { + newHour = `${filteredText}0`; + newSelection = 1; + } else if (filteredText > 12 && filteredText <= 24) { + newHour = String(filteredText - 12).padStart(2, '0'); + newSelection = 2; + setAmPmValue(CONST.TIME_PERIOD.PM); + } else if (filteredText.length <= 2) { + newHour = String(Math.min(filteredText, 12)).padStart(2, '0'); + newSelection = 2; } setHours(newHour); @@ -231,7 +221,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { if (selectionMinute.start === 0 && selectionMinute.end === 0) { // The cursor is at the start. const formattedText = `${filteredText[0]}${filteredText[2] || 0}`; - if (text[0] >= 6) { + if (filteredText[0] >= 6) { newMinute = `0${formattedText[1]}`; newSelection = 2; } else { @@ -242,27 +232,20 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { // The cursor is at the second position. // If we remove a value, prepend 0. if (filteredText.length < 2) { - newMinute = `0${text}`; + newMinute = `0${filteredText}`; newSelection = 0; setSelectionHour({start: 2, end: 2}); hourInputRef.current.focus(); } else { - newMinute = `${text[0]}${text[1]}`; + newMinute = `${filteredText[0]}${filteredText[1]}`; newSelection = 2; } - } else if (selectionMinute.start === 2 && selectionMinute.end === 2 && filteredText.length < 2) { - // The cursor is at the end and no text is selected - newMinute = `${text}0`; + } else if (filteredText.length <= 1 && filteredText <= 5) { + newMinute = `${filteredText}0`; newSelection = 1; - } else if (selectionMinute.start === 0 && selectionMinute.end === 2) { - // The user selects and replaces the text - if (filteredText <= 5) { - newMinute = `${filteredText}0`; - newSelection = 1; - } else { - newMinute = String(Math.min(filteredText, 59)).padStart(2, '0'); - newSelection = 2; - } + } else if (filteredText.length <= 2) { + newMinute = String(Math.min(filteredText, 59)).padStart(2, '0'); + newSelection = 2; } setMinute(newMinute); @@ -362,10 +345,11 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { if (selectionMinute.start !== 0 || selectionMinute.end !== 0 || e.key !== 'Backspace') { return; } - hourInputRef.current.focus(); + e.preventDefault(); + focusHourInputOnLastCharacter(); }, // eslint-disable-next-line react-hooks/exhaustive-deps - [selectionMinute.start, selectionMinute.end], + [selectionMinute.start, selectionMinute.end, focusHourInputOnLastCharacter], ); const {styleForAM, styleForPM} = StyleUtils.getStatusAMandPMButtonStyle(amPmValue); From d2a518f6d075de322ca2212524f851767f5ef362 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 21 Dec 2023 15:16:12 +0700 Subject: [PATCH 033/124] reset amount when edit waypoint --- src/libs/actions/IOU.js | 2 +- src/libs/actions/Transaction.ts | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 802f0f00fffd..258f7948bfa0 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -175,7 +175,7 @@ function clearMoneyRequest(transactionID) { * @param {String} currency */ function setMoneyRequestAmount_temporaryForRefactor(transactionID, amount, currency) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, currency ? {amount, currency} : {amount}); } /** diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 2132ae1bdc61..95bfb84092a9 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -10,6 +10,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import {RecentWaypoint, Transaction} from '@src/types/onyx'; import {OnyxData} from '@src/types/onyx/Request'; import {WaypointCollection} from '@src/types/onyx/Transaction'; +import * as IOU from '@userActions/IOU'; let recentWaypoints: RecentWaypoint[] = []; Onyx.connect({ @@ -58,6 +59,7 @@ function addStop(transactionID: string) { } function saveWaypoint(transactionID: string, index: string, waypoint: RecentWaypoint | null, isDraft = false) { + IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, 0, ''); Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { comment: { waypoints: { @@ -100,6 +102,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp } function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: boolean) { + IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, 0, ''); // Index comes from the route params and is a string const index = Number(currentIndex); const existingWaypoints = transaction?.comment?.waypoints ?? {}; @@ -240,6 +243,7 @@ function getRouteForDraft(transactionID: string, waypoints: WaypointCollection) * which will replace the existing ones. */ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, isDraft = false): Promise { + IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, 0, ''); return Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { comment: { waypoints, From 8dc2e0600ccc0e9a90c10e62bb15b8043e67717e Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 21 Dec 2023 15:30:55 +0700 Subject: [PATCH 034/124] fix lint --- src/libs/actions/Transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 95bfb84092a9..9afd59f39003 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -5,12 +5,12 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import * as CollectionUtils from '@libs/CollectionUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; +import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {RecentWaypoint, Transaction} from '@src/types/onyx'; import {OnyxData} from '@src/types/onyx/Request'; import {WaypointCollection} from '@src/types/onyx/Transaction'; -import * as IOU from '@userActions/IOU'; let recentWaypoints: RecentWaypoint[] = []; Onyx.connect({ From b67fd6128cd4e2cab747816d0f8f502e5b236268 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 21 Dec 2023 15:38:07 +0700 Subject: [PATCH 035/124] fix lint --- src/libs/actions/Transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 9afd59f39003..3efb0a8c76c9 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -5,7 +5,7 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import * as CollectionUtils from '@libs/CollectionUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import * as IOU from '@userActions/IOU'; +import * as IOU from './IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {RecentWaypoint, Transaction} from '@src/types/onyx'; From ef9189411da198bc0717818efbc2703f90556ac4 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 21 Dec 2023 16:07:36 +0700 Subject: [PATCH 036/124] fix lint --- src/libs/actions/Transaction.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index 3efb0a8c76c9..d6b191f2fbe3 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -5,12 +5,12 @@ import Onyx from 'react-native-onyx'; import * as API from '@libs/API'; import * as CollectionUtils from '@libs/CollectionUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; -import * as IOU from './IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {RecentWaypoint, Transaction} from '@src/types/onyx'; import {OnyxData} from '@src/types/onyx/Request'; import {WaypointCollection} from '@src/types/onyx/Transaction'; +import * as IOU from './IOU'; let recentWaypoints: RecentWaypoint[] = []; Onyx.connect({ From 561ea7a49c7218d78533bd601e37d6a35a8503b6 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 21 Dec 2023 16:24:04 +0700 Subject: [PATCH 037/124] fix download file issue --- src/components/AttachmentModal.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/AttachmentModal.js b/src/components/AttachmentModal.js index b1af96561ef5..9da77edf504f 100755 --- a/src/components/AttachmentModal.js +++ b/src/components/AttachmentModal.js @@ -357,6 +357,14 @@ function AttachmentModal(props) { setIsModalOpen(true); }, []); + useEffect(() => { + setSource(props.source); + }, [props.source]); + + useEffect(() => { + setIsAuthTokenRequired(props.isAuthTokenRequired); + }, [props.isAuthTokenRequired]); + const sourceForAttachmentView = props.source || source; const threeDotsMenuItems = useMemo(() => { @@ -395,7 +403,7 @@ function AttachmentModal(props) { } return menuItems; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isReceiptAttachment, props.parentReport, props.parentReportActions, props.policy, props.transaction, file]); + }, [props.isReceiptAttachment, props.parentReport, props.parentReportActions, props.policy, props.transaction, file, source]); // There are a few things that shouldn't be set until we absolutely know if the file is a receipt or an attachment. // props.isReceiptAttachment will be null until its certain what the file is, in which case it will then be true|false. From 74a354eec73cff88c2cb1ed5b582a0aa2458bdf2 Mon Sep 17 00:00:00 2001 From: Monil Bhavsar Date: Thu, 21 Dec 2023 16:25:25 +0530 Subject: [PATCH 038/124] Rename param --- src/libs/actions/Report.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1f74d4afc87a..02f58b3f80e7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -337,7 +337,7 @@ function addActions(reportID: string, text = '', file?: File) { reportComment?: string; file?: File; timezone?: string; - createdDate?: string; + clientCreatedTime?: string; }; const parameters: AddCommentOrAttachementParameters = { @@ -346,7 +346,7 @@ function addActions(reportID: string, text = '', file?: File) { commentReportActionID: file && reportCommentAction ? reportCommentAction.reportActionID : null, reportComment: reportCommentText, file, - createdDate: file ? attachmentAction?.created : reportCommentAction?.created, + clientCreatedTime: file ? attachmentAction?.created : reportCommentAction?.created, }; const optimisticData: OnyxUpdate[] = [ From 00ace7f73c8f6997eaf77d363ef9c53b6fba8484 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Thu, 21 Dec 2023 17:36:03 +0530 Subject: [PATCH 039/124] use undefined as default for userLocation Co-authored-by: Rajat --- src/pages/iou/request/step/IOURequestStepWaypoint.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.js index 323021032ecc..714f9ce7ff77 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.js +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.js @@ -75,7 +75,7 @@ const propTypes = { const defaultProps = { recentWaypoints: [], transaction: {}, - userLocation: {}, + userLocation: undefined, }; function IOURequestStepWaypoint({ From af1259fa5ea98c69de19a688d939c376b50169c8 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 22 Dec 2023 08:07:52 +0530 Subject: [PATCH 040/124] optimize usage of locationbias in query --- src/components/AddressSearch/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 71ac0edf7958..3ca49d4cf2b6 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -184,7 +184,7 @@ function AddressSearch({ language: preferredLocale, types: resultTypes, components: isLimitedToUSA ? 'country:us' : undefined, - locationbias: locationBias || 'ipbias', + ...(locationBias && {locationbias: locationBias}), }), [preferredLocale, resultTypes, isLimitedToUSA, locationBias], ); From d649930603ecc9e79aa060783fd3d77c6e625e8e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 22 Dec 2023 14:35:45 +0700 Subject: [PATCH 041/124] fix: slash in native --- src/libs/Navigation/Navigation.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 5bbfba140cb8..d1e1e16aad94 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -120,8 +120,8 @@ function getActiveRoute(): string { * @return is active */ function isActiveRoute(routePath: Route): boolean { - // We remove First forward slash from the URL before matching - return getActiveRoute().substring(1) === routePath; + // On web, we remove First forward slash from the URL before matching + return getActiveRoute().startsWith('/') ? getActiveRoute().substring(1) === routePath : getActiveRoute() === routePath ; } /** From 3f7fc648994f0a3321fbed4296bf7b763b718cc3 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 22 Dec 2023 15:30:53 +0700 Subject: [PATCH 042/124] fix: lint --- src/libs/Navigation/Navigation.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index d1e1e16aad94..8ae133ec998d 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -96,9 +96,9 @@ function getActiveRoute(): string { if (!currentRoute?.name) { return ''; } - - if(currentRoute?.path){ - return currentRoute.path; + + if (currentRoute?.path) { + return currentRoute.path; } const routeFromState = getPathFromState(navigationRef.getRootState(), linkingConfig.config); @@ -121,7 +121,7 @@ function getActiveRoute(): string { */ function isActiveRoute(routePath: Route): boolean { // On web, we remove First forward slash from the URL before matching - return getActiveRoute().startsWith('/') ? getActiveRoute().substring(1) === routePath : getActiveRoute() === routePath ; + return getActiveRoute().startsWith('/') ? getActiveRoute().substring(1) === routePath : getActiveRoute() === routePath; } /** From 33a996a08e83aed8c1ddd08cab57bd30b82abb79 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 22 Dec 2023 09:54:50 +0100 Subject: [PATCH 043/124] Add explanatory comments --- src/components/TimePicker/TimePicker.js | 23 ++++++++++++++++++----- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 2ae364de4c26..b0862fb08618 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -122,7 +122,6 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const resetMinutes = () => { setMinute('00'); setSelectionMinute({start: 0, end: 0}); - focusHourInputOnLastCharacter(); }; // This function receive value from hour input and validate it @@ -145,6 +144,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { let newSelection = selectionHour.start; if (selectionHour.start === 0 && selectionHour.end === 0) { + // The cursor is at the start of hours // When the user is entering text, the filteredText consists of three numbers const formattedText = `${filteredText[0]}${filteredText[2] || 0}`; if (formattedText > 12 && formattedText <= 24) { @@ -160,7 +160,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newSelection = 1; } } else if (selectionHour.start === 1 && selectionHour.end === 1) { - // The cursor is at the second position. + // The cursor is in-between the digits const formattedText = `${filteredText[0]}${filteredText[1]}`; if (filteredText.length < 2) { @@ -181,13 +181,20 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newSelection = 2; } } else if (filteredText.length <= 1 && filteredText < 2) { + /* + The filtered text is either 0 or 1. We must check the length of the filtered text to avoid incorrectly handling e.g. "01" as "1" + We are either replacing hours with a single digit, or removing the last digit. + In both cases, we should append 0 to the remaining value. + */ newHour = `${filteredText}0`; newSelection = 1; } else if (filteredText > 12 && filteredText <= 24) { + // We are replacing hours with a value between 12 and 24. Switch AM to PM newHour = String(filteredText - 12).padStart(2, '0'); newSelection = 2; setAmPmValue(CONST.TIME_PERIOD.PM); } else if (filteredText.length <= 2) { + // We are replacing hours with a value either 2-11, or 24+. Minimize the value to 12 and prepend 0 if needed newHour = String(Math.min(filteredText, 12)).padStart(2, '0'); newSelection = 2; } @@ -219,7 +226,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { let newSelection = selectionMinute.start; if (selectionMinute.start === 0 && selectionMinute.end === 0) { - // The cursor is at the start. + // The cursor is at the start of minutes const formattedText = `${filteredText[0]}${filteredText[2] || 0}`; if (filteredText[0] >= 6) { newMinute = `0${formattedText[1]}`; @@ -229,9 +236,9 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newSelection = 1; } } else if (selectionMinute.start === 1 && selectionMinute.end === 1) { - // The cursor is at the second position. - // If we remove a value, prepend 0. + // The cursor is in-between the digits if (filteredText.length < 2) { + // If we remove a value, prepend 0. newMinute = `0${filteredText}`; newSelection = 0; setSelectionHour({start: 2, end: 2}); @@ -241,9 +248,15 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newSelection = 2; } } else if (filteredText.length <= 1 && filteredText <= 5) { + /* + The filtered text is from 0 to 5. We must check the length of the filtered text to avoid incorrectly handling e.g. "01" as "1" + We are either replacing minutes with a single digit, or removing the last digit. + In both cases, we should append 0 to the remaining value. + */ newMinute = `${filteredText}0`; newSelection = 1; } else if (filteredText.length <= 2) { + // We are replacing minutes with a value of 6+. Minimize the value to 59 and prepend 0 if needed newMinute = String(Math.min(filteredText, 59)).padStart(2, '0'); newSelection = 2; } From c9dc08b8daffa498e73b94c829a86498aa01caa7 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 22 Dec 2023 12:49:05 +0100 Subject: [PATCH 044/124] Fix MERGED_ prefix on the WorkspaceMembers list --- src/pages/workspace/WorkspaceMembersPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceMembersPage.js b/src/pages/workspace/WorkspaceMembersPage.js index 9834d4e9e1c0..a7fe8b2f8a71 100644 --- a/src/pages/workspace/WorkspaceMembersPage.js +++ b/src/pages/workspace/WorkspaceMembersPage.js @@ -364,7 +364,7 @@ function WorkspaceMembersPage(props) { details.login === props.policy.owner || policyMember.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || !_.isEmpty(policyMember.errors), - text: props.formatPhoneNumber(details.displayName), + text: props.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: props.formatPhoneNumber(details.login), rightElement: isAdmin ? ( From 7723c2b0022a0d8684b2eddc98f4d8c09ee97c68 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 22 Dec 2023 12:52:26 +0100 Subject: [PATCH 045/124] Fix MERGED_ prefix on the RoomMembers list --- src/pages/RoomMembersPage.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/RoomMembersPage.js b/src/pages/RoomMembersPage.js index d8e7dbdaf76e..27e1cd1da2e6 100644 --- a/src/pages/RoomMembersPage.js +++ b/src/pages/RoomMembersPage.js @@ -19,6 +19,7 @@ import compose from '@libs/compose'; import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as OptionsListUtils from '@libs/OptionsListUtils'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as UserUtils from '@libs/UserUtils'; @@ -194,7 +195,7 @@ function RoomMembersPage(props) { memberDetails += ` ${details.lastName.toLowerCase()}`; } if (details.displayName) { - memberDetails += ` ${details.displayName.toLowerCase()}`; + memberDetails += ` ${PersonalDetailsUtils.getDisplayNameOrDefault(details).toLowerCase()}`; } if (details.phoneNumber) { memberDetails += ` ${details.phoneNumber.toLowerCase()}`; @@ -210,7 +211,7 @@ function RoomMembersPage(props) { accountID: Number(accountID), isSelected: _.contains(selectedMembers, Number(accountID)), isDisabled: accountID === props.session.accountID, - text: props.formatPhoneNumber(details.displayName), + text: props.formatPhoneNumber(PersonalDetailsUtils.getDisplayNameOrDefault(details)), alternateText: props.formatPhoneNumber(details.login), icons: [ { From dbfda572571e3c93e0f49de1da7c620e83716637 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 22 Dec 2023 22:36:56 +0530 Subject: [PATCH 046/124] removed direction coordinates --- src/pages/iou/request/step/IOURequestStepWaypoint.js | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepWaypoint.js b/src/pages/iou/request/step/IOURequestStepWaypoint.js index 714f9ce7ff77..3c361acd3233 100644 --- a/src/pages/iou/request/step/IOURequestStepWaypoint.js +++ b/src/pages/iou/request/step/IOURequestStepWaypoint.js @@ -95,7 +95,6 @@ function IOURequestStepWaypoint({ const {isOffline} = useNetwork(); const textInput = useRef(null); const parsedWaypointIndex = parseInt(pageIndex, 10); - const directionCoordinates = lodashGet(transaction, 'routes.route0.geometry.coordinates', []); const allWaypoints = lodashGet(transaction, 'comment.waypoints', {}); const currentWaypoint = lodashGet(allWaypoints, `waypoint${pageIndex}`, {}); @@ -113,7 +112,7 @@ function IOURequestStepWaypoint({ } }, [parsedWaypointIndex, waypointCount]); - // Construct the rectangular boundary based on user location, waypoints and direction coordinates + // Construct the rectangular boundary based on user location and waypoints const locationBias = useMemo(() => { // If there are no filled wayPoints and if user's current location cannot be retrieved, // it is futile to arrive at a biased location. Let's return @@ -141,13 +140,6 @@ function IOURequestStepWaypoint({ (lat) => lat, ); - // We will get direction coordinates when user is adding a stop after filling the Start and Finish waypoints. - // Include direction coordinates when available. - if (_.size(directionCoordinates) > 0) { - longitudes.push(..._.map(directionCoordinates, (coordinate) => coordinate[0])); - latitudes.push(..._.map(directionCoordinates, (coordinate) => coordinate[1])); - } - // When no filled waypoints are available but the current location of the user is available, // let us consider the current user's location to construct a rectangular bound if (filledWaypointCount === 0 && !_.isEmpty(userLocation)) { @@ -170,7 +162,7 @@ function IOURequestStepWaypoint({ // Format: rectangle:south,west|north,east const rectFormat = `rectangle:${south},${west}|${north},${east}`; return rectFormat; - }, [userLocation, directionCoordinates, filledWaypointCount, allWaypoints]); + }, [userLocation, filledWaypointCount, allWaypoints]); const waypointAddress = lodashGet(currentWaypoint, 'address', ''); // Hide the menu when there is only start and finish waypoint From 29959086e7e3247c6fa33f861e2c816a81f9d339 Mon Sep 17 00:00:00 2001 From: Roji Philip Date: Fri, 22 Dec 2023 22:57:02 +0530 Subject: [PATCH 047/124] update to comment text Co-authored-by: Rajat --- src/components/AddressSearch/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AddressSearch/index.js b/src/components/AddressSearch/index.js index 3ca49d4cf2b6..c2ee21eaa0cb 100644 --- a/src/components/AddressSearch/index.js +++ b/src/components/AddressSearch/index.js @@ -111,7 +111,7 @@ const propTypes = { /** Information about the network */ network: networkPropTypes.isRequired, - /** location bias based on rectangular format */ + /** Location bias for querying search results. */ locationBias: PropTypes.string, ...withLocalizePropTypes, From 8322ca3c932f06a5093d80467ec77c67f386fe89 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 22 Dec 2023 21:17:09 +0100 Subject: [PATCH 048/124] Wrap the DatePicker on SetDatePage --- src/pages/settings/Profile/CustomStatus/SetDatePage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pages/settings/Profile/CustomStatus/SetDatePage.js b/src/pages/settings/Profile/CustomStatus/SetDatePage.js index 69b89a5378ac..e0cd23af4953 100644 --- a/src/pages/settings/Profile/CustomStatus/SetDatePage.js +++ b/src/pages/settings/Profile/CustomStatus/SetDatePage.js @@ -3,6 +3,7 @@ import React, {useCallback} from 'react'; import {withOnyx} from 'react-native-onyx'; import DatePicker from '@components/DatePicker'; import FormProvider from '@components/Form/FormProvider'; +import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import ScreenWrapper from '@components/ScreenWrapper'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; @@ -58,7 +59,8 @@ function SetDatePage({translate, customStatus}) { enabledWhenOffline shouldUseDefaultValue > - Date: Sat, 23 Dec 2023 12:14:08 +0530 Subject: [PATCH 049/124] fix: suggestions popup opens with delay and appears empty --- .../BaseAutoCompleteSuggestions.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index c2320f7c0202..07937c1102d2 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -1,5 +1,5 @@ import {FlashList} from '@shopify/flash-list'; -import React, {ForwardedRef, forwardRef, ReactElement, useCallback, useEffect, useRef} from 'react'; +import React, {ForwardedRef, forwardRef, ReactElement, useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; // We take ScrollView from this package to properly handle the scrolling of AutoCompleteSuggestions in chats since one scroll is nested inside another import {ScrollView} from 'react-native-gesture-handler'; @@ -8,6 +8,8 @@ import ColorSchemeWrapper from '@components/ColorSchemeWrapper'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; import viewForwardedRef from '@src/types/utils/viewForwardedRef'; import type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps} from './types'; @@ -39,6 +41,7 @@ function BaseAutoCompleteSuggestions( }: AutoCompleteSuggestionsProps, ref: ForwardedRef, ) { + const {windowWidth, isLargeScreenWidth} = useWindowDimensions(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); @@ -64,7 +67,13 @@ function BaseAutoCompleteSuggestions( const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); - + const estimatedListSize = useMemo(() => { + const footerPadding = 40; + return { + height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length, + width: (isLargeScreenWidth ? windowWidth - variables.sideBarWidth : windowWidth) - footerPadding, + }; + }, [isLargeScreenWidth, suggestions.length, windowWidth]); useEffect(() => { rowHeight.value = withTiming(measureHeightOfSuggestionRows(suggestions.length, isSuggestionPickerLarge), { duration: 100, @@ -88,6 +97,7 @@ function BaseAutoCompleteSuggestions( Date: Sun, 24 Dec 2023 00:06:42 +0530 Subject: [PATCH 050/124] [TS Migration] DistanceMapView --- .../distanceMapViewPropTypes.js | 56 ------------------- .../{index.android.js => index.android.tsx} | 24 ++++---- src/components/DistanceMapView/index.js | 15 ----- src/components/DistanceMapView/index.tsx | 25 +++++++++ src/components/DistanceMapView/types.ts | 8 +++ 5 files changed, 46 insertions(+), 82 deletions(-) delete mode 100644 src/components/DistanceMapView/distanceMapViewPropTypes.js rename src/components/DistanceMapView/{index.android.js => index.android.tsx} (64%) delete mode 100644 src/components/DistanceMapView/index.js create mode 100644 src/components/DistanceMapView/index.tsx create mode 100644 src/components/DistanceMapView/types.ts diff --git a/src/components/DistanceMapView/distanceMapViewPropTypes.js b/src/components/DistanceMapView/distanceMapViewPropTypes.js deleted file mode 100644 index f7a3bab1879e..000000000000 --- a/src/components/DistanceMapView/distanceMapViewPropTypes.js +++ /dev/null @@ -1,56 +0,0 @@ -import PropTypes from 'prop-types'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; - -const propTypes = { - // Public access token to be used to fetch map data from Mapbox. - accessToken: PropTypes.string.isRequired, - - // Style applied to MapView component. Note some of the View Style props are not available on ViewMap - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), - - // Link to the style JSON document. - styleURL: PropTypes.string, - - // Whether map can tilt in the vertical direction. - pitchEnabled: PropTypes.bool, - - // Padding to apply when the map is adjusted to fit waypoints and directions - mapPadding: PropTypes.number, - - // Initial coordinate and zoom level - initialState: PropTypes.shape({ - location: PropTypes.arrayOf(PropTypes.number).isRequired, - zoom: PropTypes.number.isRequired, - }), - - // Locations on which to put markers - waypoints: PropTypes.arrayOf( - PropTypes.shape({ - id: PropTypes.string, - coordinate: PropTypes.arrayOf(PropTypes.number), - markerComponent: sourcePropTypes, - }), - ), - - // List of coordinates which together forms a direction. - directionCoordinates: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.number)), - - // Callback to call when the map is idle / ready - onMapReady: PropTypes.func, - - // Optional additional styles to be applied to the overlay - overlayStyle: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), -}; - -const defaultProps = { - styleURL: undefined, - pitchEnabled: false, - mapPadding: 0, - initialState: undefined, - waypoints: undefined, - directionCoordinates: undefined, - onMapReady: () => {}, - overlayStyle: undefined, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/DistanceMapView/index.android.js b/src/components/DistanceMapView/index.android.tsx similarity index 64% rename from src/components/DistanceMapView/index.android.js rename to src/components/DistanceMapView/index.android.tsx index fa40bd50673e..39eb66908e3a 100644 --- a/src/components/DistanceMapView/index.android.js +++ b/src/components/DistanceMapView/index.android.tsx @@ -1,18 +1,15 @@ import React, {useState} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import {StyleSheet, View} from 'react-native'; import BlockingView from '@components/BlockingViews/BlockingView'; import * as Expensicons from '@components/Icon/Expensicons'; import MapView from '@components/MapView'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as distanceMapViewPropTypes from './distanceMapViewPropTypes'; +import DistanceMapViewProps from './types'; -function DistanceMapView(props) { +function DistanceMapView({accessToken, style, userLocation, directionCoordinates, initialState, mapPadding, pitchEnabled, styleURL, waypoints, overlayStyle}: DistanceMapViewProps) { const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const [isMapReady, setIsMapReady] = useState(false); const {isOffline} = useNetwork(); const {translate} = useLocalize(); @@ -20,8 +17,15 @@ function DistanceMapView(props) { return ( <> { if (isMapReady) { return; @@ -30,7 +34,7 @@ function DistanceMapView(props) { }} /> {!isMapReady && ( - + ; -} - -DistanceMapView.propTypes = distanceMapViewPropTypes.propTypes; -DistanceMapView.defaultProps = distanceMapViewPropTypes.defaultProps; -DistanceMapView.displayName = 'DistanceMapView'; - -export default DistanceMapView; diff --git a/src/components/DistanceMapView/index.tsx b/src/components/DistanceMapView/index.tsx new file mode 100644 index 000000000000..6c1f03eaf1f3 --- /dev/null +++ b/src/components/DistanceMapView/index.tsx @@ -0,0 +1,25 @@ +import React from 'react'; +import MapView from '@components/MapView'; +import DistanceMapViewProps from './types'; + +function DistanceMapView({accessToken, style, userLocation, directionCoordinates, initialState, mapPadding, onMapReady, pitchEnabled, styleURL, waypoints}: DistanceMapViewProps) { + // Here we want to omit the overlayStyle prop + return ( + + ); +} + +DistanceMapView.displayName = 'DistanceMapView'; + +export default DistanceMapView; diff --git a/src/components/DistanceMapView/types.ts b/src/components/DistanceMapView/types.ts new file mode 100644 index 000000000000..368463ba6d76 --- /dev/null +++ b/src/components/DistanceMapView/types.ts @@ -0,0 +1,8 @@ +import {StyleProp, ViewStyle} from 'react-native'; +import {ComponentProps} from '@components/MapView/types'; + +type DistanceMapViewProps = ComponentProps & { + overlayStyle: StyleProp; +}; + +export default DistanceMapViewProps; From e3474d39890213034a6556ac085a1db395f3c6c0 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 24 Dec 2023 00:54:23 +0100 Subject: [PATCH 051/124] fix: comment --- src/components/CustomStatusBarAndBackground/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index 34362d571272..73bed8653f9e 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -100,7 +100,7 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack ); // Add navigation state listeners to update the status bar every time the route changes - // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properyl + // We have to pass a count as the listener id, because "react-navigation" somehow doesn't remove listeners properly useEffect(() => { if (isDisabled) { return; From bebf6a952d8fd6863394682f0be61841e14b9588 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 24 Dec 2023 00:59:14 +0100 Subject: [PATCH 052/124] fix: remove unnecessary effect --- .../CustomStatusBarAndBackground/index.tsx | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index 73bed8653f9e..74a12c836e91 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -41,6 +41,8 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack useAnimatedReaction( () => statusBarAnimation.value, (current, previous) => { + console.log('reaction'); + // Do not run if either of the animated value is null // or previous animated value is greater than or equal to the current one if (previous === null || current === null || current <= previous) { @@ -52,6 +54,9 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack ); const listenerCount = useRef(0); + + // Updates the status bar style and background color depending on the current route and theme + // This callback is triggered everytime the route changes or the theme changes const updateStatusBarStyle = useCallback( (listenerId?: number) => { // Check if this function is either called through the current navigation listener or the general useEffect which listens for theme changes. @@ -113,15 +118,6 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack return () => navigationRef.removeListener('state', listener); }, [isDisabled, theme.appBG, updateStatusBarStyle]); - // Update the status bar style everytime the theme changes - useEffect(() => { - if (isDisabled) { - return; - } - - updateStatusBarStyle(); - }, [isDisabled, theme, updateStatusBarStyle]); - // Update the global background (on web) everytime the theme changes. // The background of the html element needs to be updated, otherwise you will see a big contrast when resizing the window or when the keyboard is open on iOS web. useEffect(() => { From 1642c5dcdfa8fedb8316b4323142a33d83f9cb81 Mon Sep 17 00:00:00 2001 From: Christoph Pader Date: Sun, 24 Dec 2023 00:59:23 +0100 Subject: [PATCH 053/124] remove log --- src/components/CustomStatusBarAndBackground/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/CustomStatusBarAndBackground/index.tsx b/src/components/CustomStatusBarAndBackground/index.tsx index 74a12c836e91..f66a0204ac5e 100644 --- a/src/components/CustomStatusBarAndBackground/index.tsx +++ b/src/components/CustomStatusBarAndBackground/index.tsx @@ -41,8 +41,6 @@ function CustomStatusBarAndBackground({isNested = false}: CustomStatusBarAndBack useAnimatedReaction( () => statusBarAnimation.value, (current, previous) => { - console.log('reaction'); - // Do not run if either of the animated value is null // or previous animated value is greater than or equal to the current one if (previous === null || current === null || current <= previous) { From c734dd892da4da92a3f0febe9ae41f5760be5f69 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 25 Dec 2023 11:34:10 +0700 Subject: [PATCH 054/124] Remove useless logic in get active route --- src/libs/Navigation/Navigation.ts | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 8ae133ec998d..56223cdc1a7e 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -93,20 +93,11 @@ function getDistanceFromPathInRootNavigator(path: string): number { /** Returns the current active route */ function getActiveRoute(): string { const currentRoute = navigationRef.current && navigationRef.current.getCurrentRoute(); - if (!currentRoute?.name) { - return ''; - } if (currentRoute?.path) { return currentRoute.path; } - const routeFromState = getPathFromState(navigationRef.getRootState(), linkingConfig.config); - - if (routeFromState) { - return routeFromState; - } - return ''; } From 99989a3486156699ea502bdf8c9c70563c85a83a Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 26 Dec 2023 15:37:54 +0700 Subject: [PATCH 055/124] fix: add fallback in get active route --- src/libs/Navigation/Navigation.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 82eb728d0021..b908349d1fe8 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -96,6 +96,12 @@ function getActiveRoute(): string { return currentRoute.path; } + const routeFromState = getPathFromState(navigationRef.getRootState(), linkingConfig.config); + + if (routeFromState) { + return routeFromState; + } + return ''; } From e18a96337df33c9e2b225a7882a1363804270c23 Mon Sep 17 00:00:00 2001 From: Nam Le Date: Tue, 26 Dec 2023 18:07:43 +0700 Subject: [PATCH 056/124] add is local return --- src/libs/ReceiptUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReceiptUtils.ts b/src/libs/ReceiptUtils.ts index 737e5ddc2e31..cb987724ee19 100644 --- a/src/libs/ReceiptUtils.ts +++ b/src/libs/ReceiptUtils.ts @@ -44,7 +44,7 @@ function getThumbnailAndImageURIs(transaction: Transaction, receiptPath: string // For local files, we won't have a thumbnail yet if (isReceiptImage && (path.startsWith('blob:') || path.startsWith('file:'))) { - return {thumbnail: null, image: path}; + return {thumbnail: null, image: path, isLocalFile: true}; } if (isReceiptImage) { From e28d42437e8aa59bb1f9ea5de466abc98aca048d Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 26 Dec 2023 21:35:10 +0700 Subject: [PATCH 057/124] add right label --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 5c7ea712e772..cd0ddb04cdac 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -25,6 +25,7 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import usePermissions from '@hooks/usePermissions'; import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import categoryPropTypes from './categoryPropTypes'; @@ -239,6 +240,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const theme = useTheme(); const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); + const {canUseViolations} = usePermissions(); const isTypeRequest = iouType === CONST.IOU.TYPE.REQUEST; const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; @@ -722,6 +724,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ titleStyle={styles.flex1} disabled={didConfirm} interactive={!isReadOnly} + rightLabel={canUseViolations && Boolean(policy.requiresCategory) ? translate('common.required') : ''} /> )} {shouldShowTags && ( @@ -736,6 +739,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ style={[styles.moneyRequestMenuItem]} disabled={didConfirm} interactive={!isReadOnly} + rightLabel={canUseViolations && Boolean(policy.requiresCategory) ? translate('common.required') : ''} /> )} {shouldShowBillable && ( From bad255d478411a2c0a7ba4730eff07ccdcc8f72b Mon Sep 17 00:00:00 2001 From: Dylan Date: Tue, 26 Dec 2023 21:50:49 +0700 Subject: [PATCH 058/124] fix lint --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 6e3cee14fc46..4ea1064ef37c 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -7,6 +7,7 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; +import usePermissions from '@hooks/usePermissions'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -25,7 +26,6 @@ import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import usePermissions from '@hooks/usePermissions'; import Button from './Button'; import ButtonWithDropdownMenu from './ButtonWithDropdownMenu'; import categoryPropTypes from './categoryPropTypes'; From dcccbd4490601391a4355aef9cdaa7315a063990 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 27 Dec 2023 02:03:53 +0700 Subject: [PATCH 059/124] fix add OnyxEntry --- src/libs/ReportUtils.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index e9d01c11e3b6..fc0388431edf 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1839,7 +1839,12 @@ function canEditMoneyRequest(reportAction: OnyxEntry, fieldToEdit * Checks if the current user can edit the provided property of a money request * */ -function canEditFieldOfMoneyRequest(reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf, transaction?: Transaction): boolean { +function canEditFieldOfMoneyRequest( + reportAction: OnyxEntry, + reportID: string, + fieldToEdit: ValueOf, + transaction?: OnyxEntry, +): boolean { // A list of fields that cannot be edited by anyone, once a money request has been settled const nonEditableFieldsWhenSettled: string[] = [ CONST.EDIT_REQUEST_FIELD.AMOUNT, From 1b3bcae10541859ac79af2e4ca3a8dd236283ae2 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 26 Dec 2023 22:18:38 +0100 Subject: [PATCH 060/124] Update the hours handling logic --- src/components/TimePicker/TimePicker.js | 85 ++++++++++++------------- 1 file changed, 41 insertions(+), 44 deletions(-) diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index b0862fb08618..50c7bdfae1ce 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -127,76 +127,73 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { // This function receive value from hour input and validate it // The valid format is HH(from 00 to 12). If the user input 9, it will be 09. If user try to change 09 to 19 it would skip the first character const handleHourChange = (text) => { - if (_.isEmpty(text)) { + const trimmedText = text.trim(); + + if (_.isEmpty(trimmedText)) { resetHours(); return; } - const isOnlyNumericValue = /^\d+$/.test(text.trim()); + const isOnlyNumericValue = /^\d+$/.test(trimmedText); if (!isOnlyNumericValue) { return; } - // Remove non-numeric characters. - const filteredText = text.replace(/[^0-9]/g, ''); - - let newHour = hours; - let newSelection = selectionHour.start; + let newHour; + let newSelection; if (selectionHour.start === 0 && selectionHour.end === 0) { // The cursor is at the start of hours - // When the user is entering text, the filteredText consists of three numbers - const formattedText = `${filteredText[0]}${filteredText[2] || 0}`; - if (formattedText > 12 && formattedText <= 24) { - // The hour is between 12 and 24 – switch AM to PM. - newHour = String(formattedText - 12).padStart(2, '0'); - newSelection = 2; - setAmPmValue(CONST.TIME_PERIOD.PM); - } else if (formattedText > 24) { - newHour = `0${formattedText[1]}`; - newSelection = 2; - } else { - newHour = `${formattedText[0]}${formattedText[1]}`; + const firstDigit = trimmedText[0]; + const secondDigit = trimmedText[2]; + + if (firstDigit <= 1) { + /* + The first entered digit is 0 or 1. + If the first digit is 0, we can safely append the second digit. + If the first digit is 1, we must check the second digit to ensure it is not greater than 2, amd replace it with 0 otherwise. + */ + newHour = `${firstDigit}${firstDigit === "1" && secondDigit > 1 ? 0 : secondDigit}`; newSelection = 1; + } else { + /* + The first entered digit is 2-9. + We should replace the whole value by prepending 0 to the entered digit. + */ + newHour = `0${firstDigit}`; + newSelection = 2; } } else if (selectionHour.start === 1 && selectionHour.end === 1) { // The cursor is in-between the digits - const formattedText = `${filteredText[0]}${filteredText[1]}`; - - if (filteredText.length < 2) { - // If we remove a value, prepend 0. - newHour = `0${filteredText}`; + if (trimmedText.length < 2) { + // We have removed the first digit. Replace it with 0 and move the cursor to the start. + newHour = `0${trimmedText}`; newSelection = 0; - // If the second digit is > 2, replace the hour with 0 and the second digit. - } else if (formattedText > 12 && formattedText <= 24) { - newHour = String(formattedText - 12).padStart(2, '0'); - newSelection = 2; - setAmPmValue(CONST.TIME_PERIOD.PM); - } else if (formattedText > 24) { - newHour = `0${filteredText[1]}`; - newSelection = 2; } else { - newHour = `${filteredText[0]}${filteredText[1]}`; - setHours(newHour); + newHour = `${trimmedText[0]}${trimmedText[1] || 0}`; newSelection = 2; } - } else if (filteredText.length <= 1 && filteredText < 2) { + } else if (trimmedText.length <= 1 && trimmedText <= 1) { /* - The filtered text is either 0 or 1. We must check the length of the filtered text to avoid incorrectly handling e.g. "01" as "1" + The trimmed text is either 0 or 1. We are either replacing hours with a single digit, or removing the last digit. In both cases, we should append 0 to the remaining value. + Note: we must check the length of the filtered text to avoid incorrectly handling e.g. "01" as "1". */ - newHour = `${filteredText}0`; + newHour = `${trimmedText}0`; newSelection = 1; - } else if (filteredText > 12 && filteredText <= 24) { - // We are replacing hours with a value between 12 and 24. Switch AM to PM - newHour = String(filteredText - 12).padStart(2, '0'); + } else { + newHour = trimmedText; newSelection = 2; + } + + if (newHour > 24) { + newHour = hours; + } else if (newHour > 12) { + newHour = String(newHour - 12).padStart(2, '0'); setAmPmValue(CONST.TIME_PERIOD.PM); - } else if (filteredText.length <= 2) { - // We are replacing hours with a value either 2-11, or 24+. Minimize the value to 12 and prepend 0 if needed - newHour = String(Math.min(filteredText, 12)).padStart(2, '0'); - newSelection = 2; + } else { + newHour = newHour.padStart(2, '0'); } setHours(newHour); From 37b65591871ebd6ae6ef233d86080db851e7b6bf Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Tue, 26 Dec 2023 23:01:35 +0100 Subject: [PATCH 061/124] Update the minutes handling logic --- src/components/TimePicker/TimePicker.js | 97 +++++++++++++------------ 1 file changed, 52 insertions(+), 45 deletions(-) diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 50c7bdfae1ce..a9dd01e52d77 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -87,7 +87,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const [selectionHour, setSelectionHour] = useState({start: 0, end: 0}); const [selectionMinute, setSelectionMinute] = useState({start: 2, end: 2}); // we focus it by default so need to have selection on the end const [hours, setHours] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).hour); - const [minute, setMinute] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).minute); + const [minutes, setMinutes] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).minute); const [amPmValue, setAmPmValue] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).period); const hourInputRef = useRef(null); @@ -107,11 +107,11 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const validate = useCallback( (time) => { - const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({timeString: time || `${hours}:${minute} ${amPmValue}`, dateTimeString: defaultValue}); + const isValid = DateUtils.isTimeAtLeastOneMinuteInFuture({timeString: time || `${hours}:${minutes} ${amPmValue}`, dateTimeString: defaultValue}); setError(!isValid); return isValid; }, - [hours, minute, amPmValue, defaultValue], + [hours, minutes, amPmValue, defaultValue], ); const resetHours = () => { @@ -120,7 +120,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { }; const resetMinutes = () => { - setMinute('00'); + setMinutes('00'); setSelectionMinute({start: 0, end: 0}); }; @@ -153,13 +153,10 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { If the first digit is 0, we can safely append the second digit. If the first digit is 1, we must check the second digit to ensure it is not greater than 2, amd replace it with 0 otherwise. */ - newHour = `${firstDigit}${firstDigit === "1" && secondDigit > 1 ? 0 : secondDigit}`; + newHour = `${firstDigit}${firstDigit === '1' && secondDigit > 1 ? 0 : secondDigit}`; newSelection = 1; } else { - /* - The first entered digit is 2-9. - We should replace the whole value by prepending 0 to the entered digit. - */ + // The first entered digit is 2-9. We should replace the whole value by prepending 0 to the entered digit. newHour = `0${firstDigit}`; newSelection = 2; } @@ -203,63 +200,73 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } }; - // This function receive value from minute input and validate it - // The valid format is MM(from 00 to 59). If the user input 9, it will be 09. If user try to change 09 to 99 it would skip the character + /* + This function receives value from the minutes input and validates it. + The valid format is MM(from 00 to 59). If the user enters 9, it will be prepended to 09. If the user tries to change 09 to 99, it would skip the character + */ const handleMinutesChange = (text) => { - if (_.isEmpty(text)) { + const trimmedText = text.trim(); + + if (_.isEmpty(trimmedText)) { resetMinutes(); return; } - const isOnlyNumericValue = /^\d+$/.test(text.trim()); + const isOnlyNumericValue = /^\d+$/.test(trimmedText); if (!isOnlyNumericValue) { return; } - // Remove non-numeric characters. - const filteredText = text.replace(/[^0-9]/g, ''); - - let newMinute = minute; - let newSelection = selectionMinute.start; + let newMinute; + let newSelection; if (selectionMinute.start === 0 && selectionMinute.end === 0) { // The cursor is at the start of minutes - const formattedText = `${filteredText[0]}${filteredText[2] || 0}`; - if (filteredText[0] >= 6) { - newMinute = `0${formattedText[1]}`; - newSelection = 2; - } else { - newMinute = `${formattedText[0]}${formattedText[1]}`; + const firstDigit = trimmedText[0]; + if (firstDigit <= 5) { + // The first entered digit is 0-5, we can safely append the second digit. + newMinute = `${firstDigit}${trimmedText[2] || 0}`; newSelection = 1; + } else { + // The first entered digit is 6-9. We should replace the whole value by prepending 0 to the entered digit. + newMinute = `0${firstDigit}`; + newSelection = 2; } } else if (selectionMinute.start === 1 && selectionMinute.end === 1) { // The cursor is in-between the digits - if (filteredText.length < 2) { - // If we remove a value, prepend 0. - newMinute = `0${filteredText}`; + if (trimmedText.length < 2) { + // We have removed the first digit. Replace it with 0 and move the cursor to the start. + newMinute = `0${trimmedText}`; newSelection = 0; - setSelectionHour({start: 2, end: 2}); - hourInputRef.current.focus(); } else { - newMinute = `${filteredText[0]}${filteredText[1]}`; + newMinute = `${trimmedText[0]}${trimmedText[1] || 0}`; newSelection = 2; } - } else if (filteredText.length <= 1 && filteredText <= 5) { + } else if (trimmedText.length <= 1 && trimmedText <= 5) { /* - The filtered text is from 0 to 5. We must check the length of the filtered text to avoid incorrectly handling e.g. "01" as "1" + The trimmed text is from 0 to 5. We are either replacing minutes with a single digit, or removing the last digit. In both cases, we should append 0 to the remaining value. + Note: we must check the length of the filtered text to avoid incorrectly handling e.g. "01" as "1" */ - newMinute = `${filteredText}0`; + newMinute = `${trimmedText}0`; newSelection = 1; - } else if (filteredText.length <= 2) { - // We are replacing minutes with a value of 6+. Minimize the value to 59 and prepend 0 if needed - newMinute = String(Math.min(filteredText, 59)).padStart(2, '0'); + } else { + newMinute = trimmedText; newSelection = 2; } - setMinute(newMinute); + if (newMinute > 59) { + newMinute = minutes; + } else { + newMinute = newMinute.padStart(2, '0'); + } + + setMinutes(newMinute); setSelectionMinute({start: newSelection, end: newSelection}); + if (newSelection === 0) { + focusHourInputOnLastCharacter(); + } }; /** @@ -300,8 +307,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { return; } - const newMinute = replaceWithZeroAtPosition(minute, selectionMinute.start); - setMinute(newMinute); + const newMinute = replaceWithZeroAtPosition(minutes, selectionMinute.start); + setMinutes(newMinute); setSelectionMinute(decreaseBothSelectionByOne(selectionMinute)); } return; @@ -311,11 +318,11 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { if (isHourFocused) { handleHourChange(insertAtPosition(hours, trimmedKey, selectionHour.start, selectionHour.end)); } else if (isMinuteFocused) { - handleMinutesChange(insertAtPosition(minute, trimmedKey, selectionMinute.start, selectionMinute.end)); + handleMinutesChange(insertAtPosition(minutes, trimmedKey, selectionMinute.start, selectionMinute.end)); } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [minute, hours, selectionHour, selectionMinute], + [minutes, hours, selectionHour, selectionMinute], ); useEffect(() => { @@ -378,12 +385,12 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { }, [canUseTouchScreen, updateAmountNumberPad]); useEffect(() => { - onInputChange(`${hours}:${minute} ${amPmValue}`); + onInputChange(`${hours}:${minutes} ${amPmValue}`); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [hours, minute, amPmValue]); + }, [hours, minutes, amPmValue]); const handleSubmit = () => { - const time = `${hours}:${minute} ${amPmValue}`; + const time = `${hours}:${minutes} ${amPmValue}`; const isValid = validate(time); if (isValid) { @@ -425,7 +432,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { Date: Wed, 27 Dec 2023 09:43:13 +0700 Subject: [PATCH 062/124] Update src/libs/Navigation/Navigation.ts Co-authored-by: Abdelhafidh Belalia <16493223+s77rt@users.noreply.github.com> --- src/libs/Navigation/Navigation.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index b908349d1fe8..56f1290d97f6 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -115,8 +115,10 @@ function getActiveRoute(): string { * @return is active */ function isActiveRoute(routePath: Route): boolean { - // On web, we remove First forward slash from the URL before matching - return getActiveRoute().startsWith('/') ? getActiveRoute().substring(1) === routePath : getActiveRoute() === routePath; + let activeRoute = getActiveRoute(); + activeRoute = activeRoute.charAt(0) === '/' ? activeRoute.substring(1) : activeRoute; + + return activeRoute === routePath; } /** From 1eb6e35cf7b02e3ab0fdf798e960c9338d2a541a Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 27 Dec 2023 10:03:43 +0700 Subject: [PATCH 063/124] fix get active route --- src/libs/Navigation/Navigation.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 56f1290d97f6..28ac2add6538 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -92,6 +92,10 @@ function getDistanceFromPathInRootNavigator(path: string): number { function getActiveRoute(): string { const currentRoute = navigationRef.current && navigationRef.current.getCurrentRoute(); + if (!currentRoute?.name) { + return ''; + } + if (currentRoute?.path) { return currentRoute.path; } @@ -116,7 +120,7 @@ function getActiveRoute(): string { */ function isActiveRoute(routePath: Route): boolean { let activeRoute = getActiveRoute(); - activeRoute = activeRoute.charAt(0) === '/' ? activeRoute.substring(1) : activeRoute; + activeRoute = activeRoute.startsWith('/') ? activeRoute.substring(1) : activeRoute; return activeRoute === routePath; } From 71638073b2bc9c8cab9f55b742222fb76b994d21 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 27 Dec 2023 10:04:38 +0700 Subject: [PATCH 064/124] fix lint --- src/libs/Navigation/Navigation.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/libs/Navigation/Navigation.ts b/src/libs/Navigation/Navigation.ts index 28ac2add6538..cb9cc104285f 100644 --- a/src/libs/Navigation/Navigation.ts +++ b/src/libs/Navigation/Navigation.ts @@ -91,7 +91,6 @@ function getDistanceFromPathInRootNavigator(path: string): number { /** Returns the current active route */ function getActiveRoute(): string { const currentRoute = navigationRef.current && navigationRef.current.getCurrentRoute(); - if (!currentRoute?.name) { return ''; } From 22e80a4a075a2eb967b790b95d916b606b3d89ad Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 27 Dec 2023 15:03:51 +0700 Subject: [PATCH 065/124] optimize filter --- src/pages/RoomInvitePage.js | 15 +++++++++------ src/pages/workspace/WorkspaceInvitePage.js | 17 ++++++++++------- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index 52b27b7656f4..aebdec047895 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -94,13 +94,16 @@ function RoomInvitePage(props) { let indexOffset = 0; // Filter all options that is a part of the search term or in the personal details - const filterSelectedOptions = _.filter(selectedOptions, (option) => { - const accountID = lodashGet(option, 'accountID', null); - const isOptionInPersonalDetails = _.some(personalDetails, (personalDetail) => personalDetail.accountID === accountID); + let filterSelectedOptions = selectedOptions; + if (searchTerm !== '') { + filterSelectedOptions = _.filter(selectedOptions, (option) => { + const accountID = lodashGet(option, 'accountID', null); + const isOptionInPersonalDetails = _.some(personalDetails, (personalDetail) => personalDetail.accountID === accountID); - const isPartOfSearchTerm = option.text.toLowerCase().includes(searchTerm.trim().toLowerCase()); - return isPartOfSearchTerm || isOptionInPersonalDetails; - }); + const isPartOfSearchTerm = option.text.toLowerCase().includes(searchTerm.trim().toLowerCase()); + return isPartOfSearchTerm || isOptionInPersonalDetails; + }); + } sections.push({ title: undefined, diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 94f3ba5e7e4e..589c4971506b 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -136,13 +136,16 @@ function WorkspaceInvitePage(props) { let indexOffset = 0; // Filter all options that is a part of the search term or in the personal details - const filterSelectedOptions = _.filter(selectedOptions, (option) => { - const accountID = lodashGet(option, 'accountID', null); - const isOptionInPersonalDetails = _.some(personalDetails, (personalDetail) => personalDetail.accountID === accountID); - - const isPartOfSearchTerm = option.text.toLowerCase().includes(searchTerm.trim().toLowerCase()); - return isPartOfSearchTerm || isOptionInPersonalDetails; - }); + let filterSelectedOptions = selectedOptions; + if (searchTerm !== '') { + filterSelectedOptions = _.filter(selectedOptions, (option) => { + const accountID = lodashGet(option, 'accountID', null); + const isOptionInPersonalDetails = _.some(personalDetails, (personalDetail) => personalDetail.accountID === accountID); + + const isPartOfSearchTerm = option.text.toLowerCase().includes(searchTerm.trim().toLowerCase()); + return isPartOfSearchTerm || isOptionInPersonalDetails; + }); + } sections.push({ title: undefined, From e7976ad300639c8582aab3d6b609e07a9e05a1e9 Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 27 Dec 2023 21:25:45 +0530 Subject: [PATCH 066/124] Change style --- .../DistanceMapView/index.android.tsx | 13 +++---------- src/components/DistanceMapView/index.tsx | 19 +++---------------- 2 files changed, 6 insertions(+), 26 deletions(-) diff --git a/src/components/DistanceMapView/index.android.tsx b/src/components/DistanceMapView/index.android.tsx index 39eb66908e3a..e46b1c0c21db 100644 --- a/src/components/DistanceMapView/index.android.tsx +++ b/src/components/DistanceMapView/index.android.tsx @@ -8,24 +8,17 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import DistanceMapViewProps from './types'; -function DistanceMapView({accessToken, style, userLocation, directionCoordinates, initialState, mapPadding, pitchEnabled, styleURL, waypoints, overlayStyle}: DistanceMapViewProps) { +function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) { const styles = useThemeStyles(); const [isMapReady, setIsMapReady] = useState(false); const {isOffline} = useNetwork(); const {translate} = useLocalize(); + // eslint-disable-next-line react/jsx-props-no-spreading return ( <> { if (isMapReady) { return; diff --git a/src/components/DistanceMapView/index.tsx b/src/components/DistanceMapView/index.tsx index 6c1f03eaf1f3..2abdc29865b0 100644 --- a/src/components/DistanceMapView/index.tsx +++ b/src/components/DistanceMapView/index.tsx @@ -2,22 +2,9 @@ import React from 'react'; import MapView from '@components/MapView'; import DistanceMapViewProps from './types'; -function DistanceMapView({accessToken, style, userLocation, directionCoordinates, initialState, mapPadding, onMapReady, pitchEnabled, styleURL, waypoints}: DistanceMapViewProps) { - // Here we want to omit the overlayStyle prop - return ( - - ); +function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) { + // eslint-disable-next-line react/jsx-props-no-spreading + return ; } DistanceMapView.displayName = 'DistanceMapView'; From 3c5633f871129d2383fbe999bee52be5e4fe6a82 Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 27 Dec 2023 21:29:55 +0530 Subject: [PATCH 067/124] Fix lint --- src/components/DistanceMapView/index.android.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DistanceMapView/index.android.tsx b/src/components/DistanceMapView/index.android.tsx index e46b1c0c21db..b9dcf2a33a20 100644 --- a/src/components/DistanceMapView/index.android.tsx +++ b/src/components/DistanceMapView/index.android.tsx @@ -14,10 +14,10 @@ function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) { const {isOffline} = useNetwork(); const {translate} = useLocalize(); - // eslint-disable-next-line react/jsx-props-no-spreading return ( <> { if (isMapReady) { From 99b06d50d24e3205a4e70d69443eb82c978665ad Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 28 Dec 2023 10:16:30 +0700 Subject: [PATCH 068/124] add should show loading prop --- .../workspace/WorkspacePageWithSections.js | 21 +++++++++++++++++-- src/pages/workspace/WorkspaceSettingsPage.js | 1 + .../reimburse/WorkspaceRateAndUnitPage.js | 1 + .../reimburse/WorkspaceReimbursePage.js | 1 + 4 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/pages/workspace/WorkspacePageWithSections.js b/src/pages/workspace/WorkspacePageWithSections.js index c51ef44a0c46..a51f7861cba5 100644 --- a/src/pages/workspace/WorkspacePageWithSections.js +++ b/src/pages/workspace/WorkspacePageWithSections.js @@ -63,6 +63,9 @@ const propTypes = { /** Option to use the default scroll view */ shouldUseScrollView: PropTypes.bool, + + /** Option to show the loading page while the API is calling */ + shouldShowLoading: PropTypes.bool, }; const defaultProps = { @@ -74,6 +77,7 @@ const defaultProps = { shouldUseScrollView: false, shouldSkipVBBACall: false, backButtonRoute: '', + shouldShowLoading: true, }; function fetchData(skipVBBACal) { @@ -84,7 +88,20 @@ function fetchData(skipVBBACal) { BankAccounts.openWorkspaceView(); } -function WorkspacePageWithSections({backButtonRoute, children, footer, guidesCallTaskID, headerText, policy, reimbursementAccount, route, shouldUseScrollView, shouldSkipVBBACall, user}) { +function WorkspacePageWithSections({ + backButtonRoute, + children, + footer, + guidesCallTaskID, + headerText, + policy, + reimbursementAccount, + route, + shouldUseScrollView, + shouldSkipVBBACall, + user, + shouldShowLoading, +}) { const styles = useThemeStyles(); useNetwork({onReconnect: () => fetchData(shouldSkipVBBACall)}); @@ -125,7 +142,7 @@ function WorkspacePageWithSections({backButtonRoute, children, footer, guidesCal guidesCallTaskID={guidesCallTaskID} onBackButtonPress={() => Navigation.goBack(backButtonRoute || ROUTES.WORKSPACE_INITIAL.getRoute(policyID))} /> - {isLoading || firstRender.current ? ( + {(isLoading || firstRender.current) && shouldShowLoading ? ( ) : ( <> diff --git a/src/pages/workspace/WorkspaceSettingsPage.js b/src/pages/workspace/WorkspaceSettingsPage.js index 9bc4b755fce9..737a5f1b7cf6 100644 --- a/src/pages/workspace/WorkspaceSettingsPage.js +++ b/src/pages/workspace/WorkspaceSettingsPage.js @@ -99,6 +99,7 @@ function WorkspaceSettingsPage({policy, currencyList, windowWidth, route}) { headerText={translate('workspace.common.settings')} route={route} guidesCallTaskID={CONST.GUIDES_CALL_TASK_IDS.WORKSPACE_SETTINGS} + shouldShowLoading={false} > {(hasVBA) => ( {() => ( {() => } From 0bb341250907e7576f9fd8189a723159b653b0c9 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 28 Dec 2023 16:34:41 +0700 Subject: [PATCH 069/124] create new reset amount function --- src/CONST.ts | 1 + src/libs/actions/IOU.js | 10 +++++++++- src/libs/actions/Transaction.ts | 6 +++--- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index bc0a0c3216f0..cbc0b201ae71 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1166,6 +1166,7 @@ const CONST = { EXPENSIFY: 'Expensify', VBBA: 'ACH', }, + DEFAULT_AMOUNT: 0, TYPE: { SEND: 'send', SPLIT: 'split', diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 258f7948bfa0..f6839e32279a 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -175,7 +175,14 @@ function clearMoneyRequest(transactionID) { * @param {String} currency */ function setMoneyRequestAmount_temporaryForRefactor(transactionID, amount, currency) { - Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, currency ? {amount, currency} : {amount}); + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount, currency}); +} + +/** + * @param {String} transactionID + */ +function resetMoneyRequestAmount_temporaryForRefactor(transactionID) { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {amount: CONST.IOU.DEFAULT_AMOUNT}); } /** @@ -3407,4 +3414,5 @@ export { detachReceipt, getIOUReportID, editMoneyRequest, + resetMoneyRequestAmount_temporaryForRefactor, }; diff --git a/src/libs/actions/Transaction.ts b/src/libs/actions/Transaction.ts index d6b191f2fbe3..951851d72eb3 100644 --- a/src/libs/actions/Transaction.ts +++ b/src/libs/actions/Transaction.ts @@ -59,7 +59,7 @@ function addStop(transactionID: string) { } function saveWaypoint(transactionID: string, index: string, waypoint: RecentWaypoint | null, isDraft = false) { - IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, 0, ''); + IOU.resetMoneyRequestAmount_temporaryForRefactor(transactionID); Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION : ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, { comment: { waypoints: { @@ -102,7 +102,7 @@ function saveWaypoint(transactionID: string, index: string, waypoint: RecentWayp } function removeWaypoint(transaction: Transaction, currentIndex: string, isDraft: boolean) { - IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, 0, ''); + IOU.resetMoneyRequestAmount_temporaryForRefactor(transaction.transactionID); // Index comes from the route params and is a string const index = Number(currentIndex); const existingWaypoints = transaction?.comment?.waypoints ?? {}; @@ -243,7 +243,7 @@ function getRouteForDraft(transactionID: string, waypoints: WaypointCollection) * which will replace the existing ones. */ function updateWaypoints(transactionID: string, waypoints: WaypointCollection, isDraft = false): Promise { - IOU.setMoneyRequestAmount_temporaryForRefactor(transactionID, 0, ''); + IOU.resetMoneyRequestAmount_temporaryForRefactor(transactionID); return Onyx.merge(`${isDraft ? ONYXKEYS.COLLECTION.TRANSACTION_DRAFT : ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, { comment: { waypoints, From 97e20521c5b3711583fe0c80ccd36d9ae00b1de6 Mon Sep 17 00:00:00 2001 From: Dylan Date: Thu, 28 Dec 2023 16:49:51 +0700 Subject: [PATCH 070/124] add comment for new function --- src/libs/actions/IOU.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index ff1ee9cd1180..259483b1a133 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -179,6 +179,7 @@ function setMoneyRequestAmount_temporaryForRefactor(transactionID, amount, curre } /** + * Reset the money request amount, discarding the user-provided value. In the case of distance requests, this will effectively re-enable the default behavior of automatic amount calculation. * @param {String} transactionID */ function resetMoneyRequestAmount_temporaryForRefactor(transactionID) { From 965f2b5ab7f6775be7a6bf4d5d10faab07d1d5d2 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 28 Dec 2023 14:55:15 +0300 Subject: [PATCH 071/124] Fix bug with No emoji list after expanding --- src/pages/home/report/ReportActionCompose/SuggestionEmoji.js | 2 +- src/pages/home/report/ReportActionCompose/SuggestionMention.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js index e35d1e90bd5a..1432991846d5 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js @@ -186,7 +186,7 @@ function SuggestionEmoji({ return; } calculateEmojiSuggestion(selection.end); - }, [selection, calculateEmojiSuggestion, isComposerFocused]); + }, [selection, calculateEmojiSuggestion, isComposerFocused, isComposerFullSize]); const onSelectionChange = useCallback( (e) => { diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index e55b96ad99f5..26a49565e310 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -254,7 +254,7 @@ function SuggestionMention({ } calculateMentionSuggestion(selection.end); - }, [selection, value, previousValue, calculateMentionSuggestion]); + }, [selection, value, previousValue, calculateMentionSuggestion, isComposerFullSize]); const updateShouldShowSuggestionMenuToFalse = useCallback(() => { setSuggestionValues((prevState) => { From 804b01c65121b14689c46be55f399fc3a1b3b9c3 Mon Sep 17 00:00:00 2001 From: someone-here Date: Thu, 28 Dec 2023 18:34:33 +0530 Subject: [PATCH 072/124] Remove Stylesheet.flatten --- src/components/DistanceMapView/index.android.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/DistanceMapView/index.android.tsx b/src/components/DistanceMapView/index.android.tsx index b9dcf2a33a20..38e92163c3eb 100644 --- a/src/components/DistanceMapView/index.android.tsx +++ b/src/components/DistanceMapView/index.android.tsx @@ -1,5 +1,5 @@ import React, {useState} from 'react'; -import {StyleSheet, View} from 'react-native'; +import {View} from 'react-native'; import BlockingView from '@components/BlockingViews/BlockingView'; import * as Expensicons from '@components/Icon/Expensicons'; import MapView from '@components/MapView'; @@ -27,7 +27,7 @@ function DistanceMapView({overlayStyle, ...rest}: DistanceMapViewProps) { }} /> {!isMapReady && ( - + Date: Thu, 28 Dec 2023 18:40:43 +0530 Subject: [PATCH 073/124] Fix ComponentProps -> MapViewProps --- src/components/DistanceMapView/types.ts | 4 ++-- src/components/MapView/index.tsx | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/DistanceMapView/types.ts b/src/components/DistanceMapView/types.ts index 368463ba6d76..c280fa39f406 100644 --- a/src/components/DistanceMapView/types.ts +++ b/src/components/DistanceMapView/types.ts @@ -1,7 +1,7 @@ import {StyleProp, ViewStyle} from 'react-native'; -import {ComponentProps} from '@components/MapView/types'; +import type {MapViewProps} from '@components/MapView/MapViewTypes'; -type DistanceMapViewProps = ComponentProps & { +type DistanceMapViewProps = MapViewProps & { overlayStyle: StyleProp; }; diff --git a/src/components/MapView/index.tsx b/src/components/MapView/index.tsx index f273845fe4c0..d9da7f1dfea3 100644 --- a/src/components/MapView/index.tsx +++ b/src/components/MapView/index.tsx @@ -1,8 +1,8 @@ import React from 'react'; import MapView from './MapView'; -import {ComponentProps} from './types'; +import type {MapViewProps} from './MapViewTypes'; -function MapViewComponent(props: ComponentProps) { +function MapViewComponent(props: MapViewProps) { // eslint-disable-next-line react/jsx-props-no-spreading return ; } From be52246d0ad8ec180ff2151cc57c9190afb6b1ee Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Thu, 28 Dec 2023 18:59:34 +0530 Subject: [PATCH 074/124] Make overlayStyle optional Co-authored-by: VickyStash --- src/components/DistanceMapView/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DistanceMapView/types.ts b/src/components/DistanceMapView/types.ts index c280fa39f406..5ab3dbd8238e 100644 --- a/src/components/DistanceMapView/types.ts +++ b/src/components/DistanceMapView/types.ts @@ -2,7 +2,7 @@ import {StyleProp, ViewStyle} from 'react-native'; import type {MapViewProps} from '@components/MapView/MapViewTypes'; type DistanceMapViewProps = MapViewProps & { - overlayStyle: StyleProp; + overlayStyle?: StyleProp; }; export default DistanceMapViewProps; From bf446748dd001926083551772c60e44165eb6a3f Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Thu, 28 Dec 2023 14:34:06 +0100 Subject: [PATCH 075/124] Handle digits removal by Backspace & Delete --- src/components/TimePicker/TimePicker.js | 46 +++++++++++++++---------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index a9dd01e52d77..082bf4accaeb 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -16,7 +16,6 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import DateUtils from '@libs/DateUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; -import setSelection from './setSelection'; const propTypes = { /** Refs forwarded to the TextInputWithCurrencySymbol */ @@ -89,13 +88,17 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const [hours, setHours] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).hour); const [minutes, setMinutes] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).minute); const [amPmValue, setAmPmValue] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).period); + const [lastPressedKey, setLastPressedKey] = useState(''); const hourInputRef = useRef(null); const minuteInputRef = useRef(null); const focusMinuteInputOnFirstCharacter = useCallback(() => { - const cleanupTimer = setSelection({start: 0, end: 0}, minuteInputRef, setSelectionMinute); - return cleanupTimer; + setSelectionMinute({start: 0, end: 0}); + const timer = setTimeout(() => { + minuteInputRef.current.focus(); + }, 10); + return () => clearTimeout(timer); }, []); const focusHourInputOnLastCharacter = useCallback(() => { setSelectionHour({start: 2, end: 2}); @@ -145,15 +148,18 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { if (selectionHour.start === 0 && selectionHour.end === 0) { // The cursor is at the start of hours const firstDigit = trimmedText[0]; - const secondDigit = trimmedText[2]; + const secondDigit = trimmedText[2] || '0'; - if (firstDigit <= 1) { + if (trimmedText.length === 1) { + newHour = `0${firstDigit}`; + newSelection = 1; + } else if (firstDigit <= 1) { /* The first entered digit is 0 or 1. If the first digit is 0, we can safely append the second digit. If the first digit is 1, we must check the second digit to ensure it is not greater than 2, amd replace it with 0 otherwise. */ - newHour = `${firstDigit}${firstDigit === '1' && secondDigit > 1 ? 0 : secondDigit}`; + newHour = `${firstDigit}${firstDigit === '1' && secondDigit > 2 ? 0 : secondDigit}`; newSelection = 1; } else { // The first entered digit is 2-9. We should replace the whole value by prepending 0 to the entered digit. @@ -162,7 +168,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } } else if (selectionHour.start === 1 && selectionHour.end === 1) { // The cursor is in-between the digits - if (trimmedText.length < 2) { + if (trimmedText.length < 2 && lastPressedKey === 'Backspace') { // We have removed the first digit. Replace it with 0 and move the cursor to the start. newHour = `0${trimmedText}`; newSelection = 0; @@ -180,7 +186,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newHour = `${trimmedText}0`; newSelection = 1; } else { - newHour = trimmedText; + newHour = trimmedText.substring(0, 2).padStart(2, '0'); newSelection = 2; } @@ -189,8 +195,6 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } else if (newHour > 12) { newHour = String(newHour - 12).padStart(2, '0'); setAmPmValue(CONST.TIME_PERIOD.PM); - } else { - newHour = newHour.padStart(2, '0'); } setHours(newHour); @@ -223,7 +227,10 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { if (selectionMinute.start === 0 && selectionMinute.end === 0) { // The cursor is at the start of minutes const firstDigit = trimmedText[0]; - if (firstDigit <= 5) { + if (trimmedText.length === 1) { + newMinute = `0${firstDigit}`; + newSelection = 1; + } else if (firstDigit <= 5) { // The first entered digit is 0-5, we can safely append the second digit. newMinute = `${firstDigit}${trimmedText[2] || 0}`; newSelection = 1; @@ -234,7 +241,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } } else if (selectionMinute.start === 1 && selectionMinute.end === 1) { // The cursor is in-between the digits - if (trimmedText.length < 2) { + if (trimmedText.length < 2 && lastPressedKey === 'Backspace') { // We have removed the first digit. Replace it with 0 and move the cursor to the start. newMinute = `0${trimmedText}`; newSelection = 0; @@ -252,21 +259,16 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { newMinute = `${trimmedText}0`; newSelection = 1; } else { - newMinute = trimmedText; + newMinute = trimmedText.substring(0, 2).padStart(2, '0'); newSelection = 2; } if (newMinute > 59) { newMinute = minutes; - } else { - newMinute = newMinute.padStart(2, '0'); } setMinutes(newMinute); setSelectionMinute({start: newSelection, end: newSelection}); - if (newSelection === 0) { - focusHourInputOnLastCharacter(); - } }; /** @@ -409,6 +411,9 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { placeholder={numberFormat(0)} maxLength={2} formattedAmount={hours} + onKeyPress={(e) => { + setLastPressedKey(e.nativeEvent.key); + }} onChangeAmount={handleHourChange} role={CONST.ACCESSIBILITY_ROLE.TEXT} ref={(ref) => { @@ -433,7 +438,10 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { placeholder={numberFormat(0)} maxLength={2} formattedAmount={minutes} - onKeyPress={handleFocusOnBackspace} + onKeyPress={(e) => { + setLastPressedKey(e.nativeEvent.key); + handleFocusOnBackspace(e); + }} onChangeAmount={handleMinutesChange} role={CONST.ACCESSIBILITY_ROLE.TEXT} ref={(ref) => { From d185bf7253d1ec679e280f8e50090fd15772bee7 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 28 Dec 2023 16:58:21 +0300 Subject: [PATCH 076/124] Update fix --- src/components/AutoCompleteSuggestions/index.tsx | 9 ++++++++- src/components/AutoCompleteSuggestions/types.ts | 3 +++ src/components/EmojiSuggestions.tsx | 15 ++++++++++++++- src/components/MentionSuggestions.tsx | 6 +++++- .../AttachmentPickerWithMenuItems.js | 6 ------ .../ReportActionCompose/ReportActionCompose.js | 1 - .../report/ReportActionCompose/SuggestionEmoji.js | 2 +- .../ReportActionCompose/SuggestionMention.js | 2 +- 8 files changed, 32 insertions(+), 12 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index baca4011a177..5baa901319ea 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -14,7 +14,7 @@ import type {AutoCompleteSuggestionsProps} from './types'; * On the native platform, tapping on auto-complete suggestions will not blur the main input. */ -function AutoCompleteSuggestions({measureParentContainer = () => {}, ...props}: AutoCompleteSuggestionsProps) { +function AutoCompleteSuggestions({measureParentContainer = () => {}, isComposerFullSize, ...props}: AutoCompleteSuggestionsProps) { const StyleUtils = useStyleUtils(); const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); @@ -44,6 +44,13 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w})); }, [measureParentContainer, windowHeight, windowWidth]); + React.useEffect(() => { + if (!width) { + return; + } + measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w})); + }, [isComposerFullSize]); + const componentToRender = ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index 9130f5139d71..0632239c35b3 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -33,6 +33,9 @@ type AutoCompleteSuggestionsProps = { /** Meaures the parent container's position and dimensions. */ measureParentContainer?: (callback: MeasureParentContainerCallback) => void; + + /** Whether the composer is full size */ + isComposerFullSize?: boolean; }; export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps}; diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 2fcf8e827b4e..8a8bc5f27a62 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -34,6 +34,9 @@ type EmojiSuggestionsProps = { /** Meaures the parent container's position and dimensions. */ measureParentContainer: (callback: MeasureParentContainerCallback) => void; + + /** Whether the composer is full size */ + isComposerFullSize: boolean; }; /** @@ -41,7 +44,16 @@ type EmojiSuggestionsProps = { */ const keyExtractor = (item: SimpleEmoji, index: number): string => `${item.name}+${index}}`; -function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferredSkinToneIndex, highlightedEmojiIndex = 0, measureParentContainer = () => {}}: EmojiSuggestionsProps) { +function EmojiSuggestions({ + emojis, + onSelect, + prefix, + isEmojiPickerLarge, + preferredSkinToneIndex, + highlightedEmojiIndex = 0, + measureParentContainer = () => {}, + isComposerFullSize, +}: EmojiSuggestionsProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); /** @@ -78,6 +90,7 @@ function EmojiSuggestions({emojis, onSelect, prefix, isEmojiPickerLarge, preferr return ( void; + + /** Whether the composer is full size */ + isComposerFullSize: boolean; }; /** @@ -51,7 +54,7 @@ type MentionSuggestionsProps = { */ const keyExtractor = (item: Mention) => item.alternateText; -function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSelect, isMentionPickerLarge, measureParentContainer = () => {}}: MentionSuggestionsProps) { +function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSelect, isMentionPickerLarge, measureParentContainer = () => {}, isComposerFullSize}: MentionSuggestionsProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -128,6 +131,7 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe return ( { e.preventDefault(); - updateShouldShowSuggestionMenuToFalse(); Report.setIsComposerFullSize(reportID, false); }} // Keep focus on the composer when Collapse button is clicked. @@ -258,7 +253,6 @@ function AttachmentPickerWithMenuItems({ { e.preventDefault(); - updateShouldShowSuggestionMenuToFalse(); Report.setIsComposerFullSize(reportID, true); }} // Keep focus on the composer when Expand button is clicked. diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index b23d9b554488..f9f106eda87e 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -369,7 +369,6 @@ function ReportActionCompose({ reportParticipantIDs={reportParticipantIDs} isFullComposerAvailable={isFullComposerAvailable} isComposerFullSize={isComposerFullSize} - updateShouldShowSuggestionMenuToFalse={updateShouldShowSuggestionMenuToFalse} isBlockedFromConcierge={isBlockedFromConcierge} disabled={disabled} setMenuVisibility={setMenuVisibility} diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js index 1432991846d5..e35d1e90bd5a 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js @@ -186,7 +186,7 @@ function SuggestionEmoji({ return; } calculateEmojiSuggestion(selection.end); - }, [selection, calculateEmojiSuggestion, isComposerFocused, isComposerFullSize]); + }, [selection, calculateEmojiSuggestion, isComposerFocused]); const onSelectionChange = useCallback( (e) => { diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.js index 26a49565e310..e55b96ad99f5 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.js @@ -254,7 +254,7 @@ function SuggestionMention({ } calculateMentionSuggestion(selection.end); - }, [selection, value, previousValue, calculateMentionSuggestion, isComposerFullSize]); + }, [selection, value, previousValue, calculateMentionSuggestion]); const updateShouldShowSuggestionMenuToFalse = useCallback(() => { setSuggestionValues((prevState) => { From dd292b43b10bc0055da8aff63a83afa124635368 Mon Sep 17 00:00:00 2001 From: Yauheni Date: Thu, 28 Dec 2023 17:00:50 +0300 Subject: [PATCH 077/124] Fix eslint issues --- src/components/EmojiSuggestions.tsx | 2 +- src/components/MentionSuggestions.tsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 8a8bc5f27a62..5c98c8283103 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -90,7 +90,7 @@ function EmojiSuggestions({ return ( Date: Thu, 28 Dec 2023 17:20:57 +0300 Subject: [PATCH 078/124] Update AutoCompleteSuggestions --- src/components/AutoCompleteSuggestions/index.tsx | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 5baa901319ea..20fa635969f1 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -42,14 +42,7 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} return; } measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w})); - }, [measureParentContainer, windowHeight, windowWidth]); - - React.useEffect(() => { - if (!width) { - return; - } - measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w})); - }, [isComposerFullSize]); + }, [measureParentContainer, windowHeight, windowWidth, isComposerFullSize]); const componentToRender = ( From 367f9f1dd5f77f1070f25495eb205fc3e8ca96aa Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Thu, 28 Dec 2023 20:39:05 +0100 Subject: [PATCH 079/124] Squash setSelection into one reusable function without using timeouts --- src/components/TimePicker/TimePicker.js | 32 +++++++++---------- src/components/TimePicker/setSelection.ios.ts | 8 ----- src/components/TimePicker/setSelection.ts | 18 ----------- 3 files changed, 16 insertions(+), 42 deletions(-) delete mode 100644 src/components/TimePicker/setSelection.ios.ts delete mode 100644 src/components/TimePicker/setSelection.ts diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 082bf4accaeb..ced018b56b7d 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -74,6 +74,16 @@ function replaceWithZeroAtPosition(originalString, position) { return `${originalString.slice(0, position - 1)}0${originalString.slice(position)}`; } +function setCursorPosition(position, ref, setSelection) { + const selection = { + start: position, + end: position, + }; + setSelection(selection); + ref.current?.setNativeProps({selection}); + ref.current?.focus(); +} + function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const {numberFormat, translate} = useLocalize(); const {isExtraSmallScreenHeight} = useWindowDimensions(); @@ -93,20 +103,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const hourInputRef = useRef(null); const minuteInputRef = useRef(null); - const focusMinuteInputOnFirstCharacter = useCallback(() => { - setSelectionMinute({start: 0, end: 0}); - const timer = setTimeout(() => { - minuteInputRef.current.focus(); - }, 10); - return () => clearTimeout(timer); - }, []); - const focusHourInputOnLastCharacter = useCallback(() => { - setSelectionHour({start: 2, end: 2}); - const timer = setTimeout(() => { - hourInputRef.current.focus(); - }, 10); - return () => clearTimeout(timer); - }, []); + const focusMinuteInputOnFirstCharacter = useCallback(() => setCursorPosition(0, minuteInputRef, setSelectionMinute), []); + const focusHourInputOnLastCharacter = useCallback(() => setCursorPosition(2, hourInputRef, setSelectionHour), []); const validate = useCallback( (time) => { @@ -340,17 +338,19 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { [], ); - const arrowLeftCallback = useCallback(() => { + const arrowLeftCallback = useCallback((e) => { const isMinuteFocused = minuteInputRef.current.isFocused(); if (isMinuteFocused && selectionMinute.start === 0) { + e.preventDefault(); focusHourInputOnLastCharacter(); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [selectionHour, selectionMinute]); - const arrowRightCallback = useCallback(() => { + const arrowRightCallback = useCallback((e) => { const isHourFocused = hourInputRef.current.isFocused(); if (isHourFocused && selectionHour.start === 2) { + e.preventDefault(); focusMinuteInputOnFirstCharacter(); } // eslint-disable-next-line react-hooks/exhaustive-deps diff --git a/src/components/TimePicker/setSelection.ios.ts b/src/components/TimePicker/setSelection.ios.ts deleted file mode 100644 index 0d1dfc004bc7..000000000000 --- a/src/components/TimePicker/setSelection.ios.ts +++ /dev/null @@ -1,8 +0,0 @@ -import {RefObject} from 'react'; -import {TextInput} from 'react-native'; - -export default function setSelection(value: {start: number; end: number}, ref: RefObject) { - ref.current?.focus(); - ref.current?.setNativeProps({selection: value}); - return () => {}; -} diff --git a/src/components/TimePicker/setSelection.ts b/src/components/TimePicker/setSelection.ts deleted file mode 100644 index 36304b408f29..000000000000 --- a/src/components/TimePicker/setSelection.ts +++ /dev/null @@ -1,18 +0,0 @@ -// setSelection.ts -import {RefObject} from 'react'; -import {TextInput} from 'react-native'; - -const setSelection = (value: {start: number; end: number}, ref: RefObject, setSelectionCallback: (value: {start: number; end: number}) => void): (() => void) => { - ref.current?.focus(); - - const timer = setTimeout(() => { - setSelectionCallback(value); - }, 10); - - // Return the cleanup function - return () => { - clearTimeout(timer); - }; -}; - -export default setSelection; From be0342f71f6e15b6ba4e3709a3daa948eee20f02 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Thu, 28 Dec 2023 23:40:08 +0100 Subject: [PATCH 080/124] Revert the setSelection to be ios-specific --- src/components/TimePicker/TimePicker.js | 69 ++++++++++--------- .../TimePicker/setCursorPosition/index.ios.ts | 13 ++++ .../TimePicker/setCursorPosition/index.ts | 11 +++ .../TimePicker/setCursorPosition/types.ts | 6 ++ 4 files changed, 66 insertions(+), 33 deletions(-) create mode 100644 src/components/TimePicker/setCursorPosition/index.ios.ts create mode 100644 src/components/TimePicker/setCursorPosition/index.ts create mode 100644 src/components/TimePicker/setCursorPosition/types.ts diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index ced018b56b7d..827d8851d6f1 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -16,6 +16,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import DateUtils from '@libs/DateUtils'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; +import setCursorPosition from './setCursorPosition'; const propTypes = { /** Refs forwarded to the TextInputWithCurrencySymbol */ @@ -59,7 +60,7 @@ function insertAtPosition(originalString, newSubstring, selectionPositionFrom, s return originalString.slice(0, selectionPositionFrom) + newSubstring + originalString.slice(selectionPositionTo); } -// if we need manually to move selection to the left we need to decrease both selection start and end by one +// if we need to manually move selection to the left, we need to decrease both selection start and end by one function decreaseBothSelectionByOne({start, end}) { if (start === 0) { return {start: 0, end: 0}; @@ -74,16 +75,6 @@ function replaceWithZeroAtPosition(originalString, position) { return `${originalString.slice(0, position - 1)}0${originalString.slice(position)}`; } -function setCursorPosition(position, ref, setSelection) { - const selection = { - start: position, - end: position, - }; - setSelection(selection); - ref.current?.setNativeProps({selection}); - ref.current?.focus(); -} - function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const {numberFormat, translate} = useLocalize(); const {isExtraSmallScreenHeight} = useWindowDimensions(); @@ -98,8 +89,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { const [hours, setHours] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).hour); const [minutes, setMinutes] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).minute); const [amPmValue, setAmPmValue] = useState(() => DateUtils.get12HourTimeObjectFromDate(value).period); - const [lastPressedKey, setLastPressedKey] = useState(''); + const lastPressedKey = useRef(''); const hourInputRef = useRef(null); const minuteInputRef = useRef(null); @@ -166,7 +157,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } } else if (selectionHour.start === 1 && selectionHour.end === 1) { // The cursor is in-between the digits - if (trimmedText.length < 2 && lastPressedKey === 'Backspace') { + if (trimmedText.length < 2 && lastPressedKey.current === 'Backspace') { // We have removed the first digit. Replace it with 0 and move the cursor to the start. newHour = `0${trimmedText}`; newSelection = 0; @@ -239,7 +230,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } } else if (selectionMinute.start === 1 && selectionMinute.end === 1) { // The cursor is in-between the digits - if (trimmedText.length < 2 && lastPressedKey === 'Backspace') { + if (trimmedText.length < 2 && lastPressedKey.current === 'Backspace') { // We have removed the first digit. Replace it with 0 and move the cursor to the start. newMinute = `0${trimmedText}`; newSelection = 0; @@ -338,23 +329,35 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { [], ); - const arrowLeftCallback = useCallback((e) => { - const isMinuteFocused = minuteInputRef.current.isFocused(); - if (isMinuteFocused && selectionMinute.start === 0) { - e.preventDefault(); - focusHourInputOnLastCharacter(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectionHour, selectionMinute]); - const arrowRightCallback = useCallback((e) => { - const isHourFocused = hourInputRef.current.isFocused(); + const arrowLeftCallback = useCallback( + (e) => { + const isMinuteFocused = minuteInputRef.current.isFocused(); + if (isMinuteFocused && selectionMinute.start === 0) { + if (e) { + // Check e to be truthy to avoid crashing on Android (e is undefined there) + e.preventDefault(); + } + focusHourInputOnLastCharacter(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, + [selectionHour, selectionMinute], + ); + const arrowRightCallback = useCallback( + (e) => { + const isHourFocused = hourInputRef.current.isFocused(); - if (isHourFocused && selectionHour.start === 2) { - e.preventDefault(); - focusMinuteInputOnFirstCharacter(); - } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectionHour, selectionMinute]); + if (isHourFocused && selectionHour.start === 2) { + if (e) { + // Check e to be truthy to avoid crashing on Android (e is undefined there) + e.preventDefault(); + } + focusMinuteInputOnFirstCharacter(); + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, + [selectionHour, selectionMinute], + ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT, arrowLeftCallback, arrowConfig); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT, arrowRightCallback, arrowConfig); @@ -381,7 +384,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { ); }, [canUseTouchScreen, updateAmountNumberPad]); @@ -412,7 +415,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { maxLength={2} formattedAmount={hours} onKeyPress={(e) => { - setLastPressedKey(e.nativeEvent.key); + lastPressedKey.current = e.nativeEvent.key; }} onChangeAmount={handleHourChange} role={CONST.ACCESSIBILITY_ROLE.TEXT} @@ -439,7 +442,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { maxLength={2} formattedAmount={minutes} onKeyPress={(e) => { - setLastPressedKey(e.nativeEvent.key); + lastPressedKey.current = e.nativeEvent.key; handleFocusOnBackspace(e); }} onChangeAmount={handleMinutesChange} diff --git a/src/components/TimePicker/setCursorPosition/index.ios.ts b/src/components/TimePicker/setCursorPosition/index.ios.ts new file mode 100644 index 000000000000..7e51abc3212e --- /dev/null +++ b/src/components/TimePicker/setCursorPosition/index.ios.ts @@ -0,0 +1,13 @@ +import SetCursorPosition from './types'; + +const setCursorPosition: SetCursorPosition = (position, ref, setSelection) => { + const selection = { + start: position, + end: position, + }; + setSelection(selection); + ref.current?.focus(); + ref.current?.setNativeProps({selection}); +}; + +export default setCursorPosition; diff --git a/src/components/TimePicker/setCursorPosition/index.ts b/src/components/TimePicker/setCursorPosition/index.ts new file mode 100644 index 000000000000..4d114641909f --- /dev/null +++ b/src/components/TimePicker/setCursorPosition/index.ts @@ -0,0 +1,11 @@ +import SetCursorPosition from './types'; + +const setCursorPosition: SetCursorPosition = (position, ref, setSelection) => { + setSelection({ + start: position, + end: position, + }); + ref.current?.focus(); +}; + +export default setCursorPosition; diff --git a/src/components/TimePicker/setCursorPosition/types.ts b/src/components/TimePicker/setCursorPosition/types.ts new file mode 100644 index 000000000000..f7fd20a59e8e --- /dev/null +++ b/src/components/TimePicker/setCursorPosition/types.ts @@ -0,0 +1,6 @@ +import {RefObject} from 'react'; +import {TextInput} from 'react-native'; + +type SetCursorPosition = (position: number, ref: RefObject, setSelection: (value: {start: number; end: number}) => void) => void; + +export default SetCursorPosition; From 818831557ae14c865fbcd558eecbd1e1e8628b09 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Fri, 29 Dec 2023 12:08:47 +0800 Subject: [PATCH 081/124] return early if the room doesn't exist --- src/libs/actions/Policy.js | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index f33e6637e2de..f148f27b3713 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -270,6 +270,10 @@ function buildAnnounceRoomMembersOnyxData(policyID, accountIDs) { onyxFailureData: [], }; + if (!announceReport) { + return announceRoomMembers; + } + announceRoomMembers.onyxOptimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${announceReport.reportID}`, @@ -301,6 +305,10 @@ function removeOptimisticAnnounceRoomMembers(policyID, accountIDs) { onyxFailureData: [], }; + if (!announceReport) { + return announceRoomMembers; + } + const remainUsers = _.difference(announceReport.participantAccountIDs, accountIDs); announceRoomMembers.onyxOptimisticData.push({ onyxMethod: Onyx.METHOD.MERGE, From 9735497b1d07bd5c5b1e5ad22dbfa07d4644c9c5 Mon Sep 17 00:00:00 2001 From: DylanDylann <141406735+DylanDylann@users.noreply.github.com> Date: Fri, 29 Dec 2023 11:43:21 +0700 Subject: [PATCH 082/124] fix remove optional from tracsaction param Co-authored-by: abdulrahuman5196 <46707890+abdulrahuman5196@users.noreply.github.com> --- src/libs/ReportUtils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index fc0388431edf..a2309bbbcbe5 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1843,7 +1843,7 @@ function canEditFieldOfMoneyRequest( reportAction: OnyxEntry, reportID: string, fieldToEdit: ValueOf, - transaction?: OnyxEntry, + transaction: OnyxEntry, ): boolean { // A list of fields that cannot be edited by anyone, once a money request has been settled const nonEditableFieldsWhenSettled: string[] = [ From aeffff1fe5bbf1456c4d2fa7e328526f30b136e9 Mon Sep 17 00:00:00 2001 From: Pavlo Tsimura Date: Fri, 29 Dec 2023 11:14:51 +0100 Subject: [PATCH 083/124] Handle single-digit selection removal using NumberPad --- src/components/TimePicker/TimePicker.js | 53 +++++++++++-------------- 1 file changed, 24 insertions(+), 29 deletions(-) diff --git a/src/components/TimePicker/TimePicker.js b/src/components/TimePicker/TimePicker.js index 827d8851d6f1..b22efb489100 100644 --- a/src/components/TimePicker/TimePicker.js +++ b/src/components/TimePicker/TimePicker.js @@ -60,19 +60,28 @@ function insertAtPosition(originalString, newSubstring, selectionPositionFrom, s return originalString.slice(0, selectionPositionFrom) + newSubstring + originalString.slice(selectionPositionTo); } -// if we need to manually move selection to the left, we need to decrease both selection start and end by one -function decreaseBothSelectionByOne({start, end}) { - if (start === 0) { - return {start: 0, end: 0}; - } - return {start: start - 1, end: end - 1}; +function replaceRangeWithZeros(originalString, from, to) { + const normalizedFrom = Math.max(from, 0); + const normalizedTo = Math.min(to, 2); + const replacement = '0'.repeat(normalizedTo - normalizedFrom); + return `${originalString.slice(0, normalizedFrom)}${replacement}${originalString.slice(normalizedTo)}`; } -function replaceWithZeroAtPosition(originalString, position) { - if (position === 0 || position > 2) { - return originalString; +function clearSelectedValue(value, selection, setValue, setSelection) { + let newValue; + let newCursorPosition; + + if (selection.start !== selection.end) { + newValue = replaceRangeWithZeros(value, selection.start, selection.end); + newCursorPosition = selection.start; + } else { + const positionBeforeSelection = Math.max(selection.start - 1, 0); + newValue = replaceRangeWithZeros(value, positionBeforeSelection, selection.start); + newCursorPosition = positionBeforeSelection; } - return `${originalString.slice(0, position - 1)}0${originalString.slice(position)}`; + + setValue(newValue); + setSelection({start: newCursorPosition, end: newCursorPosition}); } function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { @@ -279,28 +288,14 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } if (key === '<' || key === 'Backspace') { if (isHourFocused) { - if (selectionHour.start === 0 && selectionHour.end === 2) { - resetHours(); - return; - } - - const newHour = replaceWithZeroAtPosition(hours, selectionHour.start); - setHours(newHour); - setSelectionHour(decreaseBothSelectionByOne(selectionHour)); + clearSelectedValue(hours, selectionHour, setHours, setSelectionHour); } else if (isMinuteFocused) { - if (selectionMinute.start === 0 && selectionMinute.end === 2) { - resetMinutes(); - return; - } - if (selectionMinute.start === 0 && selectionMinute.end === 0) { focusHourInputOnLastCharacter(); return; } - const newMinute = replaceWithZeroAtPosition(minutes, selectionMinute.start); - setMinutes(newMinute); - setSelectionMinute(decreaseBothSelectionByOne(selectionMinute)); + clearSelectedValue(minutes, selectionMinute, setMinutes, setSelectionMinute); } return; } @@ -313,7 +308,7 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } }, // eslint-disable-next-line react-hooks/exhaustive-deps - [minutes, hours, selectionHour, selectionMinute], + [minutes, hours, selectionMinute, selectionHour], ); useEffect(() => { @@ -339,8 +334,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } focusHourInputOnLastCharacter(); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, + // eslint-disable-next-line react-hooks/exhaustive-deps [selectionHour, selectionMinute], ); const arrowRightCallback = useCallback( @@ -354,8 +349,8 @@ function TimePicker({forwardedRef, defaultValue, onSubmit, onInputChange}) { } focusMinuteInputOnFirstCharacter(); } - // eslint-disable-next-line react-hooks/exhaustive-deps }, + // eslint-disable-next-line react-hooks/exhaustive-deps [selectionHour, selectionMinute], ); From ea5817ec7d940310d66b3cee0daeb843519fdd11 Mon Sep 17 00:00:00 2001 From: Vit Horacek Date: Fri, 29 Dec 2023 13:27:38 +0100 Subject: [PATCH 084/124] Make the report preview submit button green only in case of scheduled submit being off --- src/components/ReportActionItem/ReportPreview.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/ReportPreview.js b/src/components/ReportActionItem/ReportPreview.js index 4eae450a4e86..a6825ade9b6d 100644 --- a/src/components/ReportActionItem/ReportPreview.js +++ b/src/components/ReportActionItem/ReportPreview.js @@ -60,6 +60,9 @@ const propTypes = { /** The role of the current user in the policy */ role: PropTypes.string, + + /** Whether Scheduled Submit is on for this policy */ + isHarvestingEnabled: PropTypes.bool, }), /* Onyx Props */ @@ -165,6 +168,9 @@ function ReportPreview(props) { const shouldShowSubmitButton = isDraftExpenseReport && reimbursableSpend !== 0; + // The submit button should be success green colour only if the user is submitter and the policy does not have Scheduled Submit turned on + const isCurrentUserSubmitter = props.chatReport.isOwnPolicyExpenseChat && !props.policy.isHarvestingEnabled; + const getDisplayAmount = () => { if (hasPendingWaypoints) { return props.translate('common.tbd'); @@ -327,7 +333,7 @@ function ReportPreview(props) { {shouldShowSubmitButton && (