From de66326d622a902ec6c3519b70941bf082139bf8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 27 Nov 2023 16:44:30 +0800 Subject: [PATCH 001/170] only archive chat room, expense chat, and task --- src/pages/workspace/WorkspaceInitialPage.js | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 77e831e62b63..fa29cf3ded22 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -67,6 +67,14 @@ function dismissError(policyID) { Policy.removeWorkspace(policyID); } +/** + * Whether the policy report should be deleted when we delete the policy. + * @param {Object} report + */ +function shouldDeleteReport(report) { + return ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report); +} + function WorkspaceInitialPage(props) { const styles = useThemeStyles(); const policy = props.policyDraft && props.policyDraft.id ? props.policyDraft : props.policy; @@ -111,7 +119,7 @@ function WorkspaceInitialPage(props) { * Call the delete policy and hide the modal */ const confirmDeleteAndHideModal = useCallback(() => { - Policy.deleteWorkspace(policyID, policyReports, policy.name); + Policy.deleteWorkspace(policyID, _.filter(policyReports, shouldDeleteReport), policy.name); setIsDeleteModalOpen(false); // Pop the deleted workspace page before opening workspace settings. Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); From 79aaa4b766e5ad8402f11dbadc22acf3c5b0b6aa Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 27 Nov 2023 16:53:42 +0800 Subject: [PATCH 002/170] update func name and comment --- src/pages/workspace/WorkspaceInitialPage.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index fa29cf3ded22..76e8d30093cc 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -68,10 +68,10 @@ function dismissError(policyID) { } /** - * Whether the policy report should be deleted when we delete the policy. + * Whether the policy report should be archived when we delete the policy. * @param {Object} report */ -function shouldDeleteReport(report) { +function shouldArchiveReport(report) { return ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report); } @@ -119,7 +119,7 @@ function WorkspaceInitialPage(props) { * Call the delete policy and hide the modal */ const confirmDeleteAndHideModal = useCallback(() => { - Policy.deleteWorkspace(policyID, _.filter(policyReports, shouldDeleteReport), policy.name); + Policy.deleteWorkspace(policyID, _.filter(policyReports, shouldArchiveReport), policy.name); setIsDeleteModalOpen(false); // Pop the deleted workspace page before opening workspace settings. Navigation.goBack(ROUTES.SETTINGS_WORKSPACES); From 86fa1820c0b6b16d55a9f259b573682a0a7e776d Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 27 Nov 2023 16:56:11 +0800 Subject: [PATCH 003/170] add jsdoc returns --- src/pages/workspace/WorkspaceInitialPage.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index 76e8d30093cc..c899fffff4e2 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -70,6 +70,7 @@ function dismissError(policyID) { /** * Whether the policy report should be archived when we delete the policy. * @param {Object} report + * @returns {Boolean} */ function shouldArchiveReport(report) { return ReportUtils.isChatRoom(report) || ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isTaskReport(report); From 2346a024ded1c593ee6df83276f88c11d1a3243a Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 27 Nov 2023 17:00:07 +0800 Subject: [PATCH 004/170] prettier --- src/pages/workspace/WorkspaceInitialPage.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/workspace/WorkspaceInitialPage.js b/src/pages/workspace/WorkspaceInitialPage.js index c899fffff4e2..66d9f2f7f518 100644 --- a/src/pages/workspace/WorkspaceInitialPage.js +++ b/src/pages/workspace/WorkspaceInitialPage.js @@ -69,7 +69,7 @@ function dismissError(policyID) { /** * Whether the policy report should be archived when we delete the policy. - * @param {Object} report + * @param {Object} report * @returns {Boolean} */ function shouldArchiveReport(report) { From 3ecd7f9378a6a8c2ced4a172a9a9e330b6e994e8 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 13 Dec 2023 18:28:35 +0300 Subject: [PATCH 005/170] reset workspace avatar in optimistic data --- src/libs/actions/Policy.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/libs/actions/Policy.js b/src/libs/actions/Policy.js index 04f62ab0c393..6ad39dab041a 100644 --- a/src/libs/actions/Policy.js +++ b/src/libs/actions/Policy.js @@ -171,6 +171,7 @@ function deleteWorkspace(policyID, reports, policyName) { onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, value: { + avatar: '', pendingAction: CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, errors: null, }, From 0b55bfe3158869695da143e55fe80e2fa432a9c6 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 20 Dec 2023 13:38:41 +0100 Subject: [PATCH 006/170] [TS migration] Migrate 'StatePicker' component --- src/components/MenuItem.tsx | 8 +-- ...electorModal.js => StateSelectorModal.tsx} | 44 +++++++-------- .../StatePicker/{index.js => index.tsx} | 53 +++++-------------- src/libs/searchCountryOptions.ts | 1 + 4 files changed, 38 insertions(+), 68 deletions(-) rename src/components/StatePicker/{StateSelectorModal.js => StateSelectorModal.tsx} (72%) rename src/components/StatePicker/{index.js => index.tsx} (58%) diff --git a/src/components/MenuItem.tsx b/src/components/MenuItem.tsx index c2cc4abce6c5..c1d1aa7d71d2 100644 --- a/src/components/MenuItem.tsx +++ b/src/components/MenuItem.tsx @@ -84,7 +84,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & titleStyle?: ViewStyle; /** Any adjustments to style when menu item is hovered or pressed */ - hoverAndPressStyle: StyleProp>; + hoverAndPressStyle?: StyleProp>; /** Additional styles to style the description text below the title */ descriptionTextStyle?: StyleProp; @@ -174,7 +174,7 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & isSelected?: boolean; /** Prop to identify if we should load avatars vertically instead of diagonally */ - shouldStackHorizontally: boolean; + shouldStackHorizontally?: boolean; /** Prop to represent the size of the avatar images to be shown */ avatarSize?: (typeof CONST.AVATAR_SIZE)[keyof typeof CONST.AVATAR_SIZE]; @@ -219,10 +219,10 @@ type MenuItemProps = (ResponsiveProps | UnresponsiveProps) & furtherDetails?: string; /** The function that should be called when this component is LongPressed or right-clicked. */ - onSecondaryInteraction: () => void; + onSecondaryInteraction?: () => void; /** Array of objects that map display names to their corresponding tooltip */ - titleWithTooltips: DisplayNameWithTooltip[]; + titleWithTooltips?: DisplayNameWithTooltip[]; }; function MenuItem( diff --git a/src/components/StatePicker/StateSelectorModal.js b/src/components/StatePicker/StateSelectorModal.tsx similarity index 72% rename from src/components/StatePicker/StateSelectorModal.js rename to src/components/StatePicker/StateSelectorModal.tsx index 003211478529..946c54048d79 100644 --- a/src/components/StatePicker/StateSelectorModal.js +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -1,48 +1,41 @@ import {CONST as COMMON_CONST} from 'expensify-common/lib/CONST'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Modal from '@components/Modal'; import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import searchCountryOptions from '@libs/searchCountryOptions'; +import searchCountryOptions, {type CountryData} from '@libs/searchCountryOptions'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; -const propTypes = { +type State = keyof typeof COMMON_CONST.STATES; + +type StateSelectorModalProps = { /** Whether the modal is visible */ - isVisible: PropTypes.bool.isRequired, + isVisible: boolean; /** State value selected */ - currentState: PropTypes.string, + currentState?: State | ''; /** Function to call when the user selects a State */ - onStateSelected: PropTypes.func, + onStateSelected?: (state: CountryData) => void; /** Function to call when the user closes the State modal */ - onClose: PropTypes.func, + onClose?: () => void; /** The search value from the selection list */ - searchValue: PropTypes.string.isRequired, + searchValue: string; /** Function to call when the user types in the search input */ - setSearchValue: PropTypes.func.isRequired, + setSearchValue: (value: string) => void; /** Label to display on field */ - label: PropTypes.string, -}; - -const defaultProps = { - currentState: '', - onClose: () => {}, - onStateSelected: () => {}, - label: undefined, + label?: string; }; -function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, searchValue, setSearchValue, label}) { +function StateSelectorModal({currentState = '', isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -53,11 +46,11 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, setSearchValue(''); }, [isVisible, setSearchValue]); - const countryStates = useMemo( + const countryStates: CountryData[] = useMemo( () => - _.map(_.keys(COMMON_CONST.STATES), (state) => { - const stateName = translate(`allStates.${state}.stateName`); - const stateISO = translate(`allStates.${state}.stateISO`); + Object.keys(COMMON_CONST.STATES).map((state) => { + const stateName = translate(`allStates.${state as State}.stateName`); + const stateISO = translate(`allStates.${state as State}.stateISO`); return { value: stateISO, keyForList: stateISO, @@ -81,6 +74,7 @@ function StateSelectorModal({currentState, isVisible, onClose, onStateSelected, hideModalContentWhileAnimating useNativeDriver > + {/* @ts-expect-error TODO: Remove this once ScreenWrapper (https://github.com/Expensify/App/issues/25128) is migrated to TypeScript. */} void; /** Label to display on field */ - label: PropTypes.string, -}; - -const defaultProps = { - value: undefined, - forwardedRef: undefined, - errorText: '', - onInputChange: () => {}, - label: undefined, + label?: string; }; -function StatePicker({value, errorText, onInputChange, forwardedRef, label}) { +function StatePicker({value, onInputChange, label, errorText = ''}: StatePickerProps, ref: ForwardedRef) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isPickerVisible, setIsPickerVisible] = useState(false); @@ -49,20 +36,20 @@ function StatePicker({value, errorText, onInputChange, forwardedRef, label}) { setIsPickerVisible(false); }; - const updateStateInput = (state) => { + const updateStateInput = (state: CountryData) => { if (state.value !== value) { - onInputChange(state.value); + onInputChange?.(state.value); } hidePickerModal(); }; - const title = value && _.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; + const title = value && Object.keys(COMMON_CONST.STATES).includes(value) ? translate(`allStates.${value}.stateName`) : ''; const descStyle = title.length === 0 ? styles.textNormal : null; return ( ( - -)); - -StatePickerWithRef.displayName = 'StatePickerWithRef'; - -export default StatePickerWithRef; +export default React.forwardRef(StatePicker); diff --git a/src/libs/searchCountryOptions.ts b/src/libs/searchCountryOptions.ts index 8fb1cc9c37f3..1fc5d343f556 100644 --- a/src/libs/searchCountryOptions.ts +++ b/src/libs/searchCountryOptions.ts @@ -37,3 +37,4 @@ function searchCountryOptions(searchValue: string, countriesData: CountryData[]) } export default searchCountryOptions; +export type {CountryData}; From 6c9c06f3740382285a9c4acee716eaea943c537e Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 20 Dec 2023 14:11:17 +0100 Subject: [PATCH 007/170] Use nullish coalescing operator --- src/components/StatePicker/StateSelectorModal.tsx | 4 ++-- src/components/StatePicker/index.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index 946c54048d79..deee159ff906 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -82,14 +82,14 @@ function StateSelectorModal({currentState = '', isVisible, onClose = () => {}, o testID={StateSelectorModal.displayName} > From 9f5666ebfb3f3534feb9c3660122109507f61d0f Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Wed, 20 Dec 2023 16:10:23 +0100 Subject: [PATCH 008/170] Minor code improvement --- src/components/StatePicker/StateSelectorModal.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index deee159ff906..2871c2ebdaf5 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -17,7 +17,7 @@ type StateSelectorModalProps = { isVisible: boolean; /** State value selected */ - currentState?: State | ''; + currentState?: State; /** Function to call when the user selects a State */ onStateSelected?: (state: CountryData) => void; @@ -35,7 +35,7 @@ type StateSelectorModalProps = { label?: string; }; -function StateSelectorModal({currentState = '', isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { +function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStateSelected = () => {}, searchValue, setSearchValue, label}: StateSelectorModalProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); From 0095e3305c309eef535444363236f03e131b2ccf Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 29 Dec 2023 11:51:40 -0800 Subject: [PATCH 009/170] Dont make unread muted reports bold in the LHN --- src/components/LHNOptionsList/OptionRowLHN.js | 16 ++++++++-------- src/libs/ReportUtils.ts | 1 + 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.js b/src/components/LHNOptionsList/OptionRowLHN.js index f75e3390136a..71a178847fb7 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.js +++ b/src/components/LHNOptionsList/OptionRowLHN.js @@ -92,19 +92,19 @@ function OptionRowLHN(props) { return null; } + const isInFocusMode = props.viewMode === CONST.OPTION_MODE.COMPACT; const textStyle = props.isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; - const textUnreadStyle = optionItem.isUnread ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; + const textUnreadStyle = optionItem.isUnread && optionItem.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = StyleUtils.combineStyles([styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, ...textUnreadStyle], props.style); const alternateTextStyle = StyleUtils.combineStyles( - props.viewMode === CONST.OPTION_MODE.COMPACT + isInFocusMode ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2] : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting], props.style, ); - const contentContainerStyles = - props.viewMode === CONST.OPTION_MODE.COMPACT ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; + const contentContainerStyles = isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; const sidebarInnerRowStyle = StyleSheet.flatten( - props.viewMode === CONST.OPTION_MODE.COMPACT + isInFocusMode ? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter] : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); @@ -218,13 +218,13 @@ function OptionRowLHN(props) { backgroundColor={hovered && !props.isFocused ? hoveredBackgroundColor : subscriptAvatarBorderColor} mainAvatar={optionItem.icons[0]} secondaryAvatar={optionItem.icons[1]} - size={props.viewMode === CONST.OPTION_MODE.COMPACT ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT} + size={isInFocusMode ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT} /> ) : ( Date: Fri, 29 Dec 2023 12:05:31 -0800 Subject: [PATCH 010/170] Hide muted reports in focus mode --- src/libs/ReportUtils.ts | 2 +- src/libs/SidebarUtils.ts | 1 + src/pages/home/sidebar/SidebarLinksData.js | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index a22dec9600cc..723bc7fe5a53 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -3421,7 +3421,7 @@ function shouldReportBeInOptionList(report: OnyxEntry, currentReportId: // All unread chats (even archived ones) in GSD mode will be shown. This is because GSD mode is specifically for focusing the user on the most relevant chats, primarily, the unread ones if (isInGSDMode) { - return isUnread(report); + return isUnread(report) && report.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE; } // Archived reports should always be shown when in default (most recent) mode. This is because you should still be able to access and search for the chats to find them. diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index da91cb1bd473..2a9c222b0769 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -148,6 +148,7 @@ function getOrderedReportIDs( const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports); + // Filter out all the reports that shouldn't be displayed const reportsToDisplay = allReportsDictValues.filter((report) => ReportUtils.shouldReportBeInOptionList(report, currentReportId ?? '', isInGSDMode, betas, policies, true)); diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index dbc77a41817b..955300c344a4 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -140,6 +140,7 @@ const chatReportSelector = (report) => hasDraft: report.hasDraft, isPinned: report.isPinned, isHidden: report.isHidden, + notificationPreference: report.notificationPreference, errorFields: { addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, }, From c2866e530f7e935fc6dbd94552ecafc651e77645 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Thu, 4 Jan 2024 14:44:10 +0700 Subject: [PATCH 011/170] make referral banner dismissable --- .../OptionsSelector/BaseOptionsSelector.js | 27 ++++++++++++------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index 792073b72613..40d5b29b827a 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -8,7 +8,7 @@ import Button from '@components/Button'; import FixedFooter from '@components/FixedFooter'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; -import {Info} from '@components/Icon/Expensicons'; +import {Close} from '@components/Icon/Expensicons'; import OptionsList from '@components/OptionsList'; import {PressableWithoutFeedback} from '@components/Pressable'; import ShowMoreButton from '@components/ShowMoreButton'; @@ -92,7 +92,7 @@ class BaseOptionsSelector extends Component { allOptions, focusedIndex, shouldDisableRowSelection: false, - shouldShowReferralModal: false, + shouldShowReferralModal: this.props.shouldShowReferralCTA, errorMessage: '', paginationPage: 1, value: '', @@ -618,7 +618,7 @@ class BaseOptionsSelector extends Component { )} - {this.props.shouldShowReferralCTA && ( + {this.props.shouldShowReferralCTA && this.state.shouldShowReferralModal && ( { @@ -646,12 +646,21 @@ class BaseOptionsSelector extends Component { {this.props.translate(`referralProgram.${this.props.referralContentType}.buttonText2`)} - + { + e.preventDefault(); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={this.props.translate('common.close')} + > + + )} From 43a77afd47466250a642b479358a2622e5266a69 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Thu, 4 Jan 2024 10:17:52 +0100 Subject: [PATCH 012/170] Fix lint errors --- src/components/StatePicker/StateSelectorModal.tsx | 4 ++-- src/components/StatePicker/index.tsx | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index 2871c2ebdaf5..5be88a77f887 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -6,7 +6,8 @@ import ScreenWrapper from '@components/ScreenWrapper'; import SelectionList from '@components/SelectionList'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import searchCountryOptions, {type CountryData} from '@libs/searchCountryOptions'; +import searchCountryOptions from '@libs/searchCountryOptions'; +import type {CountryData} from '@libs/searchCountryOptions'; import StringUtils from '@libs/StringUtils'; import CONST from '@src/CONST'; @@ -74,7 +75,6 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat hideModalContentWhileAnimating useNativeDriver > - {/* @ts-expect-error TODO: Remove this once ScreenWrapper (https://github.com/Expensify/App/issues/25128) is migrated to TypeScript. */} Date: Fri, 5 Jan 2024 15:39:53 +0700 Subject: [PATCH 013/170] fix: 33073 --- src/components/ReportActionItem/MoneyRequestAction.js | 5 ----- src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index e0a3152a41b4..d159998b2d57 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -34,9 +34,6 @@ const propTypes = { /** The ID of the associated request report */ requestReportID: PropTypes.string.isRequired, - /** Is this IOUACTION the most recent? */ - isMostRecentIOUReportAction: PropTypes.bool.isRequired, - /** Popover context menu anchor, used for showing context menu */ contextMenuAnchor: refPropTypes, @@ -81,7 +78,6 @@ function MoneyRequestAction({ action, chatReportID, requestReportID, - isMostRecentIOUReportAction, contextMenuAnchor, checkIfContextMenuActive, chatReport, @@ -123,7 +119,6 @@ function MoneyRequestAction({ !_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && - isMostRecentIOUReportAction && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline ) { diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 435c086d913f..20ddb127f48a 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -322,7 +322,7 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( Date: Fri, 5 Jan 2024 15:51:29 +0700 Subject: [PATCH 014/170] remove most recent iou report action id --- .../ReportActionItem/MoneyRequestAction.js | 8 +------- src/pages/home/report/ReportActionItem.js | 5 ----- .../home/report/ReportActionItemParentAction.js | 1 - src/pages/home/report/ReportActionsList.js | 8 +------- .../home/report/ReportActionsListItemRenderer.js | 16 +--------------- src/pages/home/report/ReportActionsView.js | 4 ---- 6 files changed, 3 insertions(+), 39 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index d159998b2d57..46226969636e 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -115,13 +115,7 @@ function MoneyRequestAction({ let shouldShowPendingConversionMessage = false; const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); - if ( - !_.isEmpty(iouReport) && - !_.isEmpty(reportActions) && - chatReport.iouReportID && - action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && - network.isOffline - ) { + if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 20ddb127f48a..4b5a43215ee5 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -88,9 +88,6 @@ const propTypes = { /** Should the comment have the appearance of being grouped with the previous comment? */ displayAsGroup: PropTypes.bool.isRequired, - /** Is this the most recent IOU Action? */ - isMostRecentIOUReportAction: PropTypes.bool.isRequired, - /** Should we display the new marker on top of the comment? */ shouldDisplayNewMarker: PropTypes.bool.isRequired, @@ -325,7 +322,6 @@ function ReportActionItem(props) { chatReportID={originalMessage.IOUReportID ? props.report.chatReportID : props.report.reportID} requestReportID={iouReportID} action={props.action} - isMostRecentIOUReportAction={props.isMostRecentIOUReportAction} isHovered={hovered} contextMenuAnchor={popoverAnchorRef} checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} @@ -775,7 +771,6 @@ export default compose( (prevProps, nextProps) => prevProps.displayAsGroup === nextProps.displayAsGroup && prevProps.draftMessage === nextProps.draftMessage && - prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && _.isEqual(prevProps.action, nextProps.action) && diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js index c11200ccc4db..161c8048ab3d 100644 --- a/src/pages/home/report/ReportActionItemParentAction.js +++ b/src/pages/home/report/ReportActionItemParentAction.js @@ -70,7 +70,6 @@ function ReportActionItemParentAction(props) { report={props.report} action={parentReportAction} displayAsGroup={false} - isMostRecentIOUReportAction={false} shouldDisplayNewMarker={props.shouldDisplayNewMarker} index={0} /> diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 2009dc9a102d..c7f48a38aeea 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -34,9 +34,6 @@ const propTypes = { /** Sorted actions prepared for display */ sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)).isRequired, - /** The ID of the most recent IOU report action connected with the shown report */ - mostRecentIOUReportActionID: PropTypes.string, - /** The report metadata loading states */ isLoadingInitialReportActions: PropTypes.bool, @@ -73,7 +70,6 @@ const propTypes = { const defaultProps = { onScroll: () => {}, - mostRecentIOUReportActionID: '', isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, @@ -128,7 +124,6 @@ function ReportActionsList({ sortedReportActions, windowHeight, onScroll, - mostRecentIOUReportActionID, isSmallScreenWidth, personalDetailsList, currentUserPersonalDetails, @@ -398,12 +393,11 @@ function ReportActionsList({ report={report} linkedReportActionID={linkedReportActionID} displayAsGroup={ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(sortedReportActions, index)} - mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} /> ), - [report, linkedReportActionID, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker], + [report, linkedReportActionID, sortedReportActions, shouldHideThreadDividerLine, shouldDisplayNewMarker], ); // Native mobile does not render updates flatlist the changes even though component did update called. diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index ba47e804de06..01f4bc66d59d 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -22,9 +22,6 @@ const propTypes = { /** Should the comment have the appearance of being grouped with the previous comment? */ displayAsGroup: PropTypes.bool.isRequired, - /** The ID of the most recent IOU report action connected with the shown report */ - mostRecentIOUReportActionID: PropTypes.string, - /** If the thread divider line should be hidden */ shouldHideThreadDividerLine: PropTypes.bool.isRequired, @@ -36,20 +33,10 @@ const propTypes = { }; const defaultProps = { - mostRecentIOUReportActionID: '', linkedReportActionID: '', }; -function ReportActionsListItemRenderer({ - reportAction, - index, - report, - displayAsGroup, - mostRecentIOUReportActionID, - shouldHideThreadDividerLine, - shouldDisplayNewMarker, - linkedReportActionID, -}) { +function ReportActionsListItemRenderer({reportAction, index, report, displayAsGroup, shouldHideThreadDividerLine, shouldDisplayNewMarker, linkedReportActionID}) { const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && ReportUtils.isChatThread(report) && @@ -77,7 +64,6 @@ function ReportActionsListItemRenderer({ reportAction.actionName, ) } - isMostRecentIOUReportAction={reportAction.reportActionID === mostRecentIOUReportActionID} index={index} /> ); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 2758437a3962..ddcea7894251 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -14,7 +14,6 @@ import usePrevious from '@hooks/usePrevious'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {didUserLogInDuringSession} from '@libs/SessionUtils'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; @@ -87,8 +86,6 @@ function ReportActionsView(props) { const didSubscribeToReportTypingEvents = useRef(false); const isFirstRender = useRef(true); const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); - const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); @@ -257,7 +254,6 @@ function ReportActionsView(props) { report={props.report} onLayout={recordTimeToMeasureItemLayout} sortedReportActions={props.reportActions} - mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} isLoadingInitialReportActions={props.isLoadingInitialReportActions} From 6c1f6e7f2f66f24dd83dc7b53ece52bcd969a294 Mon Sep 17 00:00:00 2001 From: Aldo Canepa Date: Mon, 8 Jan 2024 10:54:26 -0300 Subject: [PATCH 015/170] Use new command for updating waypoints (WIP) --- src/libs/actions/IOU.js | 18 +++++++++++++++++- src/pages/EditRequestDistancePage.js | 2 +- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 21997023fbc8..9c00b5769a46 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1055,7 +1055,7 @@ function updateMoneyRequestDate(transactionID, transactionThreadReportID, val) { } /** - * Updates the created date of a money request + * Updates the tag of a money request * * @param {String} transactionID * @param {Number} transactionThreadReportID @@ -1069,6 +1069,21 @@ function updateMoneyRequestTag(transactionID, transactionThreadReportID, tag) { API.write('UpdateMoneyRequestTag', params, onyxData); } +/** + * Updates the waypoints of a distance money request + * + * @param {String} transactionID + * @param {Number} transactionThreadReportID + * @param {Object} waypoints + */ +function updateMoneyRequestDistance(transactionID, transactionThreadReportID, waypoints) { + const transactionChanges = { + waypoints, + }; + const {params, onyxData} = getUpdateMoneyRequestParams(transactionID, transactionThreadReportID, transactionChanges, true); + API.write('UpdateMoneyRequestDistance', params, onyxData); +} + /** * Edits an existing distance request * @@ -3509,6 +3524,7 @@ export { navigateToNextPage, updateMoneyRequestDate, updateMoneyRequestTag, + updateMoneyRequestDistance, updateMoneyRequestAmountAndCurrency, replaceReceipt, detachReceipt, diff --git a/src/pages/EditRequestDistancePage.js b/src/pages/EditRequestDistancePage.js index 0ea295c0780b..f3ea76a3390a 100644 --- a/src/pages/EditRequestDistancePage.js +++ b/src/pages/EditRequestDistancePage.js @@ -79,7 +79,7 @@ function EditRequestDistancePage({report, route, transaction, transactionBackup} return; } - IOU.editMoneyRequest(transaction, report.reportID, {waypoints}); + IOU.updateMoneyRequestDistance(transaction.transactionID, report.reportID, waypoints); // If the client is offline, then the modal can be closed as well (because there are no errors or other feedback to show them // until they come online again and sync with the server). From 8d0d4cd252609ab02d465131934ce41d4535a06f Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Tue, 9 Jan 2024 11:37:30 +0300 Subject: [PATCH 016/170] update isGroupChat getParticipantIDs getVisibleMemberIDs --- src/libs/ReportUtils.ts | 58 +++++++++++++++++------------ src/pages/ReportParticipantsPage.js | 3 +- 2 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 0e159cf69095..9dc6a1465c62 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -4132,6 +4132,30 @@ function getTaskAssigneeChatOnyxData( }; } +/** + * Checks if a report is a group chat. + * + * A report is a group chat if it meets the following conditions: + * - Not a chat thread. + * - Not a task report. + * - Not a money request / IOU report. + * - Not an archived room. + * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). + * - More than 1 participants (note that participantAccountIDs excludes the current user). + * + */ +function isGroupChat(report: OnyxEntry): boolean { + return Boolean( + report && + !isChatThread(report) && + !isTaskReport(report) && + !isMoneyRequestReport(report) && + !isArchivedRoom(report) && + !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && + (report.participantAccountIDs?.length ?? 0) > 1, + ); +} + /** * Returns an array of the participants Ids of a report * @@ -4150,6 +4174,11 @@ function getParticipantsIDs(report: OnyxEntry): number[] { const onlyUnique = [...new Set([...onlyTruthyValues])]; return onlyUnique; } + + if (isGroupChat(report) && currentUserAccountID) { + return [...new Set([...participants, currentUserAccountID])]; + } + return participants; } @@ -4169,6 +4198,11 @@ function getVisibleMemberIDs(report: OnyxEntry): number[] { const onlyUnique = [...new Set([...onlyTruthyValues])]; return onlyUnique; } + + if (isGroupChat(report) && currentUserAccountID) { + return [...new Set([...visibleChatMemberAccountIDs, currentUserAccountID])]; + } + return visibleChatMemberAccountIDs; } @@ -4228,30 +4262,6 @@ function getIOUReportActionDisplayMessage(reportAction: OnyxEntry) }); } -/** - * Checks if a report is a group chat. - * - * A report is a group chat if it meets the following conditions: - * - Not a chat thread. - * - Not a task report. - * - Not a money request / IOU report. - * - Not an archived room. - * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). - * - More than 2 participants. - * - */ -function isGroupChat(report: OnyxEntry): boolean { - return Boolean( - report && - !isChatThread(report) && - !isTaskReport(report) && - !isMoneyRequestReport(report) && - !isArchivedRoom(report) && - !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && - (report.participantAccountIDs?.length ?? 0) > 2, - ); -} - function shouldUseFullTitleToDisplay(report: OnyxEntry): boolean { return isMoneyRequestReport(report) || isPolicyExpenseChat(report) || isChatRoom(report) || isChatThread(report) || isTaskReport(report); } diff --git a/src/pages/ReportParticipantsPage.js b/src/pages/ReportParticipantsPage.js index 7dbc1c7036c4..65238fd5ea8c 100755 --- a/src/pages/ReportParticipantsPage.js +++ b/src/pages/ReportParticipantsPage.js @@ -100,7 +100,8 @@ function ReportParticipantsPage(props) { Date: Tue, 9 Jan 2024 10:57:05 +0100 Subject: [PATCH 017/170] fix IOU - Amount is not preserved in Manual page when the amount is changed in confirmation page --- src/pages/iou/steps/MoneyRequestAmountForm.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/steps/MoneyRequestAmountForm.js b/src/pages/iou/steps/MoneyRequestAmountForm.js index 536944f4a2d8..8775562d4476 100644 --- a/src/pages/iou/steps/MoneyRequestAmountForm.js +++ b/src/pages/iou/steps/MoneyRequestAmountForm.js @@ -132,9 +132,9 @@ function MoneyRequestAmountForm({amount, taxAmount, currency, isEditing, forward return; } initializeAmount(amount); - // we want to re-initialize the state only when the selected tab changes + // we want to re-initialize the state only when the selected tab or amount changes // eslint-disable-next-line react-hooks/exhaustive-deps - }, [selectedTab]); + }, [selectedTab, amount]); /** * Sets the selection and the amount accordingly to the value passed to the input From 8170a79d9ebe005a6fae2afbb3ad686ecd68a984 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Tue, 9 Jan 2024 15:15:43 +0100 Subject: [PATCH 018/170] migrate index.tsx to TypeScript --- src/components/PopoverMenu.tsx | 3 +- .../ThreeDotsMenu/{index.js => index.tsx} | 73 ++++++++----------- 2 files changed, 34 insertions(+), 42 deletions(-) rename src/components/ThreeDotsMenu/{index.js => index.tsx} (70%) diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 502bdbf83b53..52119439de45 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -46,7 +46,7 @@ type PopoverMenuItem = { type PopoverModalProps = Pick; -type PopoverMenuProps = PopoverModalProps & { +type PopoverMenuProps = Partial & { /** Callback method fired when the user requests to close the modal */ onClose: () => void; @@ -175,3 +175,4 @@ function PopoverMenu({ PopoverMenu.displayName = 'PopoverMenu'; export default React.memo(PopoverMenu); +export type {PopoverMenuItem}; diff --git a/src/components/ThreeDotsMenu/index.js b/src/components/ThreeDotsMenu/index.tsx similarity index 70% rename from src/components/ThreeDotsMenu/index.js rename to src/components/ThreeDotsMenu/index.tsx index 150487b2aa57..a6bb0ea51858 100644 --- a/src/components/ThreeDotsMenu/index.js +++ b/src/components/ThreeDotsMenu/index.tsx @@ -1,10 +1,10 @@ -import PropTypes from 'prop-types'; import React, {useRef, useState} from 'react'; +import type {ViewStyle} from 'react-native'; import {View} from 'react-native'; -import _ from 'underscore'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; +import type {AnchorAlignment} from '@components/Popover/types'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; @@ -13,68 +13,62 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as Browser from '@libs/Browser'; import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import type {AnchorPosition} from '@src/styles'; +import type IconAsset from '@src/types/utils/IconAsset'; import ThreeDotsMenuItemPropTypes from './ThreeDotsMenuItemPropTypes'; -const propTypes = { +type ThreeDotsMenuProps = { /** Tooltip for the popup icon */ - iconTooltip: PropTypes.string, + iconTooltip?: TranslationPaths; /** icon for the popup trigger */ - icon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), + icon: IconAsset; /** Any additional styles to pass to the icon container. */ - // eslint-disable-next-line react/forbid-prop-types - iconStyles: PropTypes.arrayOf(PropTypes.object), + iconStyles?: ViewStyle[]; /** The fill color to pass into the icon. */ - iconFill: PropTypes.string, + iconFill?: string; /** Function to call on icon press */ - onIconPress: PropTypes.func, + onIconPress?: () => void; /** menuItems that'll show up on toggle of the popup menu */ - menuItems: ThreeDotsMenuItemPropTypes.isRequired, + menuItems: PopoverMenuItem[]; /** The anchor position of the menu */ - anchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }).isRequired, + anchorPosition: AnchorPosition; /** The anchor alignment of the menu */ - anchorAlignment: PropTypes.shape({ - horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), - vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), - }), + anchorAlignment?: AnchorAlignment; /** Whether the popover menu should overlay the current view */ - shouldOverlay: PropTypes.bool, + shouldOverlay?: boolean; /** Whether the menu is disabled */ - disabled: PropTypes.bool, + disabled?: boolean; /** Should we announce the Modal visibility changes? */ - shouldSetModalVisibility: PropTypes.bool, + shouldSetModalVisibility?: boolean; }; -const defaultProps = { - iconTooltip: 'common.more', - disabled: false, - iconFill: undefined, - iconStyles: [], - icon: Expensicons.ThreeDots, - onIconPress: () => {}, - anchorAlignment: { +function ThreeDotsMenu({ + iconTooltip = 'common.more', + icon = Expensicons.ThreeDots, + iconFill, + iconStyles = [], + onIconPress = () => {}, + menuItems, + anchorPosition, + anchorAlignment = { horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP, // we assume that popover menu opens below the button, anchor is at TOP }, - shouldOverlay: false, - shouldSetModalVisibility: true, -}; - -function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, menuItems, anchorPosition, anchorAlignment, shouldOverlay, shouldSetModalVisibility, disabled}) { + shouldOverlay = false, + shouldSetModalVisibility = true, + disabled = false, +}: ThreeDotsMenuProps) { const theme = useTheme(); const styles = useThemeStyles(); const [isPopupMenuVisible, setPopupMenuVisible] = useState(false); @@ -119,7 +113,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me > @@ -139,10 +133,7 @@ function ThreeDotsMenu({iconTooltip, icon, iconFill, iconStyles, onIconPress, me ); } -ThreeDotsMenu.propTypes = propTypes; -ThreeDotsMenu.defaultProps = defaultProps; ThreeDotsMenu.displayName = 'ThreeDotsMenu'; export default ThreeDotsMenu; - export {ThreeDotsMenuItemPropTypes}; From cb9b1e01f49b4435765a1595a200dd9432ebabd1 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jan 2024 08:44:56 +0100 Subject: [PATCH 019/170] use correct menuItems type --- src/components/HeaderWithBackButton/types.ts | 15 ++------------- src/components/ThreeDotsMenu/index.tsx | 2 +- src/pages/workspace/WorkspacesListRow.tsx | 4 ++-- 3 files changed, 5 insertions(+), 16 deletions(-) diff --git a/src/components/HeaderWithBackButton/types.ts b/src/components/HeaderWithBackButton/types.ts index 9ffb0b5ef2f3..0a427310e4e7 100644 --- a/src/components/HeaderWithBackButton/types.ts +++ b/src/components/HeaderWithBackButton/types.ts @@ -1,22 +1,11 @@ import type {ReactNode} from 'react'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import type {Action} from '@hooks/useSingleExecution'; import type {StepCounterParams} from '@src/languages/types'; import type {AnchorPosition} from '@src/styles'; import type {PersonalDetails, Policy, Report} from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import type IconAsset from '@src/types/utils/IconAsset'; - -type ThreeDotsMenuItems = { - /** An icon element displayed on the left side */ - icon?: IconAsset; - - /** Text label */ - text: string; - - /** A callback triggered when the item is selected */ - onSelected: () => void; -}; type HeaderWithBackButtonProps = Partial & { /** Title of the Header */ @@ -62,7 +51,7 @@ type HeaderWithBackButtonProps = Partial & { shouldDisableThreeDotsButton?: boolean; /** List of menu items for more(three dots) menu */ - threeDotsMenuItems?: ThreeDotsMenuItems[]; + threeDotsMenuItems?: PopoverMenuItem[]; /** The anchor position of the menu */ threeDotsAnchorPosition?: AnchorPosition; diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index a6bb0ea51858..ced33c6b2ef9 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -23,7 +23,7 @@ type ThreeDotsMenuProps = { iconTooltip?: TranslationPaths; /** icon for the popup trigger */ - icon: IconAsset; + icon?: IconAsset; /** Any additional styles to pass to the icon container. */ iconStyles?: ViewStyle[]; diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx index d6bb3fb05385..5346ae6f5107 100755 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -4,7 +4,7 @@ import type {ValueOf} from 'type-fest'; import Avatar from '@components/Avatar'; import Icon from '@components/Icon'; import * as Illustrations from '@components/Icon/Illustrations'; -import type {MenuItemProps} from '@components/MenuItem'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import Text from '@components/Text'; import ThreeDotsMenu from '@components/ThreeDotsMenu'; import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; @@ -34,7 +34,7 @@ type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { fallbackWorkspaceIcon?: AvatarSource; /** Items for the three dots menu */ - menuItems: MenuItemProps[]; + menuItems: PopoverMenuItem[]; /** Renders the component using big screen layout or small screen layout. When layoutWidth === WorkspaceListRowLayout.NONE, * component will return null to prevent layout from jumping on initial render and when parent width changes. */ From e23837306e753ff554119ff95dc718593be068b9 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jan 2024 08:59:16 +0100 Subject: [PATCH 020/170] remove unused WorkspacesListRow component --- src/pages/workspace/WorkspacesListRow.tsx | 178 ---------------------- 1 file changed, 178 deletions(-) delete mode 100755 src/pages/workspace/WorkspacesListRow.tsx diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx deleted file mode 100755 index 5346ae6f5107..000000000000 --- a/src/pages/workspace/WorkspacesListRow.tsx +++ /dev/null @@ -1,178 +0,0 @@ -import React, {useMemo} from 'react'; -import {View} from 'react-native'; -import type {ValueOf} from 'type-fest'; -import Avatar from '@components/Avatar'; -import Icon from '@components/Icon'; -import * as Illustrations from '@components/Icon/Illustrations'; -import type {PopoverMenuItem} from '@components/PopoverMenu'; -import Text from '@components/Text'; -import ThreeDotsMenu from '@components/ThreeDotsMenu'; -import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; -import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; -import type {AvatarSource} from '@libs/UserUtils'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import type IconAsset from '@src/types/utils/IconAsset'; - -type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { - /** Name of the workspace */ - title: string; - - /** Account ID of the workspace's owner */ - ownerAccountID?: number; - - /** Type of workspace. Type personal is not valid in this context so it's omitted */ - workspaceType: typeof CONST.POLICY.TYPE.FREE | typeof CONST.POLICY.TYPE.CORPORATE | typeof CONST.POLICY.TYPE.TEAM; - - /** Icon to show next to the workspace name */ - workspaceIcon?: AvatarSource; - - /** Icon to be used when workspaceIcon is not present */ - fallbackWorkspaceIcon?: AvatarSource; - - /** Items for the three dots menu */ - menuItems: PopoverMenuItem[]; - - /** Renders the component using big screen layout or small screen layout. When layoutWidth === WorkspaceListRowLayout.NONE, - * component will return null to prevent layout from jumping on initial render and when parent width changes. */ - layoutWidth?: ValueOf; -}; - -const workspaceTypeIcon = (workspaceType: WorkspacesListRowProps['workspaceType']): IconAsset => { - switch (workspaceType) { - case CONST.POLICY.TYPE.FREE: - return Illustrations.HandCard; - case CONST.POLICY.TYPE.CORPORATE: - return Illustrations.ShieldYellow; - case CONST.POLICY.TYPE.TEAM: - return Illustrations.Mailbox; - default: - throw new Error(`Don't know which icon to serve for workspace type`); - } -}; - -function WorkspacesListRow({ - title, - menuItems, - workspaceIcon, - fallbackWorkspaceIcon, - ownerAccountID, - workspaceType, - currentUserPersonalDetails, - layoutWidth = CONST.LAYOUT_WIDTH.NONE, -}: WorkspacesListRowProps) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - - const ownerDetails = ownerAccountID && PersonalDetailsUtils.getPersonalDetailsByIDs([ownerAccountID], currentUserPersonalDetails.accountID)[0]; - - const userFriendlyWorkspaceType = useMemo(() => { - switch (workspaceType) { - case CONST.POLICY.TYPE.FREE: - return translate('workspace.type.free'); - case CONST.POLICY.TYPE.CORPORATE: - return translate('workspace.type.control'); - case CONST.POLICY.TYPE.TEAM: - return translate('workspace.type.collect'); - default: - throw new Error(`Don't know a friendly workspace name for this workspace type`); - } - }, [workspaceType, translate]); - - if (layoutWidth === CONST.LAYOUT_WIDTH.NONE) { - // To prevent layout from jumping or rendering for a split second, when - // isWide is undefined we don't assume anything and simply return null. - return null; - } - - const isWide = layoutWidth === CONST.LAYOUT_WIDTH.WIDE; - const isNarrow = layoutWidth === CONST.LAYOUT_WIDTH.NARROW; - - return ( - - - - - {title} - - {isNarrow && ( - - )} - - - {!!ownerDetails && ( - <> - - - - {PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)} - - - {ownerDetails.login} - - - - )} - - - - - - {userFriendlyWorkspaceType} - - - {translate('workspace.common.plan')} - - - - {isWide && ( - - )} - - ); -} - -WorkspacesListRow.displayName = 'WorkspacesListRow'; - -export default withCurrentUserPersonalDetails(WorkspacesListRow); From 9f2907e1c2bda85c6d0fc5fd3fb4bf019788ebd4 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jan 2024 09:16:29 +0100 Subject: [PATCH 021/170] wrap iconStyles type with StyleProp --- src/components/ThreeDotsMenu/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index ced33c6b2ef9..ced2b67826a0 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -1,5 +1,5 @@ import React, {useRef, useState} from 'react'; -import type {ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -26,7 +26,7 @@ type ThreeDotsMenuProps = { icon?: IconAsset; /** Any additional styles to pass to the icon container. */ - iconStyles?: ViewStyle[]; + iconStyles?: StyleProp; /** The fill color to pass into the icon. */ iconFill?: string; @@ -57,7 +57,7 @@ function ThreeDotsMenu({ iconTooltip = 'common.more', icon = Expensicons.ThreeDots, iconFill, - iconStyles = [], + iconStyles, onIconPress = () => {}, menuItems, anchorPosition, @@ -107,7 +107,7 @@ function ThreeDotsMenu({ e.preventDefault(); }} ref={buttonRef} - style={[styles.touchableButtonImage, ...iconStyles]} + style={[styles.touchableButtonImage, iconStyles]} role={CONST.ROLE.BUTTON} accessibilityLabel={translate(iconTooltip)} > From 518087cc0a8560160828c54a6004992739936666 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Wed, 10 Jan 2024 13:05:18 +0100 Subject: [PATCH 022/170] Sign in reassure flow --- src/components/MagicCodeInput.js | 5 + src/pages/signin/LoginForm/BaseLoginForm.js | 1 + src/pages/signin/SignInPage.js | 5 +- .../ValidateCodeForm/BaseValidateCodeForm.js | 1 + tests/perf-test/SignInPage.perf-test.js | 151 ++++++++++++++++++ .../collections/getValidCodeCredentials.ts | 11 ++ tests/utils/collections/userAccount.ts | 16 ++ 7 files changed, 189 insertions(+), 1 deletion(-) create mode 100644 tests/perf-test/SignInPage.perf-test.js create mode 100644 tests/utils/collections/getValidCodeCredentials.ts create mode 100644 tests/utils/collections/userAccount.ts diff --git a/src/components/MagicCodeInput.js b/src/components/MagicCodeInput.js index 55a65237a691..ded514aff946 100644 --- a/src/components/MagicCodeInput.js +++ b/src/components/MagicCodeInput.js @@ -61,6 +61,9 @@ const propTypes = { /** Last pressed digit on BigDigitPad */ lastPressedDigit: PropTypes.string, + + /** TestID for test */ + testID: PropTypes.string, }; const defaultProps = { @@ -77,6 +80,7 @@ const defaultProps = { maxLength: CONST.MAGIC_CODE_LENGTH, isDisableKeyboard: false, lastPressedDigit: '', + testID: '', }; /** @@ -397,6 +401,7 @@ function MagicCodeInput(props) { role={CONST.ACCESSIBILITY_ROLE.TEXT} style={[styles.inputTransparent]} textInputContainerStyles={[styles.borderNone]} + testID={props.testID} /> diff --git a/src/pages/signin/LoginForm/BaseLoginForm.js b/src/pages/signin/LoginForm/BaseLoginForm.js index de2f2900c58d..4a076562161d 100644 --- a/src/pages/signin/LoginForm/BaseLoginForm.js +++ b/src/pages/signin/LoginForm/BaseLoginForm.js @@ -267,6 +267,7 @@ function LoginForm(props) { textContentType="username" id="username" name="username" + testID="username" onBlur={() => { if (firstBlurred.current || !Visibility.isVisible() || !Visibility.hasFocus()) { return; diff --git a/src/pages/signin/SignInPage.js b/src/pages/signin/SignInPage.js index 8cb0ef9907af..f88bae02f52d 100644 --- a/src/pages/signin/SignInPage.js +++ b/src/pages/signin/SignInPage.js @@ -245,7 +245,10 @@ function SignInPageInner({credentials, account, isInModal, activeClients, prefer return ( // Bottom SafeAreaView is removed so that login screen svg displays correctly on mobile. // The SVG should flow under the Home Indicator on iOS. - + {hasError && } diff --git a/tests/perf-test/SignInPage.perf-test.js b/tests/perf-test/SignInPage.perf-test.js new file mode 100644 index 000000000000..267a869ee035 --- /dev/null +++ b/tests/perf-test/SignInPage.perf-test.js @@ -0,0 +1,151 @@ +import {fireEvent, screen} from '@testing-library/react-native'; +import React from 'react'; +import Onyx from 'react-native-onyx'; +import {measurePerformance} from 'reassure'; +import ComposeProviders from '../../src/components/ComposeProviders'; +import {LocaleContextProvider} from '../../src/components/LocaleContextProvider'; +import OnyxProvider from '../../src/components/OnyxProvider'; +import {WindowDimensionsProvider} from '../../src/components/withWindowDimensions'; +import CONST from '../../src/CONST'; +import * as Localize from '../../src/libs/Localize'; +import ONYXKEYS from '../../src/ONYXKEYS'; +import SignInPage from '../../src/pages/signin/SignInPage'; +import getValidCodeCredentials from '../utils/collections/getValidCodeCredentials'; +import userAccount, {getValidAccount} from '../utils/collections/userAccount'; +import PusherHelper from '../utils/PusherHelper'; +import * as TestHelper from '../utils/TestHelper'; +import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; +import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; + +jest.mock('../../src/libs/Navigation/Navigation', () => { + const actualNav = jest.requireActual('../../src/libs/Navigation/Navigation'); + return { + ...actualNav, + navigationRef: { + addListener: () => jest.fn(), + removeListener: () => jest.fn(), + }, + }; +}); + +const mockedNavigate = jest.fn(); +jest.mock('@react-navigation/native', () => { + const actualNav = jest.requireActual('@react-navigation/native'); + return { + ...actualNav, + useFocusEffect: jest.fn(), + useIsFocused: () => ({ + navigate: mockedNavigate, + }), + useRoute: () => jest.fn(), + useNavigation: () => ({ + navigate: jest.fn(), + addListener: () => jest.fn(), + }), + createNavigationContainerRef: jest.fn(), + }; +}); + +function SignInPageWrapper(args) { + return ( + + + + ); +} + +const runs = CONST.PERFORMANCE_TESTS.RUNS; +const login = 'test@mail.com'; + +describe('SignInPage', () => { + beforeAll(() => + Onyx.init({ + keys: ONYXKEYS, + safeEvictionKeys: [ONYXKEYS.COLLECTION.REPORT_ACTIONS], + registerStorageEventListener: () => {}, + }), + ); + + // Initialize the network key for OfflineWithFeedback + beforeEach(() => { + global.fetch = TestHelper.getGlobalFetchMock(); + wrapOnyxWithWaitForBatchedUpdates(Onyx); + Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); + }); + + // Clear out Onyx after each test so that each test starts with a clean state + afterEach(() => { + Onyx.clear(); + PusherHelper.teardown(); + }); + + test('[SignInPage] should add username and click continue button', () => { + const addListener = jest.fn(); + const scenario = async () => { + /** + * Checking the SignInPage is mounted + */ + await screen.findByTestId('SignInPage'); + + const usernameInput = screen.getByTestId('username'); + + fireEvent.changeText(usernameInput, login); + + const hintContinueButtonText = Localize.translateLocal('common.continue'); + + const continueButton = await screen.findByText(hintContinueButtonText); + + fireEvent.press(continueButton); + }; + + const navigation = {addListener}; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + [ONYXKEYS.ACCOUNT]: userAccount, + [ONYXKEYS.IS_SIDEBAR_LOADED]: false, + }), + ) + .then(() => measurePerformance(, {scenario, runs})); + }); + + test('[SignInPage] should add magic code and click Sign In button', () => { + const addListener = jest.fn(); + const scenario = async () => { + /** + * Checking the SignInPage is mounted + */ + await screen.findByTestId('SignInPage'); + + const welcomeBackText = Localize.translateLocal('welcomeText.welcomeBack'); + const enterMagicCodeText = Localize.translateLocal('welcomeText.welcomeEnterMagicCode', {login}); + + await screen.findByText(`${welcomeBackText} ${enterMagicCodeText}`); + const magicCodeInput = screen.getByTestId('validateCode'); + + fireEvent.changeText(magicCodeInput, '123456'); + + const signInButtonText = Localize.translateLocal('common.signIn'); + const signInButton = await screen.findByText(signInButtonText); + + fireEvent.press(signInButton); + }; + + const navigation = {addListener}; + + return waitForBatchedUpdates() + .then(() => + Onyx.multiSet({ + [ONYXKEYS.ACCOUNT]: getValidAccount(login), + [ONYXKEYS.CREDENTIALS]: getValidCodeCredentials(login), + [ONYXKEYS.IS_SIDEBAR_LOADED]: false, + }), + ) + .then(() => measurePerformance(, {scenario, runs})); + }); +}); diff --git a/tests/utils/collections/getValidCodeCredentials.ts b/tests/utils/collections/getValidCodeCredentials.ts new file mode 100644 index 000000000000..5ee856b61160 --- /dev/null +++ b/tests/utils/collections/getValidCodeCredentials.ts @@ -0,0 +1,11 @@ +import {randEmail, randNumber} from '@ngneat/falso'; +import type {Credentials} from '@src/types/onyx'; + +function getValidCodeCredentials(login = randEmail()): Credentials { + return { + login, + validateCode: `${randNumber()}`, + }; +} + +export default getValidCodeCredentials; diff --git a/tests/utils/collections/userAccount.ts b/tests/utils/collections/userAccount.ts new file mode 100644 index 000000000000..c1d05eb17cdf --- /dev/null +++ b/tests/utils/collections/userAccount.ts @@ -0,0 +1,16 @@ +import CONST from '@src/CONST'; +import type {Account} from '@src/types/onyx'; + +function getValidAccount(credentialLogin = ''): Account { + return { + validated: true, + primaryLogin: credentialLogin, + isSAMLRequired: false, + isSAMLEnabled: false, + isLoading: false, + requiresTwoFactorAuth: false, + } as Account; +} + +export default CONST.DEFAULT_ACCOUNT_DATA; +export {getValidAccount}; From 207484fe0739f89e5e9ab0ed99ba704d557e5a1c Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 10 Jan 2024 14:40:38 +0100 Subject: [PATCH 023/170] bring back WorkspacesListRow component --- src/pages/workspace/WorkspacesListRow.tsx | 178 ++++++++++++++++++++++ 1 file changed, 178 insertions(+) create mode 100644 src/pages/workspace/WorkspacesListRow.tsx diff --git a/src/pages/workspace/WorkspacesListRow.tsx b/src/pages/workspace/WorkspacesListRow.tsx new file mode 100644 index 000000000000..3f084b4f770b --- /dev/null +++ b/src/pages/workspace/WorkspacesListRow.tsx @@ -0,0 +1,178 @@ +import React, {useMemo} from 'react'; +import {View} from 'react-native'; +import type {ValueOf} from 'type-fest'; +import Avatar from '@components/Avatar'; +import Icon from '@components/Icon'; +import * as Illustrations from '@components/Icon/Illustrations'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; +import Text from '@components/Text'; +import ThreeDotsMenu from '@components/ThreeDotsMenu'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import type {AvatarSource} from '@libs/UserUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; +import type IconAsset from '@src/types/utils/IconAsset'; + +type WorkspacesListRowProps = WithCurrentUserPersonalDetailsProps & { + /** Name of the workspace */ + title: string; + + /** Account ID of the workspace's owner */ + ownerAccountID?: number; + + /** Type of workspace. Type personal is not valid in this context so it's omitted */ + workspaceType: typeof CONST.POLICY.TYPE.FREE | typeof CONST.POLICY.TYPE.CORPORATE | typeof CONST.POLICY.TYPE.TEAM; + + /** Icon to show next to the workspace name */ + workspaceIcon?: AvatarSource; + + /** Icon to be used when workspaceIcon is not present */ + fallbackWorkspaceIcon?: AvatarSource; + + /** Items for the three dots menu */ + menuItems: PopoverMenuItem[]; + + /** Renders the component using big screen layout or small screen layout. When layoutWidth === WorkspaceListRowLayout.NONE, + * component will return null to prevent layout from jumping on initial render and when parent width changes. */ + layoutWidth?: ValueOf; +}; + +const workspaceTypeIcon = (workspaceType: WorkspacesListRowProps['workspaceType']): IconAsset => { + switch (workspaceType) { + case CONST.POLICY.TYPE.FREE: + return Illustrations.HandCard; + case CONST.POLICY.TYPE.CORPORATE: + return Illustrations.ShieldYellow; + case CONST.POLICY.TYPE.TEAM: + return Illustrations.Mailbox; + default: + throw new Error(`Don't know which icon to serve for workspace type`); + } +}; + +function WorkspacesListRow({ + title, + menuItems, + workspaceIcon, + fallbackWorkspaceIcon, + ownerAccountID, + workspaceType, + currentUserPersonalDetails, + layoutWidth = CONST.LAYOUT_WIDTH.NONE, +}: WorkspacesListRowProps) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + const ownerDetails = ownerAccountID && PersonalDetailsUtils.getPersonalDetailsByIDs([ownerAccountID], currentUserPersonalDetails.accountID)[0]; + + const userFriendlyWorkspaceType = useMemo(() => { + switch (workspaceType) { + case CONST.POLICY.TYPE.FREE: + return translate('workspace.type.free'); + case CONST.POLICY.TYPE.CORPORATE: + return translate('workspace.type.control'); + case CONST.POLICY.TYPE.TEAM: + return translate('workspace.type.collect'); + default: + throw new Error(`Don't know a friendly workspace name for this workspace type`); + } + }, [workspaceType, translate]); + + if (layoutWidth === CONST.LAYOUT_WIDTH.NONE) { + // To prevent layout from jumping or rendering for a split second, when + // isWide is undefined we don't assume anything and simply return null. + return null; + } + + const isWide = layoutWidth === CONST.LAYOUT_WIDTH.WIDE; + const isNarrow = layoutWidth === CONST.LAYOUT_WIDTH.NARROW; + + return ( + + + + + {title} + + {isNarrow && ( + + )} + + + {!!ownerDetails && ( + <> + + + + {PersonalDetailsUtils.getDisplayNameOrDefault(ownerDetails)} + + + {ownerDetails.login} + + + + )} + + + + + + {userFriendlyWorkspaceType} + + + {translate('workspace.common.plan')} + + + + {isWide && ( + + )} + + ); +} + +WorkspacesListRow.displayName = 'WorkspacesListRow'; + +export default withCurrentUserPersonalDetails(WorkspacesListRow); From f45e662116e39b4794aee0c4739339491c2916ca Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 10 Jan 2024 20:03:26 +0530 Subject: [PATCH 024/170] [TS migration] AvatarCropModal --- ...AvatarCropModal.js => AvatarCropModal.tsx} | 99 +++++++++---------- .../{ImageCropView.js => ImageCropView.tsx} | 66 +++++++------ .../AvatarCropModal/{Slider.js => Slider.tsx} | 40 ++++---- .../gestureHandlerPropTypes.js | 21 ---- src/components/Button/index.tsx | 2 +- .../Pressable/GenericPressable/types.ts | 2 +- .../Pressable/PressableWithDelayToggle.tsx | 2 +- .../Pressable/PressableWithoutFocus.tsx | 2 +- src/libs/ControlSelection/index.native.ts | 2 +- src/libs/ControlSelection/index.ts | 15 ++- src/libs/ControlSelection/types.ts | 8 +- src/types/utils/CustomRefObject.ts | 5 - 12 files changed, 114 insertions(+), 150 deletions(-) rename src/components/AvatarCropModal/{AvatarCropModal.js => AvatarCropModal.tsx} (87%) rename src/components/AvatarCropModal/{ImageCropView.js => ImageCropView.tsx} (68%) rename src/components/AvatarCropModal/{Slider.js => Slider.tsx} (69%) delete mode 100644 src/components/AvatarCropModal/gestureHandlerPropTypes.js delete mode 100644 src/types/utils/CustomRefObject.ts diff --git a/src/components/AvatarCropModal/AvatarCropModal.js b/src/components/AvatarCropModal/AvatarCropModal.tsx similarity index 87% rename from src/components/AvatarCropModal/AvatarCropModal.js rename to src/components/AvatarCropModal/AvatarCropModal.tsx index eb3e21c3ad9d..2a9174c6d7a3 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.js +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -1,6 +1,5 @@ -import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useState} from 'react'; -import {ActivityIndicator, Image, View} from 'react-native'; +import {ActivityIndicator, Image, LayoutChangeEvent, View} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {interpolate, runOnUI, useAnimatedGestureHandler, useSharedValue, useWorkletCallback} from 'react-native-reanimated'; import Button from '@components/Button'; @@ -8,71 +7,62 @@ import HeaderGap from '@components/HeaderGap'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; import Modal from '@components/Modal'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import compose from '@libs/compose'; import cropOrRotateImage from '@libs/cropOrRotateImage'; +import {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import CONST from '@src/CONST'; +import IconAsset from '@src/types/utils/IconAsset'; import ImageCropView from './ImageCropView'; import Slider from './Slider'; -const propTypes = { +type AvatarCropModalProps = { /** Link to image for cropping */ - imageUri: PropTypes.string, + imageUri: string; /** Name of the image */ - imageName: PropTypes.string, + imageName: string; /** Type of the image file */ - imageType: PropTypes.string, + imageType: string; /** Callback to be called when user closes the modal */ - onClose: PropTypes.func, + onClose: () => void; /** Callback to be called when user saves the image */ - onSave: PropTypes.func, + onSave: (image: File | CustomRNImageManipulatorResult) => void; /** Modal visibility */ - isVisible: PropTypes.bool.isRequired, + isVisible: boolean; /** Image crop vector mask */ - maskImage: sourcePropTypes, - - ...withLocalizePropTypes, - ...windowDimensionsPropTypes, -}; - -const defaultProps = { - imageUri: '', - imageName: '', - imageType: '', - onClose: () => {}, - onSave: () => {}, - maskImage: undefined, + maskImage?: IconAsset; }; // This component can't be written using class since reanimated API uses hooks. -function AvatarCropModal(props) { +function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose = () => {}, onSave = () => {}, ...props}: AvatarCropModalProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); - const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const originalImageWidth = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const originalImageHeight = useSharedValue(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const translateY = useSharedValue(0); const translateX = useSharedValue(0); - const scale = useSharedValue(CONST.AVATAR_CROP_MODAL.MIN_SCALE); + const scale = useSharedValue(CONST.AVATAR_CROP_MODAL.MIN_SCALE); const rotation = useSharedValue(0); const translateSlider = useSharedValue(0); const isPressableEnabled = useSharedValue(true); + const {translate} = useLocalize(); + const {isSmallScreenWidth} = useWindowDimensions(); // Check if image cropping, saving or uploading is in progress const isLoading = useSharedValue(false); @@ -82,13 +72,13 @@ function AvatarCropModal(props) { const prevMaxOffsetX = useSharedValue(0); const prevMaxOffsetY = useSharedValue(0); - const [imageContainerSize, setImageContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); - const [sliderContainerSize, setSliderContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const [imageContainerSize, setImageContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); + const [sliderContainerSize, setSliderContainerSize] = useState(CONST.AVATAR_CROP_MODAL.INITIAL_SIZE); const [isImageContainerInitialized, setIsImageContainerInitialized] = useState(false); const [isImageInitialized, setIsImageInitialized] = useState(false); // An onLayout callback, that initializes the image container, for proper render of an image - const initializeImageContainer = useCallback((event) => { + const initializeImageContainer = useCallback((event: LayoutChangeEvent) => { setIsImageContainerInitialized(true); const {height, width} = event.nativeEvent.layout; @@ -98,7 +88,7 @@ function AvatarCropModal(props) { }, []); // An onLayout callback, that initializes the slider container size, for proper render of a slider - const initializeSliderContainer = useCallback((event) => { + const initializeSliderContainer = useCallback((event: LayoutChangeEvent) => { setSliderContainerSize(event.nativeEvent.layout.width); }, []); @@ -122,7 +112,6 @@ function AvatarCropModal(props) { // In order to calculate proper image position/size/animation, we have to know its size. // And we have to update image size if image url changes. - const imageUri = props.imageUri; useEffect(() => { if (!imageUri) { return; @@ -148,7 +137,7 @@ function AvatarCropModal(props) { * @param {Array} minMax * @returns {Number} */ - const clamp = useWorkletCallback((value, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []); + const clamp = useWorkletCallback((value: number, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []); /** * Returns current image size taking into account scale and rotation. @@ -177,7 +166,7 @@ function AvatarCropModal(props) { * @param {Number} newY */ const updateImageOffset = useWorkletCallback( - (offsetX, offsetY) => { + (offsetX: number, offsetY: number) => { const {height, width} = getDisplayedImageSize(); const maxOffsetX = (width - imageContainerSize) / 2; const maxOffsetY = (height - imageContainerSize) / 2; @@ -194,7 +183,7 @@ function AvatarCropModal(props) { * @param {Number} containerSize * @returns {Number} */ - const newScaleValue = useWorkletCallback((newSliderValue, containerSize) => { + const newScaleValue = useWorkletCallback((newSliderValue: number, containerSize: number) => { const {MAX_SCALE, MIN_SCALE} = CONST.AVATAR_CROP_MODAL; return (newSliderValue / containerSize) * (MAX_SCALE - MIN_SCALE) + MIN_SCALE; }); @@ -323,14 +312,14 @@ function AvatarCropModal(props) { // Svg images are converted to a png blob to preserve transparency, so we need to update the // image name and type accordingly. - const isSvg = props.imageType.includes('image/svg'); - const imageName = isSvg ? 'fileName.png' : props.imageName; - const imageType = isSvg ? 'image/png' : props.imageType; + const isSvg = imageType.includes('image/svg'); + const imgName = isSvg ? 'fileName.png' : imageName; + const imgType = isSvg ? 'image/png' : imageType; - cropOrRotateImage(props.imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name: imageName, type: imageType}) + cropOrRotateImage(imageUri, [{rotate: rotation.value % 360}, {crop}], {compress: 1, name: imgName, type: imgType}) .then((newImage) => { - props.onClose(); - props.onSave(newImage); + onClose(); + onSave(newImage); }) .catch(() => { isLoading.value = false; @@ -340,7 +329,7 @@ function AvatarCropModal(props) { /** * @param {Number} locationX */ - const sliderOnPress = (locationX) => { + const sliderOnPress = (locationX: number) => { // We are using the worklet directive here and running on the UI thread to ensure the Reanimated // shared values are updated synchronously, as they update asynchronously on the JS thread. @@ -361,7 +350,7 @@ function AvatarCropModal(props) { return ( - {props.isSmallScreenWidth && } + {isSmallScreenWidth && } - {props.translate('avatarCropModal.description')} + {translate('avatarCropModal.description')} runOnUI(sliderOnPress)(e.nativeEvent.locationX)} + accessible={false} accessibilityLabel="slider" role={CONST.ROLE.SLIDER} > @@ -422,7 +412,7 @@ function AvatarCropModal(props) { /> @@ -444,7 +434,7 @@ function AvatarCropModal(props) { style={[styles.m5]} onPress={cropAndSaveImage} pressOnEnter - text={props.translate('common.save')} + text={translate('common.save')} /> @@ -452,6 +442,5 @@ function AvatarCropModal(props) { } AvatarCropModal.displayName = 'AvatarCropModal'; -AvatarCropModal.propTypes = propTypes; -AvatarCropModal.defaultProps = defaultProps; -export default compose(withWindowDimensions, withLocalize)(AvatarCropModal); + +export default AvatarCropModal; diff --git a/src/components/AvatarCropModal/ImageCropView.js b/src/components/AvatarCropModal/ImageCropView.tsx similarity index 68% rename from src/components/AvatarCropModal/ImageCropView.js rename to src/components/AvatarCropModal/ImageCropView.tsx index 92cbe3a4da04..0f17fb6ba5b3 100644 --- a/src/components/AvatarCropModal/ImageCropView.js +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import {PanGestureHandler} from 'react-native-gesture-handler'; +import type {GestureEvent, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -9,52 +9,58 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import gestureHandlerPropTypes from './gestureHandlerPropTypes'; +import {SelectionElement} from '@libs/ControlSelection/types'; +import type IconAsset from '@src/types/utils/IconAsset'; -const propTypes = { +type ImageCropViewProps = { /** Link to image for cropping */ - imageUri: PropTypes.string, + imageUri: string; /** Size of the image container that will be rendered */ - containerSize: PropTypes.number, + containerSize: number; /** The height of the selected image */ - originalImageHeight: PropTypes.shape({value: PropTypes.number}).isRequired, + originalImageHeight: { + value: number; + }; /** The width of the selected image */ - originalImageWidth: PropTypes.shape({value: PropTypes.number}).isRequired, + originalImageWidth: { + value: number; + }; /** The rotation value of the selected image */ - rotation: PropTypes.shape({value: PropTypes.number}).isRequired, + rotation: { + value: number; + }; /** The relative image shift along X-axis */ - translateX: PropTypes.shape({value: PropTypes.number}).isRequired, + translateX: { + value: number; + }; /** The relative image shift along Y-axis */ - translateY: PropTypes.shape({value: PropTypes.number}).isRequired, + translateY: { + value: number; + }; /** The scale factor of the image */ - scale: PropTypes.shape({value: PropTypes.number}).isRequired, + scale: { + value: number; + }; /** React-native-reanimated lib handler which executes when the user is panning image */ - panGestureEventHandler: gestureHandlerPropTypes, + panGestureEventHandler: (event: GestureEvent) => void; /** Image crop vector mask */ - maskImage: PropTypes.func, + maskImage?: IconAsset; }; -const defaultProps = { - imageUri: '', - containerSize: 0, - panGestureEventHandler: () => {}, - maskImage: Expensicons.ImageCropCircleMask, -}; - -function ImageCropView(props) { +function ImageCropView({imageUri = '', containerSize = 0, panGestureEventHandler = () => {}, maskImage = Expensicons.ImageCropCircleMask, ...props}: ImageCropViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const containerStyle = StyleUtils.getWidthAndHeightStyle(props.containerSize, props.containerSize); + const containerStyle = StyleUtils.getWidthAndHeightStyle(containerSize, containerSize); const originalImageHeight = props.originalImageHeight; const originalImageWidth = props.originalImageWidth; @@ -77,22 +83,24 @@ function ImageCropView(props) { // We're preventing text selection with ControlSelection.blockElement to prevent safari // default behaviour of cursor - I-beam cursor on drag. See https://github.com/Expensify/App/issues/13688 return ( - + { + ControlSelection.blockElement(el as SelectionElement); + }} style={[containerStyle, styles.imageCropContainer]} > @@ -101,8 +109,6 @@ function ImageCropView(props) { } ImageCropView.displayName = 'ImageCropView'; -ImageCropView.propTypes = propTypes; -ImageCropView.defaultProps = defaultProps; // React.memo is needed here to prevent styles recompilation // which sometimes may cause glitches during rerender of the modal diff --git a/src/components/AvatarCropModal/Slider.js b/src/components/AvatarCropModal/Slider.tsx similarity index 69% rename from src/components/AvatarCropModal/Slider.js rename to src/components/AvatarCropModal/Slider.tsx index ba2e1471ce9e..841b8d5bd473 100644 --- a/src/components/AvatarCropModal/Slider.js +++ b/src/components/AvatarCropModal/Slider.tsx @@ -1,34 +1,29 @@ -import PropTypes from 'prop-types'; -import React, {useState} from 'react'; +import React, {useRef, useState} from 'react'; import {View} from 'react-native'; import {PanGestureHandler} from 'react-native-gesture-handler'; +import type {GestureEvent, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import Animated, {useAnimatedStyle} from 'react-native-reanimated'; import Tooltip from '@components/Tooltip'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import gestureHandlerPropTypes from './gestureHandlerPropTypes'; +import {SelectionElement} from '@libs/ControlSelection/types'; -const propTypes = { +type SliderProps = { /** React-native-reanimated lib handler which executes when the user is panning slider */ - onGesture: gestureHandlerPropTypes, + onGesture: (event: GestureEvent) => void; /** X position of the slider knob */ - sliderValue: PropTypes.shape({value: PropTypes.number}), - - ...withLocalizePropTypes, -}; - -const defaultProps = { - onGesture: () => {}, - sliderValue: {}, + sliderValue: { + value: number; + }; }; // This component can't be written using class since reanimated API uses hooks. -function Slider(props) { +function Slider({onGesture = () => {}, sliderValue = {value: 0}}: SliderProps) { const styles = useThemeStyles(); - const sliderValue = props.sliderValue; const [tooltipIsVisible, setTooltipIsVisible] = useState(true); + const {translate} = useLocalize(); // A reanimated memoized style, which tracks // a translateX shared value and updates the slider position. @@ -40,18 +35,20 @@ function Slider(props) { // default behaviour of cursor - I-beam cursor on drag. See https://github.com/Expensify/App/issues/13688 return ( { + ControlSelection.blockElement(el as SelectionElement); + }} style={styles.sliderBar} > setTooltipIsVisible(false)} onEnded={() => setTooltipIsVisible(true)} - onGestureEvent={props.onGesture} + onGestureEvent={onGesture} > {tooltipIsVisible && ( @@ -64,6 +61,5 @@ function Slider(props) { } Slider.displayName = 'Slider'; -Slider.propTypes = propTypes; -Slider.defaultProps = defaultProps; -export default withLocalize(Slider); + +export default Slider; diff --git a/src/components/AvatarCropModal/gestureHandlerPropTypes.js b/src/components/AvatarCropModal/gestureHandlerPropTypes.js deleted file mode 100644 index c473a162ba7e..000000000000 --- a/src/components/AvatarCropModal/gestureHandlerPropTypes.js +++ /dev/null @@ -1,21 +0,0 @@ -import PropTypes from 'prop-types'; - -export default PropTypes.oneOfType([ - // Executes once a gesture is triggered - PropTypes.func, - PropTypes.shape({ - current: PropTypes.shape({ - // Array of event names that will be handled by animation handler - eventNames: PropTypes.arrayOf(PropTypes.string), - - // Array of registered event handlers ids - registrations: PropTypes.arrayOf(PropTypes.number), - - // React tag of the node we want to manage - viewTag: PropTypes.number, - - // Executes once a gesture is triggered - worklet: PropTypes.func, - }), - }), -]); diff --git a/src/components/Button/index.tsx b/src/components/Button/index.tsx index fb72f0cc845f..b422f512ca47 100644 --- a/src/components/Button/index.tsx +++ b/src/components/Button/index.tsx @@ -20,7 +20,7 @@ import validateSubmitShortcut from './validateSubmitShortcut'; type ButtonWithText = { /** The text for the button label */ - text: string; + text?: string; /** Boolean whether to display the right icon */ shouldShowRightIcon?: boolean; diff --git a/src/components/Pressable/GenericPressable/types.ts b/src/components/Pressable/GenericPressable/types.ts index dc04b6fcf329..2dd2e17e0454 100644 --- a/src/components/Pressable/GenericPressable/types.ts +++ b/src/components/Pressable/GenericPressable/types.ts @@ -40,7 +40,7 @@ type PressableProps = RNPressableProps & /** * onPress callback */ - onPress: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; + onPress?: (event?: GestureResponderEvent | KeyboardEvent) => void | Promise; /** * Specifies keyboard shortcut to trigger onPressHandler diff --git a/src/components/Pressable/PressableWithDelayToggle.tsx b/src/components/Pressable/PressableWithDelayToggle.tsx index ab1fa95efeb5..86f6c9d8aff8 100644 --- a/src/components/Pressable/PressableWithDelayToggle.tsx +++ b/src/components/Pressable/PressableWithDelayToggle.tsx @@ -78,7 +78,7 @@ function PressableWithDelayToggle( return; } temporarilyDisableInteractions(); - onPress(); + onPress?.(); }; // Due to limitations in RN regarding the vertical text alignment of non-Text elements, diff --git a/src/components/Pressable/PressableWithoutFocus.tsx b/src/components/Pressable/PressableWithoutFocus.tsx index f887b0ea9b7d..240ef4a9873a 100644 --- a/src/components/Pressable/PressableWithoutFocus.tsx +++ b/src/components/Pressable/PressableWithoutFocus.tsx @@ -15,7 +15,7 @@ function PressableWithoutFocus({children, onPress, onLongPress, ...rest}: Pressa const pressAndBlur = () => { ref?.current?.blur(); - onPress(); + onPress?.(); }; return ( diff --git a/src/libs/ControlSelection/index.native.ts b/src/libs/ControlSelection/index.native.ts index b45af6da6441..2bccea946cda 100644 --- a/src/libs/ControlSelection/index.native.ts +++ b/src/libs/ControlSelection/index.native.ts @@ -1,4 +1,4 @@ -import type ControlSelectionModule from './types'; +import type {ControlSelectionModule} from './types'; function block() {} function unblock() {} diff --git a/src/libs/ControlSelection/index.ts b/src/libs/ControlSelection/index.ts index ab11e66bc369..89f1e62aa6f6 100644 --- a/src/libs/ControlSelection/index.ts +++ b/src/libs/ControlSelection/index.ts @@ -1,5 +1,4 @@ -import type CustomRefObject from '@src/types/utils/CustomRefObject'; -import type ControlSelectionModule from './types'; +import type {ControlSelectionModule, SelectionElement} from './types'; /** * Block selection on the whole app @@ -20,25 +19,25 @@ function unblock() { /** * Block selection on particular element */ -function blockElement(ref?: CustomRefObject | null) { - if (!ref) { +function blockElement(element?: SelectionElement | null) { + if (!element) { return; } // eslint-disable-next-line no-param-reassign - ref.onselectstart = () => false; + element.onselectstart = () => false; } /** * Unblock selection on particular element */ -function unblockElement(ref?: CustomRefObject | null) { - if (!ref) { +function unblockElement(element?: SelectionElement | null) { + if (!element) { return; } // eslint-disable-next-line no-param-reassign - ref.onselectstart = () => true; + element.onselectstart = () => true; } const ControlSelection: ControlSelectionModule = { diff --git a/src/libs/ControlSelection/types.ts b/src/libs/ControlSelection/types.ts index fc0b488577ec..8433a366ed91 100644 --- a/src/libs/ControlSelection/types.ts +++ b/src/libs/ControlSelection/types.ts @@ -1,10 +1,10 @@ -import type CustomRefObject from '@src/types/utils/CustomRefObject'; +type SelectionElement = T & {onselectstart: () => boolean}; type ControlSelectionModule = { block: () => void; unblock: () => void; - blockElement: (ref?: CustomRefObject | null) => void; - unblockElement: (ref?: CustomRefObject | null) => void; + blockElement: (element?: SelectionElement | null) => void; + unblockElement: (element?: SelectionElement | null) => void; }; -export default ControlSelectionModule; +export type {ControlSelectionModule, SelectionElement}; diff --git a/src/types/utils/CustomRefObject.ts b/src/types/utils/CustomRefObject.ts deleted file mode 100644 index 13bb0f27a42e..000000000000 --- a/src/types/utils/CustomRefObject.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type {RefObject} from 'react'; - -type CustomRefObject = RefObject & {onselectstart: () => boolean}; - -export default CustomRefObject; From 83c42ab3a9d8ad20df68d90a8816b3bb9a5bda25 Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 10 Jan 2024 20:25:29 +0530 Subject: [PATCH 025/170] Fix lint --- .../AvatarCropModal/AvatarCropModal.tsx | 38 ++++++++++++++----- .../AvatarCropModal/ImageCropView.tsx | 2 +- src/components/AvatarCropModal/Slider.tsx | 4 +- 3 files changed, 31 insertions(+), 13 deletions(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 2a9174c6d7a3..3f4b7d38451e 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -1,5 +1,6 @@ import React, {useCallback, useEffect, useState} from 'react'; -import {ActivityIndicator, Image, LayoutChangeEvent, View} from 'react-native'; +import {ActivityIndicator, Image, View} from 'react-native'; +import type {LayoutChangeEvent} from 'react-native'; import {GestureHandlerRootView} from 'react-native-gesture-handler'; import {interpolate, runOnUI, useAnimatedGestureHandler, useSharedValue, useWorkletCallback} from 'react-native-reanimated'; import Button from '@components/Button'; @@ -17,11 +18,10 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import compose from '@libs/compose'; import cropOrRotateImage from '@libs/cropOrRotateImage'; -import {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; +import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types'; import CONST from '@src/CONST'; -import IconAsset from '@src/types/utils/IconAsset'; +import type IconAsset from '@src/types/utils/IconAsset'; import ImageCropView from './ImageCropView'; import Slider from './Slider'; @@ -48,6 +48,12 @@ type AvatarCropModalProps = { maskImage?: IconAsset; }; +type PanHandlerContextType = { + translateX: number; + translateY: number; + translateSliderX: number; +}; + // This component can't be written using class since reanimated API uses hooks. function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose = () => {}, onSave = () => {}, ...props}: AvatarCropModalProps) { const theme = useTheme(); @@ -194,7 +200,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose */ const panGestureEventHandler = useAnimatedGestureHandler( { - onStart: (_, context) => { + onStart: (a, context: PanHandlerContextType) => { // we have to assign translate values to a context // since that is required for proper work of turbo modules. // eslint-disable-next-line no-param-reassign @@ -242,7 +248,7 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose */ const panSliderGestureEventHandler = useAnimatedGestureHandler( { - onStart: (_, context) => { + onStart: (a, context: PanHandlerContextType) => { // we have to assign this value to a context // since that is required for proper work of turbo modules. // eslint-disable-next-line no-param-reassign @@ -324,11 +330,23 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose .catch(() => { isLoading.value = false; }); - }, [originalImageHeight.value, originalImageWidth.value, scale.value, translateX.value, imageContainerSize, translateY.value, props, rotation.value, isLoading]); + }, [ + imageName, + imageUri, + imageType, + onSave, + onClose, + originalImageHeight.value, + originalImageWidth.value, + scale.value, + translateX.value, + imageContainerSize, + translateY.value, + props, + rotation.value, + isLoading, + ]); - /** - * @param {Number} locationX - */ const sliderOnPress = (locationX: number) => { // We are using the worklet directive here and running on the UI thread to ensure the Reanimated // shared values are updated synchronously, as they update asynchronously on the JS thread. diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index 0f17fb6ba5b3..e76ffc2d1f10 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -9,7 +9,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import {SelectionElement} from '@libs/ControlSelection/types'; +import type {SelectionElement} from '@libs/ControlSelection/types'; import type IconAsset from '@src/types/utils/IconAsset'; type ImageCropViewProps = { diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index 841b8d5bd473..26e35517f48e 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -1,4 +1,4 @@ -import React, {useRef, useState} from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import {PanGestureHandler} from 'react-native-gesture-handler'; import type {GestureEvent, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; @@ -7,7 +7,7 @@ import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import {SelectionElement} from '@libs/ControlSelection/types'; +import type {SelectionElement} from '@libs/ControlSelection/types'; type SliderProps = { /** React-native-reanimated lib handler which executes when the user is panning slider */ From 3c222b345b5dba0a05c17e75aa91f9510761e087 Mon Sep 17 00:00:00 2001 From: someone-here Date: Wed, 10 Jan 2024 20:34:21 +0530 Subject: [PATCH 026/170] Fix lint --- src/components/AvatarCropModal/AvatarCropModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 3f4b7d38451e..7c9313a6d394 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -342,7 +342,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose translateX.value, imageContainerSize, translateY.value, - props, rotation.value, isLoading, ]); From 6290efaa16f811f873c4e8fb1c04551e803b93e4 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Thu, 11 Jan 2024 02:01:48 +0530 Subject: [PATCH 027/170] fix: Workspace - User is scrolled down in Workspace invite page Signed-off-by: Krishna Gupta --- src/pages/RoomInvitePage.js | 113 ++++++++++-------- src/pages/workspace/WorkspaceInvitePage.js | 127 ++++++++++++--------- 2 files changed, 133 insertions(+), 107 deletions(-) diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index f290eff91669..1eb69f454ab4 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -1,3 +1,4 @@ +import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useState} from 'react'; @@ -68,6 +69,8 @@ function RoomInvitePage(props) { const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [userToInvite, setUserToInvite] = useState(null); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const navigation = useNavigation(); // Any existing participants and Expensify emails should not be eligible for invitation const excludedUsers = useMemo( @@ -92,10 +95,26 @@ function RoomInvitePage(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [props.personalDetails, props.betas, searchTerm, excludedUsers]); - const getSections = () => { - const sections = []; + useEffect(() => { + const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { + setDidScreenTransitionEnd(true); + }); + + return () => { + unsubscribeTransitionEnd(); + }; + // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + + const sections = useMemo(() => { + const sectionsArr = []; let indexOffset = 0; + if (!didScreenTransitionEnd) { + return []; + } + // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; if (searchTerm !== '') { @@ -108,7 +127,7 @@ function RoomInvitePage(props) { }); } - sections.push({ + sectionsArr.push({ title: undefined, data: filterSelectedOptions, shouldShow: true, @@ -122,7 +141,7 @@ function RoomInvitePage(props) { const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, (personalDetail) => OptionsListUtils.formatMemberForList(personalDetail, false)); const hasUnselectedUserToInvite = userToInvite && !_.contains(selectedLogins, userToInvite.login); - sections.push({ + sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, shouldShow: !_.isEmpty(personalDetailsFormatted), @@ -131,7 +150,7 @@ function RoomInvitePage(props) { indexOffset += personalDetailsFormatted.length; if (hasUnselectedUserToInvite) { - sections.push({ + sectionsArr.push({ title: undefined, data: [OptionsListUtils.formatMemberForList(userToInvite, false)], shouldShow: true, @@ -139,8 +158,8 @@ function RoomInvitePage(props) { }); } - return sections; - }; + return sectionsArr; + }, [personalDetails, searchTerm, selectedOptions, translate, userToInvite, didScreenTransitionEnd]); const toggleOption = useCallback( (option) => { @@ -204,49 +223,43 @@ function RoomInvitePage(props) { shouldEnableMaxHeight testID={RoomInvitePage.displayName} > - {({didScreenTransitionEnd}) => { - const sections = didScreenTransitionEnd ? getSections() : []; - - return ( - Navigation.goBack(backRoute)} - > - { - Navigation.goBack(backRoute); - }} - /> - - - - - - ); - }} + Navigation.goBack(backRoute)} + > + { + Navigation.goBack(backRoute); + }} + /> + + + + + ); } diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 6496fbecfc9f..6954cb030985 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -1,3 +1,4 @@ +import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useEffect, useMemo, useState} from 'react'; @@ -71,6 +72,8 @@ function WorkspaceInvitePage(props) { const [selectedOptions, setSelectedOptions] = useState([]); const [personalDetails, setPersonalDetails] = useState([]); const [usersToInvite, setUsersToInvite] = useState([]); + const [didScreenTransitionEnd, setDidScreenTransitionEnd] = useState(false); + const navigation = useNavigation(); const openWorkspaceInvitePage = () => { const policyMemberEmailsToAccountIDs = PolicyUtils.getMemberAccountIDsForWorkspace(props.policyMembers, props.personalDetails); Policy.openWorkspaceInvitePage(props.route.params.policyID, _.keys(policyMemberEmailsToAccountIDs)); @@ -86,6 +89,18 @@ function WorkspaceInvitePage(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- policyID changes remount the component }, []); + useEffect(() => { + const unsubscribeTransitionEnd = navigation.addListener('transitionEnd', () => { + setDidScreenTransitionEnd(true); + }); + + return () => { + unsubscribeTransitionEnd(); + }; + // Rule disabled because this effect is only for component did mount & will component unmount lifecycle event + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useNetwork({onReconnect: openWorkspaceInvitePage}); const excludedUsers = useMemo(() => PolicyUtils.getIneligibleInvitees(props.policyMembers, props.personalDetails), [props.policyMembers, props.personalDetails]); @@ -131,10 +146,14 @@ function WorkspaceInvitePage(props) { // eslint-disable-next-line react-hooks/exhaustive-deps -- we don't want to recalculate when selectedOptions change }, [props.personalDetails, props.policyMembers, props.betas, searchTerm, excludedUsers]); - const getSections = () => { - const sections = []; + const sections = useMemo(() => { + const sectionsArr = []; let indexOffset = 0; + if (!didScreenTransitionEnd) { + return []; + } + // Filter all options that is a part of the search term or in the personal details let filterSelectedOptions = selectedOptions; if (searchTerm !== '') { @@ -147,7 +166,7 @@ function WorkspaceInvitePage(props) { }); } - sections.push({ + sectionsArr.push({ title: undefined, data: filterSelectedOptions, shouldShow: true, @@ -160,7 +179,7 @@ function WorkspaceInvitePage(props) { const personalDetailsWithoutSelected = _.filter(personalDetails, ({login}) => !_.contains(selectedLogins, login)); const personalDetailsFormatted = _.map(personalDetailsWithoutSelected, OptionsListUtils.formatMemberForList); - sections.push({ + sectionsArr.push({ title: translate('common.contacts'), data: personalDetailsFormatted, shouldShow: !_.isEmpty(personalDetailsFormatted), @@ -172,7 +191,7 @@ function WorkspaceInvitePage(props) { const hasUnselectedUserToInvite = !_.contains(selectedLogins, userToInvite.login); if (hasUnselectedUserToInvite) { - sections.push({ + sectionsArr.push({ title: undefined, data: [OptionsListUtils.formatMemberForList(userToInvite)], shouldShow: true, @@ -181,8 +200,8 @@ function WorkspaceInvitePage(props) { } }); - return sections; - }; + return sectionsArr; + }, [personalDetails, searchTerm, selectedOptions, usersToInvite, translate, didScreenTransitionEnd]); const toggleOption = (option) => { Policy.clearErrors(props.route.params.policyID); @@ -248,56 +267,50 @@ function WorkspaceInvitePage(props) { shouldEnableMaxHeight testID={WorkspaceInvitePage.displayName} > - {({didScreenTransitionEnd}) => { - const sections = didScreenTransitionEnd ? getSections() : []; - - return ( - Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} - > - { - Policy.clearErrors(props.route.params.policyID); - Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID)); - }} - /> - { - SearchInputManager.searchInput = value; - setSearchTerm(value); - }} - headerMessage={headerMessage} - onSelectRow={toggleOption} - onConfirm={inviteUser} - showScrollIndicator - showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)} - shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} - /> - - - - - ); - }} + Navigation.goBack(ROUTES.SETTINGS_WORKSPACES)} + > + { + Policy.clearErrors(props.route.params.policyID); + Navigation.goBack(ROUTES.WORKSPACE_MEMBERS.getRoute(props.route.params.policyID)); + }} + /> + { + SearchInputManager.searchInput = value; + setSearchTerm(value); + }} + headerMessage={headerMessage} + onSelectRow={toggleOption} + onConfirm={inviteUser} + showScrollIndicator + showLoadingPlaceholder={!didScreenTransitionEnd || !OptionsListUtils.isPersonalDetailsReady(props.personalDetails)} + shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()} + /> + + + + ); } From 5a7eda2e246ece0334d9cc1613e2932ee617f077 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 11 Jan 2024 10:38:04 +0100 Subject: [PATCH 028/170] remove unused js files --- .../headerWithBackButtonPropTypes.js | 101 ------------------ .../ThreeDotsMenuItemPropTypes.js | 12 --- src/components/ThreeDotsMenu/index.tsx | 2 - 3 files changed, 115 deletions(-) delete mode 100644 src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js delete mode 100644 src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js diff --git a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js b/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js deleted file mode 100644 index 109e60adf672..000000000000 --- a/src/components/HeaderWithBackButton/headerWithBackButtonPropTypes.js +++ /dev/null @@ -1,101 +0,0 @@ -import PropTypes from 'prop-types'; -import participantPropTypes from '@components/participantPropTypes'; -import {ThreeDotsMenuItemPropTypes} from '@components/ThreeDotsMenu'; -import iouReportPropTypes from '@pages/iouReportPropTypes'; - -const propTypes = { - /** Title of the Header */ - title: PropTypes.string, - - /** Subtitle of the header */ - subtitle: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), - - /** Method to trigger when pressing download button of the header */ - onDownloadButtonPress: PropTypes.func, - - /** Method to trigger when pressing close button of the header */ - onCloseButtonPress: PropTypes.func, - - /** Method to trigger when pressing back button of the header */ - onBackButtonPress: PropTypes.func, - - /** Method to trigger when pressing more options button of the header */ - onThreeDotsButtonPress: PropTypes.func, - - /** Whether we should show a border on the bottom of the Header */ - shouldShowBorderBottom: PropTypes.bool, - - /** Whether we should show a download button */ - shouldShowDownloadButton: PropTypes.bool, - - /** Whether we should show a get assistance (question mark) button */ - shouldShowGetAssistanceButton: PropTypes.bool, - - /** Whether we should disable the get assistance button */ - shouldDisableGetAssistanceButton: PropTypes.bool, - - /** Whether we should show a pin button */ - shouldShowPinButton: PropTypes.bool, - - /** Whether we should show a more options (threedots) button */ - shouldShowThreeDotsButton: PropTypes.bool, - - /** Whether we should disable threedots button */ - shouldDisableThreeDotsButton: PropTypes.bool, - - /** List of menu items for more(three dots) menu */ - threeDotsMenuItems: ThreeDotsMenuItemPropTypes, - - /** The anchor position of the menu */ - threeDotsAnchorPosition: PropTypes.shape({ - top: PropTypes.number, - right: PropTypes.number, - bottom: PropTypes.number, - left: PropTypes.number, - }), - - /** Whether we should show a close button */ - shouldShowCloseButton: PropTypes.bool, - - /** Whether we should show a back button */ - shouldShowBackButton: PropTypes.bool, - - /** The guides call taskID to associate with the get assistance button, if we show it */ - guidesCallTaskID: PropTypes.string, - - /** Data to display a step counter in the header */ - stepCounter: PropTypes.shape({ - step: PropTypes.number, - total: PropTypes.number, - text: PropTypes.string, - }), - - /** Whether we should show an avatar */ - shouldShowAvatarWithDisplay: PropTypes.bool, - - /** Parent report, if provided it will override props.report for AvatarWithDisplay */ - parentReport: iouReportPropTypes, - - /** Report, if we're showing the details for one and using AvatarWithDisplay */ - report: iouReportPropTypes, - - /** The report's policy, if we're showing the details for a report and need info about it for AvatarWithDisplay */ - policy: PropTypes.shape({ - /** Name of the policy */ - name: PropTypes.string, - }), - - /** Policies, if we're showing the details for a report and need participant details for AvatarWithDisplay */ - personalDetails: PropTypes.objectOf(participantPropTypes), - - /** Children to wrap in Header */ - children: PropTypes.node, - - /** Single execution function to prevent concurrent navigation actions */ - singleExecution: PropTypes.func, - - /** Whether we should navigate to report page when the route have a topMostReport */ - shouldNavigateToTopMostReport: PropTypes.bool, -}; - -export default propTypes; diff --git a/src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js b/src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js deleted file mode 100644 index 9f09eabbc7f7..000000000000 --- a/src/components/ThreeDotsMenu/ThreeDotsMenuItemPropTypes.js +++ /dev/null @@ -1,12 +0,0 @@ -import PropTypes from 'prop-types'; -import sourcePropTypes from '@components/Image/sourcePropTypes'; - -const menuItemProps = PropTypes.arrayOf( - PropTypes.shape({ - icon: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]), - text: PropTypes.string, - onPress: PropTypes.func, - }), -); - -export default menuItemProps; diff --git a/src/components/ThreeDotsMenu/index.tsx b/src/components/ThreeDotsMenu/index.tsx index ced2b67826a0..920b8f9f4130 100644 --- a/src/components/ThreeDotsMenu/index.tsx +++ b/src/components/ThreeDotsMenu/index.tsx @@ -16,7 +16,6 @@ import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {AnchorPosition} from '@src/styles'; import type IconAsset from '@src/types/utils/IconAsset'; -import ThreeDotsMenuItemPropTypes from './ThreeDotsMenuItemPropTypes'; type ThreeDotsMenuProps = { /** Tooltip for the popup icon */ @@ -136,4 +135,3 @@ function ThreeDotsMenu({ ThreeDotsMenu.displayName = 'ThreeDotsMenu'; export default ThreeDotsMenu; -export {ThreeDotsMenuItemPropTypes}; From d25a29644711e3626917b9eb95d4276b28af44c3 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Thu, 11 Jan 2024 20:47:24 +0300 Subject: [PATCH 029/170] updated Header view to include current user avatar --- src/libs/ReportUtils.ts | 152 ++++++++++++++++++----------------- src/pages/home/HeaderView.js | 2 +- 2 files changed, 79 insertions(+), 75 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 9dc6a1465c62..45c583da3a86 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1336,6 +1336,80 @@ function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = return workspaceIcon; } +/** + * Checks if a report is a group chat. + * + * A report is a group chat if it meets the following conditions: + * - Not a chat thread. + * - Not a task report. + * - Not a money request / IOU report. + * - Not an archived room. + * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). + * - More than 1 participants (note that participantAccountIDs excludes the current user). + * + */ +function isGroupChat(report: OnyxEntry): boolean { + return Boolean( + report && + !isChatThread(report) && + !isTaskReport(report) && + !isMoneyRequestReport(report) && + !isArchivedRoom(report) && + !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && + (report.participantAccountIDs?.length ?? 0) > 1, + ); +} + +/** + * Returns an array of the participants Ids of a report + * + * @deprecated Use getVisibleMemberIDs instead + */ +function getParticipantsIDs(report: OnyxEntry): number[] { + if (!report) { + return []; + } + + const participants = report.participantAccountIDs ?? []; + + // Build participants list for IOU/expense reports + if (isMoneyRequestReport(report)) { + const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean) as number[]; + const onlyUnique = [...new Set([...onlyTruthyValues])]; + return onlyUnique; + } + + if (isGroupChat(report) && currentUserAccountID) { + return [...new Set([...participants, currentUserAccountID])]; + } + + return participants; +} + +/** + * Returns an array of the visible member accountIDs for a report* + */ +function getVisibleMemberIDs(report: OnyxEntry): number[] { + if (!report) { + return []; + } + + const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs ?? []; + + // Build participants list for IOU/expense reports + if (isMoneyRequestReport(report)) { + const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...visibleChatMemberAccountIDs].filter(Boolean) as number[]; + const onlyUnique = [...new Set([...onlyTruthyValues])]; + return onlyUnique; + } + + if (isGroupChat(report) && currentUserAccountID) { + return [...new Set([...visibleChatMemberAccountIDs, currentUserAccountID])]; + } + + return visibleChatMemberAccountIDs; +} + /** * Returns the appropriate icons for the given chat report using the stored personalDetails. * The Avatar sources can be URLs or Icon components according to the chat type. @@ -1452,6 +1526,10 @@ function getIcons( return isPayer ? [managerIcon, ownerIcon] : [ownerIcon, managerIcon]; } + if (isGroupChat(report)) { + return getIconsForParticipants(getVisibleMemberIDs(report), personalDetails); + } + return getIconsForParticipants(report?.participantAccountIDs ?? [], personalDetails); } @@ -4132,80 +4210,6 @@ function getTaskAssigneeChatOnyxData( }; } -/** - * Checks if a report is a group chat. - * - * A report is a group chat if it meets the following conditions: - * - Not a chat thread. - * - Not a task report. - * - Not a money request / IOU report. - * - Not an archived room. - * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). - * - More than 1 participants (note that participantAccountIDs excludes the current user). - * - */ -function isGroupChat(report: OnyxEntry): boolean { - return Boolean( - report && - !isChatThread(report) && - !isTaskReport(report) && - !isMoneyRequestReport(report) && - !isArchivedRoom(report) && - !Object.values(CONST.REPORT.CHAT_TYPE).some((chatType) => chatType === getChatType(report)) && - (report.participantAccountIDs?.length ?? 0) > 1, - ); -} - -/** - * Returns an array of the participants Ids of a report - * - * @deprecated Use getVisibleMemberIDs instead - */ -function getParticipantsIDs(report: OnyxEntry): number[] { - if (!report) { - return []; - } - - const participants = report.participantAccountIDs ?? []; - - // Build participants list for IOU/expense reports - if (isMoneyRequestReport(report)) { - const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...participants].filter(Boolean) as number[]; - const onlyUnique = [...new Set([...onlyTruthyValues])]; - return onlyUnique; - } - - if (isGroupChat(report) && currentUserAccountID) { - return [...new Set([...participants, currentUserAccountID])]; - } - - return participants; -} - -/** - * Returns an array of the visible member accountIDs for a report* - */ -function getVisibleMemberIDs(report: OnyxEntry): number[] { - if (!report) { - return []; - } - - const visibleChatMemberAccountIDs = report.visibleChatMemberAccountIDs ?? []; - - // Build participants list for IOU/expense reports - if (isMoneyRequestReport(report)) { - const onlyTruthyValues = [report.managerID, report.ownerAccountID, ...visibleChatMemberAccountIDs].filter(Boolean) as number[]; - const onlyUnique = [...new Set([...onlyTruthyValues])]; - return onlyUnique; - } - - if (isGroupChat(report) && currentUserAccountID) { - return [...new Set([...visibleChatMemberAccountIDs, currentUserAccountID])]; - } - - return visibleChatMemberAccountIDs; -} - /** * Return iou report action display message */ diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index edf6b65b2f4a..1787b33c826c 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -92,7 +92,7 @@ function HeaderView(props) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); - const participants = lodashGet(props.report, 'participantAccountIDs', []); + const participants = ReportUtils.isGroupChat(props.report) ? ReportUtils.getVisibleMemberIDs(props.report) : lodashGet(props.report, 'participantAccountIDs', []); const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, props.personalDetails); const isMultipleParticipant = participants.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant); From 4a44ef25ce41a11bf81ba6de3c8fd9c86ef02d12 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 12 Jan 2024 10:49:10 +0700 Subject: [PATCH 030/170] fix: compose box is shown after tapping header and returning back --- src/components/Composer/index.android.tsx | 8 ++++++++ src/components/Composer/index.ios.tsx | 9 ++++++++- src/hooks/useResetComposerFocus.ts | 19 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 src/hooks/useResetComposerFocus.ts diff --git a/src/components/Composer/index.android.tsx b/src/components/Composer/index.android.tsx index d60a41e0f263..ade1513c8613 100644 --- a/src/components/Composer/index.android.tsx +++ b/src/components/Composer/index.android.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {TextInput} from 'react-native'; import {StyleSheet} from 'react-native'; import RNTextInput from '@components/RNTextInput'; +import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ComposerUtils from '@libs/ComposerUtils'; @@ -28,6 +29,7 @@ function Composer( ref: ForwardedRef, ) { const textInput = useRef(null); + const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput); const styles = useThemeStyles(); const theme = useTheme(); @@ -89,6 +91,12 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} + onBlur={(e) => { + if (!isFocused) { + shouldResetFocus.current = true; // detect the input is blurred when the page is hidden + } + props?.onBlur?.(e); + }} /> ); } diff --git a/src/components/Composer/index.ios.tsx b/src/components/Composer/index.ios.tsx index b1357fef9a46..07736e5ddcba 100644 --- a/src/components/Composer/index.ios.tsx +++ b/src/components/Composer/index.ios.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import type {TextInput} from 'react-native'; import {StyleSheet} from 'react-native'; import RNTextInput from '@components/RNTextInput'; +import useResetComposerFocus from '@hooks/useResetComposerFocus'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ComposerUtils from '@libs/ComposerUtils'; @@ -28,7 +29,7 @@ function Composer( ref: ForwardedRef, ) { const textInput = useRef(null); - + const {isFocused, shouldResetFocus} = useResetComposerFocus(textInput); const styles = useThemeStyles(); const theme = useTheme(); @@ -84,6 +85,12 @@ function Composer( /* eslint-disable-next-line react/jsx-props-no-spreading */ {...props} readOnly={isDisabled} + onBlur={(e) => { + if (!isFocused) { + shouldResetFocus.current = true; // detect the input is blurred when the page is hidden + } + props?.onBlur?.(e); + }} /> ); } diff --git a/src/hooks/useResetComposerFocus.ts b/src/hooks/useResetComposerFocus.ts new file mode 100644 index 000000000000..e9f88ed93346 --- /dev/null +++ b/src/hooks/useResetComposerFocus.ts @@ -0,0 +1,19 @@ +import {useIsFocused} from '@react-navigation/native'; +import type {MutableRefObject} from 'react'; +import {useEffect, useRef} from 'react'; +import type {TextInput} from 'react-native'; + +export default function useResetComposerFocus(inputRef: MutableRefObject) { + const isFocused = useIsFocused(); + const shouldResetFocus = useRef(false); + + useEffect(() => { + if (!isFocused || !shouldResetFocus.current) { + return; + } + inputRef.current?.focus(); // focus input again + shouldResetFocus.current = false; + }, [isFocused, inputRef]); + + return {isFocused, shouldResetFocus}; +} From acc1527d622541d2d97d4ab97e17466d1b7d0c0f Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Fri, 12 Jan 2024 18:27:14 +0300 Subject: [PATCH 031/170] minor fix --- src/libs/ReportUtils.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 45c583da3a86..5ac1806356c0 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1345,7 +1345,7 @@ function getWorkspaceIcon(report: OnyxEntry, policy: OnyxEntry = * - Not a money request / IOU report. * - Not an archived room. * - Not a public / admin / announce chat room (chat type doesn't match any of the specified types). - * - More than 1 participants (note that participantAccountIDs excludes the current user). + * - More than 1 participant (note that participantAccountIDs excludes the current user). * */ function isGroupChat(report: OnyxEntry): boolean { @@ -1387,7 +1387,7 @@ function getParticipantsIDs(report: OnyxEntry): number[] { } /** - * Returns an array of the visible member accountIDs for a report* + * Returns an array of the visible member accountIDs for a report */ function getVisibleMemberIDs(report: OnyxEntry): number[] { if (!report) { From 15d87b177f2ec7c233a7ec036afbfab22cfae7cd Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Sat, 13 Jan 2024 03:28:57 +0700 Subject: [PATCH 032/170] dismiss money request banner --- .../iou/MoneyRequestReferralProgramCTA.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx index 31394e1bd0e1..a752696baca2 100644 --- a/src/pages/iou/MoneyRequestReferralProgramCTA.tsx +++ b/src/pages/iou/MoneyRequestReferralProgramCTA.tsx @@ -1,6 +1,6 @@ -import React from 'react'; +import React, {useState} from 'react'; import Icon from '@components/Icon'; -import {Info} from '@components/Icon/Expensicons'; +import {Close} from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; @@ -18,6 +18,11 @@ function MoneyRequestReferralProgramCTA({referralContentType}: MoneyRequestRefer const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); + const [isHidden, setIsHidden] = useState(false); + + if (isHidden) { + return null; + } return ( - + setIsHidden(true)} + onMouseDown={(e) => { + e.preventDefault(); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + ); } From 06d2fbb62cc56eddac9bbf4895456eb6762dd733 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 12 Jan 2024 16:49:56 -0500 Subject: [PATCH 033/170] feat: ReportActionItemCreated.js file TS migration * Created ReportActionItemCreated.tsx * feat: ReportActionItemCreated.js migration * "Migrated ReportActionItemCreated.js to TS" * "Created ReportActionItemCreated.tsx --- ...Created.js => ReportActionItemCreated.tsx} | 77 ++++++++++--------- 1 file changed, 40 insertions(+), 37 deletions(-) rename src/pages/home/report/{ReportActionItemCreated.js => ReportActionItemCreated.tsx} (68%) diff --git a/src/pages/home/report/ReportActionItemCreated.js b/src/pages/home/report/ReportActionItemCreated.tsx similarity index 68% rename from src/pages/home/report/ReportActionItemCreated.js rename to src/pages/home/report/ReportActionItemCreated.tsx index e5ec3e4b8744..22f9d6665e06 100644 --- a/src/pages/home/report/ReportActionItemCreated.js +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -1,56 +1,61 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {memo} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import participantPropTypes from '@components/participantPropTypes'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportWelcomeText from '@components/ReportWelcomeText'; +import type {WithLocalizeProps} from '@components/withLocalize'; import withLocalize from '@components/withLocalize'; -import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import withWindowDimensions from '@components/withWindowDimensions'; +import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as ReportUtils from '@libs/ReportUtils'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as Report from '@userActions/Report'; +import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; -const propTypes = { - /** The id of the report */ - reportID: PropTypes.string.isRequired, - +type OnyxProps = { /** The report currently being looked at */ - report: reportPropTypes, + report: OnyxEntry; + + /** The policy being used */ + policy: OnyxEntry; /** Personal details of all the users */ - personalDetails: PropTypes.objectOf(participantPropTypes), + personalDetails: OnyxEntry; +}; + +type ReportActionItemCreatedProps = { + /** The id of the report */ + reportID: string; + + /** The id of the policy */ + // eslint-disable-next-line react/no-unused-prop-types + policyID: string; /** The policy object for the current route */ - policy: PropTypes.shape({ + policy?: { /** The name of the policy */ - name: PropTypes.string, + name?: string; /** The URL for the policy avatar */ - avatar: PropTypes.string, - }), - - ...windowDimensionsPropTypes, -}; -const defaultProps = { - report: {}, - personalDetails: {}, - policy: {}, -}; + avatar?: string; + }; +} & WindowDimensionsProps & + WithLocalizeProps & + OnyxProps; -function ReportActionItemCreated(props) { +function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + if (!ReportUtils.isChatReport(props.report)) { return null; } @@ -60,10 +65,10 @@ function ReportActionItemCreated(props) { return ( Report.navigateToConciergeChatAndDeleteReport(props.report.reportID)} + onClose={() => navigateToConciergeChatAndDeleteReport(props.report?.reportID ?? props.reportID)} needsOffscreenAlphaCompositing > @@ -99,14 +104,12 @@ function ReportActionItemCreated(props) { ); } -ReportActionItemCreated.defaultProps = defaultProps; -ReportActionItemCreated.propTypes = propTypes; ReportActionItemCreated.displayName = 'ReportActionItemCreated'; export default compose( - withWindowDimensions, + withWindowDimensions, withLocalize, - withOnyx({ + withOnyx({ report: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, selector: reportWithoutHasDraftSelector, @@ -122,10 +125,10 @@ export default compose( memo( ReportActionItemCreated, (prevProps, nextProps) => - lodashGet(prevProps.props, 'policy.name') === lodashGet(nextProps, 'policy.name') && - lodashGet(prevProps.props, 'policy.avatar') === lodashGet(nextProps, 'policy.avatar') && - lodashGet(prevProps.props, 'report.lastReadTime') === lodashGet(nextProps, 'report.lastReadTime') && - lodashGet(prevProps.props, 'report.statusNum') === lodashGet(nextProps, 'report.statusNum') && - lodashGet(prevProps.props, 'report.stateNum') === lodashGet(nextProps, 'report.stateNum'), + prevProps.policy?.name === nextProps.policy?.name && + prevProps.policy?.avatar === nextProps.policy?.avatar && + prevProps.report?.lastReadTime === nextProps.report?.lastReadTime && + prevProps.report?.statusNum === nextProps.report?.statusNum && + prevProps.report?.stateNum === nextProps.report?.stateNum, ), ); From f2dbeb169c26723ebfbb65c837e5c210aa42a8e6 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sat, 13 Jan 2024 00:10:37 +0100 Subject: [PATCH 034/170] refactor: improving code quality --- .../home/report/ReportActionItemCreated.tsx | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 22f9d6665e06..c1af2d08321a 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -6,19 +6,16 @@ import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportWelcomeText from '@components/ReportWelcomeText'; -import type {WithLocalizeProps} from '@components/withLocalize'; -import withLocalize from '@components/withLocalize'; -import withWindowDimensions from '@components/withWindowDimensions'; -import type {WindowDimensionsProps} from '@components/withWindowDimensions/types'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as ReportUtils from '@libs/ReportUtils'; import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; +import useLocalize from '@hooks/useLocalize'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; type OnyxProps = { @@ -32,7 +29,7 @@ type OnyxProps = { personalDetails: OnyxEntry; }; -type ReportActionItemCreatedProps = { +type ReportActionItemCreatedProps = OnyxProps & { /** The id of the report */ reportID: string; @@ -48,14 +45,15 @@ type ReportActionItemCreatedProps = { /** The URL for the policy avatar */ avatar?: string; }; -} & WindowDimensionsProps & - WithLocalizeProps & - OnyxProps; - +} function ReportActionItemCreated(props: ReportActionItemCreatedProps) { + const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + const {isSmallScreenWidth, isLargeScreenWidth} = useWindowDimensions(); + if (!ReportUtils.isChatReport(props.report)) { return null; } @@ -71,25 +69,25 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { onClose={() => navigateToConciergeChatAndDeleteReport(props.report?.reportID ?? props.reportID)} needsOffscreenAlphaCompositing > - + ReportUtils.navigateToDetailsPage(props.report)} style={[styles.mh5, styles.mb3, styles.alignSelfStart]} - accessibilityLabel={props.translate('common.details')} + accessibilityLabel={translate('common.details')} role={CONST.ROLE.BUTTON} disabled={shouldDisableDetailPage} > @@ -106,22 +104,21 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { ReportActionItemCreated.displayName = 'ReportActionItemCreated'; -export default compose( - withWindowDimensions, - withLocalize, - withOnyx({ +export default withOnyx({ report: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, selector: reportWithoutHasDraftSelector, }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, + policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, }, - }), -)( + + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + + })( memo( ReportActionItemCreated, (prevProps, nextProps) => From 8e521b407ea9a934e2dfa3d031a3ba5f50ef27f4 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Sat, 13 Jan 2024 00:15:05 +0100 Subject: [PATCH 035/170] fmt: prettier --- .../home/report/ReportActionItemCreated.tsx | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index c1af2d08321a..86b7eef2c8ae 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -6,16 +6,16 @@ import MultipleAvatars from '@components/MultipleAvatars'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import ReportWelcomeText from '@components/ReportWelcomeText'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; +import useWindowDimensions from '@hooks/useWindowDimensions'; import reportWithoutHasDraftSelector from '@libs/OnyxSelectors/reportWithoutHasDraftSelector'; import * as ReportUtils from '@libs/ReportUtils'; import {navigateToConciergeChatAndDeleteReport} from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; -import useLocalize from '@hooks/useLocalize'; -import useWindowDimensions from '@hooks/useWindowDimensions'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; type OnyxProps = { @@ -45,9 +45,8 @@ type ReportActionItemCreatedProps = OnyxProps & { /** The URL for the policy avatar */ avatar?: string; }; -} +}; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { - const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -105,27 +104,26 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { ReportActionItemCreated.displayName = 'ReportActionItemCreated'; export default withOnyx({ - report: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - selector: reportWithoutHasDraftSelector, - }, + report: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + selector: reportWithoutHasDraftSelector, + }, + + policy: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + }, - policy: { - key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, - - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - - })( + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, +})( memo( ReportActionItemCreated, (prevProps, nextProps) => prevProps.policy?.name === nextProps.policy?.name && prevProps.policy?.avatar === nextProps.policy?.avatar && - prevProps.report?.lastReadTime === nextProps.report?.lastReadTime && + prevProps.report?.stateNum === nextProps.report?.stateNum && prevProps.report?.statusNum === nextProps.report?.statusNum && - prevProps.report?.stateNum === nextProps.report?.stateNum, + prevProps.report?.lastReadTime === nextProps.report?.lastReadTime, ), ); From c4929bae75fa1b3211ab91a7f103536daa17c6da Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:52:22 +0530 Subject: [PATCH 036/170] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 7c9313a6d394..ec8b87d9f232 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -27,7 +27,7 @@ import Slider from './Slider'; type AvatarCropModalProps = { /** Link to image for cropping */ - imageUri: string; + imageUri?: string; /** Name of the image */ imageName: string; From 5278e70cb628450776f0b8ec31dfe9761991bea5 Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:52:33 +0530 Subject: [PATCH 037/170] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index ec8b87d9f232..fa2e7fb6a770 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -30,7 +30,7 @@ type AvatarCropModalProps = { imageUri?: string; /** Name of the image */ - imageName: string; + imageName?: string; /** Type of the image file */ imageType: string; From 050e162046df43152186077aed05cb361725aa64 Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:52:42 +0530 Subject: [PATCH 038/170] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index fa2e7fb6a770..cf26c0e568f2 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -33,7 +33,7 @@ type AvatarCropModalProps = { imageName?: string; /** Type of the image file */ - imageType: string; + imageType?: string; /** Callback to be called when user closes the modal */ onClose: () => void; From 993d3f55882704b8e7e3dc2beb6c937ce28ab5ad Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:52:53 +0530 Subject: [PATCH 039/170] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index cf26c0e568f2..06aa8e673c4f 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -36,7 +36,7 @@ type AvatarCropModalProps = { imageType?: string; /** Callback to be called when user closes the modal */ - onClose: () => void; + onClose?: () => void; /** Callback to be called when user saves the image */ onSave: (image: File | CustomRNImageManipulatorResult) => void; From f10ab52c44707fed958b82e378d8ac40e7fa0e08 Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:53:03 +0530 Subject: [PATCH 040/170] Update src/components/AvatarCropModal/AvatarCropModal.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index 06aa8e673c4f..b5dc240f5fbf 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -39,7 +39,7 @@ type AvatarCropModalProps = { onClose?: () => void; /** Callback to be called when user saves the image */ - onSave: (image: File | CustomRNImageManipulatorResult) => void; + onSave?: (image: File | CustomRNImageManipulatorResult) => void; /** Modal visibility */ isVisible: boolean; From 4d3f0be78573d743dc3897354d77adca09e1167f Mon Sep 17 00:00:00 2001 From: Esh Tanya Gupta <77237602+esh-g@users.noreply.github.com> Date: Sat, 13 Jan 2024 15:57:18 +0530 Subject: [PATCH 041/170] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/components/AvatarCropModal/AvatarCropModal.tsx | 6 ++++++ src/components/AvatarCropModal/ImageCropView.tsx | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index b5dc240f5fbf..ba8b0bc14590 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -143,6 +143,9 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose * @param {Array} minMax * @returns {Number} */ + /** + * Validates that value is within the provided mix/max range. + */ const clamp = useWorkletCallback((value: number, [min, max]) => interpolate(value, [min, max], [min, max], 'clamp'), []); /** @@ -171,6 +174,9 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose * @param {Number} newX * @param {Number} newY */ + /** + * Validates the offset to prevent overflow, and updates the image offset. + */ const updateImageOffset = useWorkletCallback( (offsetX: number, offsetY: number) => { const {height, width} = getDisplayedImageSize(); diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index e76ffc2d1f10..d6077cd9eb1c 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -14,10 +14,10 @@ import type IconAsset from '@src/types/utils/IconAsset'; type ImageCropViewProps = { /** Link to image for cropping */ - imageUri: string; + imageUri?: string; /** Size of the image container that will be rendered */ - containerSize: number; + containerSize?: number; /** The height of the selected image */ originalImageHeight: { @@ -50,7 +50,7 @@ type ImageCropViewProps = { }; /** React-native-reanimated lib handler which executes when the user is panning image */ - panGestureEventHandler: (event: GestureEvent) => void; + panGestureEventHandler?: (event: GestureEvent) => void; /** Image crop vector mask */ maskImage?: IconAsset; From 7f4e78fe41597a8b0fbc31601436490d82ac1559 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:04:28 +0530 Subject: [PATCH 042/170] Remove JS-Doc comments --- .../AvatarCropModal/AvatarCropModal.tsx | 20 ------------------- 1 file changed, 20 deletions(-) diff --git a/src/components/AvatarCropModal/AvatarCropModal.tsx b/src/components/AvatarCropModal/AvatarCropModal.tsx index ba8b0bc14590..3190b9c75fc5 100644 --- a/src/components/AvatarCropModal/AvatarCropModal.tsx +++ b/src/components/AvatarCropModal/AvatarCropModal.tsx @@ -136,13 +136,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose }); }, [imageUri, originalImageHeight, originalImageWidth, rotation, translateSlider]); - /** - * Validates that value is within the provided mix/max range. - * - * @param {Number} value - * @param {Array} minMax - * @returns {Number} - */ /** * Validates that value is within the provided mix/max range. */ @@ -150,8 +143,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose /** * Returns current image size taking into account scale and rotation. - * - * @returns {Object} */ const getDisplayedImageSize = useWorkletCallback(() => { let height = imageContainerSize * scale.value; @@ -168,12 +159,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose return {height, width}; }, [imageContainerSize, scale]); - /** - * Validates the offset to prevent overflow, and updates the image offset. - * - * @param {Number} newX - * @param {Number} newY - */ /** * Validates the offset to prevent overflow, and updates the image offset. */ @@ -190,11 +175,6 @@ function AvatarCropModal({imageUri = '', imageName = '', imageType = '', onClose [imageContainerSize, scale, clamp], ); - /** - * @param {Number} newSliderValue - * @param {Number} containerSize - * @returns {Number} - */ const newScaleValue = useWorkletCallback((newSliderValue: number, containerSize: number) => { const {MAX_SCALE, MIN_SCALE} = CONST.AVATAR_CROP_MODAL; return (newSliderValue / containerSize) * (MAX_SCALE - MIN_SCALE) + MIN_SCALE; From 96d86459d4a5a99eea547c73a2e9dfdcb5360420 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:19:35 +0530 Subject: [PATCH 043/170] Use shared value type --- .../AvatarCropModal/ImageCropView.tsx | 25 ++++++------------- 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index d6077cd9eb1c..f086697a3e24 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -3,6 +3,7 @@ import {View} from 'react-native'; import {PanGestureHandler} from 'react-native-gesture-handler'; import type {GestureEvent, PanGestureHandlerEventPayload} from 'react-native-gesture-handler'; import Animated, {interpolate, useAnimatedStyle} from 'react-native-reanimated'; +import type {SharedValue} from 'react-native-reanimated'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -20,34 +21,22 @@ type ImageCropViewProps = { containerSize?: number; /** The height of the selected image */ - originalImageHeight: { - value: number; - }; + originalImageHeight: SharedValue; /** The width of the selected image */ - originalImageWidth: { - value: number; - }; + originalImageWidth: SharedValue; /** The rotation value of the selected image */ - rotation: { - value: number; - }; + rotation: SharedValue; /** The relative image shift along X-axis */ - translateX: { - value: number; - }; + translateX: SharedValue; /** The relative image shift along Y-axis */ - translateY: { - value: number; - }; + translateY: SharedValue; /** The scale factor of the image */ - scale: { - value: number; - }; + scale: SharedValue; /** React-native-reanimated lib handler which executes when the user is panning image */ panGestureEventHandler?: (event: GestureEvent) => void; From e9a397960ca982ae30b5ca0d17442200e62f19b8 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:26:59 +0530 Subject: [PATCH 044/170] Remove SelectionElement and use HTMLElement --- src/components/AvatarCropModal/ImageCropView.tsx | 3 +-- src/components/AvatarCropModal/Slider.tsx | 3 +-- src/libs/ControlSelection/index.ts | 6 +++--- src/libs/ControlSelection/types.ts | 8 +++----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/components/AvatarCropModal/ImageCropView.tsx b/src/components/AvatarCropModal/ImageCropView.tsx index f086697a3e24..b45d7c63b088 100644 --- a/src/components/AvatarCropModal/ImageCropView.tsx +++ b/src/components/AvatarCropModal/ImageCropView.tsx @@ -10,7 +10,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import type {SelectionElement} from '@libs/ControlSelection/types'; import type IconAsset from '@src/types/utils/IconAsset'; type ImageCropViewProps = { @@ -75,7 +74,7 @@ function ImageCropView({imageUri = '', containerSize = 0, panGestureEventHandler { - ControlSelection.blockElement(el as SelectionElement); + ControlSelection.blockElement(el as HTMLElement | null); }} style={[containerStyle, styles.imageCropContainer]} > diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index 26e35517f48e..686255ac430a 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -7,7 +7,6 @@ import Tooltip from '@components/Tooltip'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; -import type {SelectionElement} from '@libs/ControlSelection/types'; type SliderProps = { /** React-native-reanimated lib handler which executes when the user is panning slider */ @@ -36,7 +35,7 @@ function Slider({onGesture = () => {}, sliderValue = {value: 0}}: SliderProps) { return ( { - ControlSelection.blockElement(el as SelectionElement); + ControlSelection.blockElement(el as HTMLElement | null); }} style={styles.sliderBar} > diff --git a/src/libs/ControlSelection/index.ts b/src/libs/ControlSelection/index.ts index 89f1e62aa6f6..61808cfbc1b5 100644 --- a/src/libs/ControlSelection/index.ts +++ b/src/libs/ControlSelection/index.ts @@ -1,4 +1,4 @@ -import type {ControlSelectionModule, SelectionElement} from './types'; +import type {ControlSelectionModule} from './types'; /** * Block selection on the whole app @@ -19,7 +19,7 @@ function unblock() { /** * Block selection on particular element */ -function blockElement(element?: SelectionElement | null) { +function blockElement(element?: HTMLElement | null) { if (!element) { return; } @@ -31,7 +31,7 @@ function blockElement(element?: SelectionElement | null) { /** * Unblock selection on particular element */ -function unblockElement(element?: SelectionElement | null) { +function unblockElement(element?: HTMLElement | null) { if (!element) { return; } diff --git a/src/libs/ControlSelection/types.ts b/src/libs/ControlSelection/types.ts index 8433a366ed91..b40e4d6f7a84 100644 --- a/src/libs/ControlSelection/types.ts +++ b/src/libs/ControlSelection/types.ts @@ -1,10 +1,8 @@ -type SelectionElement = T & {onselectstart: () => boolean}; - type ControlSelectionModule = { block: () => void; unblock: () => void; - blockElement: (element?: SelectionElement | null) => void; - unblockElement: (element?: SelectionElement | null) => void; + blockElement: (element?: HTMLElement | null) => void; + unblockElement: (element?: HTMLElement | null) => void; }; -export type {ControlSelectionModule, SelectionElement}; +export type {ControlSelectionModule}; From 2c25037fcb763a8f79694dd4fbfe82cf73ca6db2 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:35:05 +0530 Subject: [PATCH 045/170] Use default export --- src/libs/ControlSelection/index.native.ts | 2 +- src/libs/ControlSelection/index.ts | 2 +- src/libs/ControlSelection/types.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ControlSelection/index.native.ts b/src/libs/ControlSelection/index.native.ts index 2bccea946cda..b45af6da6441 100644 --- a/src/libs/ControlSelection/index.native.ts +++ b/src/libs/ControlSelection/index.native.ts @@ -1,4 +1,4 @@ -import type {ControlSelectionModule} from './types'; +import type ControlSelectionModule from './types'; function block() {} function unblock() {} diff --git a/src/libs/ControlSelection/index.ts b/src/libs/ControlSelection/index.ts index 61808cfbc1b5..44787dc77dbe 100644 --- a/src/libs/ControlSelection/index.ts +++ b/src/libs/ControlSelection/index.ts @@ -1,4 +1,4 @@ -import type {ControlSelectionModule} from './types'; +import type ControlSelectionModule from './types'; /** * Block selection on the whole app diff --git a/src/libs/ControlSelection/types.ts b/src/libs/ControlSelection/types.ts index b40e4d6f7a84..c4ca4b713b9b 100644 --- a/src/libs/ControlSelection/types.ts +++ b/src/libs/ControlSelection/types.ts @@ -5,4 +5,4 @@ type ControlSelectionModule = { unblockElement: (element?: HTMLElement | null) => void; }; -export type {ControlSelectionModule}; +export default ControlSelectionModule; From 32e59df705bc16b4581dc6c554f1bc13b5cd3b87 Mon Sep 17 00:00:00 2001 From: someone-here Date: Sat, 13 Jan 2024 16:40:53 +0530 Subject: [PATCH 046/170] Match the optional props --- src/components/AvatarCropModal/Slider.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index 686255ac430a..89b470be2cd3 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -10,10 +10,10 @@ import ControlSelection from '@libs/ControlSelection'; type SliderProps = { /** React-native-reanimated lib handler which executes when the user is panning slider */ - onGesture: (event: GestureEvent) => void; + onGesture?: (event: GestureEvent) => void; /** X position of the slider knob */ - sliderValue: { + sliderValue?: { value: number; }; }; From 5afbe63d3d9e90b162f8db49110c442c8f202342 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 15 Jan 2024 00:10:33 +0300 Subject: [PATCH 047/170] set unread marker and read the report on visibility change --- src/libs/actions/Report.ts | 7 +++++- src/pages/home/report/ReportActionsList.js | 28 +++++++++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b182b7019846..6951a05be5a1 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -918,7 +918,7 @@ function expandURLPreview(reportID: string, reportActionID: string) { } /** Marks the new report actions as read */ -function readNewestAction(reportID: string) { +function readNewestAction(reportID: string, shouldEmitEvent = true) { const lastReadTime = DateUtils.getDBTime(); const optimisticData: OnyxUpdate[] = [ @@ -942,6 +942,11 @@ function readNewestAction(reportID: string) { }; API.write('ReadNewestAction', parameters, {optimisticData}); + + if (!shouldEmitEvent) { + return; + } + DeviceEventEmitter.emit(`readNewestAction_${reportID}`, lastReadTime); } diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index dba8ef2e11d0..d08fd2a42ea6 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -167,6 +167,7 @@ function ReportActionsList({ const reportActionSize = useRef(sortedVisibleReportActions.length); const previousLastIndex = useRef(lastActionIndex); + const visibilityCallback = useRef(() => {}); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); @@ -386,7 +387,7 @@ function ReportActionsList({ [currentUnreadMarker, sortedVisibleReportActions, report.reportID, messageManuallyMarkedUnread], ); - useEffect(() => { + const calculateUnreadMarker = () => { // Iterate through the report actions and set appropriate unread marker. // This is to avoid a warning of: // Cannot update a component (ReportActionsList) while rendering a different component (CellRenderer). @@ -404,8 +405,33 @@ function ReportActionsList({ if (!markerFound) { setCurrentUnreadMarker(null); } + }; + + useEffect(() => { + calculateUnreadMarker(); + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [sortedVisibleReportActions, report.lastReadTime, report.reportID, messageManuallyMarkedUnread, shouldDisplayNewMarker, currentUnreadMarker]); + visibilityCallback.current = () => { + if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report)) { + return; + } + + Report.readNewestAction(report.reportID, false); + userActiveSince.current = DateUtils.getDBTime(); + setCurrentUnreadMarker(null); + calculateUnreadMarker(); + }; + + useEffect(() => { + const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => visibilityCallback.current()); + + return unsubscribeVisibilityListener; + + /* eslint-disable-next-line react-hooks/exhaustive-deps */ + }, [report.reportID]); + const renderItem = useCallback( ({item: reportAction, index}) => ( Date: Mon, 15 Jan 2024 14:02:07 +0700 Subject: [PATCH 048/170] only display pending message for foreign currency transaction --- src/components/ReportActionItem/MoneyRequestAction.js | 2 +- src/libs/IOUUtils.ts | 10 ++++++---- tests/unit/IOUUtilsTest.js | 6 +++--- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index 46226969636e..d242dbe23e86 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -116,7 +116,7 @@ function MoneyRequestAction({ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { - shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); + shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || 0); } return isDeletedParentAction || isReversedTransaction ? ( diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index 11dd0f5badda..aab9aac2391d 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -110,12 +110,14 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } /** - * Returns whether or not an IOU report contains money requests in a different currency + * Returns whether or not a transaction of IOU report contains money requests in a different currency * that are either created or cancelled offline, and thus haven't been converted to the report's currency yet */ -function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { +function isTransactionPendingCurrencyConversion(iouReport: Report, transactionID: string): boolean { const reportTransactions: Transaction[] = TransactionUtils.getAllReportTransactions(iouReport.reportID); - const pendingRequestsInDifferentCurrency = reportTransactions.filter((transaction) => transaction.pendingAction && TransactionUtils.getCurrency(transaction) !== iouReport.currency); + const pendingRequestsInDifferentCurrency = reportTransactions.filter( + (transaction) => transaction.pendingAction && transaction.transactionID === transactionID && TransactionUtils.getCurrency(transaction) !== iouReport.currency, + ); return pendingRequestsInDifferentCurrency.length > 0; } @@ -127,4 +129,4 @@ function isValidMoneyRequestType(iouType: string): boolean { return moneyRequestType.includes(iouType); } -export {calculateAmount, updateIOUOwnerAndTotal, isIOUReportPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, navigateToStartStepIfScanFileCannotBeRead}; +export {calculateAmount, updateIOUOwnerAndTotal, isTransactionPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, navigateToStartStepIfScanFileCannotBeRead}; diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index ac04b74a0ca5..7f239d9bb576 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -17,7 +17,7 @@ function initCurrencyList() { } describe('IOUUtils', () => { - describe('isIOUReportPendingCurrencyConversion', () => { + describe('isTransactionPendingCurrencyConversion', () => { beforeAll(() => { Onyx.init({ keys: ONYXKEYS, @@ -34,7 +34,7 @@ describe('IOUUtils', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`]: aedPendingTransaction, }).then(() => { // We requested money offline in a different currency, we don't know the total of the iouReport until we're back online - expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(true); + expect(IOUUtils.isTransactionPendingCurrencyConversion(iouReport, aedPendingTransaction.transactionID)).toBe(true); }); }); @@ -54,7 +54,7 @@ describe('IOUUtils', () => { }, }).then(() => { // We requested money online in a different currency, we know the iouReport total and there's no need to show the pending conversion message - expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(false); + expect(IOUUtils.isTransactionPendingCurrencyConversion(iouReport, aedPendingTransaction.transactionID)).toBe(false); }); }); }); From 3016cf25dee386bde90f4ef2d39ba074e11c54c1 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 15 Jan 2024 14:24:12 +0700 Subject: [PATCH 049/170] fallback report action created if reportActions is empty --- src/libs/actions/Report.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index b182b7019846..d19fd49d1e82 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -960,7 +960,7 @@ function markCommentAsUnread(reportID: string, reportActionCreated: string) { }, null); // If no action created date is provided, use the last action's from other user - const actionCreationTime = reportActionCreated || (latestReportActionFromOtherUsers?.created ?? DateUtils.getDBTime(0)); + const actionCreationTime = reportActionCreated || (latestReportActionFromOtherUsers?.created ?? allReports?.[reportID]?.lastVisibleActionCreated ?? DateUtils.getDBTime(0)); // We subtract 1 millisecond so that the lastReadTime is updated to just before a given reportAction's created date // For example, if we want to mark a report action with ID 100 and created date '2014-04-01 16:07:02.999' unread, we set the lastReadTime to '2014-04-01 16:07:02.998' From 09dadf45d7434e97654c721ad4ef899eda118af4 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 15 Jan 2024 15:20:21 +0300 Subject: [PATCH 050/170] updated to consider manual mark as read case --- src/pages/home/report/ReportActionsList.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index d08fd2a42ea6..61e2e1ce14bb 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -414,13 +414,14 @@ function ReportActionsList({ }, [sortedVisibleReportActions, report.lastReadTime, report.reportID, messageManuallyMarkedUnread, shouldDisplayNewMarker, currentUnreadMarker]); visibilityCallback.current = () => { - if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report)) { + if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report) || messageManuallyMarkedUnread) { return; } Report.readNewestAction(report.reportID, false); userActiveSince.current = DateUtils.getDBTime(); setCurrentUnreadMarker(null); + cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); }; From e9cdc56ea85f994f2a8765b0c79535b1e4583f92 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 15 Jan 2024 13:34:24 +0100 Subject: [PATCH 051/170] Migrate Remaining Group 3 to TS --- ...ocusManager.js => ComposerFocusManager.ts} | 2 +- ...{SuggestionUtils.js => SuggestionUtils.ts} | 23 ++++++++----------- 2 files changed, 11 insertions(+), 14 deletions(-) rename src/libs/{ComposerFocusManager.js => ComposerFocusManager.ts} (86%) rename src/libs/{SuggestionUtils.js => SuggestionUtils.ts} (72%) diff --git a/src/libs/ComposerFocusManager.js b/src/libs/ComposerFocusManager.ts similarity index 86% rename from src/libs/ComposerFocusManager.js rename to src/libs/ComposerFocusManager.ts index 569e165da962..4b7037e6e2c5 100644 --- a/src/libs/ComposerFocusManager.js +++ b/src/libs/ComposerFocusManager.ts @@ -1,5 +1,5 @@ let isReadyToFocusPromise = Promise.resolve(); -let resolveIsReadyToFocus; +let resolveIsReadyToFocus: (value: void | PromiseLike) => void; function resetReadyToFocus() { isReadyToFocusPromise = new Promise((resolve) => { diff --git a/src/libs/SuggestionUtils.js b/src/libs/SuggestionUtils.ts similarity index 72% rename from src/libs/SuggestionUtils.js rename to src/libs/SuggestionUtils.ts index 45641ebb5a0f..261dcbf39edc 100644 --- a/src/libs/SuggestionUtils.js +++ b/src/libs/SuggestionUtils.ts @@ -2,11 +2,10 @@ import CONST from '@src/CONST'; /** * Return the max available index for arrow manager. - * @param {Number} numRows - * @param {Boolean} isAutoSuggestionPickerLarge - * @returns {Number} + * @param numRows + * @param isAutoSuggestionPickerLarge */ -function getMaxArrowIndex(numRows, isAutoSuggestionPickerLarge) { +function getMaxArrowIndex(numRows: number, isAutoSuggestionPickerLarge: boolean): number { // rowCount is number of emoji/mention suggestions. For small screen we can fit 3 items // and for large we show up to 20 items for mentions/emojis const rowCount = isAutoSuggestionPickerLarge @@ -19,21 +18,19 @@ function getMaxArrowIndex(numRows, isAutoSuggestionPickerLarge) { /** * Trims first character of the string if it is a space - * @param {String} str - * @returns {String} + * @param str */ -function trimLeadingSpace(str) { - return str.slice(0, 1) === ' ' ? str.slice(1) : str; +function trimLeadingSpace(str: string): string { + return str.startsWith(' ') ? str.slice(1) : str; } /** * Checks if space is available to render large suggestion menu - * @param {Number} listHeight - * @param {Number} composerHeight - * @param {Number} totalSuggestions - * @returns {Boolean} + * @param listHeight + * @param composerHeight + * @param totalSuggestions */ -function hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, totalSuggestions) { +function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight: number, totalSuggestions: number): boolean { const maxSuggestions = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER; const chatFooterHeight = CONST.CHAT_FOOTER_SECONDARY_ROW_HEIGHT + 2 * CONST.CHAT_FOOTER_SECONDARY_ROW_PADDING; const availableHeight = listHeight - composerHeight - chatFooterHeight; From 7880016e9f2b1ee53752f2f7879bf2ffd8bea2bc Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 15 Jan 2024 13:45:38 +0100 Subject: [PATCH 052/170] Add empty lines --- src/libs/ComposerFocusManager.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 4b7037e6e2c5..0dfb0d97dac3 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -6,12 +6,14 @@ function resetReadyToFocus() { resolveIsReadyToFocus = resolve; }); } + function setReadyToFocus() { if (!resolveIsReadyToFocus) { return; } resolveIsReadyToFocus(); } + function isReadyToFocus() { return isReadyToFocusPromise; } From 10fba1290fe181304f8528bfc18ab9345351747a Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 15 Jan 2024 13:49:57 +0100 Subject: [PATCH 053/170] Add return type for isReadyToFocus --- src/libs/ComposerFocusManager.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/libs/ComposerFocusManager.ts b/src/libs/ComposerFocusManager.ts index 0dfb0d97dac3..b66bbe92599e 100644 --- a/src/libs/ComposerFocusManager.ts +++ b/src/libs/ComposerFocusManager.ts @@ -14,7 +14,7 @@ function setReadyToFocus() { resolveIsReadyToFocus(); } -function isReadyToFocus() { +function isReadyToFocus(): Promise { return isReadyToFocusPromise; } From 7f2ac3fac47e9181699bd7915556122c73375112 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Mon, 15 Jan 2024 13:53:58 +0100 Subject: [PATCH 054/170] Remove params with no descriptions --- src/libs/SuggestionUtils.ts | 18 +++--------------- 1 file changed, 3 insertions(+), 15 deletions(-) diff --git a/src/libs/SuggestionUtils.ts b/src/libs/SuggestionUtils.ts index 261dcbf39edc..213a2a9e49f1 100644 --- a/src/libs/SuggestionUtils.ts +++ b/src/libs/SuggestionUtils.ts @@ -1,10 +1,6 @@ import CONST from '@src/CONST'; -/** - * Return the max available index for arrow manager. - * @param numRows - * @param isAutoSuggestionPickerLarge - */ +/** Return the max available index for arrow manager. */ function getMaxArrowIndex(numRows: number, isAutoSuggestionPickerLarge: boolean): number { // rowCount is number of emoji/mention suggestions. For small screen we can fit 3 items // and for large we show up to 20 items for mentions/emojis @@ -16,20 +12,12 @@ function getMaxArrowIndex(numRows: number, isAutoSuggestionPickerLarge: boolean) return rowCount - 1; } -/** - * Trims first character of the string if it is a space - * @param str - */ +/** Trims first character of the string if it is a space */ function trimLeadingSpace(str: string): string { return str.startsWith(' ') ? str.slice(1) : str; } -/** - * Checks if space is available to render large suggestion menu - * @param listHeight - * @param composerHeight - * @param totalSuggestions - */ +/** Checks if space is available to render large suggestion menu */ function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight: number, totalSuggestions: number): boolean { const maxSuggestions = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER; const chatFooterHeight = CONST.CHAT_FOOTER_SECONDARY_ROW_HEIGHT + 2 * CONST.CHAT_FOOTER_SECONDARY_ROW_PADDING; From ea5c3a2b6c180a94601b4d2408614db0e44dd5d4 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 15 Jan 2024 23:03:49 +0700 Subject: [PATCH 055/170] fallback string instead of number --- src/components/ReportActionItem/MoneyRequestAction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index d242dbe23e86..241b3e32447a 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -116,7 +116,7 @@ function MoneyRequestAction({ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { - shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || 0); + shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || '0'); } return isDeletedParentAction || isReversedTransaction ? ( From f64a93ad047613831d3e94a136df58d15066a25b Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Tue, 16 Jan 2024 14:08:23 +0700 Subject: [PATCH 056/170] reapply changes --- .../OptionsSelector/BaseOptionsSelector.js | 5 +++- src/components/ReferralProgramCTA.tsx | 28 +++++++++++++------ src/pages/SearchPage/SearchPageFooter.tsx | 8 ++++-- ...yForRefactorRequestParticipantsSelector.js | 12 ++++++-- .../MoneyRequestParticipantsSelector.js | 12 ++++++-- 5 files changed, 48 insertions(+), 17 deletions(-) diff --git a/src/components/OptionsSelector/BaseOptionsSelector.js b/src/components/OptionsSelector/BaseOptionsSelector.js index f7fc8ca4b77d..bbcce6fff9a6 100755 --- a/src/components/OptionsSelector/BaseOptionsSelector.js +++ b/src/components/OptionsSelector/BaseOptionsSelector.js @@ -662,7 +662,10 @@ class BaseOptionsSelector extends Component { {this.props.shouldShowReferralCTA && this.state.shouldShowReferralModal && ( - + )} diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 473d5cdbed08..68b97c343f81 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -6,7 +6,7 @@ import CONST from '@src/CONST'; import Navigation from '@src/libs/Navigation/Navigation'; import ROUTES from '@src/ROUTES'; import Icon from './Icon'; -import {Info} from './Icon/Expensicons'; +import {Close} from './Icon/Expensicons'; import {PressableWithoutFeedback} from './Pressable'; import Text from './Text'; @@ -16,9 +16,12 @@ type ReferralProgramCTAProps = { | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.START_CHAT | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.SEND_MONEY | typeof CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND; + + /** Method to trigger when pressing close button of the banner */ + onCloseButtonPress?: () => void; }; -function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) { +function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}}: ReferralProgramCTAProps) { const {translate} = useLocalize(); const styles = useThemeStyles(); const theme = useTheme(); @@ -41,12 +44,21 @@ function ReferralProgramCTA({referralContentType}: ReferralProgramCTAProps) { {translate(`referralProgram.${referralContentType}.buttonText2`)} - + { + e.preventDefault(); + }} + role={CONST.ACCESSIBILITY_ROLE.BUTTON} + accessibilityLabel={translate('common.close')} + > + + ); } diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx index e0ef67ad9ec3..a66e24d973d9 100644 --- a/src/pages/SearchPage/SearchPageFooter.tsx +++ b/src/pages/SearchPage/SearchPageFooter.tsx @@ -1,15 +1,19 @@ -import React from 'react'; +import React, {useState} from 'react'; import {View} from 'react-native'; import ReferralProgramCTA from '@components/ReferralProgramCTA'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; function SearchPageFooter() { + const [shouldShowReferralCTA, setShouldShowReferralCTA] = useState(true); const themeStyles = useThemeStyles(); return ( - + setShouldShowReferralCTA(false)} + /> ); } diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index f8c412993bab..bd5af413635d 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -80,6 +80,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const {translate} = useLocalize(); const styles = useThemeStyles(); const [searchTerm, setSearchTerm] = useState(''); + const [shouldShowReferralCTA, setShouldShowReferralCTA] = useState(true); const {isOffline} = useNetwork(); const personalDetails = usePersonalDetails(); @@ -265,9 +266,14 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const footerContent = useMemo( () => ( - - - + {shouldShowReferralCTA && ( + + setShouldShowReferralCTA(false)} + /> + + )} {shouldShowSplitBillErrorMessage && ( ( - - - + {shouldShowReferralCTA && ( + + setShouldShowReferralCTA(false)} + /> + + )} {shouldShowSplitBillErrorMessage && ( Date: Tue, 16 Jan 2024 14:32:38 +0700 Subject: [PATCH 057/170] increase pressable space --- src/components/ReferralProgramCTA.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/ReferralProgramCTA.tsx b/src/components/ReferralProgramCTA.tsx index 68b97c343f81..4a6b8b03f2b4 100644 --- a/src/components/ReferralProgramCTA.tsx +++ b/src/components/ReferralProgramCTA.tsx @@ -31,7 +31,7 @@ function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}} onPress={() => { Navigation.navigate(ROUTES.REFERRAL_DETAILS_MODAL.getRoute(referralContentType)); }} - style={[styles.p5, styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10}]} + style={[styles.w100, styles.br2, styles.highlightBG, styles.flexRow, styles.justifyContentBetween, styles.alignItemsCenter, {gap: 10, padding: 10}, styles.pl5]} accessibilityLabel="referral" role={CONST.ACCESSIBILITY_ROLE.BUTTON} > @@ -49,6 +49,7 @@ function ReferralProgramCTA({referralContentType, onCloseButtonPress = () => {}} onMouseDown={(e) => { e.preventDefault(); }} + style={[styles.touchableButtonImage]} role={CONST.ACCESSIBILITY_ROLE.BUTTON} accessibilityLabel={translate('common.close')} > From bdc980df6055cd93f1925c6fd7581f96dad51776 Mon Sep 17 00:00:00 2001 From: gijoe0295 Date: Tue, 16 Jan 2024 14:43:38 +0700 Subject: [PATCH 058/170] fix lint --- src/pages/SearchPage/SearchPageFooter.tsx | 16 ++++++++++------ ...raryForRefactorRequestParticipantsSelector.js | 2 +- .../MoneyRequestParticipantsSelector.js | 2 +- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pages/SearchPage/SearchPageFooter.tsx b/src/pages/SearchPage/SearchPageFooter.tsx index a66e24d973d9..fb3644d8e570 100644 --- a/src/pages/SearchPage/SearchPageFooter.tsx +++ b/src/pages/SearchPage/SearchPageFooter.tsx @@ -9,12 +9,16 @@ function SearchPageFooter() { const themeStyles = useThemeStyles(); return ( - - setShouldShowReferralCTA(false)} - /> - + <> + {shouldShowReferralCTA && ( + + setShouldShowReferralCTA(false)} + /> + + )} + ); } diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index bd5af413635d..fa7f13002305 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -294,7 +294,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ )} ), - [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], + [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, shouldShowReferralCTA, styles, translate], ); const itemRightSideComponent = useCallback( diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js index f5e332f8eace..59081599736c 100755 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js +++ b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsSelector.js @@ -313,7 +313,7 @@ function MoneyRequestParticipantsSelector({ )} ), - [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, styles, translate], + [handleConfirmSelection, participants.length, referralContentType, shouldShowSplitBillErrorMessage, shouldShowReferralCTA, styles, translate], ); const itemRightSideComponent = useCallback( From d9dc65336727b23811ea5906bacae2c40a726bab Mon Sep 17 00:00:00 2001 From: Pujan Date: Tue, 16 Jan 2024 18:04:56 +0530 Subject: [PATCH 059/170] private notes edit page ts changes --- ...esEditPage.js => PrivateNotesEditPage.tsx} | 80 ++++++++----------- src/types/onyx/Report.ts | 2 +- 2 files changed, 35 insertions(+), 47 deletions(-) rename src/pages/PrivateNotes/{PrivateNotesEditPage.js => PrivateNotesEditPage.tsx} (72%) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.js b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx similarity index 72% rename from src/pages/PrivateNotes/PrivateNotesEditPage.js rename to src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 0d4bc2c3e7e1..b6b178049024 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.js +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -1,12 +1,12 @@ import {useFocusEffect} from '@react-navigation/native'; +import type {RouteProp} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {OnyxCollection} from 'react-native-onyx'; +import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -14,50 +14,42 @@ import OfflineWithFeedback from '@components/OfflineWithFeedback'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withLocalize from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; -import * as Report from '@userActions/Report'; +import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type { PersonalDetails, Report } from '@src/types/onyx'; +import type { Note } from '@src/types/onyx/Report'; + +type PrivateNotesEditPageOnyxProps = { + /* Onyx Props */ -const propTypes = { /** All of the personal details for everyone */ - personalDetailsList: PropTypes.objectOf(personalDetailsPropType), + personalDetailsList: OnyxCollection, +} + +type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & { /** The report currently being looked at */ - report: reportPropTypes, - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID and accountID passed via route: /r/:reportID/notes */ - reportID: PropTypes.string, - accountID: PropTypes.string, - }), - }).isRequired, -}; - -const defaultProps = { - report: {}, - personalDetailsList: {}, -}; - -function PrivateNotesEditPage({route, personalDetailsList, report}) { + report: Report, + + route: RouteProp<{params: {reportID: string; accountID: string}}>; +} + +function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); // We need to edit the note in markdown format, but display it in HTML format const parser = new ExpensiMark(); const [privateNote, setPrivateNote] = useState( - () => Report.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(lodashGet(report, ['privateNotes', route.params.accountID, 'note'], '')).trim(), + () => ReportActions.getDraftPrivateNote(report.reportID).trim() || parser.htmlToMarkdown(report?.privateNotes?.[Number(route.params.accountID)]?.note ?? '').trim(), ); /** @@ -67,8 +59,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { */ const debouncedSavePrivateNote = useMemo( () => - _.debounce((text) => { - Report.savePrivateNotesDraft(report.reportID, text); + lodashDebounce((text: string) => { + ReportActions.savePrivateNotesDraft(report.reportID, text); }, 1000), [report.reportID], ); @@ -94,18 +86,18 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { ); const savePrivateNote = () => { - const originalNote = lodashGet(report, ['privateNotes', route.params.accountID, 'note'], ''); + const originalNote = report?.privateNotes?.[Number(route.params.accountID)]?.note ?? ''; let editedNote = ''; if (privateNote.trim() !== originalNote.trim()) { - editedNote = Report.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); - Report.updatePrivateNotes(report.reportID, route.params.accountID, editedNote); + editedNote = ReportActions.handleUserDeletedLinksInHtml(privateNote.trim(), parser.htmlToMarkdown(originalNote).trim()); + ReportActions.updatePrivateNotes(report.reportID, Number(route.params.accountID), editedNote); } // We want to delete saved private note draft after saving the note debouncedSavePrivateNote(''); Keyboard.dismiss(); - if (!_.some({...report.privateNotes, [route.params.accountID]: {note: editedNote}}, (item) => item.note)) { + if(({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); @@ -133,16 +125,16 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { > {translate( - Str.extractEmailDomain(lodashGet(personalDetailsList, [route.params.accountID, 'login'], '')) === CONST.EMAIL.GUIDES_DOMAIN + Str.extractEmailDomain(personalDetailsList?.[route.params.accountID]?.login ?? '') === CONST.EMAIL.GUIDES_DOMAIN ? 'privateNotes.sharedNoteMessage' : 'privateNotes.personalNoteMessage', )} Report.clearPrivateNotesError(report.reportID, route.params.accountID)} + onClose={() => ReportActions.clearPrivateNotesError(report.reportID, Number(route.params.accountID))} style={[styles.mb3]} > { + onChangeText={(text: string) => { debouncedSavePrivateNote(text); setPrivateNote(text); }} @@ -177,15 +169,11 @@ function PrivateNotesEditPage({route, personalDetailsList, report}) { } PrivateNotesEditPage.displayName = 'PrivateNotesEditPage'; -PrivateNotesEditPage.propTypes = propTypes; -PrivateNotesEditPage.defaultProps = defaultProps; -export default compose( - withLocalize, - withReportAndPrivateNotesOrNotFound('privateNotes.title'), - withOnyx({ +export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( + withOnyx({ personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, - }), -)(PrivateNotesEditPage); + })(PrivateNotesEditPage) +); \ No newline at end of file diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index 7cc3c508d926..22a60712597b 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -161,4 +161,4 @@ type Report = { export default Report; -export type {NotificationPreference, WriteCapability}; +export type {NotificationPreference, WriteCapability, Note}; From 1c13f5b86e49879b10a87b8de4c949c3de14fb18 Mon Sep 17 00:00:00 2001 From: Pujan Date: Tue, 16 Jan 2024 18:13:08 +0530 Subject: [PATCH 060/170] corrected the condition --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index b6b178049024..8dff3ffd54d6 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -97,7 +97,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes debouncedSavePrivateNote(''); Keyboard.dismiss(); - if(({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { + if(!({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); From 2babc2d778cd6c0d9213e72270cfe346855c958f Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Thu, 11 Jan 2024 00:49:41 +0700 Subject: [PATCH 061/170] 34265 update emoji offset --- src/CONST.ts | 1 + src/components/EmojiPicker/EmojiPickerButton.js | 17 ++++++++++++++++- src/libs/calculateAnchorPosition.ts | 2 +- .../ReportActionCompose/ReportActionCompose.js | 8 ++++++++ src/styles/index.ts | 2 +- 5 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fc4a3729bd01..881e520e0b12 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -965,6 +965,7 @@ const CONST = { SMALL_EMOJI_PICKER_SIZE: { WIDTH: '100%', }, + MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM: 83, NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT: 300, NON_NATIVE_EMOJI_PICKER_LIST_HEIGHT_WEB: 200, EMOJI_PICKER_ITEM_HEIGHT: 32, diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js index e627119270dd..b056ccb22875 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.js @@ -22,6 +22,9 @@ const propTypes = { /** Unique id for emoji picker */ emojiPickerID: PropTypes.string, + /** Emoji popup anchor offset shift vertical */ + shiftVertical: PropTypes.number, + ...withLocalizePropTypes, }; @@ -29,6 +32,7 @@ const defaultProps = { isDisabled: false, id: '', emojiPickerID: '', + shiftVertical: 0, }; function EmojiPickerButton(props) { @@ -49,7 +53,18 @@ function EmojiPickerButton(props) { return; } if (!EmojiPickerAction.emojiPickerRef.current.isEmojiPickerVisible) { - EmojiPickerAction.showEmojiPicker(props.onModalHide, props.onEmojiSelected, emojiPopoverAnchor, undefined, () => {}, props.emojiPickerID); + EmojiPickerAction.showEmojiPicker( + props.onModalHide, + props.onEmojiSelected, + emojiPopoverAnchor, + { + horizontal: 'right', + vertical: 'bottom', + shiftVertical: props.shiftVertical, + }, + () => {}, + props.emojiPickerID, + ); } else { EmojiPickerAction.emojiPickerRef.current.hideEmojiPicker(); } diff --git a/src/libs/calculateAnchorPosition.ts b/src/libs/calculateAnchorPosition.ts index 66966b7b504c..3b6617aa3ed0 100644 --- a/src/libs/calculateAnchorPosition.ts +++ b/src/libs/calculateAnchorPosition.ts @@ -22,7 +22,7 @@ export default function calculateAnchorPosition(anchorComponent: View, anchorOri if (anchorOrigin?.vertical === CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP && anchorOrigin?.horizontal === CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT) { return resolve({horizontal: x, vertical: y + height + (anchorOrigin?.shiftVertical ?? 0)}); } - return resolve({horizontal: x + width, vertical: y}); + return resolve({horizontal: x + width, vertical: y + (anchorOrigin?.shiftVertical ?? 0)}); }); }); } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js index c072666920ae..c52b8ec6760a 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.js @@ -354,6 +354,13 @@ function ReportActionCompose({ runOnJS(submitForm)(); }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); + const emojiShiftVertical = useMemo(() => { + const chatItemComposeSecondaryRowHeight = styles.chatItemComposeSecondaryRow.height + styles.chatItemComposeSecondaryRow.marginTop + styles.chatItemComposeSecondaryRow.marginBottom; + const reportActionComposeHeight = styles.chatItemComposeBox.minHeight + chatItemComposeSecondaryRowHeight; + const emojiOffsetWithComposeBox = (styles.chatItemComposeBox.minHeight - styles.chatItemEmojiButton.height) / 2; + return reportActionComposeHeight - emojiOffsetWithComposeBox - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM; + }, [styles]); + return ( @@ -453,6 +460,7 @@ function ReportActionCompose({ onModalHide={focus} onEmojiSelected={(...args) => composerRef.current.replaceSelectionWithText(...args)} emojiPickerID={report.reportID} + shiftVertical={emojiShiftVertical} /> )} createMenuPositionReportActionCompose: (windowHeight: number) => ({ horizontal: 18 + variables.sideBarWidth, - vertical: windowHeight - 83, + vertical: windowHeight - CONST.MENU_POSITION_REPORT_ACTION_COMPOSE_BOTTOM, } satisfies AnchorPosition), createMenuPositionRightSidepane: { From 089c626465bbc3a3280774717bf8b97f803b3679 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 17 Jan 2024 09:10:07 +0100 Subject: [PATCH 062/170] Use logical or since label can be an empty string --- src/components/StatePicker/index.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/components/StatePicker/index.tsx b/src/components/StatePicker/index.tsx index 09f3b1a02802..a03e4f15fba0 100644 --- a/src/components/StatePicker/index.tsx +++ b/src/components/StatePicker/index.tsx @@ -62,7 +62,9 @@ function StatePicker({value, onInputChange, label, onBlur, errorText = ''}: Stat ref={ref} shouldShowRightIcon title={title} - description={label ?? translate('common.state')} + // Label can be an empty string + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + description={label || translate('common.state')} descriptionTextStyle={descStyle} onPress={showPickerModal} /> From 3d8c0ebdee04dfc9561050a4e7c79d3e1a20f08a Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 16:09:05 +0530 Subject: [PATCH 063/170] some method fix --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 8dff3ffd54d6..6a3749d9bdc4 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -97,7 +97,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes debouncedSavePrivateNote(''); Keyboard.dismiss(); - if(!({...report.privateNotes, [route.params.accountID]: {note: editedNote}} as Note).note) { + if(!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); From 1b687411d3a190e2e892cda60d47dd6dccab1835 Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 19:02:20 +0530 Subject: [PATCH 064/170] private notes list ts migration changes --- ...esListPage.js => PrivateNotesListPage.tsx} | 94 +++++++------------ 1 file changed, 32 insertions(+), 62 deletions(-) rename src/pages/PrivateNotes/{PrivateNotesListPage.js => PrivateNotesListPage.tsx} (59%) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.js b/src/pages/PrivateNotes/PrivateNotesListPage.tsx similarity index 59% rename from src/pages/PrivateNotes/PrivateNotesListPage.js rename to src/pages/PrivateNotes/PrivateNotesListPage.tsx index 8e2f8c9f43e0..167a3523854c 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.js +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -1,68 +1,45 @@ import {useIsFocused} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; import React, {useEffect, useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {withNetwork} from '@components/OnyxProvider'; import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type { PersonalDetails, Report, Session } from '@src/types/onyx'; +import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; -const propTypes = { - /** The report currently being looked at */ - report: reportPropTypes, - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID and accountID passed via route: /r/:reportID/notes */ - reportID: PropTypes.string, - accountID: PropTypes.string, - }), - }).isRequired, - - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), +type PrivateNotesListPageOnyxProps = { + /* Onyx Props */ /** All of the personal details for everyone */ - personalDetailsList: PropTypes.objectOf(personalDetailsPropType), + personalDetailsList: OnyxCollection, - ...withLocalizePropTypes, -}; + /** Session info for the currently logged in user. */ + session: OnyxEntry; +} -const defaultProps = { - report: {}, - session: { - accountID: null, - }, - personalDetailsList: {}, -}; +type PrivateNotesListPageProps = PrivateNotesListPageOnyxProps & { + /** The report currently being looked at */ + report: Report; +} -function PrivateNotesListPage({report, personalDetailsList, session}) { +function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const isFocused = useIsFocused(); useEffect(() => { const navigateToEditPageTimeout = setTimeout(() => { - if (_.some(report.privateNotes, (item) => item.note) || !isFocused) { + if (Object.values(report.privateNotes ?? {}).some((item) => item.note) || !isFocused) { return; } Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); @@ -75,12 +52,8 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { /** * Gets the menu item for each workspace - * - * @param {Object} item - * @param {Number} index - * @returns {JSX} */ - function getMenuItem(item, index) { + function getMenuItem(item, index: number) { const keyTitle = item.translationKey ? translate(item.translationKey) : item.title; return ( { - const privateNoteBrickRoadIndicator = (accountID) => (!_.isEmpty(lodashGet(report, ['privateNotes', accountID, 'errors'], '')) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''); - return _.chain(lodashGet(report, 'privateNotes', {})) - .map((privateNote, accountID) => ({ - title: Number(lodashGet(session, 'accountID', null)) === Number(accountID) ? translate('privateNotes.myNote') : lodashGet(personalDetailsList, [accountID, 'login'], ''), - action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), - brickRoadIndicator: privateNoteBrickRoadIndicator(accountID), - note: lodashGet(privateNote, 'note', ''), - disabled: Number(session.accountID) !== Number(accountID), - })) - .value(); + const privateNoteBrickRoadIndicator = (accountID: number) => report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; + return Object.keys(report.privateNotes ?? {}) + .map((accountID: string) => { + const privateNote = report.privateNotes?.[Number(accountID)]; + return { + title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '', + action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), + brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)), + note: privateNote?.note ?? '', + disabled: Number(session?.accountID) !== Number(accountID), + } + }) }, [report, personalDetailsList, session, translate]); return ( @@ -133,25 +108,20 @@ function PrivateNotesListPage({report, personalDetailsList, session}) { onCloseButtonPress={() => Navigation.dismissModal()} /> {translate('privateNotes.personalNoteMessage')} - {_.map(privateNotes, (item, index) => getMenuItem(item, index))} + {privateNotes.map((item, index) => getMenuItem(item, index))} ); } -PrivateNotesListPage.propTypes = propTypes; -PrivateNotesListPage.defaultProps = defaultProps; PrivateNotesListPage.displayName = 'PrivateNotesListPage'; -export default compose( - withLocalize, - withReportAndPrivateNotesOrNotFound('privateNotes.title'), - withOnyx({ +export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( + withOnyx({ personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, session: { key: ONYXKEYS.SESSION, }, - }), - withNetwork(), -)(PrivateNotesListPage); + })(PrivateNotesListPage) +); From e5db70922171b0163282659ecd1260fb1032e35b Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 19:31:00 +0530 Subject: [PATCH 065/170] corrected back route --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 6a3749d9bdc4..db7a1299bb5c 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -112,7 +112,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes > Navigation.goBack(ROUTES.PRIVATE_NOTES_VIEW.getRoute(report.reportID, route.params.accountID))} + onBackButtonPress={() => Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID))} shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> From bf7f887234f0e7f9714570d5bd911e1d613e3fd5 Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 21:34:37 +0530 Subject: [PATCH 066/170] removed notes view page --- src/ROUTES.ts | 4 - src/SCREENS.ts | 1 - .../AppNavigator/ModalStackNavigators.tsx | 1 - src/libs/Navigation/linkingConfig.ts | 1 - .../PrivateNotes/PrivateNotesViewPage.js | 112 ------------------ 5 files changed, 119 deletions(-) delete mode 100644 src/pages/PrivateNotes/PrivateNotesViewPage.js diff --git a/src/ROUTES.ts b/src/ROUTES.ts index 37003a09a0cd..532516bf0f42 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -224,10 +224,6 @@ const ROUTES = { route: 'r/:reportID/assignee', getRoute: (reportID: string) => `r/${reportID}/assignee` as const, }, - PRIVATE_NOTES_VIEW: { - route: 'r/:reportID/notes/:accountID', - getRoute: (reportID: string, accountID: string | number) => `r/${reportID}/notes/${accountID}` as const, - }, PRIVATE_NOTES_LIST: { route: 'r/:reportID/notes', getRoute: (reportID: string) => `r/${reportID}/notes` as const, diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 703cb309d641..bf131078466b 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -180,7 +180,6 @@ const SCREENS = { }, PRIVATE_NOTES: { - VIEW: 'PrivateNotes_View', LIST: 'PrivateNotes_List', EDIT: 'PrivateNotes_Edit', }, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index b0f33af0ce2e..1d586c6f7378 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -269,7 +269,6 @@ const EditRequestStackNavigator = createModalStackNavigator({ - [SCREENS.PRIVATE_NOTES.VIEW]: () => require('../../../pages/PrivateNotes/PrivateNotesViewPage').default as React.ComponentType, [SCREENS.PRIVATE_NOTES.LIST]: () => require('../../../pages/PrivateNotes/PrivateNotesListPage').default as React.ComponentType, [SCREENS.PRIVATE_NOTES.EDIT]: () => require('../../../pages/PrivateNotes/PrivateNotesEditPage').default as React.ComponentType, }); diff --git a/src/libs/Navigation/linkingConfig.ts b/src/libs/Navigation/linkingConfig.ts index 1a495e92eb80..f0a031a88302 100644 --- a/src/libs/Navigation/linkingConfig.ts +++ b/src/libs/Navigation/linkingConfig.ts @@ -278,7 +278,6 @@ const linkingConfig: LinkingOptions = { }, [SCREENS.RIGHT_MODAL.PRIVATE_NOTES]: { screens: { - [SCREENS.PRIVATE_NOTES.VIEW]: ROUTES.PRIVATE_NOTES_VIEW.route, [SCREENS.PRIVATE_NOTES.LIST]: ROUTES.PRIVATE_NOTES_LIST.route, [SCREENS.PRIVATE_NOTES.EDIT]: ROUTES.PRIVATE_NOTES_EDIT.route, }, diff --git a/src/pages/PrivateNotes/PrivateNotesViewPage.js b/src/pages/PrivateNotes/PrivateNotesViewPage.js deleted file mode 100644 index f71259a2b685..000000000000 --- a/src/pages/PrivateNotes/PrivateNotesViewPage.js +++ /dev/null @@ -1,112 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import React from 'react'; -import {ScrollView} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; -import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import ScreenWrapper from '@components/ScreenWrapper'; -import withLocalize from '@components/withLocalize'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import Navigation from '@libs/Navigation/Navigation'; -import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; -import personalDetailsPropType from '@pages/personalDetailsPropType'; -import reportPropTypes from '@pages/reportPropTypes'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; - -const propTypes = { - /** All of the personal details for everyone */ - personalDetailsList: PropTypes.objectOf(personalDetailsPropType), - - /** The report currently being looked at */ - report: reportPropTypes, - route: PropTypes.shape({ - /** Params from the URL path */ - params: PropTypes.shape({ - /** reportID and accountID passed via route: /r/:reportID/notes */ - reportID: PropTypes.string, - accountID: PropTypes.string, - }), - }).isRequired, - - /** Session of currently logged in user */ - session: PropTypes.shape({ - /** Currently logged in user accountID */ - accountID: PropTypes.number, - }), -}; - -const defaultProps = { - report: {}, - session: { - accountID: null, - }, - personalDetailsList: {}, -}; - -function PrivateNotesViewPage({route, personalDetailsList, session, report}) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const isCurrentUserNote = Number(session.accountID) === Number(route.params.accountID); - const privateNote = lodashGet(report, ['privateNotes', route.params.accountID, 'note'], ''); - - const getFallbackRoute = () => { - const privateNotes = lodashGet(report, 'privateNotes', {}); - - if (_.keys(privateNotes).length === 1) { - return ROUTES.HOME; - } - - return ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID); - }; - - return ( - - Navigation.goBack(getFallbackRoute())} - subtitle={isCurrentUserNote ? translate('privateNotes.myNote') : `${lodashGet(personalDetailsList, [route.params.accountID, 'login'], '')} note`} - shouldShowBackButton - onCloseButtonPress={() => Navigation.dismissModal()} - /> - - - isCurrentUserNote && Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, route.params.accountID))} - shouldShowRightIcon={isCurrentUserNote} - numberOfLinesTitle={0} - shouldRenderAsHTML - brickRoadIndicator={!_.isEmpty(lodashGet(report, ['privateNotes', route.params.accountID, 'errors'], '')) ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''} - disabled={!isCurrentUserNote} - shouldGreyOutWhenDisabled={false} - /> - - - - ); -} - -PrivateNotesViewPage.displayName = 'PrivateNotesViewPage'; -PrivateNotesViewPage.propTypes = propTypes; -PrivateNotesViewPage.defaultProps = defaultProps; - -export default compose( - withLocalize, - withReportAndPrivateNotesOrNotFound('privateNotes.title'), - withOnyx({ - personalDetailsList: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - }), -)(PrivateNotesViewPage); From 2baa8784975e99dcd3ba9dd4b2658fc46abd84ba Mon Sep 17 00:00:00 2001 From: Pujan Date: Wed, 17 Jan 2024 21:51:03 +0530 Subject: [PATCH 067/170] removed notes view for types --- src/libs/Navigation/types.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 8d227fa6f697..f87ed5094a82 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -331,10 +331,6 @@ type ProcessMoneyRequestHoldNavigatorParamList = { }; type PrivateNotesNavigatorParamList = { - [SCREENS.PRIVATE_NOTES.VIEW]: { - reportID: string; - accountID: string; - }; [SCREENS.PRIVATE_NOTES.LIST]: { reportID: string; accountID: string; From 11a225f4b1ec4265c27c097710fb00dd2a978874 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 22:47:27 +0300 Subject: [PATCH 068/170] Update src/pages/home/report/ReportActionItemCreated.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/pages/home/report/ReportActionItemCreated.tsx | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 86b7eef2c8ae..edf8b1c9c48e 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -37,14 +37,6 @@ type ReportActionItemCreatedProps = OnyxProps & { // eslint-disable-next-line react/no-unused-prop-types policyID: string; - /** The policy object for the current route */ - policy?: { - /** The name of the policy */ - name?: string; - - /** The URL for the policy avatar */ - avatar?: string; - }; }; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); From 9bad7da3a1dd29734e7a79f226e7d1ac81cc1b44 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 22:48:50 +0300 Subject: [PATCH 069/170] Update src/pages/home/report/ReportActionItemCreated.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/pages/home/report/ReportActionItemCreated.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index edf8b1c9c48e..bc423c72afc7 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -22,7 +22,7 @@ type OnyxProps = { /** The report currently being looked at */ report: OnyxEntry; - /** The policy being used */ + /** The policy object for the current route */ policy: OnyxEntry; /** Personal details of all the users */ From 0d802ffca22643faab94f5828ddc608ba8d54481 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 22:49:20 +0300 Subject: [PATCH 070/170] Update src/pages/home/report/ReportActionItemCreated.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/pages/home/report/ReportActionItemCreated.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index bc423c72afc7..47dc71cf43cd 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -18,7 +18,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetailsList, Policy, Report} from '@src/types/onyx'; import AnimatedEmptyStateBackground from './AnimatedEmptyStateBackground'; -type OnyxProps = { +type ReportActionItemCreatedOnyxProps = { /** The report currently being looked at */ report: OnyxEntry; From 34636db8d9d3288d7fcdfddea8ce37e52d7dfeca Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 22:53:37 +0300 Subject: [PATCH 071/170] Update src/pages/home/report/ReportActionItemCreated.tsx MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Fábio Henriques --- src/pages/home/report/ReportActionItemCreated.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 47dc71cf43cd..5ca74647fe4e 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -54,8 +54,8 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { return ( navigateToConciergeChatAndDeleteReport(props.report?.reportID ?? props.reportID)} needsOffscreenAlphaCompositing From 3be468ac7e28bc2fa18a16c98c212449a565783d Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 20:28:11 +0000 Subject: [PATCH 072/170] fixing typescript checks --- src/pages/home/report/ReportActionItemCreated.tsx | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItemCreated.tsx b/src/pages/home/report/ReportActionItemCreated.tsx index 5ca74647fe4e..82c6bebd9ba1 100644 --- a/src/pages/home/report/ReportActionItemCreated.tsx +++ b/src/pages/home/report/ReportActionItemCreated.tsx @@ -29,14 +29,13 @@ type ReportActionItemCreatedOnyxProps = { personalDetails: OnyxEntry; }; -type ReportActionItemCreatedProps = OnyxProps & { +type ReportActionItemCreatedProps = ReportActionItemCreatedOnyxProps & { /** The id of the report */ reportID: string; /** The id of the policy */ // eslint-disable-next-line react/no-unused-prop-types policyID: string; - }; function ReportActionItemCreated(props: ReportActionItemCreatedProps) { const styles = useThemeStyles(); @@ -95,7 +94,7 @@ function ReportActionItemCreated(props: ReportActionItemCreatedProps) { ReportActionItemCreated.displayName = 'ReportActionItemCreated'; -export default withOnyx({ +export default withOnyx({ report: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, selector: reportWithoutHasDraftSelector, From 0fd9e84898529dbf98a84abd4e59e843becdc66a Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Wed, 17 Jan 2024 23:38:59 +0300 Subject: [PATCH 073/170] created getGroupChatParticipantIDs --- src/libs/ReportUtils.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5ac1806356c0..0cc0d5ffef3f 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1360,6 +1360,10 @@ function isGroupChat(report: OnyxEntry): boolean { ); } +function getGroupChatParticipantIDs(participants: number[]): number[] { + return [...new Set([...participants, ...(currentUserAccountID ? [currentUserAccountID] : [])])]; +} + /** * Returns an array of the participants Ids of a report * @@ -1379,8 +1383,8 @@ function getParticipantsIDs(report: OnyxEntry): number[] { return onlyUnique; } - if (isGroupChat(report) && currentUserAccountID) { - return [...new Set([...participants, currentUserAccountID])]; + if (isGroupChat(report)) { + return getGroupChatParticipantIDs(participants); } return participants; @@ -1403,8 +1407,8 @@ function getVisibleMemberIDs(report: OnyxEntry): number[] { return onlyUnique; } - if (isGroupChat(report) && currentUserAccountID) { - return [...new Set([...visibleChatMemberAccountIDs, currentUserAccountID])]; + if (isGroupChat(report)) { + return getGroupChatParticipantIDs(visibleChatMemberAccountIDs); } return visibleChatMemberAccountIDs; From 0258fb46b0a715ffcad5591d2a600e8e41f82889 Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 14:54:07 +0530 Subject: [PATCH 074/170] removed jsdoc type --- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 167a3523854c..60ea21610c0b 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -80,7 +80,6 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot /** * Returns a list of private notes on the given chat report - * @returns {Array} the menu item list */ const privateNotes = useMemo(() => { const privateNoteBrickRoadIndicator = (accountID: number) => report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; From 6d44c76847b5e80302f33b5693fd94460297cd98 Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 14:59:58 +0530 Subject: [PATCH 075/170] prettier --- .../PrivateNotes/PrivateNotesEditPage.tsx | 23 ++++++----- .../PrivateNotes/PrivateNotesListPage.tsx | 39 +++++++++---------- 2 files changed, 30 insertions(+), 32 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index db7a1299bb5c..b78431601898 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -2,10 +2,10 @@ import {useFocusEffect} from '@react-navigation/native'; import type {RouteProp} from '@react-navigation/native'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; -import type {OnyxCollection} from 'react-native-onyx'; import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; +import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; import InputWrapper from '@components/Form/InputWrapper'; @@ -24,23 +24,22 @@ import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type { PersonalDetails, Report } from '@src/types/onyx'; -import type { Note } from '@src/types/onyx/Report'; +import type {PersonalDetails, Report} from '@src/types/onyx'; +import type {Note} from '@src/types/onyx/Report'; type PrivateNotesEditPageOnyxProps = { /* Onyx Props */ /** All of the personal details for everyone */ - personalDetailsList: OnyxCollection, -} + personalDetailsList: OnyxCollection; +}; type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & { - /** The report currently being looked at */ - report: Report, + report: Report; route: RouteProp<{params: {reportID: string; accountID: string}}>; -} +}; function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { const styles = useThemeStyles(); @@ -97,7 +96,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes debouncedSavePrivateNote(''); Keyboard.dismiss(); - if(!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { + if (!Object.values({...report.privateNotes, [route.params.accountID]: {note: editedNote}}).some((item) => item.note)) { ReportUtils.navigateToDetailsPage(report); } else { Navigation.goBack(ROUTES.PRIVATE_NOTES_LIST.getRoute(report.reportID)); @@ -132,7 +131,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes ReportActions.clearPrivateNotesError(report.reportID, Number(route.params.accountID))} style={[styles.mb3]} @@ -175,5 +174,5 @@ export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( personalDetailsList: { key: ONYXKEYS.PERSONAL_DETAILS_LIST, }, - })(PrivateNotesEditPage) -); \ No newline at end of file + })(PrivateNotesEditPage), +); diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 60ea21610c0b..ef0d279e3de3 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -2,6 +2,7 @@ import {useIsFocused} from '@react-navigation/native'; import React, {useEffect, useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; @@ -14,23 +15,22 @@ import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAn import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type { PersonalDetails, Report, Session } from '@src/types/onyx'; -import type { OnyxCollection, OnyxEntry } from 'react-native-onyx'; +import type {PersonalDetails, Report, Session} from '@src/types/onyx'; type PrivateNotesListPageOnyxProps = { /* Onyx Props */ /** All of the personal details for everyone */ - personalDetailsList: OnyxCollection, + personalDetailsList: OnyxCollection; /** Session info for the currently logged in user. */ session: OnyxEntry; -} +}; type PrivateNotesListPageProps = PrivateNotesListPageOnyxProps & { /** The report currently being looked at */ report: Report; -} +}; function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); @@ -42,13 +42,13 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot if (Object.values(report.privateNotes ?? {}).some((item) => item.note) || !isFocused) { return; } - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session.accountID)); + Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session?.accountID ?? '')); }, CONST.ANIMATED_TRANSITION); return () => { clearTimeout(navigateToEditPageTimeout); }; - }, [report.privateNotes, report.reportID, session.accountID, isFocused]); + }, [report.privateNotes, report.reportID, session?.accountID, isFocused]); /** * Gets the menu item for each workspace @@ -82,18 +82,17 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot * Returns a list of private notes on the given chat report */ const privateNotes = useMemo(() => { - const privateNoteBrickRoadIndicator = (accountID: number) => report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''; - return Object.keys(report.privateNotes ?? {}) - .map((accountID: string) => { - const privateNote = report.privateNotes?.[Number(accountID)]; - return { - title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '', - action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), - brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)), - note: privateNote?.note ?? '', - disabled: Number(session?.accountID) !== Number(accountID), - } - }) + const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''); + return Object.keys(report.privateNotes ?? {}).map((accountID: string) => { + const privateNote = report.privateNotes?.[Number(accountID)]; + return { + title: Number(session?.accountID) === Number(accountID) ? translate('privateNotes.myNote') : personalDetailsList?.[accountID]?.login ?? '', + action: () => Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, accountID)), + brickRoadIndicator: privateNoteBrickRoadIndicator(Number(accountID)), + note: privateNote?.note ?? '', + disabled: Number(session?.accountID) !== Number(accountID), + }; + }); }, [report, personalDetailsList, session, translate]); return ( @@ -122,5 +121,5 @@ export default withReportAndPrivateNotesOrNotFound('privateNotes.title')( session: { key: ONYXKEYS.SESSION, }, - })(PrivateNotesListPage) + })(PrivateNotesListPage), ); From 6c8201a9e763c66a473c6771e919ae8ae2f861de Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 18:07:18 +0530 Subject: [PATCH 076/170] ref type fixes --- src/libs/updateMultilineInputRange/types.ts | 2 +- .../PrivateNotes/PrivateNotesEditPage.tsx | 8 ++-- .../PrivateNotes/PrivateNotesListPage.tsx | 47 +++++++++---------- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/libs/updateMultilineInputRange/types.ts b/src/libs/updateMultilineInputRange/types.ts index d1b134b09a99..ce8f553c51f8 100644 --- a/src/libs/updateMultilineInputRange/types.ts +++ b/src/libs/updateMultilineInputRange/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput, shouldAutoFocus?: boolean) => void; +type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput | null, shouldAutoFocus?: boolean) => void; export default UpdateMultilineInputRange; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index b78431601898..c6095a318029 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -5,6 +5,7 @@ import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; import React, {useCallback, useMemo, useRef, useState} from 'react'; import {Keyboard} from 'react-native'; +import type {TextInput as TextInputRN} from 'react-native'; import type {OnyxCollection} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import FormProvider from '@components/Form/FormProvider'; @@ -54,7 +55,6 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes /** * Save the draft of the private note. This debounced so that we're not ceaselessly saving your edit. Saving the draft * allows one to navigate somewhere else and come back to the private note and still have it in edit mode. - * @param {String} newDraft */ const debouncedSavePrivateNote = useMemo( () => @@ -65,8 +65,8 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes ); // To focus on the input field when the page loads - const privateNotesInput = useRef(null); - const focusTimeoutRef = useRef(null); + const privateNotesInput = useRef(null); + const focusTimeoutRef = useRef(null); useFocusEffect( useCallback(() => { @@ -115,6 +115,7 @@ function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotes shouldShowBackButton onCloseButtonPress={() => Navigation.dismissModal()} /> + {/* @ts-expect-error TODO: Remove this once FormProvider (https://github.com/Expensify/App/issues/31972) is migrated to TypeScript. */} void; + brickRoadIndicator: ValueOf | undefined; + note: string; + disabled: boolean; +}; + function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -53,28 +61,19 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot /** * Gets the menu item for each workspace */ - function getMenuItem(item, index: number) { - const keyTitle = item.translationKey ? translate(item.translationKey) : item.title; + function getMenuItem(item: NoteListItem) { return ( - - - + ); } @@ -82,7 +81,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot * Returns a list of private notes on the given chat report */ const privateNotes = useMemo(() => { - const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : ''); + const privateNoteBrickRoadIndicator = (accountID: number) => (report.privateNotes?.[accountID].errors ? CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR : undefined); return Object.keys(report.privateNotes ?? {}).map((accountID: string) => { const privateNote = report.privateNotes?.[Number(accountID)]; return { @@ -106,7 +105,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot onCloseButtonPress={() => Navigation.dismissModal()} /> {translate('privateNotes.personalNoteMessage')} - {privateNotes.map((item, index) => getMenuItem(item, index))} + {privateNotes.map((item) => getMenuItem(item))} ); } From 5d52ddad9d98d51c294cf80811e4b5c23d669d35 Mon Sep 17 00:00:00 2001 From: Pujan Date: Thu, 18 Jan 2024 18:29:54 +0530 Subject: [PATCH 077/170] added key --- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 2cc958e730c1..550234a0707e 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -64,6 +64,7 @@ function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNot function getMenuItem(item: NoteListItem) { return ( Date: Thu, 18 Jan 2024 19:30:41 +0300 Subject: [PATCH 078/170] implemented useCallback Ref --- src/hooks/useCallbackRef.ts | 13 +++++++++++++ src/pages/home/report/ReportActionsList.js | 16 +++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 src/hooks/useCallbackRef.ts diff --git a/src/hooks/useCallbackRef.ts b/src/hooks/useCallbackRef.ts new file mode 100644 index 000000000000..075cbe08bbbc --- /dev/null +++ b/src/hooks/useCallbackRef.ts @@ -0,0 +1,13 @@ +import {useEffect, useMemo, useRef} from 'react'; + +const useCallbackRef = unknown>(callback: T): T => { + const calbackRef = useRef(callback); + + useEffect(() => { + calbackRef.current = callback; + }); + + return useMemo(() => ((...args) => calbackRef.current(...args)) as T, []); +}; + +export default useCallbackRef; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 61e2e1ce14bb..a4fe94183695 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -9,6 +9,7 @@ import InvertedFlatList from '@components/InvertedFlatList'; import {withPersonalDetails} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; +import useCallbackRef from '@hooks/useCallbackRef'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useReportScrollManager from '@hooks/useReportScrollManager'; @@ -167,7 +168,6 @@ function ReportActionsList({ const reportActionSize = useRef(sortedVisibleReportActions.length); const previousLastIndex = useRef(lastActionIndex); - const visibilityCallback = useRef(() => {}); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); @@ -387,7 +387,7 @@ function ReportActionsList({ [currentUnreadMarker, sortedVisibleReportActions, report.reportID, messageManuallyMarkedUnread], ); - const calculateUnreadMarker = () => { + const calculateUnreadMarker = useCallback(() => { // Iterate through the report actions and set appropriate unread marker. // This is to avoid a warning of: // Cannot update a component (ReportActionsList) while rendering a different component (CellRenderer). @@ -405,15 +405,13 @@ function ReportActionsList({ if (!markerFound) { setCurrentUnreadMarker(null); } - }; + }, [sortedVisibleReportActions, shouldDisplayNewMarker, currentUnreadMarker, report.reportID]); useEffect(() => { calculateUnreadMarker(); + }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [sortedVisibleReportActions, report.lastReadTime, report.reportID, messageManuallyMarkedUnread, shouldDisplayNewMarker, currentUnreadMarker]); - - visibilityCallback.current = () => { + const visibilityCallback = useCallbackRef(() => { if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report) || messageManuallyMarkedUnread) { return; } @@ -423,10 +421,10 @@ function ReportActionsList({ setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); - }; + }); useEffect(() => { - const unsubscribeVisibilityListener = Visibility.onVisibilityChange(() => visibilityCallback.current()); + const unsubscribeVisibilityListener = Visibility.onVisibilityChange(visibilityCallback); return unsubscribeVisibilityListener; From bd4d978f94884911335142ff990235dc57410153 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 14:57:27 +0700 Subject: [PATCH 079/170] Revert "fallback string instead of number" This reverts commit ea5c3a2b6c180a94601b4d2408614db0e44dd5d4. --- src/components/ReportActionItem/MoneyRequestAction.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index 241b3e32447a..d242dbe23e86 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -116,7 +116,7 @@ function MoneyRequestAction({ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { - shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || '0'); + shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || 0); } return isDeletedParentAction || isReversedTransaction ? ( From 4cfb299c7e87b596678843c6630b4a93afa1b95b Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 14:57:44 +0700 Subject: [PATCH 080/170] Revert "only display pending message for foreign currency transaction" This reverts commit 43c38ba40b27b6f8450c4280f9cf6cd720640067. --- src/components/ReportActionItem/MoneyRequestAction.js | 2 +- src/libs/IOUUtils.ts | 10 ++++------ tests/unit/IOUUtilsTest.js | 6 +++--- 3 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index d242dbe23e86..46226969636e 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -116,7 +116,7 @@ function MoneyRequestAction({ const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { - shouldShowPendingConversionMessage = IOUUtils.isTransactionPendingCurrencyConversion(iouReport, (action && action.originalMessage && action.originalMessage.IOUTransactionID) || 0); + shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); } return isDeletedParentAction || isReversedTransaction ? ( diff --git a/src/libs/IOUUtils.ts b/src/libs/IOUUtils.ts index aab9aac2391d..11dd0f5badda 100644 --- a/src/libs/IOUUtils.ts +++ b/src/libs/IOUUtils.ts @@ -110,14 +110,12 @@ function updateIOUOwnerAndTotal(iouReport: OnyxEntry, actorAccountID: nu } /** - * Returns whether or not a transaction of IOU report contains money requests in a different currency + * Returns whether or not an IOU report contains money requests in a different currency * that are either created or cancelled offline, and thus haven't been converted to the report's currency yet */ -function isTransactionPendingCurrencyConversion(iouReport: Report, transactionID: string): boolean { +function isIOUReportPendingCurrencyConversion(iouReport: Report): boolean { const reportTransactions: Transaction[] = TransactionUtils.getAllReportTransactions(iouReport.reportID); - const pendingRequestsInDifferentCurrency = reportTransactions.filter( - (transaction) => transaction.pendingAction && transaction.transactionID === transactionID && TransactionUtils.getCurrency(transaction) !== iouReport.currency, - ); + const pendingRequestsInDifferentCurrency = reportTransactions.filter((transaction) => transaction.pendingAction && TransactionUtils.getCurrency(transaction) !== iouReport.currency); return pendingRequestsInDifferentCurrency.length > 0; } @@ -129,4 +127,4 @@ function isValidMoneyRequestType(iouType: string): boolean { return moneyRequestType.includes(iouType); } -export {calculateAmount, updateIOUOwnerAndTotal, isTransactionPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, navigateToStartStepIfScanFileCannotBeRead}; +export {calculateAmount, updateIOUOwnerAndTotal, isIOUReportPendingCurrencyConversion, isValidMoneyRequestType, navigateToStartMoneyRequestStep, navigateToStartStepIfScanFileCannotBeRead}; diff --git a/tests/unit/IOUUtilsTest.js b/tests/unit/IOUUtilsTest.js index 7f239d9bb576..ac04b74a0ca5 100644 --- a/tests/unit/IOUUtilsTest.js +++ b/tests/unit/IOUUtilsTest.js @@ -17,7 +17,7 @@ function initCurrencyList() { } describe('IOUUtils', () => { - describe('isTransactionPendingCurrencyConversion', () => { + describe('isIOUReportPendingCurrencyConversion', () => { beforeAll(() => { Onyx.init({ keys: ONYXKEYS, @@ -34,7 +34,7 @@ describe('IOUUtils', () => { [`${ONYXKEYS.COLLECTION.TRANSACTION}${aedPendingTransaction.transactionID}`]: aedPendingTransaction, }).then(() => { // We requested money offline in a different currency, we don't know the total of the iouReport until we're back online - expect(IOUUtils.isTransactionPendingCurrencyConversion(iouReport, aedPendingTransaction.transactionID)).toBe(true); + expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(true); }); }); @@ -54,7 +54,7 @@ describe('IOUUtils', () => { }, }).then(() => { // We requested money online in a different currency, we know the iouReport total and there's no need to show the pending conversion message - expect(IOUUtils.isTransactionPendingCurrencyConversion(iouReport, aedPendingTransaction.transactionID)).toBe(false); + expect(IOUUtils.isIOUReportPendingCurrencyConversion(iouReport)).toBe(false); }); }); }); From db3e96cc82c6467ef16d54c748a5dbb154b847c6 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 14:57:57 +0700 Subject: [PATCH 081/170] Revert "remove most recent iou report action id" This reverts commit 964763547f578d20b5bb3d21120e0189b33b37ba. --- .../ReportActionItem/MoneyRequestAction.js | 8 +++++++- src/pages/home/report/ReportActionItem.js | 5 +++++ .../home/report/ReportActionItemParentAction.js | 1 + src/pages/home/report/ReportActionsList.js | 8 +++++++- .../home/report/ReportActionsListItemRenderer.js | 16 +++++++++++++++- src/pages/home/report/ReportActionsView.js | 4 ++++ 6 files changed, 39 insertions(+), 3 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index 46226969636e..d159998b2d57 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -115,7 +115,13 @@ function MoneyRequestAction({ let shouldShowPendingConversionMessage = false; const isDeletedParentAction = ReportActionsUtils.isDeletedParentAction(action); const isReversedTransaction = ReportActionsUtils.isReversedTransaction(action); - if (!_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline) { + if ( + !_.isEmpty(iouReport) && + !_.isEmpty(reportActions) && + chatReport.iouReportID && + action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && + network.isOffline + ) { shouldShowPendingConversionMessage = IOUUtils.isIOUReportPendingCurrencyConversion(iouReport); } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 2f8e86de5cdb..55b294936f49 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -89,6 +89,9 @@ const propTypes = { /** Should the comment have the appearance of being grouped with the previous comment? */ displayAsGroup: PropTypes.bool.isRequired, + /** Is this the most recent IOU Action? */ + isMostRecentIOUReportAction: PropTypes.bool.isRequired, + /** Should we display the new marker on top of the comment? */ shouldDisplayNewMarker: PropTypes.bool.isRequired, @@ -346,6 +349,7 @@ function ReportActionItem(props) { chatReportID={originalMessage.IOUReportID ? props.report.chatReportID : props.report.reportID} requestReportID={iouReportID} action={props.action} + isMostRecentIOUReportAction={props.isMostRecentIOUReportAction} isHovered={hovered} contextMenuAnchor={popoverAnchorRef} checkIfContextMenuActive={toggleContextMenuFromActiveReportAction} @@ -819,6 +823,7 @@ export default compose( (prevProps, nextProps) => prevProps.displayAsGroup === nextProps.displayAsGroup && prevProps.draftMessage === nextProps.draftMessage && + prevProps.isMostRecentIOUReportAction === nextProps.isMostRecentIOUReportAction && prevProps.shouldDisplayNewMarker === nextProps.shouldDisplayNewMarker && _.isEqual(prevProps.emojiReactions, nextProps.emojiReactions) && _.isEqual(prevProps.action, nextProps.action) && diff --git a/src/pages/home/report/ReportActionItemParentAction.js b/src/pages/home/report/ReportActionItemParentAction.js index 7c9f08b35ce7..d1a294881eb9 100644 --- a/src/pages/home/report/ReportActionItemParentAction.js +++ b/src/pages/home/report/ReportActionItemParentAction.js @@ -73,6 +73,7 @@ function ReportActionItemParentAction(props) { report={props.report} action={parentReportAction} displayAsGroup={false} + isMostRecentIOUReportAction={false} shouldDisplayNewMarker={props.shouldDisplayNewMarker} index={props.index} /> diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 635466219f2b..8d79e7af8dd4 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -34,6 +34,9 @@ const propTypes = { /** Sorted actions prepared for display */ sortedReportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)).isRequired, + /** The ID of the most recent IOU report action connected with the shown report */ + mostRecentIOUReportActionID: PropTypes.string, + /** The report metadata loading states */ isLoadingInitialReportActions: PropTypes.bool, @@ -70,6 +73,7 @@ const propTypes = { const defaultProps = { onScroll: () => {}, + mostRecentIOUReportActionID: '', isLoadingInitialReportActions: false, isLoadingOlderReportActions: false, isLoadingNewerReportActions: false, @@ -124,6 +128,7 @@ function ReportActionsList({ sortedReportActions, windowHeight, onScroll, + mostRecentIOUReportActionID, isSmallScreenWidth, personalDetailsList, currentUserPersonalDetails, @@ -410,11 +415,12 @@ function ReportActionsList({ report={report} linkedReportActionID={linkedReportActionID} displayAsGroup={ReportActionsUtils.isConsecutiveActionMadeByPreviousActor(sortedReportActions, index)} + mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} /> ), - [report, linkedReportActionID, sortedReportActions, shouldHideThreadDividerLine, shouldDisplayNewMarker], + [report, linkedReportActionID, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker], ); // Native mobile does not render updates flatlist the changes even though component did update called. diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index 791a3f78c67b..a9ae2b4c73b9 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -22,6 +22,9 @@ const propTypes = { /** Should the comment have the appearance of being grouped with the previous comment? */ displayAsGroup: PropTypes.bool.isRequired, + /** The ID of the most recent IOU report action connected with the shown report */ + mostRecentIOUReportActionID: PropTypes.string, + /** If the thread divider line should be hidden */ shouldHideThreadDividerLine: PropTypes.bool.isRequired, @@ -33,10 +36,20 @@ const propTypes = { }; const defaultProps = { + mostRecentIOUReportActionID: '', linkedReportActionID: '', }; -function ReportActionsListItemRenderer({reportAction, index, report, displayAsGroup, shouldHideThreadDividerLine, shouldDisplayNewMarker, linkedReportActionID}) { +function ReportActionsListItemRenderer({ + reportAction, + index, + report, + displayAsGroup, + mostRecentIOUReportActionID, + shouldHideThreadDividerLine, + shouldDisplayNewMarker, + linkedReportActionID, +}) { const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && ReportUtils.isChatThread(report) && @@ -65,6 +78,7 @@ function ReportActionsListItemRenderer({reportAction, index, report, displayAsGr reportAction.actionName, ) } + isMostRecentIOUReportAction={reportAction.reportActionID === mostRecentIOUReportActionID} index={index} /> ); diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ddcea7894251..2758437a3962 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -14,6 +14,7 @@ import usePrevious from '@hooks/usePrevious'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import {isUserCreatedPolicyRoom} from '@libs/ReportUtils'; import {didUserLogInDuringSession} from '@libs/SessionUtils'; import {ReactionListContext} from '@pages/home/ReportScreenContext'; @@ -86,6 +87,8 @@ function ReportActionsView(props) { const didSubscribeToReportTypingEvents = useRef(false); const isFirstRender = useRef(true); const hasCachedActions = useInitialValue(() => _.size(props.reportActions) > 0); + const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); + const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); @@ -254,6 +257,7 @@ function ReportActionsView(props) { report={props.report} onLayout={recordTimeToMeasureItemLayout} sortedReportActions={props.reportActions} + mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} isLoadingInitialReportActions={props.isLoadingInitialReportActions} From 86a41083ee018b2409aefe02d87b3901e479be24 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 14:58:05 +0700 Subject: [PATCH 082/170] Revert "fix: 33073" This reverts commit ad03aca8426f0ad13f65dc3b0fd75165a2a2a214. --- src/components/ReportActionItem/MoneyRequestAction.js | 5 +++++ src/pages/home/report/ReportActionItem.js | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyRequestAction.js b/src/components/ReportActionItem/MoneyRequestAction.js index d159998b2d57..e0a3152a41b4 100644 --- a/src/components/ReportActionItem/MoneyRequestAction.js +++ b/src/components/ReportActionItem/MoneyRequestAction.js @@ -34,6 +34,9 @@ const propTypes = { /** The ID of the associated request report */ requestReportID: PropTypes.string.isRequired, + /** Is this IOUACTION the most recent? */ + isMostRecentIOUReportAction: PropTypes.bool.isRequired, + /** Popover context menu anchor, used for showing context menu */ contextMenuAnchor: refPropTypes, @@ -78,6 +81,7 @@ function MoneyRequestAction({ action, chatReportID, requestReportID, + isMostRecentIOUReportAction, contextMenuAnchor, checkIfContextMenuActive, chatReport, @@ -119,6 +123,7 @@ function MoneyRequestAction({ !_.isEmpty(iouReport) && !_.isEmpty(reportActions) && chatReport.iouReportID && + isMostRecentIOUReportAction && action.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.ADD && network.isOffline ) { diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 55b294936f49..00ce20c19a85 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -346,7 +346,7 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( Date: Fri, 19 Jan 2024 15:15:18 +0700 Subject: [PATCH 083/170] fix 33073 --- src/pages/home/report/ReportActionItem.js | 2 +- src/pages/home/report/ReportActionsView.js | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 00ce20c19a85..55b294936f49 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -346,7 +346,7 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( _.size(props.reportActions) > 0); - const mostRecentIOUReportActionID = useInitialValue(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions)); - + const mostRecentIOUReportActionID = useMemo(() => ReportActionsUtils.getMostRecentIOURequestActionID(props.reportActions), [props.reportActions]); const prevNetworkRef = useRef(props.network); const prevAuthTokenType = usePrevious(props.session.authTokenType); From a79485c2d1c14f81623058ce9ce4a84c9c82e501 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 19 Jan 2024 11:29:29 +0100 Subject: [PATCH 084/170] Selection List migration --- src/components/SectionList/index.android.tsx | 28 +- src/components/SectionList/index.tsx | 20 +- src/components/SectionList/types.ts | 9 - .../{BaseListItem.js => BaseListItem.tsx} | 22 +- ...SelectionList.js => BaseSelectionList.tsx} | 222 +++++++-------- .../{RadioListItem.js => RadioListItem.tsx} | 8 +- .../{UserListItem.js => UserListItem.tsx} | 22 +- src/components/SelectionList/index.android.js | 17 -- .../SelectionList/index.android.tsx | 22 ++ src/components/SelectionList/index.ios.js | 16 -- src/components/SelectionList/index.ios.tsx | 21 ++ .../SelectionList/{index.js => index.tsx} | 9 +- src/components/SelectionList/types.ts | 266 ++++++++++++++++++ src/components/SubscriptAvatar.tsx | 1 + 14 files changed, 473 insertions(+), 210 deletions(-) delete mode 100644 src/components/SectionList/types.ts rename src/components/SelectionList/{BaseListItem.js => BaseListItem.tsx} (90%) rename src/components/SelectionList/{BaseSelectionList.js => BaseSelectionList.tsx} (76%) rename src/components/SelectionList/{RadioListItem.js => RadioListItem.tsx} (87%) rename src/components/SelectionList/{UserListItem.js => UserListItem.tsx} (67%) delete mode 100644 src/components/SelectionList/index.android.js create mode 100644 src/components/SelectionList/index.android.tsx delete mode 100644 src/components/SelectionList/index.ios.js create mode 100644 src/components/SelectionList/index.ios.tsx rename src/components/SelectionList/{index.js => index.tsx} (82%) create mode 100644 src/components/SelectionList/types.ts diff --git a/src/components/SectionList/index.android.tsx b/src/components/SectionList/index.android.tsx index 1aa9b501146c..ab37bc733849 100644 --- a/src/components/SectionList/index.android.tsx +++ b/src/components/SectionList/index.android.tsx @@ -1,19 +1,21 @@ import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; import {SectionList as RNSectionList} from 'react-native'; -import type ForwardedSectionList from './types'; +import type {SectionListProps} from 'react-native'; -// eslint-disable-next-line react/function-component-definition -const SectionListWithRef: ForwardedSectionList = (props, ref) => ( - -); +function SectionListWithRef(props: SectionListProps, ref: ForwardedRef>) { + return ( + + ); +} SectionListWithRef.displayName = 'SectionListWithRef'; diff --git a/src/components/SectionList/index.tsx b/src/components/SectionList/index.tsx index 4af7ad33705c..4671bbf34d78 100644 --- a/src/components/SectionList/index.tsx +++ b/src/components/SectionList/index.tsx @@ -1,15 +1,17 @@ import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; import {SectionList as RNSectionList} from 'react-native'; -import type ForwardedSectionList from './types'; +import type {SectionListProps} from 'react-native'; -// eslint-disable-next-line react/function-component-definition -const SectionList: ForwardedSectionList = (props, ref) => ( - -); +function SectionList(props: SectionListProps, ref: ForwardedRef>) { + return ( + + ); +} SectionList.displayName = 'SectionList'; diff --git a/src/components/SectionList/types.ts b/src/components/SectionList/types.ts deleted file mode 100644 index 4648172aabfd..000000000000 --- a/src/components/SectionList/types.ts +++ /dev/null @@ -1,9 +0,0 @@ -import type {ForwardedRef} from 'react'; -import type {SectionList, SectionListProps} from 'react-native'; - -type ForwardedSectionList = { - (props: SectionListProps, ref: ForwardedRef): React.ReactNode; - displayName: string; -}; - -export default ForwardedSectionList; diff --git a/src/components/SelectionList/BaseListItem.js b/src/components/SelectionList/BaseListItem.tsx similarity index 90% rename from src/components/SelectionList/BaseListItem.js rename to src/components/SelectionList/BaseListItem.tsx index 6a067ea0fe3d..217e4dab5d67 100644 --- a/src/components/SelectionList/BaseListItem.js +++ b/src/components/SelectionList/BaseListItem.tsx @@ -1,4 +1,3 @@ -import lodashGet from 'lodash/get'; import React from 'react'; import {View} from 'react-native'; import Icon from '@components/Icon'; @@ -12,10 +11,10 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import RadioListItem from './RadioListItem'; -import {baseListItemPropTypes} from './selectionListPropTypes'; +import type {BaseListItemProps, RadioItem, User} from './types'; import UserListItem from './UserListItem'; -function BaseListItem({ +function BaseListItem({ item, isFocused = false, isDisabled = false, @@ -26,12 +25,12 @@ function BaseListItem({ onDismissError = () => {}, rightHandSideComponent, keyForList, -}) { +}: BaseListItemProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const isUserItem = lodashGet(item, 'icons.length', 0) > 0; + const isUserItem = 'icons' in item && item?.icons?.length && item.icons.length > 0; const ListItem = isUserItem ? UserListItem : RadioListItem; const rightHandSideComponentRender = () => { @@ -49,8 +48,8 @@ function BaseListItem({ return ( onDismissError(item)} - pendingAction={item.pendingAction} - errors={item.errors} + pendingAction={isUserItem ? item.pendingAction : undefined} + errors={isUserItem ? item.errors : undefined} errorRowStyles={styles.ph5} > )} + onSelectRow(item)} showTooltip={showTooltip} /> + {!canSelectMultiple && item.isSelected && !rightHandSideComponent && ( - {Boolean(item.invitedSecondaryLogin) && ( + {isUserItem && item.invitedSecondaryLogin && ( {translate('workspace.people.invitedBySecondaryLogin', {secondaryLogin: item.invitedSecondaryLogin})} @@ -140,6 +141,5 @@ function BaseListItem({ } BaseListItem.displayName = 'BaseListItem'; -BaseListItem.propTypes = baseListItemPropTypes; export default BaseListItem; diff --git a/src/components/SelectionList/BaseSelectionList.js b/src/components/SelectionList/BaseSelectionList.tsx similarity index 76% rename from src/components/SelectionList/BaseSelectionList.js rename to src/components/SelectionList/BaseSelectionList.tsx index 960618808fd9..26a206c227ec 100644 --- a/src/components/SelectionList/BaseSelectionList.js +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -1,8 +1,8 @@ import {useFocusEffect, useIsFocused} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; import {View} from 'react-native'; -import _ from 'underscore'; +import type {LayoutChangeEvent, SectionList as RNSectionList, TextInput as RNTextInput, SectionListRenderItemInfo} from 'react-native'; import ArrowKeyFocusManager from '@components/ArrowKeyFocusManager'; import Button from '@components/Button'; import Checkbox from '@components/Checkbox'; @@ -13,69 +13,60 @@ import SafeAreaConsumer from '@components/SafeAreaConsumer'; import SectionList from '@components/SectionList'; import Text from '@components/Text'; import TextInput from '@components/TextInput'; -import withKeyboardState, {keyboardStatePropTypes} from '@components/withKeyboardState'; import useActiveElementRole from '@hooks/useActiveElementRole'; import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; import useLocalize from '@hooks/useLocalize'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import BaseListItem from './BaseListItem'; -import {propTypes as selectionListPropTypes} from './selectionListPropTypes'; - -const propTypes = { - ...keyboardStatePropTypes, - ...selectionListPropTypes, -}; - -function BaseSelectionList({ - sections, - canSelectMultiple = false, - onSelectRow, - onSelectAll, - onDismissError, - textInputLabel = '', - textInputPlaceholder = '', - textInputValue = '', - textInputHint = '', - textInputMaxLength, - inputMode = CONST.INPUT_MODE.TEXT, - onChangeText, - initiallyFocusedOptionKey = '', - onScroll, - onScrollBeginDrag, - headerMessage = '', - confirmButtonText = '', - onConfirm, - headerContent, - footerContent, - showScrollIndicator = false, - showLoadingPlaceholder = false, - showConfirmButton = false, - shouldPreventDefaultFocusOnSelectRow = false, - isKeyboardShown = false, - containerStyle = [], - disableInitialFocusOptionStyle = false, - inputRef = null, - disableKeyboardShortcuts = false, - children, - shouldStopPropagation = false, - shouldShowTooltips = true, - shouldUseDynamicMaxToRenderPerBatch = false, - rightHandSideComponent, -}) { - const theme = useTheme(); +import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, RadioItem, Section, SectionListDataType, User} from './types'; + +function BaseSelectionList( + { + sections, + canSelectMultiple = false, + onSelectRow, + onSelectAll, + onDismissError, + textInputLabel = '', + textInputPlaceholder = '', + textInputValue = '', + textInputHint, + textInputMaxLength, + inputMode = CONST.INPUT_MODE.TEXT, + onChangeText, + initiallyFocusedOptionKey = '', + onScroll, + onScrollBeginDrag, + headerMessage = '', + confirmButtonText = '', + onConfirm = () => {}, + headerContent, + footerContent, + showScrollIndicator = false, + showLoadingPlaceholder = false, + showConfirmButton = false, + shouldPreventDefaultFocusOnSelectRow = false, + containerStyle, + isKeyboardShown = false, + disableKeyboardShortcuts = false, + children, + shouldStopPropagation = false, + shouldShowTooltips = true, + shouldUseDynamicMaxToRenderPerBatch = false, + rightHandSideComponent, + }: BaseSelectionListProps, + inputRef: ForwardedRef, +) { const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); - const listRef = useRef(null); - const textInputRef = useRef(null); - const focusTimeoutRef = useRef(null); - const shouldShowTextInput = Boolean(textInputLabel); - const shouldShowSelectAll = Boolean(onSelectAll); + const listRef = useRef>>(null); + const textInputRef = useRef(null); + const focusTimeoutRef = useRef(null); + const shouldShowTextInput = !!textInputLabel; + const shouldShowSelectAll = !!onSelectAll; const activeElementRole = useActiveElementRole(); const isFocused = useIsFocused(); const [maxToRenderPerBatch, setMaxToRenderPerBatch] = useState(shouldUseDynamicMaxToRenderPerBatch ? 0 : CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); @@ -87,26 +78,24 @@ function BaseSelectionList({ * - `disabledOptionsIndexes`: Contains the indexes of all the disabled items in the list, to be used by the ArrowKeyFocusManager * - `itemLayouts`: Contains the layout information for each item, header and footer in the list, * so we can calculate the position of any given item when scrolling programmatically - * - * @return {{itemLayouts: [{offset: number, length: number}], disabledOptionsIndexes: *[], allOptions: *[]}} */ - const flattenedSections = useMemo(() => { - const allOptions = []; + const flattenedSections = useMemo>(() => { + const allOptions: TItem[] = []; - const disabledOptionsIndexes = []; + const disabledOptionsIndexes: number[] = []; let disabledIndex = 0; let offset = 0; const itemLayouts = [{length: 0, offset}]; - const selectedOptions = []; + const selectedOptions: TItem[] = []; - _.each(sections, (section, sectionIndex) => { + sections.forEach((section, sectionIndex) => { const sectionHeaderHeight = variables.optionsListSectionHeaderHeight; itemLayouts.push({length: sectionHeaderHeight, offset}); offset += sectionHeaderHeight; - _.each(section.data, (item, optionIndex) => { + section.data?.forEach((item, optionIndex) => { // Add item to the general flattened array allOptions.push({ ...item, @@ -115,7 +104,7 @@ function BaseSelectionList({ }); // If disabled, add to the disabled indexes array - if (section.isDisabled || item.isDisabled) { + if (!!section.isDisabled || item.isDisabled) { disabledOptionsIndexes.push(disabledIndex); } disabledIndex += 1; @@ -155,34 +144,34 @@ function BaseSelectionList({ }, [canSelectMultiple, sections]); // If `initiallyFocusedOptionKey` is not passed, we fall back to `-1`, to avoid showing the highlight on the first member - const [focusedIndex, setFocusedIndex] = useState(() => _.findIndex(flattenedSections.allOptions, (option) => option.keyForList === initiallyFocusedOptionKey)); + const [focusedIndex, setFocusedIndex] = useState(() => flattenedSections.allOptions.findIndex((option) => option.keyForList === initiallyFocusedOptionKey)); // Disable `Enter` shortcut if the active element is a button or checkbox - const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole); + const disableEnterShortcut = activeElementRole && [CONST.ROLE.BUTTON, CONST.ROLE.CHECKBOX].includes(activeElementRole as ButtonOrCheckBoxRoles); /** * Scrolls to the desired item index in the section list * - * @param {Number} index - the index of the item to scroll to - * @param {Boolean} animated - whether to animate the scroll + * @param index - the index of the item to scroll to + * @param animated - whether to animate the scroll */ const scrollToIndex = useCallback( - (index, animated = true) => { + (index: number, animated = true) => { const item = flattenedSections.allOptions[index]; if (!listRef.current || !item) { return; } - const itemIndex = item.index; - const sectionIndex = item.sectionIndex; + const itemIndex = item.index ?? -1; + const sectionIndex = item.sectionIndex ?? -1; // Note: react-native's SectionList automatically strips out any empty sections. // So we need to reduce the sectionIndex to remove any empty sections in front of the one we're trying to scroll to. // Otherwise, it will cause an index-out-of-bounds error and crash the app. let adjustedSectionIndex = sectionIndex; for (let i = 0; i < sectionIndex; i++) { - if (_.isEmpty(lodashGet(sections, `[${i}].data`))) { + if (sections[i].data) { adjustedSectionIndex--; } } @@ -197,10 +186,10 @@ function BaseSelectionList({ /** * Logic to run when a row is selected, either with click/press or keyboard hotkeys. * - * @param {Object} item - the list item - * @param {Boolean} shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard) + * @param item - the list item + * @param shouldUnfocusRow - flag to decide if we should unfocus all rows. True when selecting a row with click or press (not keyboard) */ - const selectRow = (item, shouldUnfocusRow = false) => { + const selectRow = (item: TItem, shouldUnfocusRow = false) => { // In single-selection lists we don't care about updating the focused index, because the list is closed after selecting an item if (canSelectMultiple) { if (sections.length > 1) { @@ -233,15 +222,15 @@ function BaseSelectionList({ }; const selectAllRow = () => { - onSelectAll(); + onSelectAll?.(); + if (shouldShowTextInput && shouldPreventDefaultFocusOnSelectRow && textInputRef.current) { textInputRef.current.focus(); } }; - const selectFocusedOption = (e) => { - const focusedItemKey = lodashGet(e, ['target', 'attributes', 'id', 'value']); - const focusedOption = focusedItemKey ? _.find(flattenedSections.allOptions, (option) => option.keyForList === focusedItemKey) : flattenedSections.allOptions[focusedIndex]; + const selectFocusedOption = () => { + const focusedOption = flattenedSections.allOptions[focusedIndex]; if (!focusedOption || focusedOption.isDisabled) { return; @@ -254,8 +243,8 @@ function BaseSelectionList({ * This function is used to compute the layout of any given item in our list. * We need to implement it so that we can programmatically scroll to items outside the virtual render window of the SectionList. * - * @param {Array} data - This is the same as the data we pass into the component - * @param {Number} flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: + * @param data - This is the same as the data we pass into the component + * @param flatDataArrayIndex - This index is provided by React Native, and refers to a flat array with data from all the sections. This flat array has some quirks: * * 1. It ALWAYS includes a list header and a list footer, even if we don't provide/render those. * 2. Each section includes a header, even if we don't provide/render one. @@ -263,10 +252,8 @@ function BaseSelectionList({ * For example, given a list with two sections, two items in each section, no header, no footer, and no section headers, the flat array might look something like this: * * [{header}, {sectionHeader}, {item}, {item}, {sectionHeader}, {item}, {item}, {footer}] - * - * @returns {Object} */ - const getItemLayout = (data, flatDataArrayIndex) => { + const getItemLayout = (data: Array> | null, flatDataArrayIndex: number) => { const targetItem = flattenedSections.itemLayouts[flatDataArrayIndex]; if (!targetItem) { @@ -284,8 +271,8 @@ function BaseSelectionList({ }; }; - const renderSectionHeader = ({section}) => { - if (!section.title || _.isEmpty(section.data)) { + const renderSectionHeader = ({section}: {section: SectionListDataType}) => { + if (!section.title || !section.data) { return null; } @@ -300,9 +287,10 @@ function BaseSelectionList({ ); }; - const renderItem = ({item, index, section}) => { - const normalizedIndex = index + lodashGet(section, 'indexOffset', 0); - const isDisabled = section.isDisabled || item.isDisabled; + const renderItem = ({item, index, section}: SectionListRenderItemInfo>) => { + const indexOffset = section.indexOffset ? section.indexOffset : 0; + const normalizedIndex = index + indexOffset; + const isDisabled = !!section.isDisabled || item.isDisabled; const isItemFocused = !isDisabled && focusedIndex === normalizedIndex; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. const showTooltip = shouldShowTooltips && normalizedIndex < 10; @@ -312,11 +300,9 @@ function BaseSelectionList({ item={item} isFocused={isItemFocused} isDisabled={isDisabled} - isHide={!maxToRenderPerBatch} showTooltip={showTooltip} canSelectMultiple={canSelectMultiple} onSelectRow={() => selectRow(item, true)} - disableIsFocusStyle={disableInitialFocusOptionStyle} onDismissError={onDismissError} shouldPreventDefaultFocusOnSelectRow={shouldPreventDefaultFocusOnSelectRow} rightHandSideComponent={rightHandSideComponent} @@ -326,11 +312,10 @@ function BaseSelectionList({ }; const scrollToFocusedIndexOnFirstRender = useCallback( - ({nativeEvent}) => { + (nativeEvent: LayoutChangeEvent) => { if (shouldUseDynamicMaxToRenderPerBatch) { - const listHeight = lodashGet(nativeEvent, 'layout.height', 0); - const itemHeight = lodashGet(nativeEvent, 'layout.y', 0); - + const listHeight = nativeEvent.nativeEvent.layout.height; + const itemHeight = nativeEvent.nativeEvent.layout.y; setMaxToRenderPerBatch((Math.ceil(listHeight / itemHeight) || 0) + CONST.MAX_TO_RENDER_PER_BATCH.DEFAULT); } @@ -344,7 +329,7 @@ function BaseSelectionList({ ); const updateAndScrollToFocusedIndex = useCallback( - (newFocusedIndex) => { + (newFocusedIndex: number) => { setFocusedIndex(newFocusedIndex); scrollToIndex(newFocusedIndex, true); }, @@ -355,7 +340,12 @@ function BaseSelectionList({ useFocusEffect( useCallback(() => { if (shouldShowTextInput) { - focusTimeoutRef.current = setTimeout(() => textInputRef.current.focus(), CONST.ANIMATED_TRANSITION); + focusTimeoutRef.current = setTimeout(() => { + if (!textInputRef.current) { + return; + } + textInputRef.current.focus(); + }, CONST.ANIMATED_TRANSITION); } return () => { if (!focusTimeoutRef.current) { @@ -382,7 +372,7 @@ function BaseSelectionList({ /** Selects row when pressing Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ENTER, selectFocusedOption, { captureOnInputs: true, - shouldBubble: () => !flattenedSections.allOptions[focusedIndex], + shouldBubble: !flattenedSections.allOptions[focusedIndex], shouldStopPropagation, isActive: !disableKeyboardShortcuts && !disableEnterShortcut && isFocused, }); @@ -390,8 +380,8 @@ function BaseSelectionList({ /** Calls confirm action when pressing CTRL (CMD) + Enter */ useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.CTRL_ENTER, onConfirm, { captureOnInputs: true, - shouldBubble: () => !flattenedSections.allOptions[focusedIndex], - isActive: !disableKeyboardShortcuts && Boolean(onConfirm) && isFocused, + shouldBubble: !flattenedSections.allOptions[focusedIndex], + isActive: !disableKeyboardShortcuts && !!onConfirm && isFocused, }); return ( @@ -401,19 +391,22 @@ function BaseSelectionList({ maxIndex={flattenedSections.allOptions.length - 1} onFocusedIndexChanged={updateAndScrollToFocusedIndex} > - {/* */} {({safeAreaPaddingBottomStyle}) => ( - + {shouldShowTextInput && ( { - if (inputRef) { - // eslint-disable-next-line no-param-reassign - inputRef.current = el; + ref={(element) => { + textInputRef.current = element as RNTextInput; + + if (!inputRef) { + return; + } + + if (typeof inputRef === 'function') { + inputRef(element as RNTextInput); } - textInputRef.current = el; }} label={textInputLabel} accessibilityLabel={textInputLabel} @@ -427,16 +420,16 @@ function BaseSelectionList({ selectTextOnFocus spellCheck={false} onSubmitEditing={selectFocusedOption} - blurOnSubmit={Boolean(flattenedSections.allOptions.length)} + blurOnSubmit={!!flattenedSections.allOptions.length} /> )} - {Boolean(headerMessage) && ( + {!!headerMessage && ( {headerMessage} )} - {Boolean(headerContent) && headerContent} + {!!headerContent && headerContent} {flattenedSections.allOptions.length === 0 && showLoadingPlaceholder ? ( ) : ( @@ -474,7 +467,7 @@ function BaseSelectionList({ onScrollBeginDrag={onScrollBeginDrag} keyExtractor={(item) => item.keyForList} extraData={focusedIndex} - indicatorStyle={theme.white} + indicatorStyle="white" keyboardShouldPersistTaps="always" showsVerticalScrollIndicator={showScrollIndicator} initialNumToRender={12} @@ -500,7 +493,7 @@ function BaseSelectionList({ /> )} - {Boolean(footerContent) && {footerContent}} + {!!footerContent && {footerContent}} )} @@ -509,6 +502,5 @@ function BaseSelectionList({ } BaseSelectionList.displayName = 'BaseSelectionList'; -BaseSelectionList.propTypes = propTypes; -export default withKeyboardState(BaseSelectionList); +export default forwardRef(BaseSelectionList); diff --git a/src/components/SelectionList/RadioListItem.js b/src/components/SelectionList/RadioListItem.tsx similarity index 87% rename from src/components/SelectionList/RadioListItem.js rename to src/components/SelectionList/RadioListItem.tsx index 2de0c96932ea..769eaa80df4b 100644 --- a/src/components/SelectionList/RadioListItem.js +++ b/src/components/SelectionList/RadioListItem.tsx @@ -3,10 +3,11 @@ import {View} from 'react-native'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import useThemeStyles from '@hooks/useThemeStyles'; -import {radioListItemPropTypes} from './selectionListPropTypes'; +import type {RadioListItemProps} from './types'; -function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}) { +function RadioListItem({item, showTooltip, textStyles, alternateTextStyles}: RadioListItemProps) { const styles = useThemeStyles(); + return ( - {Boolean(item.alternateText) && ( + {!!item.alternateText && ( - {Boolean(item.icons) && ( + {!!item.icons && ( )} @@ -26,19 +23,19 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style text={item.text} > {item.text} - {Boolean(item.alternateText) && ( + {!!item.alternateText && ( {item.alternateText} @@ -46,12 +43,11 @@ function UserListItem({item, textStyles, alternateTextStyles, showTooltip, style )} - {Boolean(item.rightElement) && item.rightElement} + {!!item.rightElement && item.rightElement} ); } UserListItem.displayName = 'UserListItem'; -UserListItem.propTypes = userListItemPropTypes; export default UserListItem; diff --git a/src/components/SelectionList/index.android.js b/src/components/SelectionList/index.android.js deleted file mode 100644 index 53d5b6bbce06..000000000000 --- a/src/components/SelectionList/index.android.js +++ /dev/null @@ -1,17 +0,0 @@ -import React, {forwardRef} from 'react'; -import {Keyboard} from 'react-native'; -import BaseSelectionList from './BaseSelectionList'; - -const SelectionList = forwardRef((props, ref) => ( - Keyboard.dismiss()} - /> -)); - -SelectionList.displayName = 'SelectionList'; - -export default SelectionList; diff --git a/src/components/SelectionList/index.android.tsx b/src/components/SelectionList/index.android.tsx new file mode 100644 index 000000000000..8487c6e2cc67 --- /dev/null +++ b/src/components/SelectionList/index.android.tsx @@ -0,0 +1,22 @@ +import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; +import {Keyboard} from 'react-native'; +import type {TextInput} from 'react-native'; +import BaseSelectionList from './BaseSelectionList'; +import type {BaseSelectionListProps, RadioItem, User} from './types'; + +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { + return ( + Keyboard.dismiss()} + /> + ); +} + +SelectionList.displayName = 'SelectionList'; + +export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/index.ios.js b/src/components/SelectionList/index.ios.js deleted file mode 100644 index 7f2a282aeb89..000000000000 --- a/src/components/SelectionList/index.ios.js +++ /dev/null @@ -1,16 +0,0 @@ -import React, {forwardRef} from 'react'; -import {Keyboard} from 'react-native'; -import BaseSelectionList from './BaseSelectionList'; - -const SelectionList = forwardRef((props, ref) => ( - Keyboard.dismiss()} - /> -)); - -SelectionList.displayName = 'SelectionList'; - -export default SelectionList; diff --git a/src/components/SelectionList/index.ios.tsx b/src/components/SelectionList/index.ios.tsx new file mode 100644 index 000000000000..9c32d38314e2 --- /dev/null +++ b/src/components/SelectionList/index.ios.tsx @@ -0,0 +1,21 @@ +import React, {forwardRef} from 'react'; +import type {ForwardedRef} from 'react'; +import {Keyboard} from 'react-native'; +import type {TextInput} from 'react-native'; +import BaseSelectionList from './BaseSelectionList'; +import type {BaseSelectionListProps, RadioItem, User} from './types'; + +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { + return ( + + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + ref={ref} + onScrollBeginDrag={() => Keyboard.dismiss()} + /> + ); +} + +SelectionList.displayName = 'SelectionList'; + +export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/index.js b/src/components/SelectionList/index.tsx similarity index 82% rename from src/components/SelectionList/index.js rename to src/components/SelectionList/index.tsx index 24ea60d29be5..93754926cacb 100644 --- a/src/components/SelectionList/index.js +++ b/src/components/SelectionList/index.tsx @@ -1,9 +1,12 @@ import React, {forwardRef, useEffect, useState} from 'react'; +import type {ForwardedRef} from 'react'; import {Keyboard} from 'react-native'; +import type {TextInput} from 'react-native'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import BaseSelectionList from './BaseSelectionList'; +import type {BaseSelectionListProps, RadioItem, User} from './types'; -const SelectionList = forwardRef((props, ref) => { +function SelectionList(props: BaseSelectionListProps, ref: ForwardedRef) { const [isScreenTouched, setIsScreenTouched] = useState(false); const touchStart = () => setIsScreenTouched(true); @@ -39,8 +42,8 @@ const SelectionList = forwardRef((props, ref) => { }} /> ); -}); +} SelectionList.displayName = 'SelectionList'; -export default SelectionList; +export default forwardRef(SelectionList); diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts new file mode 100644 index 000000000000..72b3510909b8 --- /dev/null +++ b/src/components/SelectionList/types.ts @@ -0,0 +1,266 @@ +import type {ReactElement, ReactNode} from 'react'; +import type {GestureResponderEvent, InputModeOptions, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native'; +import type {SubAvatar} from '@components/SubscriptAvatar'; +import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; + +type CommonListItemProps = { + /** Whether this item is focused (for arrow key controls) */ + isFocused?: boolean; + + /** Style to be applied to Text */ + textStyles?: StyleProp; + + /** Style to be applied on the alternate text */ + alternateTextStyles?: StyleProp; + + /** Whether this item is disabled */ + isDisabled?: boolean; + + /** Whether this item should show Tooltip */ + showTooltip: boolean; + + /** Whether to use the Checkbox (multiple selection) instead of the Checkmark (single selection) */ + canSelectMultiple?: boolean; + + /** Callback to fire when the item is pressed */ + onSelectRow: (item: TItem) => void; + + /** Callback to fire when an error is dismissed */ + onDismissError?: (item: TItem) => void; + + /** Component to display on the right side */ + rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; +}; + +type User = { + /** Text to display */ + text: string; + + /** Alternate text to display */ + alternateText?: string; + + /** Key used internally by React */ + keyForList: string; + + /** Whether this option is selected */ + isSelected?: boolean; + + /** Whether this option is disabled for selection */ + isDisabled?: boolean; + + /** User accountID */ + accountID?: number; + + /** User login */ + login?: string; + + /** Element to show on the right side of the item */ + rightElement?: ReactElement; + + /** Icons for the user (can be multiple if it's a Workspace) */ + icons?: SubAvatar[]; + + /** Errors that this user may contain */ + errors?: Errors; + + /** The type of action that's pending */ + pendingAction?: PendingAction; + + invitedSecondaryLogin?: string; + + /** Represents the index of the section it came from */ + sectionIndex?: number; + + /** Represents the index of the option within the section it came from */ + index?: number; +}; + +type UserListItemProps = CommonListItemProps & { + /** The section list item */ + item: User; + + /** Additional styles to apply to text */ + style?: StyleProp; +}; + +type RadioItem = { + /** Text to display */ + text: string; + + /** Alternate text to display */ + alternateText?: string; + + /** Key used internally by React */ + keyForList: string; + + /** Whether this option is selected */ + isSelected?: boolean; + + /** Whether this option is disabled for selection */ + isDisabled?: boolean; + + /** Represents the index of the section it came from */ + sectionIndex?: number; + + /** Represents the index of the option within the section it came from */ + index?: number; +}; + +type RadioListItemProps = CommonListItemProps & { + /** The section list item */ + item: RadioItem; +}; + +type BaseListItemProps = CommonListItemProps & { + item: TItem; + shouldPreventDefaultFocusOnSelectRow?: boolean; + keyForList?: string; +}; + +type Section = { + /** Title of the section */ + title?: string; + + /** The initial index of this section given the total number of options in each section's data array */ + indexOffset?: number; + + /** Array of options */ + data?: TItem[]; + + /** Whether this section items disabled for selection */ + isDisabled?: boolean; +}; + +type BaseSelectionListProps = Partial & { + /** Sections for the section list */ + sections: Array>>; + + /** Whether this is a multi-select list */ + canSelectMultiple?: boolean; + + /** Callback to fire when a row is pressed */ + onSelectRow: (item: TItem) => void; + + /** Callback to fire when "Select All" checkbox is pressed. Only use along with `canSelectMultiple` */ + onSelectAll?: () => void; + + /** Callback to fire when an error is dismissed */ + onDismissError?: () => void; + + /** Label for the text input */ + textInputLabel?: string; + + /** Placeholder for the text input */ + textInputPlaceholder?: string; + + /** Hint for the text input */ + textInputHint?: string; + + /** Value for the text input */ + textInputValue?: string; + + /** Max length for the text input */ + textInputMaxLength?: number; + + /** Callback to fire when the text input changes */ + onChangeText?: (text: string) => void; + + /** Input mode for the text input */ + inputMode?: InputModeOptions; + + /** Item `keyForList` to focus initially */ + initiallyFocusedOptionKey?: string; + + /** Callback to fire when the list is scrolled */ + onScroll?: () => void; + + /** Callback to fire when the list is scrolled and the user begins dragging */ + onScrollBeginDrag?: () => void; + + /** Message to display at the top of the list */ + headerMessage?: string; + + /** Text to display on the confirm button */ + confirmButtonText?: string; + + /** Callback to fire when the confirm button is pressed */ + onConfirm?: (e?: GestureResponderEvent | KeyboardEvent | undefined) => void; + + /** Whether to show the vertical scroll indicator */ + showScrollIndicator?: boolean; + + /** Whether to show the loading placeholder */ + showLoadingPlaceholder?: boolean; + + /** Whether to show the default confirm button */ + showConfirmButton?: boolean; + + /** Whether tooltips should be shown */ + shouldShowTooltips?: boolean; + + /** Whether to stop automatic form submission on pressing enter key or not */ + shouldStopPropagation?: boolean; + + /** Whether to prevent default focusing of options and focus the textinput when selecting an option */ + shouldPreventDefaultFocusOnSelectRow?: boolean; + + /** Custom content to display in the header */ + headerContent?: ReactNode; + + /** Custom content to display in the footer */ + footerContent?: ReactNode; + + /** Whether to use dynamic maxToRenderPerBatch depending on the visible number of elements */ + shouldUseDynamicMaxToRenderPerBatch?: boolean; + + /** Whether keyboard shortcuts should be disabled */ + disableKeyboardShortcuts?: boolean; + + /** Whether to disable initial styling for focused option */ + disableInitialFocusOptionStyle?: boolean; + + /** Styles to apply to SelectionList container */ + containerStyle?: ViewStyle; + + /** Whether keyboard is visible on the screen */ + isKeyboardShown?: boolean; + + /** Whether focus event should be delayed */ + shouldDelayFocus?: boolean; + + /** Component to display on the right side of each child */ + rightHandSideComponent?: ((item: TItem) => ReactElement) | ReactElement | null; +}; + +type ItemLayout = { + length: number; + offset: number; +}; + +type FlattenedSectionsReturn = { + allOptions: TItem[]; + selectedOptions: TItem[]; + disabledOptionsIndexes: number[]; + itemLayouts: ItemLayout[]; + allSelected: boolean; +}; + +type ButtonOrCheckBoxRoles = 'button' | 'checkbox'; + +type SectionListDataType = SectionListData>; + +export type { + BaseSelectionListProps, + CommonListItemProps, + UserListItemProps, + Section, + RadioListItemProps, + BaseListItemProps, + User, + RadioItem, + FlattenedSectionsReturn, + ItemLayout, + ButtonOrCheckBoxRoles, + SectionListDataType, +}; diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 00cf248ad838..2e2ae6d06e0f 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -104,3 +104,4 @@ function SubscriptAvatar({mainAvatar = {}, secondaryAvatar = {}, size = CONST.AV SubscriptAvatar.displayName = 'SubscriptAvatar'; export default memo(SubscriptAvatar); +export type {SubAvatar}; From 15f349a6aa4bb207b7391cbf08b726e201149c7c Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Fri, 19 Jan 2024 12:18:19 +0100 Subject: [PATCH 085/170] Fix label error --- src/components/SelectionList/BaseSelectionList.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseSelectionList.tsx b/src/components/SelectionList/BaseSelectionList.tsx index 26a206c227ec..d97c47c84ee7 100644 --- a/src/components/SelectionList/BaseSelectionList.tsx +++ b/src/components/SelectionList/BaseSelectionList.tsx @@ -20,6 +20,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import BaseListItem from './BaseListItem'; import type {BaseSelectionListProps, ButtonOrCheckBoxRoles, FlattenedSectionsReturn, RadioItem, Section, SectionListDataType, User} from './types'; @@ -272,7 +273,7 @@ function BaseSelectionList( }; const renderSectionHeader = ({section}: {section: SectionListDataType}) => { - if (!section.title || !section.data) { + if (!section.title || isEmptyObject(section.data)) { return null; } From 9e0c27acd2a6484d7094a30f1a465635211fdbea Mon Sep 17 00:00:00 2001 From: Aldo Canepa Garay <87341702+aldo-expensify@users.noreply.github.com> Date: Fri, 19 Jan 2024 11:42:42 -0300 Subject: [PATCH 086/170] Fix comment --- 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 722d9198751e..0cf427cb2f0c 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -1169,7 +1169,7 @@ function updateMoneyRequestMerchant(transactionID, transactionThreadReportID, va } /** - * Updates the tag date of a money request + * Updates the tag of a money request * * @param {String} transactionID * @param {Number} transactionThreadReportID From 3bd59508671e60b0496f754cb935a95484ecc96e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 22:43:23 +0700 Subject: [PATCH 087/170] add comment --- src/pages/home/report/ReportActionItem.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 55b294936f49..60503424f663 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -346,6 +346,8 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( Date: Fri, 19 Jan 2024 23:47:38 +0700 Subject: [PATCH 088/170] update constant value --- src/components/EmojiPicker/EmojiPickerButton.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/components/EmojiPicker/EmojiPickerButton.js b/src/components/EmojiPicker/EmojiPickerButton.js index b056ccb22875..832715e3214c 100644 --- a/src/components/EmojiPicker/EmojiPickerButton.js +++ b/src/components/EmojiPicker/EmojiPickerButton.js @@ -11,6 +11,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import getButtonState from '@libs/getButtonState'; import * as EmojiPickerAction from '@userActions/EmojiPickerAction'; +import CONST from '@src/CONST'; const propTypes = { /** Flag to disable the emoji picker button */ @@ -58,8 +59,8 @@ function EmojiPickerButton(props) { props.onEmojiSelected, emojiPopoverAnchor, { - horizontal: 'right', - vertical: 'bottom', + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, shiftVertical: props.shiftVertical, }, () => {}, From 10b518f42ff588243009076b83d0e0cea8c6d46d Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Fri, 19 Jan 2024 20:00:32 +0300 Subject: [PATCH 089/170] minor fix --- src/hooks/useCallbackRef.ts | 13 ------------- src/pages/home/report/ReportActionsList.js | 11 ++++------- 2 files changed, 4 insertions(+), 20 deletions(-) delete mode 100644 src/hooks/useCallbackRef.ts diff --git a/src/hooks/useCallbackRef.ts b/src/hooks/useCallbackRef.ts deleted file mode 100644 index 075cbe08bbbc..000000000000 --- a/src/hooks/useCallbackRef.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {useEffect, useMemo, useRef} from 'react'; - -const useCallbackRef = unknown>(callback: T): T => { - const calbackRef = useRef(callback); - - useEffect(() => { - calbackRef.current = callback; - }); - - return useMemo(() => ((...args) => calbackRef.current(...args)) as T, []); -}; - -export default useCallbackRef; diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 30888f47beba..2c3032a4986b 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -9,7 +9,6 @@ import InvertedFlatList from '@components/InvertedFlatList'; import {withPersonalDetails} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; -import useCallbackRef from '@hooks/useCallbackRef'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useReportScrollManager from '@hooks/useReportScrollManager'; @@ -412,7 +411,7 @@ function ReportActionsList({ calculateUnreadMarker(); }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); - const visibilityCallback = useCallbackRef(() => { + const onVisibilityChange = useCallback(() => { if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report) || messageManuallyMarkedUnread) { return; } @@ -422,15 +421,13 @@ function ReportActionsList({ setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); - }); + }, [calculateUnreadMarker, messageManuallyMarkedUnread, report]); useEffect(() => { - const unsubscribeVisibilityListener = Visibility.onVisibilityChange(visibilityCallback); + const unsubscribeVisibilityListener = Visibility.onVisibilityChange(onVisibilityChange); return unsubscribeVisibilityListener; - - /* eslint-disable-next-line react-hooks/exhaustive-deps */ - }, [report.reportID]); + }, [onVisibilityChange]); const renderItem = useCallback( ({item: reportAction, index}) => ( From d49c487c24e839fe65f47a17078a30c688d9a351 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Fri, 19 Jan 2024 20:12:19 +0300 Subject: [PATCH 090/170] minor change --- src/pages/home/report/ReportActionsList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 2c3032a4986b..5cee5a77ea46 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -412,7 +412,7 @@ function ReportActionsList({ }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); const onVisibilityChange = useCallback(() => { - if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report) || messageManuallyMarkedUnread) { + if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report)) { return; } @@ -421,7 +421,7 @@ function ReportActionsList({ setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); - }, [calculateUnreadMarker, messageManuallyMarkedUnread, report]); + }, [calculateUnreadMarker, report]); useEffect(() => { const unsubscribeVisibilityListener = Visibility.onVisibilityChange(onVisibilityChange); From 0fe26e25a02f27b6c68d012caf7fe097540943f3 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Fri, 19 Jan 2024 21:19:39 +0300 Subject: [PATCH 091/170] updated according to comment --- src/pages/home/HeaderView.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/HeaderView.js b/src/pages/home/HeaderView.js index 3f467523ca2a..660d19e8871d 100644 --- a/src/pages/home/HeaderView.js +++ b/src/pages/home/HeaderView.js @@ -92,7 +92,7 @@ function HeaderView(props) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); - const participants = ReportUtils.isGroupChat(props.report) ? ReportUtils.getVisibleMemberIDs(props.report) : lodashGet(props.report, 'participantAccountIDs', []); + const participants = ReportUtils.getParticipantsIDs(props.report); const participantPersonalDetails = OptionsListUtils.getPersonalDetailsForAccountIDs(participants, props.personalDetails); const isMultipleParticipant = participants.length > 1; const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips(participantPersonalDetails, isMultipleParticipant); From 5f81b8d58b435bcbe8de9daf995ea893eb37116b Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 19 Jan 2024 11:28:46 -0800 Subject: [PATCH 092/170] Include isUnreadWithMention in selector --- src/pages/home/sidebar/SidebarLinksData.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index d29420f182f5..4d98e865f4cd 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -14,6 +14,7 @@ import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; +import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import reportPropTypes from '@pages/reportPropTypes'; import CONST from '@src/CONST'; @@ -202,6 +203,7 @@ const chatReportSelector = (report) => parentReportActionID: report.parentReportActionID, parentReportID: report.parentReportID, isDeletedParentAction: report.isDeletedParentAction, + isUnreadWithMention: ReportUtils.isUnreadWithMention(report), }; /** From 7faf409807187502c6ffe1c593f72d9fdbb350fd Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Sun, 21 Jan 2024 23:48:14 +0300 Subject: [PATCH 093/170] updated to run our effect only when there is new message received --- src/pages/home/report/ReportActionsList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 5cee5a77ea46..17ea4c99ec88 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -412,7 +412,7 @@ function ReportActionsList({ }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); const onVisibilityChange = useCallback(() => { - if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !ReportUtils.isUnread(report)) { + if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || userActiveSince.current > (report.lastVisibleActionCreated || '')) { return; } From 1a022dafedd9c745eadd87bfa44cd34c580ddd80 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 22 Jan 2024 00:13:58 +0300 Subject: [PATCH 094/170] refined logic --- src/pages/home/report/ReportActionsList.js | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 17ea4c99ec88..66b0decedb42 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -143,6 +143,7 @@ function ReportActionsList({ const route = useRoute(); const opacity = useSharedValue(0); const userActiveSince = useRef(null); + const userInactiveSince = useRef(null); const markerInit = () => { if (!cacheUnreadMarkers.has(report.reportID)) { @@ -412,7 +413,23 @@ function ReportActionsList({ }, [calculateUnreadMarker, report.lastReadTime, messageManuallyMarkedUnread]); const onVisibilityChange = useCallback(() => { - if (!Visibility.isVisible() || scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || userActiveSince.current > (report.lastVisibleActionCreated || '')) { + if (!Visibility.isVisible()) { + userInactiveSince.current = DateUtils.getDBTime(); + return; + } + + if ( + scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || + !( + sortedVisibleReportActions && + _.some( + sortedVisibleReportActions, + (reportAction) => + userInactiveSince.current < reportAction.created && + (ReportActionsUtils.isReportPreviewAction(reportAction) ? !reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(), + ) + ) + ) { return; } @@ -421,7 +438,7 @@ function ReportActionsList({ setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); - }, [calculateUnreadMarker, report]); + }, [calculateUnreadMarker, report, sortedVisibleReportActions]); useEffect(() => { const unsubscribeVisibilityListener = Visibility.onVisibilityChange(onVisibilityChange); From 48a38dc2307b69d0c02adca029c0a2590269d95c Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 22 Jan 2024 14:50:48 +0100 Subject: [PATCH 095/170] create getLastBusinessDayOfMonth --- src/libs/DateUtils.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 1a10eb03a00e..6e56d19a89b4 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -730,6 +730,26 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone { }; } +/** + * Returns the latest business day of input date month + * + * param {Date} inputDate + * returns {number} + */ +function getLastBusinessDayOfMonth(inputDate: Date): number { + const currentDate = new Date(inputDate); + + // Set the date to the last day of the month + currentDate.setMonth(currentDate.getMonth() + 1, 0); + + // Loop backward to find the latest business day + while (currentDate.getDay() === 0 || currentDate.getDay() === 6) { + currentDate.setDate(currentDate.getDate() - 1); + } + + return currentDate.getDate(); +} + const DateUtils = { formatToDayOfWeek, formatToLongDateWithWeekday, @@ -774,6 +794,7 @@ const DateUtils = { getWeekEndsOn, isTimeAtLeastOneMinuteInFuture, formatToSupportedTimezone, + getLastBusinessDayOfMonth, }; export default DateUtils; From e3dfa0693cdaef4f91c701b7bde7325af68ab3af Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 22 Jan 2024 14:50:52 +0100 Subject: [PATCH 096/170] test getLastBusinessDayOfMonth --- tests/unit/DateUtilsTest.js | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js index 7480da456d7f..17b25f24e327 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.js @@ -213,4 +213,30 @@ describe('DateUtils', () => { }); }); }); + + describe('getLastBusinessDayOfMonth', () => { + const scenarios = [ + { + // Last business of May in 2025 + inputDate: new Date(2025, 4), + expectedResult: 30, + }, + { + // Last business of January in 2024 + inputDate: new Date(2024, 0), + expectedResult: 31, + }, + { + // Last business of September in 2023 + inputDate: new Date(2023, 8), + expectedResult: 29, + }, + ]; + + test.each(scenarios)('returns a last business day of an input date', ({inputDate, expectedResult}) => { + const lastBusinessDay = DateUtils.getLastBusinessDayOfMonth(inputDate); + + expect(lastBusinessDay).toEqual(expectedResult); + }); + }); }); From 698dbd9095901a0f87acdfc6df86e0690a14a43f Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Mon, 22 Jan 2024 19:07:54 +0500 Subject: [PATCH 097/170] feat: allow policy admins to edit the report fields of a non-settled report --- .../ReportActionItem/MoneyReportView.tsx | 29 ++++++++++++++----- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index 4fcca3e518a5..7e0ff9487232 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -1,6 +1,8 @@ import React, {useMemo} from 'react'; import type {StyleProp, TextStyle} from 'react-native'; import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import MenuItemWithTopDescription from '@components/MenuItemWithTopDescription'; @@ -17,9 +19,10 @@ import * as CurrencyUtils from '@libs/CurrencyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; -import type {PolicyReportField, Report} from '@src/types/onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; -type MoneyReportViewProps = { +type MoneyReportViewComponentProps = { /** The report currently being looked at */ report: Report; @@ -30,7 +33,14 @@ type MoneyReportViewProps = { shouldShowHorizontalRule: boolean; }; -function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportViewProps) { +type MoneyReportViewOnyxProps = { + /** Policies that the user is part of */ + policies: OnyxCollection; +}; + +type MoneyReportViewProps = MoneyReportViewComponentProps & MoneyReportViewOnyxProps; + +function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule, policies}: MoneyReportViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); @@ -57,7 +67,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: () => policyReportFields.sort(({orderWeight: firstOrderWeight}, {orderWeight: secondOrderWeight}) => firstOrderWeight - secondOrderWeight), [policyReportFields], ); - + const isAdmin = ReportUtils.isPolicyAdmin(report.policyID ?? '', policies); return ( @@ -65,6 +75,7 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: {canUseReportFields && sortedPolicyReportFields.map((reportField) => { const title = ReportUtils.getReportFieldTitle(report, reportField); + const isDisabled = !isAdmin || isSettled; return ( {}} - shouldShowRightIcon - disabled={false} + shouldShowRightIcon={!isDisabled} + disabled={isDisabled} wrapperStyle={[styles.pv2, styles.taskDescriptionMenuItem]} shouldGreyOutWhenDisabled={false} numberOfLinesTitle={0} @@ -165,4 +176,8 @@ function MoneyReportView({report, policyReportFields, shouldShowHorizontalRule}: MoneyReportView.displayName = 'MoneyReportView'; -export default MoneyReportView; +export default withOnyx({ + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + }, +})(MoneyReportView); From 11e55f61c795266788f41d8e665d06a04d3916f7 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Mon, 22 Jan 2024 15:11:01 +0100 Subject: [PATCH 098/170] Fix some translations --- src/languages/es.ts | 64 ++++++++++++++++++++++----------------------- 1 file changed, 32 insertions(+), 32 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 2478c8ba8bd2..6730951b885a 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -441,10 +441,10 @@ export default { copyEmailToClipboard: 'Copiar email al portapapeles', markAsUnread: 'Marcar como no leído', markAsRead: 'Marcar como leído', - editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`, - deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`, + editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, + deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, deleteConfirmation: ({action}: DeleteConfirmationParams) => - `¿Estás seguro de que quieres eliminar este ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'pedido' : 'comentario'}`, + `¿Estás seguro de que quieres eliminar esta ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, onlyVisible: 'Visible sólo para', replyInThread: 'Responder en el hilo', joinThread: 'Unirse al hilo', @@ -459,16 +459,16 @@ export default { reportActionsView: { beginningOfArchivedRoomPartOne: 'Te perdiste la fiesta en ', beginningOfArchivedRoomPartTwo: ', no hay nada que ver aquí.', - beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}: BeginningOfChatHistoryDomainRoomPartOneParams) => `Colabora aquí con todos los participantes de ${domainRoom}! 🎉\nUtiliza `, + beginningOfChatHistoryDomainRoomPartOne: ({domainRoom}: BeginningOfChatHistoryDomainRoomPartOneParams) => `¡Colabora aquí con todos los participantes de ${domainRoom}! 🎉\nUtiliza `, beginningOfChatHistoryDomainRoomPartTwo: ' para chatear con compañeros, compartir consejos o hacer una pregunta.', beginningOfChatHistoryAdminRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAdminRoomPartOneParams) => - `Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `, + `¡Este es el lugar para que los administradores de ${workspaceName} colaboren! 🎉\nUsa `, beginningOfChatHistoryAdminRoomPartTwo: ' para chatear sobre temas como la configuración del espacio de trabajo y mas.', beginningOfChatHistoryAdminOnlyPostingRoom: 'Solo los administradores pueden enviar mensajes en esta sala.', beginningOfChatHistoryAnnounceRoomPartOne: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartOneParams) => - `Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `, + `¡Este es el lugar para que todos los miembros de ${workspaceName} colaboren! 🎉\nUsa `, beginningOfChatHistoryAnnounceRoomPartTwo: ({workspaceName}: BeginningOfChatHistoryAnnounceRoomPartTwo) => ` para chatear sobre cualquier cosa relacionada con ${workspaceName}.`, - beginningOfChatHistoryUserRoomPartOne: 'Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', + beginningOfChatHistoryUserRoomPartOne: '¡Este es el lugar para colaborar! 🎉\nUsa este espacio para chatear sobre cualquier cosa relacionada con ', beginningOfChatHistoryUserRoomPartTwo: '.', beginningOfChatHistory: 'Aquí comienzan tus conversaciones con ', beginningOfChatHistoryPolicyExpenseChatPartOne: '¡La colaboración entre ', @@ -581,8 +581,8 @@ export default { transactionPendingText: 'La transacción tarda unos días en contabilizarse desde la fecha en que se utilizó la tarjeta.', requestCount: ({count, scanningReceipts = 0}: RequestCountParams) => `${count} ${Str.pluralize('solicitude', 'solicitudes', count)}${scanningReceipts > 0 ? `, ${scanningReceipts} escaneando` : ''}`, - deleteRequest: 'Eliminar pedido', - deleteConfirmation: '¿Estás seguro de que quieres eliminar este pedido?', + deleteRequest: 'Eliminar solicitud', + deleteConfirmation: '¿Estás seguro de que quieres eliminar esta solicitud?', settledExpensify: 'Pagado', settledElsewhere: 'Pagado de otra forma', settleExpensify: ({formattedAmount}: SettleExpensifyCardParams) => (formattedAmount ? `Pagar ${formattedAmount} con Expensify` : `Pagar con Expensify`), @@ -865,7 +865,7 @@ export default { }, }, passwordConfirmationScreen: { - passwordUpdated: 'Contraseña actualizada!', + passwordUpdated: '¡Contraseña actualizada!', allSet: 'Todo está listo. Guarda tu contraseña en un lugar seguro.', }, privateNotes: { @@ -922,7 +922,7 @@ export default { enableWallet: 'Habilitar Billetera', bankAccounts: 'Cuentas bancarias', addBankAccountToSendAndReceive: 'Añade una cuenta bancaria para enviar y recibir pagos directamente en la aplicación.', - addBankAccount: 'Agregar cuenta bancaria', + addBankAccount: 'Añadir cuenta bancaria', assignedCards: 'Tarjetas asignadas', assignedCardsDescription: 'Son tarjetas asignadas por un administrador del Espacio de Trabajo para gestionar los gastos de la empresa.', expensifyCard: 'Tarjeta Expensify', @@ -1211,7 +1211,7 @@ export default { }, statusPage: { status: 'Estado', - statusExplanation: 'Agrega un emoji para que tus colegas y amigos puedan saber fácilmente qué está pasando. ¡También puedes agregar un mensaje opcionalmente!', + statusExplanation: 'Añade un emoji para que tus colegas y amigos puedan saber fácilmente qué está pasando. ¡También puedes añadir un mensaje opcionalmente!', today: 'Hoy', clearStatus: 'Borrar estado', save: 'Guardar', @@ -1813,7 +1813,7 @@ export default { resultsAreLimited: 'Los resultados de búsqueda están limitados.', }, genericErrorPage: { - title: '¡Uh-oh, algo salió mal!', + title: '¡Oh-oh, algo salió mal!', body: { helpTextMobile: 'Intenta cerrar y volver a abrir la aplicación o cambiar a la', helpTextWeb: 'web.', @@ -1896,12 +1896,12 @@ export default { }, notAvailable: { title: 'Actualización no disponible', - message: 'No existe ninguna actualización disponible! Inténtalo de nuevo más tarde.', + message: '¡No existe ninguna actualización disponible! Inténtalo de nuevo más tarde.', okay: 'Vale', }, error: { title: 'Comprobación fallida', - message: 'No hemos podido comprobar si existe una actualización. Inténtalo de nuevo más tarde!', + message: 'No hemos podido comprobar si existe una actualización. ¡Inténtalo de nuevo más tarde!', }, }, report: { @@ -2419,7 +2419,7 @@ export default { }, parentReportAction: { deletedMessage: '[Mensaje eliminado]', - deletedRequest: '[Pedido eliminado]', + deletedRequest: '[Solicitud eliminada]', reversedTransaction: '[Transacción anulada]', deletedTask: '[Tarea eliminada]', hiddenMessage: '[Mensaje oculto]', @@ -2443,13 +2443,13 @@ export default { flagDescription: 'Todos los mensajes marcados se enviarán a un moderador para su revisión.', chooseAReason: 'Elige abajo un motivo para reportarlo:', spam: 'Spam', - spamDescription: 'Promoción fuera de tema no solicitada', + spamDescription: 'Publicidad no solicitada', inconsiderate: 'Desconsiderado', inconsiderateDescription: 'Frase insultante o irrespetuosa, con intenciones cuestionables', intimidation: 'Intimidación', intimidationDescription: 'Persigue agresivamente una agenda sobre objeciones válidas', bullying: 'Bullying', - bullyingDescription: 'Apunta a un individuo para obtener obediencia', + bullyingDescription: 'Se dirige a un individuo para obtener obediencia', harassment: 'Acoso', harassmentDescription: 'Comportamiento racista, misógino u otro comportamiento discriminatorio', assault: 'Agresion', @@ -2457,8 +2457,8 @@ export default { flaggedContent: 'Este mensaje ha sido marcado por violar las reglas de nuestra comunidad y el contenido se ha ocultado.', hideMessage: 'Ocultar mensaje', revealMessage: 'Revelar mensaje', - levelOneResult: 'Envia una advertencia anónima y el mensaje es reportado para revisión.', - levelTwoResult: 'Mensaje ocultado del canal, más advertencia anónima y mensaje reportado para revisión.', + levelOneResult: 'Envía una advertencia anónima y el mensaje es reportado para revisión.', + levelTwoResult: 'Mensaje ocultado en el canal, más advertencia anónima y mensaje reportado para revisión.', levelThreeResult: 'Mensaje eliminado del canal, más advertencia anónima y mensaje reportado para revisión.', }, teachersUnitePage: { @@ -2491,7 +2491,7 @@ export default { companySpend: 'Gastos de empresa', }, distance: { - addStop: 'Agregar parada', + addStop: 'Añadir parada', deleteWaypoint: 'Eliminar punto de ruta', deleteWaypointConfirmation: '¿Estás seguro de que quieres eliminar este punto de ruta?', address: 'Dirección', @@ -2565,21 +2565,21 @@ export default { allTagLevelsRequired: 'Todas las etiquetas son obligatorias', autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`, billableExpense: 'La opción facturable ya no es válida', - cashExpenseWithNoReceipt: ({amount}: ViolationsCashExpenseWithNoReceiptParams) => `Recibo obligatorio para montos mayores a ${amount}`, + cashExpenseWithNoReceipt: ({amount}: ViolationsCashExpenseWithNoReceiptParams) => `Recibo obligatorio para cantidades mayores de ${amount}`, categoryOutOfPolicy: 'La categoría ya no es válida', conversionSurcharge: ({surcharge}: ViolationsConversionSurchargeParams) => `${surcharge}% de recargo aplicado`, - customUnitOutOfPolicy: 'Unidad ya no es válida', - duplicatedTransaction: 'Potencial duplicado', + customUnitOutOfPolicy: 'La unidad ya no es válida', + duplicatedTransaction: 'Posible duplicado', fieldRequired: 'Los campos del informe son obligatorios', futureDate: 'Fecha futura no permitida', invoiceMarkup: ({invoiceMarkup}: ViolationsInvoiceMarkupParams) => `Incrementado un ${invoiceMarkup}%`, maxAge: ({maxAge}: ViolationsMaxAgeParams) => `Fecha de más de ${maxAge} días`, missingCategory: 'Falta categoría', - missingComment: 'Descripción obligatoria para categoría seleccionada', + missingComment: 'Descripción obligatoria para la categoría seleccionada', missingTag: ({tagName}: ViolationsMissingTagParams) => `Falta ${tagName}`, modifiedAmount: 'Importe superior al del recibo escaneado', modifiedDate: 'Fecha difiere del recibo escaneado', - nonExpensiworksExpense: 'Gasto no es de Expensiworks', + nonExpensiworksExpense: 'Gasto no proviene de Expensiworks', overAutoApprovalLimit: ({formattedLimitAmount}: ViolationsOverAutoApprovalLimitParams) => `Importe supera el límite de aprobación automática de ${formattedLimitAmount}`, overCategoryLimit: ({categoryLimit}: ViolationsOverCategoryLimitParams) => `Importe supera el límite para la categoría de ${categoryLimit}/persona`, overLimit: ({amount}: ViolationsOverLimitParams) => `Importe supera el límite de ${amount}/persona`, @@ -2590,22 +2590,22 @@ export default { rter: ({brokenBankConnection, isAdmin, email, isTransactionOlderThan7Days, member}: ViolationsRterParams) => { if (brokenBankConnection) { return isAdmin - ? `No se puede adjuntar recibo debido a una conexión con su banco que ${email} necesita arreglar` - : 'No se puede adjuntar recibo debido a una conexión con su banco que necesitas arreglar'; + ? `No se puede adjuntar recibo debido a un problema con la conexión a su banco que ${email} necesita arreglar` + : 'No se puede adjuntar recibo debido a un problema con la conexión a su banco que necesitas arreglar'; } if (!isTransactionOlderThan7Days) { return isAdmin - ? `Pídele a ${member} que marque la transacción como efectivo o espera 7 días e intenta de nuevo` - : 'Esperando adjuntar automáticamente a transacción de tarjeta de crédito'; + ? `Píde a ${member} que marque la transacción como efectivo o espera 7 días e inténtalo de nuevo` + : 'Esperando a adjuntar automáticamente la transacción de tarjeta de crédito'; } return ''; }, smartscanFailed: 'No se pudo escanear el recibo. Introduce los datos manualmente', someTagLevelsRequired: 'Falta etiqueta', - tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `Le etiqueta ${tagName} ya no es válida`, + tagOutOfPolicy: ({tagName}: ViolationsTagOutOfPolicyParams) => `La etiqueta ${tagName} ya no es válida`, taxAmountChanged: 'El importe del impuesto fue modificado', taxOutOfPolicy: ({taxName}: ViolationsTaxOutOfPolicyParams) => `${taxName} ya no es válido`, taxRateChanged: 'La tasa de impuesto fue modificada', - taxRequired: 'Falta tasa de impuesto', + taxRequired: 'Falta la tasa de impuesto', }, } satisfies EnglishTranslation; From 84f6980b96dc7d1d3e26984bae5070a304e9d509 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Mon, 22 Jan 2024 16:05:12 +0100 Subject: [PATCH 099/170] =?UTF-8?q?=C3=8Fmportes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/languages/es.ts | 72 ++++++++++++++++++++++----------------------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 6730951b885a..2c4070ad30ad 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -441,7 +441,7 @@ export default { copyEmailToClipboard: 'Copiar email al portapapeles', markAsUnread: 'Marcar como no leído', markAsRead: 'Marcar como leído', - editAction: ({action}: EditActionParams) => `Edit ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, + editAction: ({action}: EditActionParams) => `Editar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, deleteAction: ({action}: DeleteActionParams) => `Eliminar ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, deleteConfirmation: ({action}: DeleteConfirmationParams) => `¿Estás seguro de que quieres eliminar esta ${action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? 'solicitud' : 'comentario'}`, @@ -627,22 +627,22 @@ export default { tagSelection: ({tagName}: TagSelectionParams) => `Seleccione una ${tagName} para organizar mejor tu dinero.`, categorySelection: 'Seleccione una categoría para organizar mejor tu dinero.', error: { - invalidCategoryLength: 'El largo de la categoría escogida excede el máximo permitido (255). Por favor escoge otra categoría o acorta la categoría primero.', - invalidAmount: 'Por favor ingresa un monto válido antes de continuar.', + invalidCategoryLength: 'El largo de la categoría escogida excede el máximo permitido (255). Por favor, escoge otra categoría o acorta la categoría primero.', + invalidAmount: 'Por favor, ingresa un importe válido antes de continuar.', invalidTaxAmount: ({amount}: RequestAmountParams) => `El importe máximo del impuesto es ${amount}`, - invalidSplit: 'La suma de las partes no equivale al monto total', + invalidSplit: 'La suma de las partes no equivale al importe total', other: 'Error inesperado, por favor inténtalo más tarde', - genericCreateFailureMessage: 'Error inesperado solicitando dinero, Por favor, inténtalo más tarde', + genericCreateFailureMessage: 'Error inesperado solicitando dinero. Por favor, inténtalo más tarde', receiptFailureMessage: 'El recibo no se subió. ', saveFileMessage: 'Guarda el archivo ', loseFileMessage: 'o descarta este error y piérdelo', genericDeleteFailureMessage: 'Error inesperado eliminando la solicitud de dinero. Por favor, inténtalo más tarde', genericEditFailureMessage: 'Error inesperado al guardar la solicitud de dinero. Por favor, inténtalo más tarde', genericSmartscanFailureMessage: 'La transacción tiene campos vacíos', - duplicateWaypointsErrorMessage: 'Por favor elimina los puntos de ruta duplicados', - atLeastTwoDifferentWaypoints: 'Por favor introduce al menos dos direcciones diferentes', - splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor actualiza tu selección.', - invalidMerchant: 'Por favor ingrese un comerciante correcto.', + duplicateWaypointsErrorMessage: 'Por favor, elimina los puntos de ruta duplicados', + atLeastTwoDifferentWaypoints: 'Por favor, introduce al menos dos direcciones diferentes', + splitBillMultipleParticipantsErrorMessage: 'Solo puedes dividir una cuenta entre un único espacio de trabajo o con usuarios individuales. Por favor, actualiza tu selección.', + invalidMerchant: 'Por favor, introduce un comerciante correcto.', }, waitingOnEnabledWallet: ({submitterDisplayName}: WaitingOnBankAccountParams) => `Inició el pago, pero no se procesará hasta que ${submitterDisplayName} active su Billetera`, enableWallet: 'Habilitar Billetera', @@ -1375,35 +1375,35 @@ export default { headerTitle: 'Condiciones y tarifas', haveReadAndAgree: 'He leído y acepto recibir ', electronicDisclosures: 'divulgaciones electrónicas', - agreeToThe: 'Estoy de acuerdo con la ', - walletAgreement: 'Acuerdo de billetera', + agreeToThe: 'Estoy de acuerdo con el ', + walletAgreement: 'Acuerdo de la billetera', enablePayments: 'Habilitar pagos', - feeAmountZero: '$0', + feeAmountZero: 'US$0', monthlyFee: 'Cuota mensual', inactivity: 'Inactividad', - electronicFundsInstantFee: '1.5%', - noOverdraftOrCredit: 'Sin función de sobregiro / crédito', + electronicFundsInstantFee: '1,5%', + noOverdraftOrCredit: 'Sin función de sobregiro/crédito', electronicFundsWithdrawal: 'Retiro electrónico de fondos', standard: 'Estándar', shortTermsForm: { expensifyPaymentsAccount: ({walletProgram}: WalletProgramParams) => `La billetera Expensify es emitida por ${walletProgram}.`, perPurchase: 'Por compra', - atmWithdrawal: 'Retiro de cajero automático', + atmWithdrawal: 'Retiro en cajeros automáticos', cashReload: 'Recarga de efectivo', inNetwork: 'en la red', outOfNetwork: 'fuera de la red', - atmBalanceInquiry: 'Consulta de saldo de cajero automático', + atmBalanceInquiry: 'Consulta de saldo en cajeros automáticos', inOrOutOfNetwork: '(dentro o fuera de la red)', customerService: 'Servicio al cliente', automatedOrLive: '(agente automatizado o en vivo)', afterTwelveMonths: '(después de 12 meses sin transacciones)', weChargeOneFee: 'Cobramos un tipo de tarifa.', - fdicInsurance: 'Sus fondos son elegibles para el seguro de la FDIC.', - generalInfo: 'Para obtener información general sobre cuentas prepagas, visite', + fdicInsurance: 'Tus fondos pueden acogerse al seguro de la FDIC.', + generalInfo: 'Para obtener información general sobre cuentas de prepago, visite', conditionsDetails: 'Encuentra detalles y condiciones para todas las tarifas y servicios visitando', conditionsPhone: 'o llamando al +1 833-400-0904.', instant: '(instantáneo)', - electronicFundsInstantFeeMin: '(mínimo $0.25)', + electronicFundsInstantFeeMin: '(mínimo US$0,25)', }, longTermsForm: { listOfAllFees: 'Una lista de todas las tarifas de la billetera Expensify', @@ -1417,30 +1417,30 @@ export default { customerServiceDetails: 'No hay tarifas de servicio al cliente.', inactivityDetails: 'No hay tarifa de inactividad.', sendingFundsTitle: 'Enviar fondos a otro titular de cuenta', - sendingFundsDetails: 'No se aplica ningún cargo por enviar fondos a otro titular de cuenta utilizando su saldo cuenta bancaria o tarjeta de débito', + sendingFundsDetails: 'No se aplica ningún cargo por enviar fondos a otro titular de cuenta utilizando tu saldo cuenta bancaria o tarjeta de débito', electronicFundsStandardDetails: - 'No hay cargo por transferir fondos desde su billetera Expensify ' + - 'a su cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' + - '1-3 negocios días.', + 'No hay cargo por transferir fondos desde tu billetera Expensify ' + + 'a tu cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' + + '1-3 días laborables.', electronicFundsInstantDetails: - 'Hay una tarifa para transferir fondos desde su billetera Expensify a ' + - 'su tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + - 'generalmente se completa dentro de varios minutos. La tarifa es el 1.5% del monto de la ' + - 'transferencia (con una tarifa mínima de $ 0.25). ', + 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + + 'la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + + 'generalmente se completa dentro de varios minutos. La tarifa es el 1,5% del importe de la ' + + 'transferencia (con una tarifa mínima de US$0,25). ', fdicInsuranceBancorp: - 'Sus fondos son elegibles para el seguro de la FDIC. Sus fondos se mantendrán en o ' + - `transferido a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, sus fondos ` + - `están asegurados a $ 250,000 por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, - fdicInsuranceBancorp2: 'para detalles.', - contactExpensifyPayments: `Comuníquese con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, por correoelectrónico a`, + 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + + `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + + `están asegurados hasta US$250.000 por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, + fdicInsuranceBancorp2: 'para más detalles.', + contactExpensifyPayments: `Comunícate con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, o por correo electrónico a`, contactExpensifyPayments2: 'o inicie sesión en', - generalInformation: 'Para obtener información general sobre cuentas prepagas, visite', - generalInformation2: 'Si tiene una queja sobre una cuenta prepaga, llame al Consumer Financial Oficina de Protección al 1-855-411-2372 o visite', + generalInformation: 'Para obtener información general sobre cuentas de prepago, visite', + generalInformation2: 'Si tienes alguna queja sobre una cuenta de prepago, llama al Consumer Financial Oficina de Protección al 1-855-411-2372 o visita', printerFriendlyView: 'Ver versión para imprimir', automated: 'Automatizado', liveAgent: 'Agente en vivo', instant: 'Instantáneo', - electronicFundsInstantFeeMin: 'Mínimo $0.25', + electronicFundsInstantFeeMin: 'Mínimo US$0,25', }, }, activateStep: { @@ -1448,7 +1448,7 @@ export default { activatedTitle: '¡Billetera activada!', activatedMessage: 'Felicidades, tu Billetera está configurada y lista para hacer pagos.', checkBackLaterTitle: 'Un momento...', - checkBackLaterMessage: 'Todavía estamos revisando tu información. Por favor, vuelva más tarde.', + checkBackLaterMessage: 'Todavía estamos revisando tu información. Por favor, vuelve más tarde.', continueToPayment: 'Continuar al pago', continueToTransfer: 'Continuar a la transferencia', }, From fa47c0da6c59f2b86adadc1b819c9f99d9a0ae7b Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 22 Jan 2024 16:18:07 +0100 Subject: [PATCH 100/170] re-test From 453de73dbdbad0dd979e6c1741225dbbea40e18a Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Mon, 22 Jan 2024 16:56:16 +0100 Subject: [PATCH 101/170] use date-fns --- src/libs/DateUtils.ts | 18 ++++++++++-------- tests/unit/DateUtilsTest.js | 2 +- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 6e56d19a89b4..188e0e54afe8 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -5,9 +5,12 @@ import { eachDayOfInterval, eachMonthOfInterval, endOfDay, + endOfMonth, endOfWeek, format, formatDistanceToNow, + getDate, + getDay, getDayOfYear, isAfter, isBefore, @@ -737,17 +740,16 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone { * returns {number} */ function getLastBusinessDayOfMonth(inputDate: Date): number { - const currentDate = new Date(inputDate); + let currentDate = endOfMonth(inputDate); + const dayOfWeek = getDay(currentDate); - // Set the date to the last day of the month - currentDate.setMonth(currentDate.getMonth() + 1, 0); - - // Loop backward to find the latest business day - while (currentDate.getDay() === 0 || currentDate.getDay() === 6) { - currentDate.setDate(currentDate.getDate() - 1); + if (dayOfWeek === 0) { + currentDate = subDays(currentDate, 2); + } else if (dayOfWeek === 6) { + currentDate = subDays(currentDate, 1); } - return currentDate.getDate(); + return getDate(currentDate); } const DateUtils = { diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js index 17b25f24e327..a8bdc6c6b7bc 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.js @@ -214,7 +214,7 @@ describe('DateUtils', () => { }); }); - describe('getLastBusinessDayOfMonth', () => { + describe.only('getLastBusinessDayOfMonth', () => { const scenarios = [ { // Last business of May in 2025 From 2aadb0a4fa973b1f64774c5ff70d98ad13283914 Mon Sep 17 00:00:00 2001 From: Pujan Date: Mon, 22 Jan 2024 23:59:32 +0530 Subject: [PATCH 102/170] used StackScreenProps --- src/libs/Navigation/types.ts | 5 +---- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 15 ++++++++------- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 17 +---------------- 3 files changed, 10 insertions(+), 27 deletions(-) diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index fcd01aabda9e..2f1469e40a19 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -331,10 +331,7 @@ type ProcessMoneyRequestHoldNavigatorParamList = { }; type PrivateNotesNavigatorParamList = { - [SCREENS.PRIVATE_NOTES.LIST]: { - reportID: string; - accountID: string; - }; + [SCREENS.PRIVATE_NOTES.LIST]: undefined; [SCREENS.PRIVATE_NOTES.EDIT]: { reportID: string; accountID: string; diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index c6095a318029..6292a2e3c412 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -1,5 +1,5 @@ import {useFocusEffect} from '@react-navigation/native'; -import type {RouteProp} from '@react-navigation/native'; +import type {StackScreenProps} from '@react-navigation/stack'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; @@ -18,6 +18,7 @@ import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import type {PrivateNotesNavigatorParamList} from '@libs/Navigation/types'; import * as ReportUtils from '@libs/ReportUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import withReportAndPrivateNotesOrNotFound from '@pages/home/report/withReportAndPrivateNotesOrNotFound'; @@ -25,6 +26,7 @@ import * as ReportActions from '@userActions/Report'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type SCREENS from '@src/SCREENS'; import type {PersonalDetails, Report} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; @@ -35,12 +37,11 @@ type PrivateNotesEditPageOnyxProps = { personalDetailsList: OnyxCollection; }; -type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & { - /** The report currently being looked at */ - report: Report; - - route: RouteProp<{params: {reportID: string; accountID: string}}>; -}; +type PrivateNotesEditPageProps = PrivateNotesEditPageOnyxProps & + StackScreenProps & { + /** The report currently being looked at */ + report: Report; + }; function PrivateNotesEditPage({route, personalDetailsList, report}: PrivateNotesEditPageProps) { const styles = useThemeStyles(); diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 550234a0707e..30bd90bed5b6 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -1,5 +1,4 @@ -import {useIsFocused} from '@react-navigation/native'; -import React, {useEffect, useMemo} from 'react'; +import React, {useMemo} from 'react'; import {ScrollView} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -43,20 +42,6 @@ type NoteListItem = { function PrivateNotesListPage({report, personalDetailsList, session}: PrivateNotesListPageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const isFocused = useIsFocused(); - - useEffect(() => { - const navigateToEditPageTimeout = setTimeout(() => { - if (Object.values(report.privateNotes ?? {}).some((item) => item.note) || !isFocused) { - return; - } - Navigation.navigate(ROUTES.PRIVATE_NOTES_EDIT.getRoute(report.reportID, session?.accountID ?? '')); - }, CONST.ANIMATED_TRANSITION); - - return () => { - clearTimeout(navigateToEditPageTimeout); - }; - }, [report.privateNotes, report.reportID, session?.accountID, isFocused]); /** * Gets the menu item for each workspace From 44d82985dedb00395a913b1ee59278513b1de9a7 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 22 Jan 2024 22:59:49 +0300 Subject: [PATCH 103/170] fixed unread marker inconsistency --- src/pages/home/report/ReportActionsList.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 66b0decedb42..5803e97aaf63 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -435,6 +435,7 @@ function ReportActionsList({ Report.readNewestAction(report.reportID, false); userActiveSince.current = DateUtils.getDBTime(); + lastReadTimeRef.current = userInactiveSince.current; setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); From 95f6022c3e0540edfe2dcdc85bd32cd2032954f7 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 22 Jan 2024 17:06:35 -0500 Subject: [PATCH 104/170] add logging --- src/pages/LogInWithShortLivedAuthTokenPage.tsx | 2 ++ src/pages/LogOutPreviousUserPage.js | 2 ++ src/pages/signin/SAMLSignInPage/index.native.js | 3 +++ 3 files changed, 7 insertions(+) diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index c5f8a9c20d5b..16e990392f14 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -18,6 +18,7 @@ import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {Account} from '@src/types/onyx'; +import Log from '@libs/Log'; type LogInWithShortLivedAuthTokenPageOnyxProps = { /** The details about the account that the user is signing in with */ @@ -38,6 +39,7 @@ function LogInWithShortLivedAuthTokenPage({route, account}: LogInWithShortLivedA // Try to authenticate using the shortLivedToken if we're not already trying to load the accounts if (token && !account?.isLoading) { + Log.info('LogInWithShortLivedAuthTokenPage - Successfully received shortLivedAuthToken. Signing in...'); Session.signInWithShortLivedAuthToken(email, token); return; } diff --git a/src/pages/LogOutPreviousUserPage.js b/src/pages/LogOutPreviousUserPage.js index 5c8a39204467..974823f489ed 100644 --- a/src/pages/LogOutPreviousUserPage.js +++ b/src/pages/LogOutPreviousUserPage.js @@ -4,6 +4,7 @@ import React, {useEffect} from 'react'; import {Linking} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import FullScreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; +import Log from '@libs/Log'; import * as SessionUtils from '@libs/SessionUtils'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -51,6 +52,7 @@ function LogOutPreviousUserPage(props) { // On Enabling 2FA, authToken stored in Onyx becomes expired and hence we need to fetch new authToken const shouldForceLogin = lodashGet(props, 'route.params.shouldForceLogin', '') === 'true'; if (shouldForceLogin) { + Log.info('LogOutPreviousUserPage - forcing login with shortLivedAuthToken'); const email = lodashGet(props, 'route.params.email', ''); const shortLivedAuthToken = lodashGet(props, 'route.params.shortLivedAuthToken', ''); Session.signInWithShortLivedAuthToken(email, shortLivedAuthToken); diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index 502e26e337b9..7211122b5d24 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -7,6 +7,7 @@ import HeaderWithBackButton from '@components/HeaderWithBackButton'; import SAMLLoadingIndicator from '@components/SAMLLoadingIndicator'; import ScreenWrapper from '@components/ScreenWrapper'; import getPlatform from '@libs/getPlatform'; +import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import * as Session from '@userActions/Session'; import CONFIG from '@src/CONFIG'; @@ -36,6 +37,7 @@ function SAMLSignInPage({credentials}) { */ const handleNavigationStateChange = useCallback( ({url}) => { + Log.info('SAMLSignInPage - Handling SAML navigation change'); // If we've gotten a callback then remove the option to navigate back to the sign in page if (url.includes('loginCallback')) { shouldShowNavigation(false); @@ -43,6 +45,7 @@ function SAMLSignInPage({credentials}) { const searchParams = new URLSearchParams(new URL(url).search); if (searchParams.has('shortLivedAuthToken')) { + Log.info('SAMLSignInPage - Successfully received shortLivedAuthToken. Signing in...'); const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken); } From 454c2b1ff8d466be6e8c88617299047e1798a634 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 22 Jan 2024 17:29:46 -0500 Subject: [PATCH 105/170] check for account.isLoading --- src/pages/signin/SAMLSignInPage/index.native.js | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pages/signin/SAMLSignInPage/index.native.js b/src/pages/signin/SAMLSignInPage/index.native.js index 7211122b5d24..9fe60e56353e 100644 --- a/src/pages/signin/SAMLSignInPage/index.native.js +++ b/src/pages/signin/SAMLSignInPage/index.native.js @@ -20,13 +20,20 @@ const propTypes = { /** The email/phone the user logged in with */ login: PropTypes.string, }), + + /** State of the logging in user's account */ + account: PropTypes.shape({ + /** Whether the account is loading */ + isLoading: PropTypes.bool, + }), }; const defaultProps = { credentials: {}, + account: {}, }; -function SAMLSignInPage({credentials}) { +function SAMLSignInPage({credentials, account}) { const samlLoginURL = `${CONFIG.EXPENSIFY.SAML_URL}?email=${credentials.login}&referer=${CONFIG.EXPENSIFY.EXPENSIFY_CASH_REFERER}&platform=${getPlatform()}`; const [showNavigation, shouldShowNavigation] = useState(true); @@ -44,7 +51,7 @@ function SAMLSignInPage({credentials}) { } const searchParams = new URLSearchParams(new URL(url).search); - if (searchParams.has('shortLivedAuthToken')) { + if (searchParams.has('shortLivedAuthToken') && !account.isLoading) { Log.info('SAMLSignInPage - Successfully received shortLivedAuthToken. Signing in...'); const shortLivedAuthToken = searchParams.get('shortLivedAuthToken'); Session.signInWithShortLivedAuthToken(credentials.login, shortLivedAuthToken); @@ -57,7 +64,7 @@ function SAMLSignInPage({credentials}) { Navigation.navigate(ROUTES.HOME); } }, - [credentials.login, shouldShowNavigation], + [credentials.login, shouldShowNavigation, account.isLoading], ); return ( @@ -95,4 +102,5 @@ SAMLSignInPage.displayName = 'SAMLSignInPage'; export default withOnyx({ credentials: {key: ONYXKEYS.CREDENTIALS}, + account: {key: ONYXKEYS.ACCOUNT}, })(SAMLSignInPage); From 420878d882ae2bf1474658a941ffdb7ea32a8cf4 Mon Sep 17 00:00:00 2001 From: Andrew Rosiclair Date: Mon, 22 Jan 2024 17:49:32 -0500 Subject: [PATCH 106/170] prettier --- src/pages/LogInWithShortLivedAuthTokenPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/LogInWithShortLivedAuthTokenPage.tsx b/src/pages/LogInWithShortLivedAuthTokenPage.tsx index 16e990392f14..811c35fff34e 100644 --- a/src/pages/LogInWithShortLivedAuthTokenPage.tsx +++ b/src/pages/LogInWithShortLivedAuthTokenPage.tsx @@ -12,13 +12,13 @@ import TextLink from '@components/TextLink'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; +import Log from '@libs/Log'; import Navigation from '@libs/Navigation/Navigation'; import type {PublicScreensParamList} from '@libs/Navigation/types'; import * as Session from '@userActions/Session'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import type {Account} from '@src/types/onyx'; -import Log from '@libs/Log'; type LogInWithShortLivedAuthTokenPageOnyxProps = { /** The details about the account that the user is signing in with */ From e16d28286b4f3681c0638e1a6396d61b2d6c903b Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 23 Jan 2024 09:37:09 +0700 Subject: [PATCH 107/170] edit comment --- src/pages/home/report/ReportActionItem.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 60503424f663..80fb341a2cf8 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -346,8 +346,7 @@ function ReportActionItem(props) { const iouReportID = originalMessage.IOUReportID ? originalMessage.IOUReportID.toString() : '0'; children = ( Date: Tue, 23 Jan 2024 11:14:18 +0100 Subject: [PATCH 108/170] minor improvements --- src/libs/DateUtils.ts | 2 +- tests/unit/DateUtilsTest.js | 13 +++++++++---- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 188e0e54afe8..526769723531 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -734,7 +734,7 @@ function formatToSupportedTimezone(timezoneInput: Timezone): Timezone { } /** - * Returns the latest business day of input date month + * Returns the last business day of given date month * * param {Date} inputDate * returns {number} diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js index a8bdc6c6b7bc..be38eae9251d 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.js @@ -217,23 +217,28 @@ describe('DateUtils', () => { describe.only('getLastBusinessDayOfMonth', () => { const scenarios = [ { - // Last business of May in 2025 + // Last business day of May in 2025 inputDate: new Date(2025, 4), expectedResult: 30, }, { - // Last business of January in 2024 + // Last business day of February in 2024 + inputDate: new Date(2024, 2), + expectedResult: 29, + }, + { + // Last business day of January in 2024 inputDate: new Date(2024, 0), expectedResult: 31, }, { - // Last business of September in 2023 + // Last business day of September in 2023 inputDate: new Date(2023, 8), expectedResult: 29, }, ]; - test.each(scenarios)('returns a last business day of an input date', ({inputDate, expectedResult}) => { + test.each(scenarios)('returns a last business day based on the input date', ({inputDate, expectedResult}) => { const lastBusinessDay = DateUtils.getLastBusinessDayOfMonth(inputDate); expect(lastBusinessDay).toEqual(expectedResult); From 86bbc8ae39c18c59ba62dcd45da59518254a3e3e Mon Sep 17 00:00:00 2001 From: Mykhailo Kravchenko Date: Tue, 23 Jan 2024 11:14:51 +0100 Subject: [PATCH 109/170] remove only --- tests/unit/DateUtilsTest.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/DateUtilsTest.js b/tests/unit/DateUtilsTest.js index be38eae9251d..a752eea1a990 100644 --- a/tests/unit/DateUtilsTest.js +++ b/tests/unit/DateUtilsTest.js @@ -214,7 +214,7 @@ describe('DateUtils', () => { }); }); - describe.only('getLastBusinessDayOfMonth', () => { + describe('getLastBusinessDayOfMonth', () => { const scenarios = [ { // Last business day of May in 2025 From a0c1445aa3fee982bcd3ff03f8daf5192443a4f9 Mon Sep 17 00:00:00 2001 From: Eduardo Date: Tue, 23 Jan 2024 12:41:22 +0100 Subject: [PATCH 110/170] Add null support to errors instead of changing the value --- src/CONST.ts | 2 +- src/types/onyx/Account.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index eb2e77404f72..0b10e5767328 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -831,7 +831,7 @@ const CONST = { }, WEEK_STARTS_ON: 1, // Monday DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, - DEFAULT_ACCOUNT_DATA: {errors: undefined, success: '', isLoading: false}, + DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, FORMS: { LOGIN_FORM: 'LoginForm', diff --git a/src/types/onyx/Account.ts b/src/types/onyx/Account.ts index 0ea3e05e8d6a..4e7c5396b649 100644 --- a/src/types/onyx/Account.ts +++ b/src/types/onyx/Account.ts @@ -50,7 +50,7 @@ type Account = { /** The active policy ID. Initiating a SmartScan will create an expense on this policy by default. */ activePolicyID?: string; - errors?: OnyxCommon.Errors; + errors?: OnyxCommon.Errors | null; success?: string; codesAreCopied?: boolean; twoFactorAuthStep?: TwoFactorAuthStep; From b502e6456673606fd51a8ec0f8838adcc196e448 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 12:50:04 +0100 Subject: [PATCH 111/170] Update src/languages/es.ts Co-authored-by: Ionatan Wiznia --- src/languages/es.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/languages/es.ts b/src/languages/es.ts index 2c4070ad30ad..6970b905ff5e 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -2595,7 +2595,7 @@ export default { } if (!isTransactionOlderThan7Days) { return isAdmin - ? `Píde a ${member} que marque la transacción como efectivo o espera 7 días e inténtalo de nuevo` + ? `Pide a ${member} que marque la transacción como efectivo o espera 7 días e inténtalo de nuevo` : 'Esperando a adjuntar automáticamente la transacción de tarjeta de crédito'; } return ''; From c682722c94469a99af196cec2a4cf33cdad633ce Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 17 Jan 2024 12:22:35 +0100 Subject: [PATCH 112/170] add metrics for SearchPage --- src/CONST.ts | 2 ++ src/components/OptionsList/BaseOptionsList.tsx | 10 ++++++++++ src/libs/OptionsListUtils.js | 7 ++++++- src/pages/home/sidebar/SidebarLinks.js | 4 ++++ 4 files changed, 22 insertions(+), 1 deletion(-) diff --git a/src/CONST.ts b/src/CONST.ts index bc56e345de7c..552c94ceaf6c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -716,6 +716,8 @@ const CONST = { REPORT_INITIAL_RENDER: 'report_initial_render', SWITCH_REPORT: 'switch_report', SIDEBAR_LOADED: 'sidebar_loaded', + OPEN_SEARCH: 'open_search', + LOAD_SEARCH_OPTIONS: 'load_search_options', COLD: 'cold', WARM: 'warm', REPORT_ACTION_ITEM_LAYOUT_DEBOUNCE_TIME: 1500, diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index c1e4562a0c2d..d056b858b3e8 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -9,6 +9,7 @@ import SectionList from '@components/SectionList'; import Text from '@components/Text'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import Performance from '@libs/Performance'; import type {OptionData} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; import variables from '@styles/variables'; @@ -108,6 +109,15 @@ function BaseOptionsList( flattenedData.current = buildFlatSectionArray(); }); + useEffect(() => { + if (isLoading) { + return; + } + + // Mark the end of the search page load time. This data is collected only for Search page. + Performance.markEnd(CONST.TIMING.OPEN_SEARCH); + }, [isLoading]); + const onViewableItemsChanged = () => { if (didLayout.current || !onLayout) { return; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 2973228af51f..b0a13fda9df6 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -23,6 +23,7 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +import Performance from './Performance'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -1651,7 +1652,8 @@ function getOptions( * @returns {Object} */ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { - return getOptions(reports, personalDetails, { + Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); + const options = getOptions(reports, personalDetails, { betas, searchInputValue: searchValue.trim(), includeRecentReports: true, @@ -1666,6 +1668,9 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { includeMoneyRequests: true, includeTasks: true, }); + Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); + + return options; } /** diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index ffcba2048d18..d2a80e713f5a 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -29,6 +29,7 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import Performance from '@libs/Performance'; import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus'; const basePropTypes = { @@ -123,6 +124,9 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority return; } + // Capture metric for opening the search page + Performance.markStart(CONST.TIMING.OPEN_SEARCH) + Navigation.navigate(ROUTES.SEARCH); }, [isCreateMenuOpen]); From 9f56657e8d15c36ae96b71094f339825437dce2c Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Tue, 23 Jan 2024 18:09:32 +0300 Subject: [PATCH 113/170] fix condition to consider usage on different device --- src/pages/home/report/ReportActionsList.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 5803e97aaf63..ce8dcb10ef5f 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -417,7 +417,9 @@ function ReportActionsList({ userInactiveSince.current = DateUtils.getDBTime(); return; } - + // In case the user read new messages (after being inactive) with other device we should + // show marker based on report.lastReadTime + const newMessageTimeReference = userInactiveSince.current > report.lastReadTime ? userActiveSince.current : report.lastReadTime; if ( scrollingVerticalOffset.current >= MSG_VISIBLE_THRESHOLD || !( @@ -425,8 +427,8 @@ function ReportActionsList({ _.some( sortedVisibleReportActions, (reportAction) => - userInactiveSince.current < reportAction.created && - (ReportActionsUtils.isReportPreviewAction(reportAction) ? !reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(), + newMessageTimeReference < reportAction.created && + (ReportActionsUtils.isReportPreviewAction(reportAction) ? reportAction.childLastActorAccountID : reportAction.actorAccountID) !== Report.getCurrentUserAccountID(), ) ) ) { @@ -435,7 +437,7 @@ function ReportActionsList({ Report.readNewestAction(report.reportID, false); userActiveSince.current = DateUtils.getDBTime(); - lastReadTimeRef.current = userInactiveSince.current; + lastReadTimeRef.current = newMessageTimeReference; setCurrentUnreadMarker(null); cacheUnreadMarkers.delete(report.reportID); calculateUnreadMarker(); From ed557d57d7bf9f27b04c388614f52741d1192e0e Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 17 Jan 2024 12:22:36 +0100 Subject: [PATCH 114/170] lint files --- src/libs/OptionsListUtils.js | 2 +- src/pages/home/sidebar/SidebarLinks.js | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index b0a13fda9df6..5b494cb09254 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -14,6 +14,7 @@ import * as Localize from './Localize'; import * as LoginUtils from './LoginUtils'; import ModifiedExpenseMessage from './ModifiedExpenseMessage'; import Navigation from './Navigation/Navigation'; +import Performance from './Performance'; import Permissions from './Permissions'; import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PhoneNumber from './PhoneNumber'; @@ -23,7 +24,6 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -import Performance from './Performance'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index d2a80e713f5a..08791f4e16fd 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -20,6 +20,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import Navigation from '@libs/Navigation/Navigation'; import onyxSubscribe from '@libs/onyxSubscribe'; +import Performance from '@libs/Performance'; import SidebarUtils from '@libs/SidebarUtils'; import * as ReportActionContextMenu from '@pages/home/report/ContextMenu/ReportActionContextMenu'; import safeAreaInsetPropTypes from '@pages/safeAreaInsetPropTypes'; @@ -29,7 +30,6 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import Performance from '@libs/Performance'; import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus'; const basePropTypes = { @@ -125,7 +125,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority } // Capture metric for opening the search page - Performance.markStart(CONST.TIMING.OPEN_SEARCH) + Performance.markStart(CONST.TIMING.OPEN_SEARCH); Navigation.navigate(ROUTES.SEARCH); }, [isCreateMenuOpen]); From e4c678f36fb92fa281a093d28abddb6f637c3479 Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 17 Jan 2024 13:40:27 +0100 Subject: [PATCH 115/170] use Timing to track metric --- src/components/OptionsList/BaseOptionsList.tsx | 2 ++ src/libs/OptionsListUtils.js | 3 +++ src/pages/home/sidebar/SidebarLinks.js | 2 ++ 3 files changed, 7 insertions(+) diff --git a/src/components/OptionsList/BaseOptionsList.tsx b/src/components/OptionsList/BaseOptionsList.tsx index d056b858b3e8..8cac059436b5 100644 --- a/src/components/OptionsList/BaseOptionsList.tsx +++ b/src/components/OptionsList/BaseOptionsList.tsx @@ -9,6 +9,7 @@ import SectionList from '@components/SectionList'; import Text from '@components/Text'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; +import Timing from '@libs/actions/Timing'; import Performance from '@libs/Performance'; import type {OptionData} from '@libs/ReportUtils'; import StringUtils from '@libs/StringUtils'; @@ -115,6 +116,7 @@ function BaseOptionsList( } // Mark the end of the search page load time. This data is collected only for Search page. + Timing.end(CONST.TIMING.OPEN_SEARCH); Performance.markEnd(CONST.TIMING.OPEN_SEARCH); }, [isLoading]); diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 5b494cb09254..15e2e5ca269c 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -24,6 +24,7 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +import Timing from './actions/Timing'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -1652,6 +1653,7 @@ function getOptions( * @returns {Object} */ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { + Timing.start(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markStart(CONST.TIMING.LOAD_SEARCH_OPTIONS); const options = getOptions(reports, personalDetails, { betas, @@ -1668,6 +1670,7 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { includeMoneyRequests: true, includeTasks: true, }); + Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); Performance.markEnd(CONST.TIMING.LOAD_SEARCH_OPTIONS); return options; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 08791f4e16fd..3c7aa4352911 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -30,6 +30,7 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import Timing from '@libs/actions/Timing'; import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus'; const basePropTypes = { @@ -125,6 +126,7 @@ function SidebarLinks({onLinkClick, insets, optionListItems, isLoading, priority } // Capture metric for opening the search page + Timing.start(CONST.TIMING.OPEN_SEARCH); Performance.markStart(CONST.TIMING.OPEN_SEARCH); Navigation.navigate(ROUTES.SEARCH); From ff3da8489fff129872ffc3603d0f82fcb060121a Mon Sep 17 00:00:00 2001 From: Tomasz Misiukiewicz Date: Wed, 17 Jan 2024 13:50:50 +0100 Subject: [PATCH 116/170] lint files --- src/libs/OptionsListUtils.js | 2 +- src/pages/home/sidebar/SidebarLinks.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 15e2e5ca269c..d44df3c6c39c 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -7,6 +7,7 @@ import Onyx from 'react-native-onyx'; import _ from 'underscore'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import Timing from './actions/Timing'; import * as CollectionUtils from './CollectionUtils'; import * as ErrorUtils from './ErrorUtils'; import * as LocalePhoneNumber from './LocalePhoneNumber'; @@ -24,7 +25,6 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; -import Timing from './actions/Timing'; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 3c7aa4352911..09362d88555c 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -17,6 +17,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import Timing from '@libs/actions/Timing'; import KeyboardShortcut from '@libs/KeyboardShortcut'; import Navigation from '@libs/Navigation/Navigation'; import onyxSubscribe from '@libs/onyxSubscribe'; @@ -30,7 +31,6 @@ import * as Session from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import Timing from '@libs/actions/Timing'; import SignInOrAvatarWithOptionalStatus from './SignInOrAvatarWithOptionalStatus'; const basePropTypes = { From 4971adf81f2236b33ac4f05567a0c8dde9cc65a3 Mon Sep 17 00:00:00 2001 From: someone-here Date: Tue, 23 Jan 2024 21:58:49 +0530 Subject: [PATCH 117/170] Prettify --- src/components/AvatarCropModal/Slider.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/AvatarCropModal/Slider.tsx b/src/components/AvatarCropModal/Slider.tsx index f69fba776718..9a9da65befa0 100644 --- a/src/components/AvatarCropModal/Slider.tsx +++ b/src/components/AvatarCropModal/Slider.tsx @@ -10,7 +10,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import ControlSelection from '@libs/ControlSelection'; type SliderProps = { - /** React-native-reanimated lib handler which executes when the user is panning slider */ gestureCallbacks: { onBegin: () => void; From 65202dfca711dcb426a28ada45f8c52a14a88369 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 18:38:19 +0100 Subject: [PATCH 118/170] Use Localize --- .../EnablePayments/TermsPage/LongTermsForm.js | 152 +++++++++--------- .../TermsPage/ShortTermsForm.js | 58 +++---- 2 files changed, 107 insertions(+), 103 deletions(-) diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index b29cb0c777f7..4147d38a98c0 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -8,95 +8,97 @@ import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Localize from '@libs/Localize'; +import useLocalize from "@hooks/useLocalize"; import CONST from '@src/CONST'; -const termsData = [ - { - title: Localize.translateLocal('termsStep.longTermsForm.openingAccountTitle'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.openingAccountDetails'), - }, - { - title: Localize.translateLocal('termsStep.monthlyFee'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.monthlyFeeDetails'), - }, - { - title: Localize.translateLocal('termsStep.longTermsForm.customerServiceTitle'), - subTitle: Localize.translateLocal('termsStep.longTermsForm.automated'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.customerServiceDetails'), - }, - { - title: Localize.translateLocal('termsStep.longTermsForm.customerServiceTitle'), - subTitle: Localize.translateLocal('termsStep.longTermsForm.liveAgent'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.customerServiceDetails'), - }, - { - title: Localize.translateLocal('termsStep.inactivity'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.inactivityDetails'), - }, - { - title: Localize.translateLocal('termsStep.longTermsForm.sendingFundsTitle'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.sendingFundsDetails'), - }, - { - title: Localize.translateLocal('termsStep.electronicFundsWithdrawal'), - subTitle: Localize.translateLocal('termsStep.standard'), - rightText: Localize.translateLocal('termsStep.feeAmountZero'), - details: Localize.translateLocal('termsStep.longTermsForm.electronicFundsStandardDetails'), - }, - { - title: Localize.translateLocal('termsStep.electronicFundsWithdrawal'), - subTitle: Localize.translateLocal('termsStep.longTermsForm.instant'), - rightText: Localize.translateLocal('termsStep.electronicFundsInstantFee'), - subRightText: Localize.translateLocal('termsStep.longTermsForm.electronicFundsInstantFeeMin'), - details: Localize.translateLocal('termsStep.longTermsForm.electronicFundsInstantDetails'), - }, -]; +function LongTermsForm() { + const theme = useTheme(); + const styles = useThemeStyles(); + const {translate} = useLocalize(); -const getLongTermsSections = (styles) => - _.map(termsData, (section, index) => ( - // eslint-disable-next-line react/no-array-index-key - - - - {section.title} - {Boolean(section.subTitle) && {section.subTitle}} - - - {section.rightText} - {Boolean(section.subRightText) && {section.subRightText}} + const termsData = [ + { + title: translate('termsStep.longTermsForm.openingAccountTitle'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.openingAccountDetails'), + }, + { + title: translate('termsStep.monthlyFee'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.monthlyFeeDetails'), + }, + { + title: translate('termsStep.longTermsForm.customerServiceTitle'), + subTitle: translate('termsStep.longTermsForm.automated'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.customerServiceDetails'), + }, + { + title: translate('termsStep.longTermsForm.customerServiceTitle'), + subTitle: translate('termsStep.longTermsForm.liveAgent'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.customerServiceDetails'), + }, + { + title: translate('termsStep.inactivity'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.inactivityDetails'), + }, + { + title: translate('termsStep.longTermsForm.sendingFundsTitle'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.sendingFundsDetails'), + }, + { + title: translate('termsStep.electronicFundsWithdrawal'), + subTitle: translate('termsStep.standard'), + rightText: translate('termsStep.feeAmountZero'), + details: translate('termsStep.longTermsForm.electronicFundsStandardDetails'), + }, + { + title: translate('termsStep.electronicFundsWithdrawal'), + subTitle: translate('termsStep.longTermsForm.instant'), + rightText: translate('termsStep.electronicFundsInstantFee'), + subRightText: translate('termsStep.longTermsForm.electronicFundsInstantFeeMin'), + details: translate('termsStep.longTermsForm.electronicFundsInstantDetails'), + }, + ]; + + const getLongTermsSections = () => + _.map(termsData, (section, index) => ( + // eslint-disable-next-line react/no-array-index-key + + + + {section.title} + {Boolean(section.subTitle) && {section.subTitle}} + + + {section.rightText} + {Boolean(section.subRightText) && {section.subRightText}} + + {section.details} - {section.details} - - )); + )); -function LongTermsForm() { - const theme = useTheme(); - const styles = useThemeStyles(); return ( <> - {getLongTermsSections(styles)} + {getLongTermsSections()} - {Localize.translateLocal('termsStep.longTermsForm.fdicInsuranceBancorp')} {CONST.TERMS.FDIC_PREPAID}{' '} - {Localize.translateLocal('termsStep.longTermsForm.fdicInsuranceBancorp2')} + {translate('termsStep.longTermsForm.fdicInsuranceBancorp')} {CONST.TERMS.FDIC_PREPAID}{' '} + {translate('termsStep.longTermsForm.fdicInsuranceBancorp2')} - {Localize.translateLocal('termsStep.noOverdraftOrCredit')} + {translate('termsStep.noOverdraftOrCredit')} - {Localize.translateLocal('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE}{' '} - {Localize.translateLocal('termsStep.longTermsForm.contactExpensifyPayments2')} {CONST.NEW_EXPENSIFY_URL}. + {translate('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE}{' '} + {translate('termsStep.longTermsForm.contactExpensifyPayments2')} {CONST.NEW_EXPENSIFY_URL}. - {Localize.translateLocal('termsStep.longTermsForm.generalInformation')} {CONST.TERMS.CFPB_PREPAID} + {translate('termsStep.longTermsForm.generalInformation')} {CONST.TERMS.CFPB_PREPAID} {'. '} - {Localize.translateLocal('termsStep.longTermsForm.generalInformation2')} {CONST.TERMS.CFPB_COMPLAINT}. + {translate('termsStep.longTermsForm.generalInformation2')} {CONST.TERMS.CFPB_COMPLAINT}. @@ -109,7 +111,7 @@ function LongTermsForm() { style={styles.ml1} href={CONST.FEES_URL} > - {Localize.translateLocal('termsStep.longTermsForm.printerFriendlyView')} + {translate('termsStep.longTermsForm.printerFriendlyView')} diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js index 77f77f3cb34b..c42b3b23ec2f 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js @@ -3,9 +3,10 @@ import {View} from 'react-native'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as Localize from '@libs/Localize'; import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import CONST from '@src/CONST'; +import useLocalize from "@hooks/useLocalize"; +import * as CurrencyUtils from "@libs/CurrencyUtils"; const propTypes = { /** The user's wallet */ @@ -18,10 +19,11 @@ const defaultProps = { function ShortTermsForm(props) { const styles = useThemeStyles(); + const {translate} = useLocalize(); return ( <> - {Localize.translateLocal('termsStep.shortTermsForm.expensifyPaymentsAccount', { + {translate('termsStep.shortTermsForm.expensifyPaymentsAccount', { walletProgram: props.userWallet.walletProgramID === CONST.WALLET.MTL_WALLET_PROGRAM_ID ? CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS : CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK, })} @@ -31,19 +33,19 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.monthlyFee')} + {translate('termsStep.monthlyFee')} - {Localize.translateLocal('termsStep.feeAmountZero')} + {CurrencyUtils.convertToDisplayString(0, 'USD')} - {Localize.translateLocal('termsStep.shortTermsForm.perPurchase')} + {translate('termsStep.shortTermsForm.perPurchase')} - {Localize.translateLocal('termsStep.feeAmountZero')} + {CurrencyUtils.convertToDisplayString(0, 'USD')} @@ -52,28 +54,28 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.shortTermsForm.atmWithdrawal')} + {translate('termsStep.shortTermsForm.atmWithdrawal')} - {Localize.translateLocal('common.na')} + {translate('common.na')} - {Localize.translateLocal('termsStep.shortTermsForm.inNetwork')} + {translate('termsStep.shortTermsForm.inNetwork')} - {Localize.translateLocal('common.na')} + {translate('common.na')} - {Localize.translateLocal('termsStep.shortTermsForm.outOfNetwork')} + {translate('termsStep.shortTermsForm.outOfNetwork')} - {Localize.translateLocal('termsStep.shortTermsForm.cashReload')} + {translate('termsStep.shortTermsForm.cashReload')} - {Localize.translateLocal('common.na')} + {translate('common.na')} @@ -83,11 +85,11 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.shortTermsForm.atmBalanceInquiry')} {Localize.translateLocal('termsStep.shortTermsForm.inOrOutOfNetwork')} + {translate('termsStep.shortTermsForm.atmBalanceInquiry')} {translate('termsStep.shortTermsForm.inOrOutOfNetwork')} - {Localize.translateLocal('common.na')} + {translate('common.na')} @@ -95,11 +97,11 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.shortTermsForm.customerService')} {Localize.translateLocal('termsStep.shortTermsForm.automatedOrLive')} + {translate('termsStep.shortTermsForm.customerService')} {translate('termsStep.shortTermsForm.automatedOrLive')} - {Localize.translateLocal('termsStep.feeAmountZero')} + {CurrencyUtils.convertToDisplayString(0, 'USD')} @@ -107,40 +109,40 @@ function ShortTermsForm(props) { - {Localize.translateLocal('termsStep.inactivity')} {Localize.translateLocal('termsStep.shortTermsForm.afterTwelveMonths')} + {translate('termsStep.inactivity')} {translate('termsStep.shortTermsForm.afterTwelveMonths')} - {Localize.translateLocal('termsStep.feeAmountZero')} + {CurrencyUtils.convertToDisplayString(0, 'USD')} - {Localize.translateLocal('termsStep.shortTermsForm.weChargeOneFee')} + {translate('termsStep.shortTermsForm.weChargeOneFee')} - {Localize.translateLocal('termsStep.electronicFundsWithdrawal')} {Localize.translateLocal('termsStep.shortTermsForm.instant')} + {translate('termsStep.electronicFundsWithdrawal')} {translate('termsStep.shortTermsForm.instant')} - {Localize.translateLocal('termsStep.electronicFundsInstantFee')} - {Localize.translateLocal('termsStep.shortTermsForm.electronicFundsInstantFeeMin')} + {translate('termsStep.electronicFundsInstantFee')} + {translate('termsStep.shortTermsForm.electronicFundsInstantFeeMin')} - {Localize.translateLocal('termsStep.noOverdraftOrCredit')} - {Localize.translateLocal('termsStep.shortTermsForm.fdicInsurance')} + {translate('termsStep.noOverdraftOrCredit')} + {translate('termsStep.shortTermsForm.fdicInsurance')} - {Localize.translateLocal('termsStep.shortTermsForm.generalInfo')} {CONST.TERMS.CFPB_PREPAID}. + {translate('termsStep.shortTermsForm.generalInfo')} {CONST.TERMS.CFPB_PREPAID}. - {Localize.translateLocal('termsStep.shortTermsForm.conditionsDetails')} {CONST.TERMS.USE_EXPENSIFY_FEES}{' '} - {Localize.translateLocal('termsStep.shortTermsForm.conditionsPhone')} + {translate('termsStep.shortTermsForm.conditionsDetails')} {CONST.TERMS.USE_EXPENSIFY_FEES}{' '} + {translate('termsStep.shortTermsForm.conditionsPhone')} From 155457c07dfec28b0bcf2ef75b8993551561940f Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 19:30:11 +0100 Subject: [PATCH 119/170] More keys --- src/languages/en.ts | 7 +++---- src/languages/es.ts | 7 +++---- src/languages/types.ts | 3 +++ .../EnablePayments/TermsPage/LongTermsForm.js | 21 ++++++++++--------- .../TermsPage/ShortTermsForm.js | 6 +++--- 5 files changed, 23 insertions(+), 21 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index b6da38df21a0..7db0c9be37d1 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -66,6 +66,7 @@ import type { StepCounterParams, TagSelectionParams, TaskCreatedActionParams, + TermsParams, ThreadRequestReportNameParams, ThreadSentMoneyReportNameParams, ToValidateLoginParams, @@ -1357,10 +1358,8 @@ export default { agreeToThe: 'I agree to the', walletAgreement: 'Wallet agreement', enablePayments: 'Enable payments', - feeAmountZero: '$0', monthlyFee: 'Monthly fee', inactivity: 'Inactivity', - electronicFundsInstantFee: '1.5%', noOverdraftOrCredit: 'No overdraft/credit feature.', electronicFundsWithdrawal: 'Electronic funds withdrawal', standard: 'Standard', @@ -1382,7 +1381,7 @@ export default { conditionsDetails: 'Find details and conditions for all fees and services by visiting', conditionsPhone: 'or calling +1 833-400-0904.', instant: '(instant)', - electronicFundsInstantFeeMin: '(min $0.25)', + electronicFundsInstantFeeMin: ({amount}: TermsParams) => `(min ${amount})`, }, longTermsForm: { listOfAllFees: 'A list of all Expensify Wallet fees', @@ -1418,7 +1417,7 @@ export default { automated: 'Automated', liveAgent: 'Live Agent', instant: 'Instant', - electronicFundsInstantFeeMin: 'Min $0.25', + electronicFundsInstantFeeMin: ({amount}: TermsParams) => `Min ${amount}`, }, }, activateStep: { diff --git a/src/languages/es.ts b/src/languages/es.ts index 6970b905ff5e..2d2a0c0165b8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -66,6 +66,7 @@ import type { StepCounterParams, TagSelectionParams, TaskCreatedActionParams, + TermsParams, ThreadRequestReportNameParams, ThreadSentMoneyReportNameParams, ToValidateLoginParams, @@ -1378,10 +1379,8 @@ export default { agreeToThe: 'Estoy de acuerdo con el ', walletAgreement: 'Acuerdo de la billetera', enablePayments: 'Habilitar pagos', - feeAmountZero: 'US$0', monthlyFee: 'Cuota mensual', inactivity: 'Inactividad', - electronicFundsInstantFee: '1,5%', noOverdraftOrCredit: 'Sin función de sobregiro/crédito', electronicFundsWithdrawal: 'Retiro electrónico de fondos', standard: 'Estándar', @@ -1403,7 +1402,7 @@ export default { conditionsDetails: 'Encuentra detalles y condiciones para todas las tarifas y servicios visitando', conditionsPhone: 'o llamando al +1 833-400-0904.', instant: '(instantáneo)', - electronicFundsInstantFeeMin: '(mínimo US$0,25)', + electronicFundsInstantFeeMin: ({amount}: TermsParams) => `(mínimo ${amount})`, }, longTermsForm: { listOfAllFees: 'Una lista de todas las tarifas de la billetera Expensify', @@ -1440,7 +1439,7 @@ export default { automated: 'Automatizado', liveAgent: 'Agente en vivo', instant: 'Instantáneo', - electronicFundsInstantFeeMin: 'Mínimo US$0,25', + electronicFundsInstantFeeMin: ({amount}: TermsParams) => `Mínimo ${amount}`, }, }, activateStep: { diff --git a/src/languages/types.ts b/src/languages/types.ts index 3185b7a8f6f1..b288ccaeb703 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -287,6 +287,8 @@ type TranslationFlatObject = { [TKey in TranslationPaths]: TranslateType; }; +type TermsParams = {amount: string}; + export type { ApprovedAmountParams, AddressLineParams, @@ -353,6 +355,7 @@ export type { StepCounterParams, TagSelectionParams, TaskCreatedActionParams, + TermsParams, ThreadRequestReportNameParams, ThreadSentMoneyReportNameParams, ToValidateLoginParams, diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index 4147d38a98c0..bdbe87f27b4d 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -10,56 +10,57 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useLocalize from "@hooks/useLocalize"; import CONST from '@src/CONST'; +import * as CurrencyUtils from "@libs/CurrencyUtils"; function LongTermsForm() { const theme = useTheme(); const styles = useThemeStyles(); - const {translate} = useLocalize(); + const {translate, numberFormat} = useLocalize(); const termsData = [ { title: translate('termsStep.longTermsForm.openingAccountTitle'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.openingAccountDetails'), }, { title: translate('termsStep.monthlyFee'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.monthlyFeeDetails'), }, { title: translate('termsStep.longTermsForm.customerServiceTitle'), subTitle: translate('termsStep.longTermsForm.automated'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.customerServiceDetails'), }, { title: translate('termsStep.longTermsForm.customerServiceTitle'), subTitle: translate('termsStep.longTermsForm.liveAgent'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.customerServiceDetails'), }, { title: translate('termsStep.inactivity'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.inactivityDetails'), }, { title: translate('termsStep.longTermsForm.sendingFundsTitle'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.sendingFundsDetails'), }, { title: translate('termsStep.electronicFundsWithdrawal'), subTitle: translate('termsStep.standard'), - rightText: translate('termsStep.feeAmountZero'), + rightText: CurrencyUtils.convertToDisplayString(0, 'USD'), details: translate('termsStep.longTermsForm.electronicFundsStandardDetails'), }, { title: translate('termsStep.electronicFundsWithdrawal'), subTitle: translate('termsStep.longTermsForm.instant'), - rightText: translate('termsStep.electronicFundsInstantFee'), - subRightText: translate('termsStep.longTermsForm.electronicFundsInstantFeeMin'), + rightText: `${numberFormat(1.5)}%`, + subRightText: translate('termsStep.longTermsForm.electronicFundsInstantFeeMin', {amount: CurrencyUtils.convertToDisplayString(25, 'USD')}), details: translate('termsStep.longTermsForm.electronicFundsInstantDetails'), }, ]; diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js index c42b3b23ec2f..3d96d2209dd5 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js @@ -19,7 +19,7 @@ const defaultProps = { function ShortTermsForm(props) { const styles = useThemeStyles(); - const {translate} = useLocalize(); + const {translate, numberFormat} = useLocalize(); return ( <> @@ -130,8 +130,8 @@ function ShortTermsForm(props) { - {translate('termsStep.electronicFundsInstantFee')} - {translate('termsStep.shortTermsForm.electronicFundsInstantFeeMin')} + {numberFormat(1.5)}% + {translate('termsStep.shortTermsForm.electronicFundsInstantFeeMin', {amount: CurrencyUtils.convertToDisplayString(25, 'USD')})} From c3222d8e7803bda371f4c9810343a1a0cc1f3a6a Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 19:38:04 +0100 Subject: [PATCH 120/170] Another one --- src/languages/en.ts | 7 +++---- src/languages/es.ts | 7 +++---- src/pages/EnablePayments/TermsPage/LongTermsForm.js | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 7db0c9be37d1..43972bd7a023 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1404,10 +1404,9 @@ export default { 'There is a fee to transfer funds from your Expensify Wallet to ' + 'your linked debit card using the instant transfer option. This transfer usually completes within ' + 'several minutes. The fee is 1.5% of the transfer amount (with a minimum fee of $0.25).', - fdicInsuranceBancorp: - 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + - `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + - `to $250,000 by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, + fdicInsuranceBancorp: ({amount}: TermsParams) => 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + + `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + + `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, fdicInsuranceBancorp2: 'for details.', contactExpensifyPayments: `Contact ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} by calling +1 833-400-0904, by email at`, contactExpensifyPayments2: 'or sign in at', diff --git a/src/languages/es.ts b/src/languages/es.ts index 2d2a0c0165b8..cc3b09d491f0 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1426,10 +1426,9 @@ export default { 'la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + 'generalmente se completa dentro de varios minutos. La tarifa es el 1,5% del importe de la ' + 'transferencia (con una tarifa mínima de US$0,25). ', - fdicInsuranceBancorp: - 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + - `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + - `están asegurados hasta US$250.000 por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, + fdicInsuranceBancorp: ({amount}: TermsParams) => 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + + `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + + `están asegurados hasta ${amount} por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, fdicInsuranceBancorp2: 'para más detalles.', contactExpensifyPayments: `Comunícate con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, o por correo electrónico a`, contactExpensifyPayments2: 'o inicie sesión en', diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index bdbe87f27b4d..17015df2de52 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -88,7 +88,7 @@ function LongTermsForm() { {getLongTermsSections()} - {translate('termsStep.longTermsForm.fdicInsuranceBancorp')} {CONST.TERMS.FDIC_PREPAID}{' '} + {translate('termsStep.longTermsForm.fdicInsuranceBancorp', {amount: CurrencyUtils.convertToDisplayString(25000000, 'USD')})} {CONST.TERMS.FDIC_PREPAID}{' '} {translate('termsStep.longTermsForm.fdicInsuranceBancorp2')} {translate('termsStep.noOverdraftOrCredit')} From da41efb6a64e444c7611ad528c9d702e42012dbf Mon Sep 17 00:00:00 2001 From: Sibtain Ali Date: Tue, 23 Jan 2024 23:47:20 +0500 Subject: [PATCH 121/170] feat: merge with main --- src/components/ReportActionItem/MoneyReportView.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/ReportActionItem/MoneyReportView.tsx b/src/components/ReportActionItem/MoneyReportView.tsx index ae3cc4c91b86..ed7c05b828a9 100644 --- a/src/components/ReportActionItem/MoneyReportView.tsx +++ b/src/components/ReportActionItem/MoneyReportView.tsx @@ -21,8 +21,8 @@ import * as ReportUtils from '@libs/ReportUtils'; import AnimatedEmptyStateBackground from '@pages/home/report/AnimatedEmptyStateBackground'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; import ROUTES from '@src/ROUTES'; +import type {Policy, PolicyReportField, Report} from '@src/types/onyx'; type MoneyReportViewComponentProps = { /** The report currently being looked at */ From f8dc358cdc564546208b34c954a00b7c496f359b Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 19:57:37 +0100 Subject: [PATCH 122/170] Last one --- src/languages/en.ts | 6 +++--- src/languages/es.ts | 8 ++++---- src/languages/types.ts | 3 +++ src/pages/EnablePayments/TermsPage/LongTermsForm.js | 2 +- 4 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 43972bd7a023..f49befad2c94 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -19,6 +19,7 @@ import type { DeleteConfirmationParams, DidSplitAmountMessageParams, EditActionParams, + ElectronicFundsParams, EnterMagicCodeParams, FormattedMaxLengthParams, GoBackMessageParams, @@ -1400,10 +1401,9 @@ export default { 'There is no fee to transfer funds from your Expensify Wallet ' + 'to your bank account using the standard option. This transfer usually completes within 1-3 business' + ' days.', - electronicFundsInstantDetails: - 'There is a fee to transfer funds from your Expensify Wallet to ' + + electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => 'There is a fee to transfer funds from your Expensify Wallet to ' + 'your linked debit card using the instant transfer option. This transfer usually completes within ' + - 'several minutes. The fee is 1.5% of the transfer amount (with a minimum fee of $0.25).', + `several minutes. The fee is ${percentage}% of the transfer amount (with a minimum fee of ${amount}).`, fdicInsuranceBancorp: ({amount}: TermsParams) => 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, diff --git a/src/languages/es.ts b/src/languages/es.ts index cc3b09d491f0..20a0d6042019 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -18,6 +18,7 @@ import type { DeleteConfirmationParams, DidSplitAmountMessageParams, EditActionParams, + ElectronicFundsParams, EnglishTranslation, EnterMagicCodeParams, FormattedMaxLengthParams, @@ -1421,11 +1422,10 @@ export default { 'No hay cargo por transferir fondos desde tu billetera Expensify ' + 'a tu cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' + '1-3 días laborables.', - electronicFundsInstantDetails: - 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + + electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + 'la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + - 'generalmente se completa dentro de varios minutos. La tarifa es el 1,5% del importe de la ' + - 'transferencia (con una tarifa mínima de US$0,25). ', + `generalmente se completa dentro de varios minutos. La tarifa es el ${percentage}% del importe de la ` + + `transferencia (con una tarifa mínima de ${amount}). `, fdicInsuranceBancorp: ({amount}: TermsParams) => 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + `están asegurados hasta ${amount} por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, diff --git a/src/languages/types.ts b/src/languages/types.ts index b288ccaeb703..11adf01ac252 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -289,6 +289,8 @@ type TranslationFlatObject = { type TermsParams = {amount: string}; +type ElectronicFundsParams = {percentage: string; amount: string}; + export type { ApprovedAmountParams, AddressLineParams, @@ -307,6 +309,7 @@ export type { DeleteConfirmationParams, DidSplitAmountMessageParams, EditActionParams, + ElectronicFundsParams, EnglishTranslation, EnterMagicCodeParams, FormattedMaxLengthParams, diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index 17015df2de52..0f96e5a10417 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -61,7 +61,7 @@ function LongTermsForm() { subTitle: translate('termsStep.longTermsForm.instant'), rightText: `${numberFormat(1.5)}%`, subRightText: translate('termsStep.longTermsForm.electronicFundsInstantFeeMin', {amount: CurrencyUtils.convertToDisplayString(25, 'USD')}), - details: translate('termsStep.longTermsForm.electronicFundsInstantDetails'), + details: translate('termsStep.longTermsForm.electronicFundsInstantDetails', {percentage: numberFormat(1.5), amount: CurrencyUtils.convertToDisplayString(25, 'USD')}), }, ]; From caaee618b6a31a70edd3fccc32367e6860420631 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 20:05:14 +0100 Subject: [PATCH 123/170] Prettier --- src/languages/en.ts | 10 ++++++---- src/languages/es.ts | 10 ++++++---- src/pages/EnablePayments/TermsPage/LongTermsForm.js | 8 ++++---- src/pages/EnablePayments/TermsPage/ShortTermsForm.js | 4 ++-- 4 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index f49befad2c94..121214fa7493 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -1401,12 +1401,14 @@ export default { 'There is no fee to transfer funds from your Expensify Wallet ' + 'to your bank account using the standard option. This transfer usually completes within 1-3 business' + ' days.', - electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => 'There is a fee to transfer funds from your Expensify Wallet to ' + + electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => + 'There is a fee to transfer funds from your Expensify Wallet to ' + 'your linked debit card using the instant transfer option. This transfer usually completes within ' + `several minutes. The fee is ${percentage}% of the transfer amount (with a minimum fee of ${amount}).`, - fdicInsuranceBancorp: ({amount}: TermsParams) => 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + - `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + - `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, + fdicInsuranceBancorp: ({amount}: TermsParams) => + 'Your funds are eligible for FDIC insurance. Your funds will be held at or ' + + `transferred to ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, an FDIC-insured institution. Once there, your funds are insured up ` + + `to ${amount} by the FDIC in the event ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} fails. See`, fdicInsuranceBancorp2: 'for details.', contactExpensifyPayments: `Contact ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} by calling +1 833-400-0904, by email at`, contactExpensifyPayments2: 'or sign in at', diff --git a/src/languages/es.ts b/src/languages/es.ts index 20a0d6042019..dca38136a02d 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -1422,13 +1422,15 @@ export default { 'No hay cargo por transferir fondos desde tu billetera Expensify ' + 'a tu cuenta bancaria utilizando la opción estándar. Esta transferencia generalmente se completa en' + '1-3 días laborables.', - electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + + electronicFundsInstantDetails: ({percentage, amount}: ElectronicFundsParams) => + 'Hay una tarifa para transferir fondos desde tu billetera Expensify a ' + 'la tarjeta de débito vinculada utilizando la opción de transferencia instantánea. Esta transferencia ' + `generalmente se completa dentro de varios minutos. La tarifa es el ${percentage}% del importe de la ` + `transferencia (con una tarifa mínima de ${amount}). `, - fdicInsuranceBancorp: ({amount}: TermsParams) => 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + - `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + - `están asegurados hasta ${amount} por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, + fdicInsuranceBancorp: ({amount}: TermsParams) => + 'Tus fondos pueden acogerse al seguro de la FDIC. Tus fondos se mantendrán o serán ' + + `transferidos a ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK}, una institución asegurada por la FDIC. Una vez allí, tus fondos ` + + `están asegurados hasta ${amount} por la FDIC en caso de que ${CONST.WALLET.PROGRAM_ISSUERS.BANCORP_BANK} quiebre. Ver`, fdicInsuranceBancorp2: 'para más detalles.', contactExpensifyPayments: `Comunícate con ${CONST.WALLET.PROGRAM_ISSUERS.EXPENSIFY_PAYMENTS} llamando al + 1833-400-0904, o por correo electrónico a`, contactExpensifyPayments2: 'o inicie sesión en', diff --git a/src/pages/EnablePayments/TermsPage/LongTermsForm.js b/src/pages/EnablePayments/TermsPage/LongTermsForm.js index 0f96e5a10417..fad19c5ecf6f 100644 --- a/src/pages/EnablePayments/TermsPage/LongTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/LongTermsForm.js @@ -6,11 +6,11 @@ import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import useLocalize from "@hooks/useLocalize"; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import CONST from '@src/CONST'; -import * as CurrencyUtils from "@libs/CurrencyUtils"; function LongTermsForm() { const theme = useTheme(); @@ -93,8 +93,8 @@ function LongTermsForm() { {translate('termsStep.noOverdraftOrCredit')} - {translate('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE}{' '} - {translate('termsStep.longTermsForm.contactExpensifyPayments2')} {CONST.NEW_EXPENSIFY_URL}. + {translate('termsStep.longTermsForm.contactExpensifyPayments')} {CONST.EMAIL.CONCIERGE} {translate('termsStep.longTermsForm.contactExpensifyPayments2')}{' '} + {CONST.NEW_EXPENSIFY_URL}. {translate('termsStep.longTermsForm.generalInformation')} {CONST.TERMS.CFPB_PREPAID} diff --git a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js index 3d96d2209dd5..40824f47b036 100644 --- a/src/pages/EnablePayments/TermsPage/ShortTermsForm.js +++ b/src/pages/EnablePayments/TermsPage/ShortTermsForm.js @@ -2,11 +2,11 @@ import React from 'react'; import {View} from 'react-native'; import Text from '@components/Text'; import TextLink from '@components/TextLink'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as CurrencyUtils from '@libs/CurrencyUtils'; import userWalletPropTypes from '@pages/EnablePayments/userWalletPropTypes'; import CONST from '@src/CONST'; -import useLocalize from "@hooks/useLocalize"; -import * as CurrencyUtils from "@libs/CurrencyUtils"; const propTypes = { /** The user's wallet */ From fe4fd495e59e083a6d42cca64e11f0a1de5dafa4 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 23 Jan 2024 20:58:50 +0100 Subject: [PATCH 124/170] Requested review changes --- src/libs/SuggestionUtils.ts | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/libs/SuggestionUtils.ts b/src/libs/SuggestionUtils.ts index a51acbdab579..96379ce49ef3 100644 --- a/src/libs/SuggestionUtils.ts +++ b/src/libs/SuggestionUtils.ts @@ -1,10 +1,14 @@ import CONST from '@src/CONST'; -/** Trims first character of the string if it is a space */ +/** + * Trims first character of the string if it is a space + */ function trimLeadingSpace(str: string): string { return str.startsWith(' ') ? str.slice(1) : str; } -/** Checks if space is available to render large suggestion menu */ +/** + * Checks if space is available to render large suggestion menu + */ function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight: number, totalSuggestions: number): boolean { const maxSuggestions = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER; const chatFooterHeight = CONST.CHAT_FOOTER_SECONDARY_ROW_HEIGHT + 2 * CONST.CHAT_FOOTER_SECONDARY_ROW_PADDING; From b0397fb13d2b48745e6ba2fea2181274a9b43950 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 23 Jan 2024 21:22:21 +0100 Subject: [PATCH 125/170] Bold options in all SelectionLists --- src/components/SelectionList/BaseListItem.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/SelectionList/BaseListItem.tsx b/src/components/SelectionList/BaseListItem.tsx index 217e4dab5d67..71845931ba52 100644 --- a/src/components/SelectionList/BaseListItem.tsx +++ b/src/components/SelectionList/BaseListItem.tsx @@ -105,7 +105,7 @@ function BaseListItem({ textStyles={[ styles.optionDisplayName, isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText, - isUserItem ? styles.sidebarLinkTextBold : null, + styles.sidebarLinkTextBold, styles.pre, item.alternateText ? styles.mb1 : null, ]} From fdee1b8483288e41b1db79c3a70ece66c57276cd Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 23 Jan 2024 21:27:01 +0100 Subject: [PATCH 126/170] Fix typescript --- src/components/SelectionList/types.ts | 4 ++-- src/components/SubscriptAvatar.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 72b3510909b8..4b99e1127dac 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,6 +1,6 @@ import type {ReactElement, ReactNode} from 'react'; import type {GestureResponderEvent, InputModeOptions, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native'; -import type {SubAvatar} from '@components/SubscriptAvatar'; +import type {SubscriptAvatarProps} from '@components/SubscriptAvatar'; import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; @@ -59,7 +59,7 @@ type User = { rightElement?: ReactElement; /** Icons for the user (can be multiple if it's a Workspace) */ - icons?: SubAvatar[]; + icons?: SubscriptAvatarProps[]; /** Errors that this user may contain */ errors?: Errors; diff --git a/src/components/SubscriptAvatar.tsx b/src/components/SubscriptAvatar.tsx index 936dbd3e3713..bcbca6e2958b 100644 --- a/src/components/SubscriptAvatar.tsx +++ b/src/components/SubscriptAvatar.tsx @@ -86,4 +86,4 @@ function SubscriptAvatar({mainAvatar, secondaryAvatar, size = CONST.AVATAR_SIZE. SubscriptAvatar.displayName = 'SubscriptAvatar'; export default memo(SubscriptAvatar); -export type {SubAvatar}; +export type {SubscriptAvatarProps}; From 27e35d7fe53d3b9bd1a68b95b5e184afe49b93e5 Mon Sep 17 00:00:00 2001 From: Filip Solecki Date: Tue, 23 Jan 2024 22:08:03 +0100 Subject: [PATCH 127/170] Fix Icon type --- src/components/SelectionList/types.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index 4b99e1127dac..a82ddef6febb 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -1,7 +1,6 @@ import type {ReactElement, ReactNode} from 'react'; import type {GestureResponderEvent, InputModeOptions, SectionListData, StyleProp, TextStyle, ViewStyle} from 'react-native'; -import type {SubscriptAvatarProps} from '@components/SubscriptAvatar'; -import type {Errors, PendingAction} from '@src/types/onyx/OnyxCommon'; +import type {Errors, Icon, PendingAction} from '@src/types/onyx/OnyxCommon'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; type CommonListItemProps = { @@ -59,7 +58,7 @@ type User = { rightElement?: ReactElement; /** Icons for the user (can be multiple if it's a Workspace) */ - icons?: SubscriptAvatarProps[]; + icons?: Icon[]; /** Errors that this user may contain */ errors?: Errors; From 970183a3393eef074145d88e511c45bd87b454fb Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 23 Jan 2024 16:11:46 -0800 Subject: [PATCH 128/170] Fix typo in merge conflict resolution --- src/components/LHNOptionsList/OptionRowLHN.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 09e10ab98c12..b085625c2914 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -61,15 +61,13 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const textStyle = isFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; const textUnreadStyle = optionItem?.isUnread && optionItem.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; const displayNameStyle = [styles.optionDisplayName, styles.optionDisplayNameCompact, styles.pre, textUnreadStyle, style]; - const alternateTextStyle = - isInFocusMode - ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style] - : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, style]; + const alternateTextStyle = isInFocusMode + ? [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, styles.optionAlternateTextCompact, styles.ml2, style] + : [textStyle, styles.optionAlternateText, styles.pre, styles.textLabelSupporting, style]; - const contentContainerStyles = - isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; + const contentContainerStyles = isInFocusMode ? [styles.flex1, styles.flexRow, styles.overflowHidden, StyleUtils.getCompactContentContainerStyles()] : [styles.flex1]; const sidebarInnerRowStyle = StyleSheet.flatten( - IsInFocusMode + isInFocusMode ? [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRowCompact, styles.justifyContentCenter] : [styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter], ); From 9c9c654f7996df03a781eb16b0c40dfcc6b89fe6 Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 23 Jan 2024 16:14:47 -0800 Subject: [PATCH 129/170] Don't include muted reports in unread count --- src/libs/UnreadIndicatorUpdater/index.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/libs/UnreadIndicatorUpdater/index.ts b/src/libs/UnreadIndicatorUpdater/index.ts index 2a7019686308..b4f3cd34a8c4 100644 --- a/src/libs/UnreadIndicatorUpdater/index.ts +++ b/src/libs/UnreadIndicatorUpdater/index.ts @@ -26,8 +26,12 @@ export default function getUnreadReportsForUnreadIndicator(reports: OnyxCollecti * Chats with hidden preference remain invisible in the LHN and are not considered "unread." * They are excluded from the LHN rendering, but not filtered from the "option list." * This ensures they appear in Search, but not in the LHN or unread count. + * + * Furthermore, muted reports may or may not appear in the LHN depending on priority mode, + * but they should not be considered in the unread indicator count. */ - report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN, + report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.HIDDEN && + report?.notificationPreference !== CONST.REPORT.NOTIFICATION_PREFERENCE.MUTE, ); } From e10613266db79081bd75b7e632573d2c30f62768 Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Wed, 24 Jan 2024 06:57:09 +0100 Subject: [PATCH 130/170] fix android picker text color in light theme --- src/components/Picker/BasePicker.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/Picker/BasePicker.tsx b/src/components/Picker/BasePicker.tsx index 678bb6f06403..1bee95532104 100644 --- a/src/components/Picker/BasePicker.tsx +++ b/src/components/Picker/BasePicker.tsx @@ -49,10 +49,6 @@ function BasePicker( // reference to @react-native-picker/picker const picker = useRef(null); - // Windows will reuse the text color of the select for each one of the options - // so we might need to color accordingly so it doesn't blend with the background. - const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: theme.text} : {}; - useEffect(() => { if (!!value || !items || items.length !== 1 || !onInputChange) { return; @@ -152,6 +148,10 @@ function BasePicker( return theme.text; }, [theme]); + // Windows will reuse the text color of the select for each one of the options + // so we might need to color accordingly so it doesn't blend with the background. + const pickerPlaceholder = Object.keys(placeholder).length > 0 ? {...placeholder, color: itemColor} : {}; + const hasError = !!errorText; if (isDisabled) { From ecf641ed69d9cb37ff16de3bea129f653211618d Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 24 Jan 2024 07:21:58 +0000 Subject: [PATCH 131/170] Update version to 1.4.31-0 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 4 ++-- ios/NewExpensifyTests/Info.plist | 4 ++-- ios/NotificationServiceExtension/Info.plist | 4 ++-- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 425a4c2ddd4b..7d8f11d2c446 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043001 - versionName "1.4.30-1" + versionCode 1001043100 + versionName "1.4.31-0" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 86b6c0374898..348bf04c1c8d 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -19,7 +19,7 @@ CFBundlePackageType APPL CFBundleShortVersionString - 1.4.30 + 1.4.31 CFBundleSignature ???? CFBundleURLTypes @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.30.1 + 1.4.31.0 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 9c3f1c7d74f6..90a43a7b7e8f 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -15,10 +15,10 @@ CFBundlePackageType BNDL CFBundleShortVersionString - 1.4.30 + 1.4.31 CFBundleSignature ???? CFBundleVersion - 1.4.30.1 + 1.4.31.0 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 3461a6f34880..bda4092cacc7 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -3,9 +3,9 @@ CFBundleShortVersionString - 1.4.30 + 1.4.31 CFBundleVersion - 1.4.30.1 + 1.4.31.0 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index bf806ea2ab76..8060ccc46be9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.30-1", + "version": "1.4.31-0", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.30-1", + "version": "1.4.31-0", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 002f1468a811..538bc231d6ac 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.30-1", + "version": "1.4.31-0", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 2bfd72289d32e66b4650764abe9fb0ed18473f2e Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 24 Jan 2024 07:38:53 +0000 Subject: [PATCH 132/170] Update version to 1.4.31-1 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index 7d8f11d2c446..a5e95b58c434 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043100 - versionName "1.4.31-0" + versionCode 1001043101 + versionName "1.4.31-1" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index 348bf04c1c8d..d39050776c39 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.0 + 1.4.31.1 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 90a43a7b7e8f..2e70022b92b1 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.0 + 1.4.31.1 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index bda4092cacc7..8e84b096b8b1 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.0 + 1.4.31.1 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index 8060ccc46be9..ab22eda99adf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-0", + "version": "1.4.31-1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-0", + "version": "1.4.31-1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 538bc231d6ac..06c3db7c9d53 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-0", + "version": "1.4.31-1", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 7c8336b07fc3735708f6b648982c42ce2f87da60 Mon Sep 17 00:00:00 2001 From: OSBotify Date: Wed, 24 Jan 2024 07:51:21 +0000 Subject: [PATCH 133/170] Update version to 1.4.31-2 --- android/app/build.gradle | 4 ++-- ios/NewExpensify/Info.plist | 2 +- ios/NewExpensifyTests/Info.plist | 2 +- ios/NotificationServiceExtension/Info.plist | 2 +- package-lock.json | 4 ++-- package.json | 2 +- 6 files changed, 8 insertions(+), 8 deletions(-) diff --git a/android/app/build.gradle b/android/app/build.gradle index a5e95b58c434..49f1b017d5e0 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -98,8 +98,8 @@ android { minSdkVersion rootProject.ext.minSdkVersion targetSdkVersion rootProject.ext.targetSdkVersion multiDexEnabled rootProject.ext.multiDexEnabled - versionCode 1001043101 - versionName "1.4.31-1" + versionCode 1001043102 + versionName "1.4.31-2" } flavorDimensions "default" diff --git a/ios/NewExpensify/Info.plist b/ios/NewExpensify/Info.plist index d39050776c39..34341662d137 100644 --- a/ios/NewExpensify/Info.plist +++ b/ios/NewExpensify/Info.plist @@ -40,7 +40,7 @@ CFBundleVersion - 1.4.31.1 + 1.4.31.2 ITSAppUsesNonExemptEncryption LSApplicationQueriesSchemes diff --git a/ios/NewExpensifyTests/Info.plist b/ios/NewExpensifyTests/Info.plist index 2e70022b92b1..38073f64d814 100644 --- a/ios/NewExpensifyTests/Info.plist +++ b/ios/NewExpensifyTests/Info.plist @@ -19,6 +19,6 @@ CFBundleSignature ???? CFBundleVersion - 1.4.31.1 + 1.4.31.2 diff --git a/ios/NotificationServiceExtension/Info.plist b/ios/NotificationServiceExtension/Info.plist index 8e84b096b8b1..8550e23db7b1 100644 --- a/ios/NotificationServiceExtension/Info.plist +++ b/ios/NotificationServiceExtension/Info.plist @@ -5,7 +5,7 @@ CFBundleShortVersionString 1.4.31 CFBundleVersion - 1.4.31.1 + 1.4.31.2 NSExtension NSExtensionPointIdentifier diff --git a/package-lock.json b/package-lock.json index ab22eda99adf..f05837e853ba 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "new.expensify", - "version": "1.4.31-1", + "version": "1.4.31-2", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "new.expensify", - "version": "1.4.31-1", + "version": "1.4.31-2", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 06c3db7c9d53..2ba358b438e6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "new.expensify", - "version": "1.4.31-1", + "version": "1.4.31-2", "author": "Expensify, Inc.", "homepage": "https://new.expensify.com", "description": "New Expensify is the next generation of Expensify: a reimagination of payments based atop a foundation of chat.", From 2ff79c0afc5bf287b8ae9eeae04029bf0b910e6f Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jan 2024 10:10:34 +0100 Subject: [PATCH 134/170] Replace nullish coalescing with logical or --- src/components/StatePicker/StateSelectorModal.tsx | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index 5be88a77f887..cc6f88617907 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -82,14 +82,18 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat testID={StateSelectorModal.displayName} > Date: Wed, 24 Jan 2024 15:16:15 +0530 Subject: [PATCH 135/170] removed comment --- src/pages/PrivateNotes/PrivateNotesEditPage.tsx | 2 -- src/pages/PrivateNotes/PrivateNotesListPage.tsx | 2 -- 2 files changed, 4 deletions(-) diff --git a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx index 6292a2e3c412..6b96ee657d65 100644 --- a/src/pages/PrivateNotes/PrivateNotesEditPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesEditPage.tsx @@ -31,8 +31,6 @@ import type {PersonalDetails, Report} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; type PrivateNotesEditPageOnyxProps = { - /* Onyx Props */ - /** All of the personal details for everyone */ personalDetailsList: OnyxCollection; }; diff --git a/src/pages/PrivateNotes/PrivateNotesListPage.tsx b/src/pages/PrivateNotes/PrivateNotesListPage.tsx index 30bd90bed5b6..d7fb1f6497be 100644 --- a/src/pages/PrivateNotes/PrivateNotesListPage.tsx +++ b/src/pages/PrivateNotes/PrivateNotesListPage.tsx @@ -17,8 +17,6 @@ import ROUTES from '@src/ROUTES'; import type {PersonalDetails, Report, Session} from '@src/types/onyx'; type PrivateNotesListPageOnyxProps = { - /* Onyx Props */ - /** All of the personal details for everyone */ personalDetailsList: OnyxCollection; From 260f78e3be8550dd01ac78b8a91d255bfa75d3b0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 10:59:51 +0100 Subject: [PATCH 136/170] fix: add correct condition --- src/components/LHNOptionsList/OptionRowLHN.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index 1932cf6c6b7f..a36a9b2b8451 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -113,7 +113,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const report = ReportUtils.getReport(optionItem.reportID ?? ''); const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null); - const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips?.length ?? 0) > 2; + const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && !optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips?.length ?? 0) > 2; const fullTitle = isGroupChat ? getGroupChatName(!isEmptyObject(report) ? report : null) : optionItem.text; const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar; From a9685ca2c8e3dc4f8c1314f9916b2d4c17001501 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 11:20:33 +0100 Subject: [PATCH 137/170] fix: add more strict comparision --- src/components/LHNOptionsList/OptionRowLHN.tsx | 2 +- src/types/onyx/Report.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/LHNOptionsList/OptionRowLHN.tsx b/src/components/LHNOptionsList/OptionRowLHN.tsx index a36a9b2b8451..218cdc70a86e 100644 --- a/src/components/LHNOptionsList/OptionRowLHN.tsx +++ b/src/components/LHNOptionsList/OptionRowLHN.tsx @@ -113,7 +113,7 @@ function OptionRowLHN({reportID, isFocused = false, onSelectRow = () => {}, opti const report = ReportUtils.getReport(optionItem.reportID ?? ''); const isStatusVisible = !!emojiCode && ReportUtils.isOneOnOneChat(!isEmptyObject(report) ? report : null); - const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && !optionItem.chatType && !optionItem.isThread && (optionItem.displayNamesWithTooltips?.length ?? 0) > 2; + const isGroupChat = optionItem.type === CONST.REPORT.TYPE.CHAT && optionItem.chatType !== '' && !optionItem.isThread && (optionItem.displayNamesWithTooltips?.length ?? 0) > 2; const fullTitle = isGroupChat ? getGroupChatName(!isEmptyObject(report) ? report : null) : optionItem.text; const subscriptAvatarBorderColor = isFocused ? focusedBackgroundColor : theme.sidebar; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index b1571e7514e4..ab04126ff782 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -15,7 +15,7 @@ type Note = { type Report = { /** The specific type of chat */ - chatType?: ValueOf; + chatType?: ValueOf | ''; /** Whether the report has a child that is an outstanding money request that is awaiting action from the current user */ hasOutstandingChildRequest?: boolean; From 7617114e87e97f4c7a4dd0cea0ba2b82155cba92 Mon Sep 17 00:00:00 2001 From: VickyStash Date: Wed, 24 Jan 2024 11:30:59 +0100 Subject: [PATCH 138/170] Remove outdated TODO --- src/components/StatePicker/StateSelectorModal.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/StatePicker/StateSelectorModal.tsx b/src/components/StatePicker/StateSelectorModal.tsx index cc6f88617907..798d3be7a698 100644 --- a/src/components/StatePicker/StateSelectorModal.tsx +++ b/src/components/StatePicker/StateSelectorModal.tsx @@ -89,7 +89,6 @@ function StateSelectorModal({currentState, isVisible, onClose = () => {}, onStat onBackButtonPress={onClose} /> Date: Wed, 24 Jan 2024 10:43:29 +0000 Subject: [PATCH 139/170] Revert "Handle API errors to trigger force upgrades of the app " --- assets/animations/Update.lottie | Bin 88965 -> 0 bytes src/CONST.ts | 4 -- src/Expensify.js | 16 +---- src/ONYXKEYS.ts | 4 -- .../ErrorBoundary/BaseErrorBoundary.tsx | 14 ++-- src/components/LottieAnimations/index.tsx | 6 -- src/languages/en.ts | 6 -- src/languages/es.ts | 6 -- .../Environment/betaChecker/index.android.ts | 2 +- src/libs/HttpUtils.ts | 5 -- .../LocalNotification/BrowserNotifications.ts | 2 +- .../{AppUpdate/index.ts => AppUpdate.ts} | 3 +- .../AppUpdate/updateApp/index.android.ts | 6 -- .../AppUpdate/updateApp/index.desktop.ts | 6 -- .../actions/AppUpdate/updateApp/index.ios.ts | 6 -- src/libs/actions/AppUpdate/updateApp/index.ts | 6 -- src/libs/actions/UpdateRequired.ts | 22 ------- .../migrations/PersonalDetailsByAccountID.js | 6 ++ src/pages/ErrorPage/UpdateRequiredView.tsx | 60 ------------------ src/styles/index.ts | 13 ---- src/styles/utils/index.ts | 8 --- src/styles/utils/spacing.ts | 4 -- src/styles/variables.ts | 4 -- tests/unit/MigrationTest.js | 25 ++++++++ 24 files changed, 39 insertions(+), 195 deletions(-) delete mode 100644 assets/animations/Update.lottie rename src/libs/actions/{AppUpdate/index.ts => AppUpdate.ts} (71%) delete mode 100644 src/libs/actions/AppUpdate/updateApp/index.android.ts delete mode 100644 src/libs/actions/AppUpdate/updateApp/index.desktop.ts delete mode 100644 src/libs/actions/AppUpdate/updateApp/index.ios.ts delete mode 100644 src/libs/actions/AppUpdate/updateApp/index.ts delete mode 100644 src/libs/actions/UpdateRequired.ts delete mode 100644 src/pages/ErrorPage/UpdateRequiredView.tsx diff --git a/assets/animations/Update.lottie b/assets/animations/Update.lottie deleted file mode 100644 index 363486ec2267b6a7e131f974562fa829706bf324..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 88965 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�oLpDUoRyCB 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 gK=qaB*q0!L(2b4Y_SO5S3 diff --git a/src/CONST.ts b/src/CONST.ts index 5fee60e57617..cea26c789799 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -788,7 +788,6 @@ const CONST = { EXP_ERROR: 666, MANY_WRITES_ERROR: 665, UNABLE_TO_RETRY: 'unableToRetry', - UPDATE_REQUIRED: 426, }, HTTP_STATUS: { // When Cloudflare throttles @@ -819,9 +818,6 @@ const CONST = { GATEWAY_TIMEOUT: 'Gateway Timeout', EXPENSIFY_SERVICE_INTERRUPTED: 'Expensify service interrupted', DUPLICATE_RECORD: 'A record already exists with this ID', - - // The "Upgrade" is intentional as the 426 HTTP code means "Upgrade Required" and sent by the API. We use the "Update" language everywhere else in the front end when this gets returned. - UPDATE_REQUIRED: 'Upgrade Required', }, ERROR_TYPE: { SOCKET: 'Expensify\\Auth\\Error\\Socket', diff --git a/src/Expensify.js b/src/Expensify.js index 12003968b284..0707ba069241 100644 --- a/src/Expensify.js +++ b/src/Expensify.js @@ -13,7 +13,6 @@ import AppleAuthWrapper from './components/SignInButtons/AppleAuthWrapper'; import SplashScreenHider from './components/SplashScreenHider'; import UpdateAppModal from './components/UpdateAppModal'; import withLocalize, {withLocalizePropTypes} from './components/withLocalize'; -import CONST from './CONST'; import * as EmojiPickerAction from './libs/actions/EmojiPickerAction'; import * as Report from './libs/actions/Report'; import * as User from './libs/actions/User'; @@ -77,9 +76,6 @@ const propTypes = { /** Whether the app is waiting for the server's response to determine if a room is public */ isCheckingPublicRoom: PropTypes.bool, - /** True when the user must update to the latest minimum version of the app */ - updateRequired: PropTypes.bool, - /** Whether we should display the notification alerting the user that focus mode has been auto-enabled */ focusModeNotification: PropTypes.bool, @@ -95,7 +91,6 @@ const defaultProps = { isSidebarLoaded: false, screenShareRequest: null, isCheckingPublicRoom: true, - updateRequired: false, focusModeNotification: false, }; @@ -209,10 +204,6 @@ function Expensify(props) { return null; } - if (props.updateRequired) { - throw new Error(CONST.ERROR.UPDATE_REQUIRED); - } - return ( {/* We include the modal for showing a new update at the top level so the option is always present. */} - {/* If the update is required we won't show this option since a full screen update view will be displayed instead. */} - {props.updateAvailable && !props.updateRequired ? : null} + {props.updateAvailable ? : null} {props.screenShareRequest ? ( element MAX_CANVAS_WIDTH: 'maxCanvasWidth', - /** Indicates whether an forced upgrade is required */ - UPDATE_REQUIRED: 'updateRequired', - /** Collection Keys */ COLLECTION: { DOWNLOAD: 'download_', @@ -445,7 +442,6 @@ type OnyxValues = { [ONYXKEYS.MAX_CANVAS_AREA]: number; [ONYXKEYS.MAX_CANVAS_HEIGHT]: number; [ONYXKEYS.MAX_CANVAS_WIDTH]: number; - [ONYXKEYS.UPDATE_REQUIRED]: boolean; // Collections [ONYXKEYS.COLLECTION.DOWNLOAD]: OnyxTypes.Download; diff --git a/src/components/ErrorBoundary/BaseErrorBoundary.tsx b/src/components/ErrorBoundary/BaseErrorBoundary.tsx index 2f775aa4bef1..6a0f1a0ae55e 100644 --- a/src/components/ErrorBoundary/BaseErrorBoundary.tsx +++ b/src/components/ErrorBoundary/BaseErrorBoundary.tsx @@ -1,9 +1,7 @@ -import React, {useState} from 'react'; +import React from 'react'; import {ErrorBoundary} from 'react-error-boundary'; import BootSplash from '@libs/BootSplash'; import GenericErrorPage from '@pages/ErrorPage/GenericErrorPage'; -import UpdateRequiredView from '@pages/ErrorPage/UpdateRequiredView'; -import CONST from '@src/CONST'; import type {BaseErrorBoundaryProps, LogError} from './types'; /** @@ -13,19 +11,15 @@ import type {BaseErrorBoundaryProps, LogError} from './types'; */ function BaseErrorBoundary({logError = () => {}, errorMessage, children}: BaseErrorBoundaryProps) { - const [errorContent, setErrorContent] = useState(''); - const catchError = (errorObject: Error, errorInfo: React.ErrorInfo) => { - logError(errorMessage, errorObject, JSON.stringify(errorInfo)); + const catchError = (error: Error, errorInfo: React.ErrorInfo) => { + logError(errorMessage, error, JSON.stringify(errorInfo)); // We hide the splash screen since the error might happened during app init BootSplash.hide(); - setErrorContent(errorObject.message); }; - const updateRequired = errorContent === CONST.ERROR.UPDATE_REQUIRED; - return ( : } + fallback={} onError={catchError} > {children} diff --git a/src/components/LottieAnimations/index.tsx b/src/components/LottieAnimations/index.tsx index fd593421232d..d42d471eba5e 100644 --- a/src/components/LottieAnimations/index.tsx +++ b/src/components/LottieAnimations/index.tsx @@ -1,4 +1,3 @@ -import variables from '@styles/variables'; import type DotLottieAnimation from './types'; const DotLottieAnimations: Record = { @@ -52,11 +51,6 @@ const DotLottieAnimations: Record = { w: 853, h: 480, }, - Update: { - file: require('@assets/animations/Update.lottie'), - w: variables.updateAnimationW, - h: variables.updateAnimationH, - }, Coin: { file: require('@assets/animations/Coin.lottie'), w: 375, diff --git a/src/languages/en.ts b/src/languages/en.ts index 712113cb89a9..bb22c7a7856a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -299,7 +299,6 @@ export default { showing: 'Showing', of: 'of', default: 'Default', - update: 'Update', }, location: { useCurrent: 'Use current location', @@ -773,11 +772,6 @@ export default { isShownOnProfile: 'Your timezone is shown on your profile.', getLocationAutomatically: 'Automatically determine your location.', }, - updateRequiredView: { - updateRequired: 'Update required', - pleaseInstall: 'Please update to the latest version of New Expensify', - toGetLatestChanges: 'For mobile or desktop, download and install the latest version. For web, refresh your browser.', - }, initialSettingsPage: { about: 'About', aboutPage: { diff --git a/src/languages/es.ts b/src/languages/es.ts index d46f275a8109..858fe29a8faf 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -288,7 +288,6 @@ export default { showing: 'Mostrando', of: 'de', default: 'Predeterminado', - update: 'Actualizar', }, location: { useCurrent: 'Usar ubicación actual', @@ -767,11 +766,6 @@ export default { isShownOnProfile: 'Tu zona horaria se muestra en tu perfil.', getLocationAutomatically: 'Detecta tu ubicación automáticamente.', }, - updateRequiredView: { - updateRequired: 'Actualización requerida', - pleaseInstall: 'Por favor, actualice la última versión de Nuevo Expensify', - toGetLatestChanges: 'Para móvil o escritorio, descarga e instala la última versión. Para la web, actualiza tu navegador.', - }, initialSettingsPage: { about: 'Acerca de', aboutPage: { diff --git a/src/libs/Environment/betaChecker/index.android.ts b/src/libs/Environment/betaChecker/index.android.ts index 4b912e0daaa5..aeb1527457f7 100644 --- a/src/libs/Environment/betaChecker/index.android.ts +++ b/src/libs/Environment/betaChecker/index.android.ts @@ -1,6 +1,6 @@ import Onyx from 'react-native-onyx'; import semver from 'semver'; -import * as AppUpdate from '@libs/actions/AppUpdate'; +import * as AppUpdate from '@userActions/AppUpdate'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import pkg from '../../../../package.json'; diff --git a/src/libs/HttpUtils.ts b/src/libs/HttpUtils.ts index 16afc377bba3..22e342ac847b 100644 --- a/src/libs/HttpUtils.ts +++ b/src/libs/HttpUtils.ts @@ -6,7 +6,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {RequestType} from '@src/types/onyx/Request'; import type Response from '@src/types/onyx/Response'; import * as NetworkActions from './actions/Network'; -import * as UpdateRequired from './actions/UpdateRequired'; import * as ApiUtils from './ApiUtils'; import HttpsError from './Errors/HttpsError'; @@ -129,10 +128,6 @@ function processHTTPRequest(url: string, method: RequestType = 'get', body: Form alert('Too many auth writes', message); } } - if (response.jsonCode === CONST.JSON_CODE.UPDATE_REQUIRED) { - // Trigger a modal and disable the app as the user needs to upgrade to the latest minimum version to continue - UpdateRequired.alertUser(); - } return response as Promise; }); } diff --git a/src/libs/Notification/LocalNotification/BrowserNotifications.ts b/src/libs/Notification/LocalNotification/BrowserNotifications.ts index 0c3f3ec60203..e65bd3d0021f 100644 --- a/src/libs/Notification/LocalNotification/BrowserNotifications.ts +++ b/src/libs/Notification/LocalNotification/BrowserNotifications.ts @@ -2,9 +2,9 @@ import Str from 'expensify-common/lib/str'; import type {ImageSourcePropType} from 'react-native'; import EXPENSIFY_ICON_URL from '@assets/images/expensify-logo-round-clearspace.png'; -import * as AppUpdate from '@libs/actions/AppUpdate'; import ModifiedExpenseMessage from '@libs/ModifiedExpenseMessage'; import * as ReportUtils from '@libs/ReportUtils'; +import * as AppUpdate from '@userActions/AppUpdate'; import type {Report, ReportAction} from '@src/types/onyx'; import focusApp from './focusApp'; import type {LocalNotificationClickHandler, LocalNotificationData} from './types'; diff --git a/src/libs/actions/AppUpdate/index.ts b/src/libs/actions/AppUpdate.ts similarity index 71% rename from src/libs/actions/AppUpdate/index.ts rename to src/libs/actions/AppUpdate.ts index 69c80a089831..29ee2a4547ab 100644 --- a/src/libs/actions/AppUpdate/index.ts +++ b/src/libs/actions/AppUpdate.ts @@ -1,6 +1,5 @@ import Onyx from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; -import updateApp from './updateApp'; function triggerUpdateAvailable() { Onyx.set(ONYXKEYS.UPDATE_AVAILABLE, true); @@ -10,4 +9,4 @@ function setIsAppInBeta(isBeta: boolean) { Onyx.set(ONYXKEYS.IS_BETA, isBeta); } -export {triggerUpdateAvailable, setIsAppInBeta, updateApp}; +export {triggerUpdateAvailable, setIsAppInBeta}; diff --git a/src/libs/actions/AppUpdate/updateApp/index.android.ts b/src/libs/actions/AppUpdate/updateApp/index.android.ts deleted file mode 100644 index f6a6387a8aef..000000000000 --- a/src/libs/actions/AppUpdate/updateApp/index.android.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Link from '@userActions/Link'; -import CONST from '@src/CONST'; - -export default function updateApp() { - Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.ANDROID); -} diff --git a/src/libs/actions/AppUpdate/updateApp/index.desktop.ts b/src/libs/actions/AppUpdate/updateApp/index.desktop.ts deleted file mode 100644 index fb3a7d649baa..000000000000 --- a/src/libs/actions/AppUpdate/updateApp/index.desktop.ts +++ /dev/null @@ -1,6 +0,0 @@ -import {Linking} from 'react-native'; -import CONST from '@src/CONST'; - -export default function updateApp() { - Linking.openURL(CONST.APP_DOWNLOAD_LINKS.DESKTOP); -} diff --git a/src/libs/actions/AppUpdate/updateApp/index.ios.ts b/src/libs/actions/AppUpdate/updateApp/index.ios.ts deleted file mode 100644 index 8b66521bb9c8..000000000000 --- a/src/libs/actions/AppUpdate/updateApp/index.ios.ts +++ /dev/null @@ -1,6 +0,0 @@ -import * as Link from '@userActions/Link'; -import CONST from '@src/CONST'; - -export default function updateApp() { - Link.openExternalLink(CONST.APP_DOWNLOAD_LINKS.IOS); -} diff --git a/src/libs/actions/AppUpdate/updateApp/index.ts b/src/libs/actions/AppUpdate/updateApp/index.ts deleted file mode 100644 index 8c2b191029a2..000000000000 --- a/src/libs/actions/AppUpdate/updateApp/index.ts +++ /dev/null @@ -1,6 +0,0 @@ -/** - * On web or mWeb we can simply refresh the page and the user should have the new version of the app downloaded. - */ -export default function updateApp() { - window.location.reload(); -} diff --git a/src/libs/actions/UpdateRequired.ts b/src/libs/actions/UpdateRequired.ts deleted file mode 100644 index 26f0a119ac8d..000000000000 --- a/src/libs/actions/UpdateRequired.ts +++ /dev/null @@ -1,22 +0,0 @@ -import Onyx from 'react-native-onyx'; -import getEnvironment from '@libs/Environment/getEnvironment'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; - -function alertUser() { - // For now, we will pretty much never have to do this on a platform other than production. - // We should only update the minimum app version in the API after all platforms of a new version have been deployed to PRODUCTION. - // As staging is always ahead of production there is no reason to "force update" those apps. - getEnvironment().then((environment) => { - if (environment !== CONST.ENVIRONMENT.PRODUCTION) { - return; - } - - Onyx.set(ONYXKEYS.UPDATE_REQUIRED, true); - }); -} - -export { - // eslint-disable-next-line import/prefer-default-export - alertUser, -}; diff --git a/src/libs/migrations/PersonalDetailsByAccountID.js b/src/libs/migrations/PersonalDetailsByAccountID.js index 24aece8f5a97..c08ec6fb2c43 100644 --- a/src/libs/migrations/PersonalDetailsByAccountID.js +++ b/src/libs/migrations/PersonalDetailsByAccountID.js @@ -251,6 +251,12 @@ export default function () { delete newReport.lastActorEmail; } + if (lodashHas(newReport, ['participants'])) { + reportWasModified = true; + Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing participants from report ${newReport.reportID}`); + delete newReport.participants; + } + if (lodashHas(newReport, ['ownerEmail'])) { reportWasModified = true; Log.info(`[Migrate Onyx] PersonalDetailsByAccountID migration: removing ownerEmail from report ${newReport.reportID}`); diff --git a/src/pages/ErrorPage/UpdateRequiredView.tsx b/src/pages/ErrorPage/UpdateRequiredView.tsx deleted file mode 100644 index 2a73215d2293..000000000000 --- a/src/pages/ErrorPage/UpdateRequiredView.tsx +++ /dev/null @@ -1,60 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import Button from '@components/Button'; -import Header from '@components/Header'; -import HeaderGap from '@components/HeaderGap'; -import Lottie from '@components/Lottie'; -import LottieAnimations from '@components/LottieAnimations'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useSafeAreaInsets from '@hooks/useSafeAreaInsets'; -import useStyleUtils from '@hooks/useStyleUtils'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as AppUpdate from '@libs/actions/AppUpdate'; - -function UpdateRequiredView() { - const insets = useSafeAreaInsets(); - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); - const {translate} = useLocalize(); - const {isSmallScreenWidth} = useWindowDimensions(); - return ( - - - -
- - - - - - - {translate('updateRequiredView.pleaseInstall')} - - - {translate('updateRequiredView.toGetLatestChanges')} - - - -