From de66326d622a902ec6c3519b70941bf082139bf8 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Mon, 27 Nov 2023 16:44:30 +0800 Subject: [PATCH 01/96] 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 02/96] 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 03/96] 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 04/96] 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 05/96] 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 0095e3305c309eef535444363236f03e131b2ccf Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 29 Dec 2023 11:51:40 -0800 Subject: [PATCH 06/96] 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 07/96] 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 ad03aca8426f0ad13f65dc3b0fd75165a2a2a214 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 5 Jan 2024 15:39:53 +0700 Subject: [PATCH 08/96] 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 09/96] 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 c5131b01eb4740ad0663ea2c28ab3929613bbba2 Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Tue, 9 Jan 2024 10:57:05 +0100 Subject: [PATCH 10/96] 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 11/96] 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 12/96] 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 13/96] 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 14/96] 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 15/96] 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 16/96] 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 17/96] [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 18/96] 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 19/96] 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 20/96] 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 21/96] 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 4a44ef25ce41a11bf81ba6de3c8fd9c86ef02d12 Mon Sep 17 00:00:00 2001 From: tienifr Date: Fri, 12 Jan 2024 10:49:10 +0700 Subject: [PATCH 22/96] 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 06d2fbb62cc56eddac9bbf4895456eb6762dd733 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Fri, 12 Jan 2024 16:49:56 -0500 Subject: [PATCH 23/96] 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 24/96] 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 25/96] 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 26/96] 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 27/96] 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 28/96] 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 29/96] 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 30/96] 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 31/96] 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 32/96] 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 33/96] 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 34/96] 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 35/96] 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 36/96] 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 37/96] 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 38/96] 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 09dadf45d7434e97654c721ad4ef899eda118af4 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 15 Jan 2024 15:20:21 +0300 Subject: [PATCH 39/96] 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 40/96] 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 41/96] 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 42/96] 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 43/96] 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 44/96] 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 2babc2d778cd6c0d9213e72270cfe346855c958f Mon Sep 17 00:00:00 2001 From: Cong Pham Date: Thu, 11 Jan 2024 00:49:41 +0700 Subject: [PATCH 45/96] 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 11a225f4b1ec4265c27c097710fb00dd2a978874 Mon Sep 17 00:00:00 2001 From: Antony Kithinzi Date: Wed, 17 Jan 2024 22:47:27 +0300 Subject: [PATCH 46/96] 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 47/96] 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 48/96] 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 49/96] 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 50/96] 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 9194fd136a405e4d5532d33729f89a44d6345295 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Thu, 18 Jan 2024 19:30:41 +0300 Subject: [PATCH 51/96] 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 52/96] 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 53/96] 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 54/96] 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 55/96] 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 56/96] 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 3bd59508671e60b0496f754cb935a95484ecc96e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 19 Jan 2024 22:43:23 +0700 Subject: [PATCH 57/96] 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 58/96] 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 59/96] 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 60/96] 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 5f81b8d58b435bcbe8de9daf995ea893eb37116b Mon Sep 17 00:00:00 2001 From: rory Date: Fri, 19 Jan 2024 11:28:46 -0800 Subject: [PATCH 61/96] 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 62/96] 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 63/96] 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 64/96] 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 65/96] 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 11e55f61c795266788f41d8e665d06a04d3916f7 Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Mon, 22 Jan 2024 15:11:01 +0100 Subject: [PATCH 66/96] 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 67/96] =?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 68/96] 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 69/96] 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 44d82985dedb00395a913b1ee59278513b1de9a7 Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Mon, 22 Jan 2024 22:59:49 +0300 Subject: [PATCH 70/96] 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 e16d28286b4f3681c0638e1a6396d61b2d6c903b Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 23 Jan 2024 09:37:09 +0700 Subject: [PATCH 71/96] 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 72/96] 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 73/96] 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 74/96] 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 75/96] 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 9f56657e8d15c36ae96b71094f339825437dce2c Mon Sep 17 00:00:00 2001 From: Fitsum Abebe Date: Tue, 23 Jan 2024 18:09:32 +0300 Subject: [PATCH 76/96] 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 4971adf81f2236b33ac4f05567a0c8dde9cc65a3 Mon Sep 17 00:00:00 2001 From: someone-here Date: Tue, 23 Jan 2024 21:58:49 +0530 Subject: [PATCH 77/96] 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 78/96] 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 79/96] 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 80/96] 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 f8dc358cdc564546208b34c954a00b7c496f359b Mon Sep 17 00:00:00 2001 From: Rocio Perez-Cano Date: Tue, 23 Jan 2024 19:57:37 +0100 Subject: [PATCH 81/96] 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 82/96] 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 83/96] 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 970183a3393eef074145d88e511c45bd87b454fb Mon Sep 17 00:00:00 2001 From: rory Date: Tue, 23 Jan 2024 16:11:46 -0800 Subject: [PATCH 84/96] 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 85/96] 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 260f78e3be8550dd01ac78b8a91d255bfa75d3b0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 10:59:51 +0100 Subject: [PATCH 86/96] 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 87/96] 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 baf76b44de497fd4175f79bb9dbf13cd6ffc1e46 Mon Sep 17 00:00:00 2001 From: Vit Horacek <36083550+mountiny@users.noreply.github.com> Date: Wed, 24 Jan 2024 10:43:29 +0000 Subject: [PATCH 88/96] 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 zcmZsiV~}KBx2+3Zc6FECW!pBpY}@LxZQGS)+qP}nwtee;zjN-nC*uBCk!#MFW2})G z`^S#GpOlpZ{qYM32nh0f0RiQQ+?Y6ie`bC!xbJ19XKiL|WbZ&@Zf|2vJi!m&O9%U- zyX(9K-|YgF?FO{Jr<9tGiQWztQt5*eiv7}Dxy`SZ1iC?!E>nff07L;1oXDVrJe^eC z+}JS{YkPdjG~e&s>Yz7l|v>DUe-C z4c(>}+qk&AX98bd!O%r@C!`99Q#3D8DnPE4SmQLkZ<~5b$2&HpLys*I10O5Xz+gIB zIdQUOw14~heEjP8IJB}~+G?<|+5B|)dQ7kFbbTAUvgumcX#eP!S*X2fYT7z?e*b)b zOjqyf^4!n=x;fdqvVop_y&1y0xOUFs?fSZXxnSM;{J5a4Z~yQG?OglogY#MB{dyU_ z^=0>UIrOx3Q5xsH^-=ZpvQg#z@vfD+{@mC0<>7rrgH-uCX7jGhYSZ;z^s(G<$@_7A z_C{+{%6fIa6^wwl`Tmym{A5D&kSdA7YU6!C;qCn$abIwASABgAEJ4Yo<&fcsify3pFyTl{HH zdw+3RY4yZ9qBD6w+(~Wyj2Dfp6GM1$6V&x7c4>lep`FHgoC-4-@-?+d+LGl)_kvX! zSI>Lc*;yB^?>uJ@=Dk*rB^tf&Ui`qF57-aF1np~fHnD^9>NFX@(IYj8+ewQWael(v zO0enletO|HxS6tPMZ*nyH9UpV(nSvnv}vGT;v4`x)yK|n-5W6=hmShyV+T}<;xTWA zPkQ@V^R|o~ILqDX-{EBlngmV{>3DhEeH6)L zb+vab6&>|`eO`Wk*l0!wE-`~!KY93S;QX~jUzhHZd#-h8=7AOoyi7poFBPEe@N68E(Z@?Pi7B;CMalEL+_zbdqXNzto+^ zIeE4Fqvid!-D&fhUa0E4)t7tf-s)Hy_IVLai|6%n@o@y<&;__8@Ehq;U8r~d6Bt2u zRmvD-zQw}PUQ*Vkf4o8JxOSAa_3^oh5q%ojAmC0Y&ri2+klhjs~qRjsS;v0 z($dE>eDU-WU0`eT)+^_0rS#)VU4)Ew=H zQN?s!+lO}LP2ZYL=la(TV5!0DV5#$=tLyEyPHhLz+yB+(>wfF&{_v~)YiF$JjrA+% z(#8JmaOx@R^EngLs}RD=?P-TG`5LFcxc00NX9BU4a{>uWUaQ)THNt8#NXX7#-D{4`aNN$GyoaKb&Qi@kd6! z$`hA2Vk^(O4)C(|bmjH-K3DV>Z9`mDKEhM^uwWC1;vqfd*eUu!mUS|0;J+)bsW3Z& z`T7cHxwqz_(961q!{KD-wx4lWxr|2_#Vjy+;qb<=WLT-TjmeH1We+!R)O`5-jJHf` zI{{)aa0j}SpV7@cb(oX9la|H91+ch}jgElzG%^{@AHSaJ+D78&f#x7#^rIJO7={eV zkt~U(%b(NplJdlk`aD?)KQ$0{cWHES5~=>&ei-`Tx0!kxqW8C>6UkF$GcOFP8xkN` z%|NIXm$?t_KOy|lHx$hgT{cbcRO9XG{e|XXs(E|of!Eh!*RbAMLpYVp;w&ZC+PJDZ*T~=OK@K*onvXR?ZnA5*sr~1V~g1cZ-nngsqb&o9yj3r$Vl=TIO*CcKy>bg zz7&3^ri;?)Hq=Vog-P3mj>9+r;jO=j;tH zsJ(#l%U2SDjh<99z$jDX2;u(xrL~Ur1l}U;Ey9W&+FlkXYr+H8@^I5jAyEwHrd~NC zkEfV#%3Y&IUd99?=SHWc5Kp4nY1GrT_Ws6Vb73{n-QEkryK(1;#~oy6NuKBtGv$ z<+&T1{}1Uu2u%YRV=epx19ARIg%5h?OAS(bwF%7hO(k!|fzk27?toG}*qej$tX)4d zUIeV&woly<#^XE0DZzK$kl_0`Lcw?a7vj6h<%6y<>-G2}jxHsBszVXE>{5M4kxc8& z)kYlMbXYBhLCc@f3$|kTDJrVS>UIUlXQ~>mX*WWPlw*h1!E|+|p-j(>%XCEM*m`~8 zM&_^zmkwTDmTq2boLK-IQoIG69@dE=t#&2J2d<`HcGS_2*G(S4Nv^uqC-$RRJ(B#k zaZMY0&yCK``9uRfh2ly6e>fT|D|x0A2}whUemBKDSZ^($lp%+Eb`;`U`NI(xZIcv8 z4_~KDQB%_-?Z(!Q-N?>y&EW)9qAt@0WIk??ktuOnN6=G>6sQ67ZyegyhsRvl3VC+fwf2>7UEgmh@bC0>F>Vg+dCLqXNMf5X8wb?YwD}p9 zLLpIG8{Oy=ySAF{?0eyh&&^@NX%{b?iDEKygKq`0mKVmgKUa9eeXk#_5K5QN-lzsg!330-{5)J36 zqv^jR1lefJwjuKm`2A}jBvgi)_bSWWf<`qK&H z12bIXwcMkra)7ohS?>xpC)>$iJUd!?X1G9Xa`WJOb5BbCO0y5v3KX$IZnt!g-`|FWE2M z==x<87hMC-Q-e9Yj2-%zmXV0ORDX24v0VlKg_Bkjn#IFIg>qq_LWYDUrA2lDcpPct zTHLbCY>pQ+4(g1Ra2`w-walQ1QLg zT`N~O{>{%h%7^l6x%3iK!FpAm-ok3-Z>q{+1vf*P;dz4u{3dEy09O)Wxm^!eQt}8u zVFU?{+o(bSGE-j2Bc!zSLw)w}kooMU>vP-QYESft4OUIS-sxsO?lIlnSvFs@x?4_+ z5GM1Y8fnn@B_u_4-Ae(!Ghy}@wQMa^#Ob9Ew=F`N|4}jnXQS0{uwvm09p1~|qha}u z#lHbTc=dl_i+$O431t0;NK*sN5v|AW1exb?OMTDx;rN(SriGpE|CHq_b`%h{fEyXD zMUkP5ILd!Ud3lHh!-HRTu5Lc zR=&!%0>s6vuBCsj;v{vT%g{C~3_3G#FLDxWTx!J4fw37>sWf#rREQfpZ$A;#rF*)o zT&74^N*D**u6R^b+~pd{#4KaFoW5K?V4tcu!W=JEKzvB_GcR>{f6aB-0t9*I`*^$F ztw*s`X08&}t^b&&1AjWC!Zo3Ci&qHvQaRzY-uiB9mXg{Ey(QkQM!UCXKN7aK(_b7V zYUh3$H*7^T*0!iKAYh!#2?0-@b+KEyAsS$r^=LTYFvI<1R;437K7hDUx6RH(KEI?$ zhQDyw3}`9ar|AzX)XSX>P*&$0b;IGpdp0AFw$nsZ=tkrchvj_W8wExxcw-Brb=A+ev(oQ@=_&gbf{v97250`l4ra@f1wY-{0dcAc(Z6ZEu z)P0oi34WoPbisR*y|yIj=y9gU=~3?#cX$P^c^)1p`{(CeGHn)5CCU~XonqG+JvQx_ zG;@%a-5A<#!HE|3Z#Ta+uKIu+v_YyIdQQiI<_Y!44#;(?6&#Vw{`Y|$|2`8k6U2*X zO%nbxgk&gzGaxkHo=x5tI;458=lSFFXP2G3i5{_^eCnVP_cwoSp@rS3_qH1^mAMhm zUn+s-U!TR;K<8e8{R#ZlO=Qo?oW?yomwhniIljxvk?h6qo#2?U>=~?iCVvI1KYWwb zNIZW9WBvXq)y@TE^mjW@^xwOHe@Fh=|3@o(w$Qv3T+@XzIQ*~4zwVmq&PG!I$hQnT z|Kt5{``aY=ZHY%|Dtpm2#Ktq^I|r<}CtP_le??U*l?iymWcH$p8Z6s?4}TjJKVU%9 z{#V`c*}DGUhyEMW9=zwQ?UA9_@mopt&vC_$xR*MT>&*YlV5!#zYp8eZf6~F4cSvY| z$6CtZ{yzCz1m2(>O19BV@Wqh5s18pO0M6c+l(Rhv(txYSY^Zn^VV)^h!RTQk(%_~1 zJXt5)D6m{I49IN5K7 z(Jby^QACiLFkYqfBhE&_QMr((Oh;&tkPaldY%KdJNj00!`?YAcxGB__H6vpTaNacy zpyn{&@Cm|zy{*m=Xq7m$-Rms*g)*=$!0jhI3+~XNO z7J$R0i}+&BA&VMKRZu%QM71n)SiGg2wxpUL{qj7}+cYqiDSLi#*yt`jXcZ~qf{j>d z1_uRku=lJa4LRa2QTjo0Oif}9x`Z0}6<4-lB4ohXZO!URUky@6%CFv5PxXBQWf^-d zcd4&`-B%CQk@{S<>fz+JR0&Ad2@Y~nlGN|(Wd(n5)sDVbdk1d6`=Kb*{HPmzQDPmo zhd~QU;;O#u=J9)_=dIQ$AnI4o(4?2**JG}Lvp_jaEGi~K1It2~(S#|JJ>eq=7hv2< zw{fokA^vSCcM?Y|ZRkm#F6)xV;%w9mixTBS2K%ds^esf6?o7<4I55a}{vod8#E8*? zceP7V#U(iOuPjbL_2A&LM~nV|(zd;+o*I@YxBnlhV99C7Kww>%v9#(vt|ntIm-eoP zA}{b2m-PK$rCz)Cb35Rhp>Fh93aQ%?IuUiNomsjOPg$<5n*_1H=&`6LG?doUNE%JM zKC6s0hR%L3ii~A-if?Klg=dVAe z7>P@!IKolWiP$L{$8aHN5b6(!qxnnK=$+~dH8ue(ZD#!6yz&^ljk+;mw=-v#5-2UcIpdieIDzP^*Z<9ZEJouCB*kL3>x8E0*U|9zp zgU+JF?ziFGMC(};1@-}RZ#mlt9UM}9uTjKujPS!H$b--5Vt zIHS5-Myi|80tnD@6Q*hKP%S5?T|X$ z$~x|$uFg4$&!t;wE)*BNO1{Q@{0PnY=_=w|A4bHiv_uyT z-++_gY{;)G2`i6_7_G-0rqQ5p6Dn+(2jOJIn%vMMt1!1*@Yd|1=x0troDZ-SEYc;2 zue`|CE1}W zo3Bc`J81!2@aB#|a+sWr?Jy?zwYx0R?dGVa_Ee&o3;=>?{<$vwSNW)_+irsh_#y+E z&T`tD>qYiZmY7m2(5S2?@5I)=Pa-2Gg}h|AXigYW4U(LUQ}GfuUy%suH7*`N75Yiy z-Xd;SDlz|CgiuXFkBF&Oo;?pVl9y;>D*nFoCmL*yixbFXvKJ zg@{XLrsrS`%=JEz)eZ@wk8qLIC&7HrD&aJqhKHl+$Jo8T!+-@*NvE)pD?h`n41VuF zy5eLj9qiibaPB|vYYNU{MOJ)9DpQ#Kh(@Sb^{Te$%KHqv)n^|L%-VExcsMx8Jx5QW z((fZj(_afP?KwBd)iAhv+WOj;tF(*fYu5BrAAm5qsc5eo8S8?{RB~MF^9;?`&eP2r zR-_tP%R)$Dy-<&LNX?%Tjqlq8?x*#C2=GilaZ zS+pG5n(!>d;_&Awg|(_Em1x6Mzx*(Vex8AMlMzl0_B@RaL2;&IzvC?wLf1cyQDZ9$ z#>ghv9kv5>lnQSG);M}%h~N#vd>XCV`OdaATV*gI;>vs zl9HdF++}NhpnhU}lGmzyPf9#$X_vIRfL35mpt~|nhjLq^O1 z1N|gn65c-ZlsJRtW0F!|XHLdkXV3AqJ9bKTQP{@#eUe3Pg2XLs`wIloCRG0P%v#cx zsz@H_td@clxK{&13l3m(6%0@P9GiM*43^r_{@0YYNWurEcGeX+2|orsBtxWjUD(jW zzqbj3n#`Z;@~%00&x>6qS6dj+&#t`DjZnPVf!InORmOhNT)<>mJ3XsgejzdhOP3)S zF5Aa2XIO}lV$5#ilWV6k$P{!QY=Qxc34wxcdVN2>acHqOah;m8`A_Q$;Yp~jy^;MF>|H_HZvlv=jHQOWE?`tXlH7Mc@29vPC)5zXi6(Od8VQ< z-35-IexH_Fw%W(*hzf^Vh@PC3Liz@-rBn~5A(Y$K*o(=tZg!_I_o#92I-#x~94 zC$W_@p(TKlm)UA=*Y_hlt*mD(R$_8A;v+y6S=XnwwkbqPi}na{zA8!37Jd$##L##S ze!SO~Fgr~9PEu~fAHBEC$yCI`+f5s;Ht^teZ9nv29)#$e0%dFOmuBwpaGn$shJ`7U z0&;fwOG;pQ*@Adf6+R58tP&ev1Xv0+Iah#J<|~M(QuZ2dd+xq7NOG~xdq@C%rR+q7WEc6JQV^>K5zTR&$DEsOhFwew{L^YzR(cc04AQ8>p0J@oozNk>G0fAUjunqm zj>4Rb!X*6V!SE|@jyRWQ7}10xmifB7FdUL)EY|Z-kWvQ2=(`cWcIaw{k&wP{pK4K{pTg+zA#49p@sDn|?Iv-$%4F z&<|W3xu{TES(X#$M<(%p$6x_jHwz z7hck$>lF^autBoM3z92C)udST3wjW(U9$H+A=QXc4?Fm*bN*{9PR+gaz4(J4AeM(H z*CT?)w<(fjKWZ=CHC*+H?p`}I+Wdf(Hu^K80$AVG(VZ)wK=#evPsm|k#Hh9{B?{Hj zHEd?7L^sIq9KmYfm_X`cvB1Dlfajm$^1>3y!t~e}WUqVp4o3#geUbxWwo3J0&Ig0u z?D%kGgdf$7sumh4Uz^5u&XcynL*MOn4?95EU^+KTK=tMPG#6a#?*c_$2q~rxegOqd zIoR-5@4ckaVU(s;b)T{Q!z^wHjyo}Rp2zeJMXxxNBp zadbAf?BzIbDXX0YC-fOmUQ*WLJPAyIm)>1QGN8leoW)#W?K|Nj*C&!PIRdyyybW5lQtY<<#vh746h5fNQL6))r6c zqlt4228u}3*tIH&&rX?r_XW8;hwY~L_gMjx+n%K+*0J#($$lF6Ly|>B<5&wW)uT2) z>|0F>u24okyJGZAU>(Y&Lcr?8IYq@bA>d+;<6PD0E%Bm5QF6IL*O};!>$)CEirp={ zRg-z3qGsG~RrGDt4DaZb$Gi+i$ac+SC@di)c5SH?}v5$hXB9WC7(NY`ux!ZjD`8O%DZiHB^hHCR&j z-Uf+#N_ozFmdmt4WLZ;0O5YP#j5RI2gNluUD**-Xne$tRZr!C0ABA<4k<0HSh2l7> z1*E^6R^diT3L=kak{BaDhfy@fQL4GtGWjl(6~rE^z?E;X>9;h|UF>i8K$;?P^$% zWod*m3yH*13PrG8HDKa|z*+t}P0yab}_AZj=q{zJEi;`T;Q;U*x%+f-k zC9IvzTQZD$r`w!XiV)p@Z#)MnyDW|IWJ|z5X;+-Phs|S!m8-Ms?Z*({x=Q4V57+Na zke#o~b)uD=<(aw0==w699KvTHI&CjVHc>u+D55OTHc@N{bj~21IMsOMq1}P7brJ13cTD(+Ho{=oc{IHCm-{>3+C zsn8cJR6~gQnP~CFH8B*r6$E`>5*?4J3M}x0v~YT>17WUsd)uLZ`+dK@Uc%$iD(c06 z_PH}M_v$=3e)w@ohl)(2dukNc2hU~+EB6hBV?9J55I}45Yp;zXzaQ@ehnT;3~3+0>~Sc-=7Ov-s^ zCzcrXC6%fa#>vPI03NNaf_!U+n26Yt3=MZUPGESXXPkP@Kh?EVs@D8RDqdEe5%!$r z^hc{xk-)xtx?d~S-nsLff(!{lkg2#C(9;h~b|%@}&C?0DG>S~FQ$G7A25vxG^zg_B z2(==nGn36BZXzX;dDvCX682_oc5o2S_Ov;#2PQ@pIZAYUdFC8=9XO|sJrJvb^%I!h zJ+rtakQMYaM7^;rN>CGVO>+F!bh*mqF_sC{j~LRUQ-6eCym%LF>Ly$Eq>M36x~X_Q zH>?u!=q7X#c32|f2zvpLSiCzAEM=@{AUBBd}UxE*Z|? zxp;MTgq)l~dYW~z)_s#v2lXNW-J^$A2uk+!DCk`0JT!z4imJjKGC{;eM7+O*p!%#m zNXK^(Ml?%}m6L%YR3MW~!_WS-83EBsjicC3^=&JsP%BHtn>AS_uF*L%3aXXBH}y*- zK>yu8o{__ug;WpZz-GRSwxaK`>mV_HrquO2k|pR}i8CsQlLR*u1;dn77eaS;$c=mb zlMD4zgafxSj2U~%It@ad2Jkzfp1l6BKPy)2^ypeC+{#!u^G9b3^`N@+z-c893Z=Vg zZSY|OHsUb#LT5M~-jB@)rNf-6wl#lcVRg<#hUrhBdm9emMO(yUVB92IfN;PbJ`Pg{i$d6XMxED6zk)qwl`7??!Dt1*BXZQ<>SN( zR!@_FE9H_QDii^5jrl)%&a-HQ{gp8=IV@tKa$*A{qa75B&JgbwN8tBC4xF$J6u383kIt^{cBloKbLN|-z zFDRCk&|8f_fqughiXH>osfSp-+I0a4b@~e7xe+b7i0Ru9z23>z6-qdl$B?#iDD9O5 zMLT1k`@{y)p@a3QV635sUeL2hWU0bEMM;cTScgWzz8DPq$<>1X6J`bzMNTG z#AV7D)1uXrVwUo!)nzY{OBzD;;$9pl5Ql-PS;qe%O?w*Wki&{%Vx59>WlO+f>46VH z&G#o1ylm1;?Xhx~gZZ@{=#Yk30c^;uBBJ6_;3q%*#-3Ow2yu(?M?9BK7O@u5zjjxE zD^($Q)OJ?u=MBrr5R&Op58~2jZh~fhecdXv(c3ytS>Q=t6>cZ(VvtYX9 z7OAbDsk5zfu08&% z2nd}uS$K`sWF`*CYk+F6?>&IfO5iuveUN}|#~}h8Xgs`b2p1C^svob^C0fS8t-X-M z(sCBn)@>pQL^r#fBLwLne zRSCmkzdSiF{&!2Jz;YewZlTIUd~f*MBm6IwX4t$WR+}>ZPp_6jBAT{y*AWrEUfAYR z)^k_2$sA(vI!wlwQ1*I(t%M27zdn$@r5S25ZQ!a3${g-G;bKuvNFBkRJl9)@7%?#Zd zi|6*xM409;nwmO4_Kn~60(yV2Q7ED5xnmk*P$l;JsBD8dwD#wX6BdR(L@v%w)DBcU z#(VWR3!KV#j$uxj~Zn!0YYiLdXz^Eh{`YU z?-KDjkXSrkl5otv9w7mxLU#zq%>1xO0q?xuVphb!mU4hz9+}4XGB4QzL=laok%7xR z(!pQq%EaoW$r6#0B*a-mgt^)Ykhi%sA5sszaBx-n&i{@NupA_6$Wo}qi+5Xx{>wO> z*A(jDvbHwKEs?J_GHnVLRx~2Jum(~4Q`;S%_GD%?utaQVJiJ^@Vd)}DC04X-@xG(5 zxCdZZj55MdwnEBVb6eMEZzY;UU4&KR&0TxG^YV+$69w#Kg9Av^)h`UPI)ro|XVLfB zqTPm)8v(C+kEQq=m8&bNM^XHTN+Pr@yC=i}l-Fu? z#r9}5sl2jLQX*D!pq0vD;#_;KD`%*f={4N#s2xdMjHgN;UV;(&uq}yXr_z~j_C~=l+%=5e@SXELO@X5Yke$7v>!PtP zloiA_aLzAwV%8z=u~5l#9nddsq9K9lM-Y|kW*7nWE~tRoWO%au(gC}OS=YAk*3_zn zB$2r?8cTn#+~2*6%lhnhAs;3+f!Yz_)hPh9=>>C|YmCS3BM7fv6?v7f=R5;611zE3 z2X2x!?!~rml4+Ec*TdzB)$4!IF;<3WZ*S+XPgbok(${mI_Pr`X9OYxi@khHJ11Vnn zf>)kV&Ro?W1;o83-*BVeqe1*YQ6JVVXEDc5UJovh&pT2!Spc?jP(+o{Jzi$t(EiYVq*Xoq0h*A%IiPZals4#B{iqUv z>3MwKn9EG}j#la)x%HTTqgDp^y2DKgZYNqq+UZPO#-Z+pLhVq0q>&AwISVA&`*84h zw7<#lzQ2A-I)+Pieb~MQ^T;Faa&3RSWqk(5Z+>2XL!DbAZ0=q;n=#gkF_4ZyY&JHJ z*dJ7W*wWN)vpQR!<01w!JY|>_2Vyd_yhGowhMC>uj}52R;-3v@<`Pv=PbFT->FK_y zwbXzd-jA2a@vOj0H&v69-_kl|z8#R_(3~9ykF7phtLprSb;@h{2vym?M`k|*m2t$x z%TzkWF7)H4v!asRoC6LD2Vk7`rP^R>nOl`m9-G(5JQuC&;*)7QCXR7$0DEfzO*IuS zYo=oxr7^f^-Uabj_D)SDx_!j2kuy)%QFXE8C+uE4KWikV`a1xTf&Fv>ekfhP1e$(z z74JZ{b&HX=v5kNZdAuJCv2J<4-mAGf&`q8$gG=reg5rN1A9){#09w>^aiLmfI<$2& zaMlfOIffusr+-c{Pvde=Mj;*=%<~6MO zspAwmK1xRWfti7Z<6wXvqy_a{j1Kv7Z}sx&`W1#PP~t+0Eef*6sF}?`-TTyBtCGeb zukZiRKez(@Fv) z#Q`x9TwRzC`X$4DjX(s1e^6=*f%7Q1yBkgHqu^}AvDyK|jPjU`>j6Fx4HaivKswQvSNxTr{6E+6=N#-{+Qn)gI26D#N%XHX>YLN>}J7e zHH=sx!7bZ~;Vahn7FZ=OJ?&Kn7<0uwNSIq(AR41m$AWL6VgZUboqBey^hPr$r=tH% zA($M^)j^no$WB}FT<%t=vmUn;#8|l22Y1aUx?QZ#q5+Q%5=*`yFGNEL6ADYnV_R}$ zB%F}oNDic0OffDDL;VgNt|(n*$4^KqcK-(Qr~`R#u&oqN17M>G#Yn5XCx zf+{fQXuZ;k&QkvJ4SR#=b^3~FDBFVS1wV1Kp%^fK+H=`J4A>xkdV>2|;iY*#o zornE4F0WT5+1EQ;7*~5YT}1|?#8Kb3e7!5nrOZ)exJWa*%BZx;aYf|H{5Va%M1N!~ zm|Sv{tMJU`uR~bEGu17?T76Coxr%6ep3C z7#|Z4?mG~LsR=E^^7l~lS7kWWrh?7nAXF%;CU~wyKL**D=*odMnPv+^%1^dW5CfT=d>{v z2h8@Oh%~$8CZz`{2IjC^?!~gYPsGwJss>F=rNzwaV_}2$TI~mpgRu5vc5B2DJ;iEUN%)N# z*x?#8kn;szE&d0^qrN7#vV&Mdl~y1!6~!p_1JPK}G8p}%L1AA(%}H}N+TI{)oEXue zJkqnKpJ;S0>KE&A&LBFh4cmt#n87&)yj zyeO%D>&N-5X11?g4~Qs1B$WfWOKP2d9yj-I64a)x1b^BBCSjNaOOU*7pTdX;Zc})G z&gl7aBLSX#>S3RUz&sn?z7=YY9>!iI-7Zr5y6R-m!(@QrW4HZ(m^xlUs-r$)7CODftDQA=p{83@z9tQX<0J`-!U{sJk)J@J zGJJ>k+s33jM8x$IK;FitlbO|fxZoPfnSsjOvtAe#9C=D6^{3^ijOq`g^SKaZJbp0c z*ZUkm(hmYj0nT|Y0iC(QphkzFPQ`Dxz{n1!oM_$-@Q3qx&`zc&Gu$ng271y*+3uhe zyY`zV&_$(*Ep<=mj>-Wc+>71_pgb%Eymu1-q{o!%(Sl$w-23D&?beIDu4I~SBK>vx zGq9e8vZCK)`qbWECu7Mncq(9Z=LNDoYY#-cTJo?DRFc>mcY*!W_M1n`)jZ8Z>(#h> z9Kpcc4j*Bn_f*SXAZJt!n1v#<-Drb%ky~kG`wG_{{>rXEfUEJoAFgS-E^m6dLohlv z@Jp%qqtLdQ(Fp!fA@9KDcj`esRvQ&=*voencuOzOeAjeWlGT$4=r9SW^f(jb`nP4F z+W~v#u@FekSZ*KW*g0y~{BOd4gJkGsPRY9bcm1GJ_)pCLvii1Xgw)<@NY=~zRvM>% z*LsHT_&)0JyyagJWBHwg(8(a&3tFIKl8yxi zT;y3+kRvWXGo-+OzV7$b4dwTg{hkUSX*pk^0NEWH7cIbvq86<9@%(o>|H+JFBXE^H z$9)Jw=u}@T88KF@6*&JKYA>OKB*pPP9Fjv{vllz7RZ|%?b0u+TWn9{Ne!h$2uZ%iy zXARI+ucef9(S+}(Gvj@4H+^loX-b>2RPiGvw)>4t?J6KD_J`Lr)BcZ3dX{`GgIw)+ z-0e@E!I>&_?XkP*b$7}j0(t041>!3kUPmy$84;?Xuoj0VD^AG8f@a@=o|<3Sp5{3h zXKB;#c2>!U0o0zUhO2)D$6QuY4+`DSt!gZ0T0(%_w-Km)8~jG1$}T0Q6CE4fVWpdd z`$L*hA>w#tJO+NQ8OD@3p&HH^P9Gy*Ra-dO=tpk@QV2KF72U{mStn+O>NE~*^nAhh zjM{DO5R=-=4;Pw)SAm)h)+YDVc5;mvTA%7>aist751~VS!V<`%@^U`exPghLbTuW^ zMGIC)VxLE}O%EP6pe+PTp%!n4b6B#V31~)*=+F^OWjl@;UMwM4HcrS@W$y3gkKXDg zi&|mo5E=g_JZrN}K$14hpNMiRS=Kd9I5SQ-kbOJQl^>L_VKEP^W!$ooO&rS8zZ;!k zDp<0rnP!Jdb}=n^KVpUvRGujtBqyEhwLgq|##%fdJ=lIo;*7_3SW(|X))#r3EAB`R z?;WeQ^-w}~Rw#^0pK>YdT~ zPcB9XdvhLKTmlU_V7HYp-!{m7jwxZQ@CgkkOy`!+tZcse7=I*7B6xW82>VApb~w^_ z!^Wl#Rrg=Y0qbhkg~ZpJcEXHV4u=>EV5eS zvhW1H0;)k@5}-l-;Fs}hpRHU)H5N3@QOntU(Zjxch*URjJqd@qiMeZUl#W}v%>cgT zBA@a=b&~{G=|o8WFEy@AViqozEUXE1gc@jQ%&J8^LsMJl+LH){`hs9%;X56TI}t=0 z@Es_=d2~YzWbK@ynMA9~}+Lf&hHj@v! zMwLulrSAgK6L?aK=Mbq}#-R5Kwl0>4omtP{!=~v(q1&$8N~=p`T#*p0$qxojBRbs{ zVVd#XORB6QuOEDyJu+QstGEU|PbJ50LviBcts-wM{0vs?ZpX@^Zc@Hx85GP|SO=H< zhT<4??ZC$uLEdJ=?5u1g^~)hPNshWL!<8CQc{JgEOuW|WdUT*BgYEsYob-21;z~iS zGjx|QVMjW&tEOQ8Nn+BQF&qF)mM8Dk*NARQZA-Nj2V>B<>)JvG;E01EB#+zgT^3@t z=rU-bqgK#g2wy?HqbBXsg!kNQsMFJYs7-;}Lmb3ShO-yvX(b^(tkYAE@SP~=6Kann z^ho%rZzmun9Wnw%UzLVfCoZEZWXUcM61lZh((Y!-fk+%*ng~Uhew;Nz&zxXv_8J7= z+<`22j8)0K`07@TM{qWk&VjeWR9Hd<><6C2n zj*RUcVc7nYAaE?q(cJ@~SNqW5`eF0wmZur#b{3|4-JM^b2OT&a$R#R=_9$>7zvkwG zWC2kh@c;)lgJ3AaUU&51cUEvEJ+>Jj!Il$U5`dY7t;`7ZaDk!?trL(Tp2eNRTb#E^ zBZIa@?<#y9jl<@z$)uTz?|)#P3MrOW9^lEf?DSk_>uP;SOS`=5a&(`%8y}^udv8s! z?Kd@oGU3c^5bXO~A15(LMn9RvGt+=0}ld4I>vCd-ZaR>2ZyDJmsgx-W8DayivhZHN3*nPZV4 zV|N&e095(w1o)yqEePL#z0Kzrofbbl+_~V~t-Nf#RSBW9LFrNT2$ zvbx<@r?u4F)3$t{JVlH&St5wPl;C7M3lTlyQc{!_d1E=XDA}?)31{{@C!=Cwh?=i9 z6^w*W0eHrE=!koPa2wUG|7_At!gMJi3I??@B#xo2=E*H0?JwI{hzkQ9*&KI@HsSPx zNujg~z23<1cjR{A4t7N`(8N3*a2>1GfwUQ7s{~I3|D^jk37suu_-$Pxvgf>`j+(`8 zE))@Eudt#Mv5GQGgtwA9=PYL765O#j4LgH=$aJvp|9%yo9x zp+=9zPCx>KZhNp$6xh)@KC$5@B=4t6yaDO^I|EqZT%Q1MN6_JRfpd4vMXrkD`j*Dw zgeJ&I-xmcAr(0A&{Vxt>8u+n?Lt!}Q>CJnB4ie`OnMqs^StbOiQmK{L*wLi;M9^|l z$sd>P1A@nbq)nz)v$_%p;KI;xq}{!FUmygA%Y3$>JeKKi-8PcOL;m{&X05)x0&AjS zt$3h4gxeMp#XG+H-La+*UJStmhi-|9!P37S0emy5CW!LSJ(68P&_5TlMF z*@Mo-6AQvgk)Czes;)T8KRtfe;vD>x`?8j-q&>pX-_*2z2y^!26~g8XUi;w6pW&Ae zI&vYganhU%MOc7mshgC;boouUSdTg3%1^J_*NB>FrjJwNa^Hlz9F_cDTBw9sjp7L*|KKtJ-~sBj*p zWfb&ImSz11!(N?_EYPUa!f+gipJeArpP_H4Bg*dLMXx?yf4hgi$dCXYGCeH6 zdI>52<;*e#iuw|&q7}o*Rf1M`%6bz&JmUFgzdUPC3lJ6Ki*$3cN>G=AVTmz?9TnuPlU*%&u4{|_JU6lB@9MT=It(zab`+qP}nwr$(CZQH1{ZJRImKIg=JH(td1 zX`{~`eYUZF)*5ScwAF9}3VV7kteIEghQ2XIIwHO31cs5Xg26gQdp2xSe~Fi+0*TGc zE>A-blw;kL}et`U^{c1{pKJ&R3tVpYR1dcOH|B^0uDV0=zN zy|9k^PK%(9XN9?4J+=xg;V8gnXP(4N5WP#W6H}CKgB^C1m5J+XaI7bHu_MkvQekS z7#j^I!WD8$s^1wnvjG0S#T6~G64it;goaFjJt4PVl~(RuA>wyWT;(OA(lKORk>}lL z%7h5EPO#XH&*QBg!Md~nVtZ>o{Qv-wuvyH-RMYL1!3Vt5naZ(UJtK}vpzGjT@J^ru zZzIW@r<)wTF$}eWQOH-Ht}s6764rj4+n7eTATKV&zTi&F;!4sJh+muaj_vD86iHvI zj|{UQC7$+M%%Iy(;A}qq!q>u*0939nFgnCEBvG0s;>S>xKvcJc7g_V|(}TXn+U`|y zF%IB+enH>UN-R8j@=zDEW@TN>GM>ewgbyLBI+rrroGt1L64?vqrje$Xq~6FA(wC;X zEu584kUWm!TZX$f$VU=mHScY20A1!7sMzVfh2}UME#VSRnwzy1^QbgmgHg>;Uqw^? z63(wLWuKIu3fu2-N;43Z{_C{|lt0u=!k&__pZ2Xu&hNl!<|1sIO{(6tlXi5*y}XU5 zv|zcd#+JsprARUPdvoYx%f!A2vGiW1CAwhB#G5&#UbWQ3ys*?)t zRL`6`9lSE9Qe0NzU<7o`-s|wE5a$<05~x;4+z}6UGd4KuEqopyw!a@@sDIJh4=KA1 zH^aLk4h;=GY3OzWUq$RY#$~C@IooQWXw5d8oWc;>nGxOx!-(iSdrHNRS)O$KiSlV~M&d>Djm zD3?W;^{B2{uwNl){;C38iUjw zPfu&o!zf3Iy@iLISqmLLg3JAVnQ$F5-5}s(rtQ1W9Z|#z+w>$Uq4>M>k@aq)=KgJM zDQX?wh-v8M_ws2Cjl@)N+(HoPZusp7*`*DqCVpG)dm78=EyPn~s)nYg=r0fXWqHv< zv+ije2;whPkK<-SHhaT$9kT}qBZ8|G*je}o+h1LXt~;dIgj~pc{1A%-l&reOCZ1Ol zeNA`b$SM_4g_B;OantEGY9?F`vyjA3-uPg@+%q=g%WT=(Gn)W__&f*vb<;G^&836% zT^|4{w~EFy%kB^>2b$UKNbaHy~hJ8MFado;s@F~Z7zD{R0EOH z@{}hzfdr^tuZ~y1l;+X|SI5XLP*ZKd;A? zeLjF)HfXpnD_U?<*PN?>W}Fds%O*Yp*VJ*6YVT#dW6x}hj~jO-m+h`RT(n9=uCCkd z++p}QlSU3l z=S{%|tI={dQS8xpmBGykIeewSeKw&}v{Eh;^XjlWu~5Lpuzwe_{q>lW)kariPt-Ys z11+Zv6%3L9-nRD&TO?}DW3B*rPa#%YHy$+tn$nra4uO>L$Y~JgW9fD zh8(4%Yrh$-n9Z(bNkUGYg6aNMMV(@0-}Wc*9oMd)pIt(tKC znAsZsSf|(O>{jx#s7L%xZTqUxMLTI$=E>%P9pWZh0}w*BDq?@59zEEiSQuDXC3g)7 z=KztkPHcU?SloJz!_o5iAw4RqvmknBML6Ay!%Ih0N^}Y?wwE@#S5@A@`-5dqLG$St zq#{#C)SO&QG`37-OqSNo%f{a}Puh5pJ`D$emBlMF`s->nU}0qTB5rW6ORaJmJu!+- zeX(DSfhlKG8Di+DbByHkMp7A0WW%U z;=o{LJMSow5MaTunmePR1vED9_*=%%A1jHBa$akXkTB`@2NIC{nRRQ-uF6ltl50r9JMFwx z*?VE_F1NTKRCx`KPaON-rO zdULb!=vj`Ex#hnZwIKi?G3X_SJCqk_&pUXq<2v6J@Xm&#@AK{@p$)mpeuORf`{^xJ z!p(%%8T0U{;9^~)jmw?vgGGF-$xisL{chZu7ac*J9*G>80t3?frF)~7_{yRr48qjt z$fqDg@vGX?G>8rVngc?7Gv@4pp%#*mIHhVuKiQ+n!5s92sm>*B-`w@2`1hB5Ke^)0 ze0#7JclO&g_eI)bzW<_uEN}h$2c@OnH7kB9Rl!;R*)+ZIv`jbvbq!rW#F_!RWzDz_ zsF!-Y*qKmjrO2wGRvGx06#Hd-%L3wpgR{H&zvZ;~4K?Vs_jH!ysAl~;r4J1g%f6!r z{-`EW9K2|WfAlTV*||^*q_(xVp;e2{W(BKW=c>|U{4{bxZY8JEGxY;L=S!kQChFSw zu(yuJX_>SeiA0A9l8`$z=!G)b^4IY0%Cez{f19{NZ;Uosj822fJ7T$W+1wyy)7rgh zB5nQXTEJ`VbrzD7Ze^-b3LZ(dK5hNZ^WB(axi@CYbZgkVey@U_K7uDg-28p;7Uf`D z;1|+@1i0jwPbV`w0wwgF7A=|vQiNUx0t<)!J4DlPu$YAf$eJnB5h!gZaIc!fK<(kr z`FOb38Pyvr-3H;6JN+OH-g&3M(_~e2PIg-!=EW!m4=O+?DMuB(Wq=GyQCjqj5MWta z1n^xTq7Y!rm<#u~0Tn1oo%Yo98HV(xpOp~-&Z>wZW1=P~Jk#2ZVrxGJj*McPnY5BA z^YOO6kwrR=1yWdH34dW!-FVxwVL*!+fc7(|mvzN1CqZm1DS-{DGUsx&w;(P_aPJ;2 z1*UGbeR%novo&C@6P${^_0ho4)892X{Gs~@yxYWzJKp5J3**||M7-ePLdrpwl`zsJ zg85pya(%n8qHTR4a%h8N7GMzXz6i^N3~zKo`)=M*@t*$_(X!$Lj)}p9&KSBb|`J z`UqB=oy7;606l=S#Fc$J>iEIF#^txKjH`6X+Br?efTNoprREQ;T`Q0ES?L$h z`4^<+mHmUB2u)@{{^Pn7R6L!8eif;o4Ie2Pn4z7N8N5v~Gu04;*3bQov6h<${R0V* z7;7A-2i8l=*lRb`r*g`xP5s^wHZM=S;YCQyIy)dCct9m2->!a>{MRZ-bTC97 zIwO>g^Tr(7CTzd;4spGOGU7eY*mTWbCVeNwe6Qq3n~|&3bz*m#r}p zj%kQV_NF9eViAXS1CankLyEa|6yC72j#~x`Q%#A>lkjIF#?wQJ0Zi&O$J3)bXT>I> zi$Wa2HY6qP(!QU_J;f%%Qjxt38B>rA?B3569OIMWs>_FF%qYtT7aSJ^@CFuonu@7S zs>%oJ;Rp-3sdmfb;*A2dg>bd)1Y7P(&^u2gCA#aTAy{}I8Bw^CC+iig22nuy1+1tp zP3Jj0VpqpKP<#`knDU6C3Ra^PH&bXO9yf$4?^BV61&Eds^ChS)l)}Z1Dh|b}hkY|N zsYexB*hf^xwzrwHtUtTLI}^U-@<_Q(1i0r{cCx5On6G0XCK={K!4w+{&L$z%p zQ|mz!_rRGwW$s3VyyFCkt`{%PG?puWc4!CQI~gcw-vIacAt_`}&q#%zcukUsYnKiD zWiZUoh76MmH=asn122wpe~&y~Hain2J1prIA-gA#Q6noIL`xb-Z#=FMh(P4`3UR3T z$Gg3c0CiHiil17qbOJ;E? z;CzdG@dDlNoT1Fo)RKZ5-#;!U-*e@&3rX+f*EpED+;%UpJXZS#J~(?^^Dz1(om#-k zsuL$i@u8pK1GFs#`D$tbXevyHY<%GYZ5@V_;`8`0Y@#r$DIF&@If=8-2v1l(f zT7@=@qkEzd{+=Ti^b_7daoPvv9Ky>c^dU~4O?0Akt7;2w!aXQ`$j=WU=D1AW4jW>4`4PEn>*YZl_arx1=@j z+u)RY@feqTZQn4sU;%V)Fl8lsrik4-y09qT=p;M^S zF$HEM5MBt9MhNBNQ|E`Lky3M5MjjZc?-Ct#Hz{>n58Zp#s9?bGhHYH7=Q?5rb5k;_Og^cNm~h6fPk5p#niPX~2Tu$|*a-<)4w`J*oNc(B0GCPic0@HdE! zcIS^Dhgdfbum1c#bH`(6m<2B*H90=7kR=~4_bJ=n-yOtxoZ5LdNIf8{j}S`T?~U8a zIqx{c(TUVCw#(lyG9WXTV;O9AQki&8hiTjggAQJ%25tx38{>$tv)rEOd5EW2P6R&T zLI4*RGCsKgJ}>7vUz{5XEsqJ*dx(w9wJN|)oOmtEAQ$t^$V;&6<~=I~F{P-t)T^HC zuH37Bg&5-drI=fxa~y64%NEo~VrZ#E0wHB4qo z&X7GH+Rl>ShfFQ!Cubx^67SL>o>HG@JsIAw*mdyZ*e)XvFe+WM$St=TlxoUc*EmxZ zl*scf@OG`Y%qlT$MTKXpEjvSw5r6f&JocrkF~%kCRxgA*7x(aO7Cx_r@ot(iH=cIw zJegSfnLWqUWMyX{teTHZG;x;F-k*`q%8Mrj z2j_i@=ccJ+UP>t~DKcJY%HJUT<+i3;wa(%hiG|z1f2P^x{o)ui(kY!r+m8K$*QJQG zt-4)e+JEesOZ?hx^TAIwl-#jz4eb=r=aWI8tC>meFQ~sD2Rv}WNA~lXYy&<1O<1CPgOpH3@#_ zdRd_7`?AmXyW>;qW+YZJToOMVziV}B>BCr!0m3v%s3tDHZL=+_1F|Cfk1~@+)i6~zO=sFC~V%KgRB|`3|o6m^BFJ6nM zyxlI#_Y=I*7kEV$P;lcitHg}gRjSF0JFs$Z>L(%JupP(T!Wws}Zl z8&PZSS`S-KQqU@xa8UEf>t4<*l0>)uoimk@6~nweT)x!bZqLy2SZL2`&LzV=+`Q`E zhQ9CDg=NOMy237J8HHIqqS{^zP%D3Tul?dYb)F*ij4)`mE6nL!Sa(%HctX z07ZBBqa6KV5;ZiQ_WjYP%J?@!*nq=THru)1SdKvHiY{VlhB+ejGyCcrV6dR7IH~#- zuHj_J6XaM6yyCp8T0jNEy{${A#-ih7E2U$Yw*9PO(*iZ=S~+p;^IDm+-^HhGn9TFJ z#DcQm&U-&q7}9>KqPJAWac5dJS|E*3)Q4omj{rhRdBY5^YV?#Os9m|VozrdO_3gFV z9nI0zJsAv5T;dKBT~vl4p?$f9MK)Nhf~AXLVtvbmvTCUYvRlu>{qE}Gq1eUvwcZ6? zDvQrxp~f%uubFFBl?#S+LDa&qr+bLsnqe@PSs_Xi)0V82WSPgX`9$G(fL{G8nmlV0 z*Wv3@YihUGYnOox+2PwJVne;ZqBe7=EY00|;+XnOhO&j_a@MpPT8ScfrznlF@AE3Vj@aFsrTE`8>K%WI)ct1PS zG#+HRqw_c1Wz383TgM`6N4JUut-6Y~BpwGlBk4n{_^ra)WBUSV2)3L0JW>U{@5EyBm zu@2wK(nSu067g=pJpozYxT$k~D%r%pb^#pp+balnCITx^$>nc|&(wR0nj2+dN?9X3rwI|lP2 zWKdHCEiB92R?Sjlu7*$lMajwVW@kU`@B)j-A2|vZ2WHRJIx~e-V$kj3Y*EZgmtP(y zV{f<&=rLcZ#}%&`AQdya{mEs&hp^OwNa(_4XLY?%(PpkBtk3&e*Jk)e6ZC~vW3VG$ zDED54G&Px&L11QZ7NmD_?aPVpuzp_&1Hg1EH;vy})$ARdUcQ_pwP(Hj6fERNJ^fmD zYbEZie*1b?Co^M+X%d@G?ktp>uEUSO{{l?|gdHfbMH{JE0gmdEsH$*P8$%T?y5{l{ z>1={$V(Er%i^4(Vg?$vhUNkus!GzdhLQnno;|*isRe$>N+GC74o#;G)S6pF!;B_Ul zAsLB(*VIOS=-#7j(}yZO>SmN0wU=wU&3ER2eaa9Njek91)PtX;D4np(h@P)NfKf44 zvba&s`a=`ekvE=$mHg8#X5}EF2D}JJpYT3QG%WLc}l{0 zF-2hQ!@9ey*Yz`)YQ8bcB@_b}rHEu+t7Yxu6zTD|6qt?$s`CBXp^pU_wZ7{x-i3PH zDJdivwvsxtQxQtNX+pMYYKO2{$KN{U1({Ol%nHh;M*cCXsc`c?Z&GbDkCj}Jx2wsi z-49qa3~OCb+Dgp{Pn}Z1UWRS3Hh?Gaw=k(g_dz4PhH1(uZnaFb3AE*hoS-zjE$lK92MS2 z>A}ejz3&UN!V`cjJ!S~Mpj}dm8mX+xDgQpfIH)$s*eJVokqnGW zn~wqeQ)XmrNRoJcl;e?LRQ?uU?&;X$02m;sxeUL+{3Z7=!BR{#wGoar^**HURu}`o zBo~Wdev=j^G;Mx`8sRc(&Ye@xhOgc!ik@}^Zez2%FsK`YH3gd)s0nM*&%FsS9UB9H z1M~qzjNik=P*3ONd!_}xH8pmxpa6fgkb|$1~2Ozc{2jg4K$g`s(aXog|2vjjM_)EDVlRr>dspn$n7xrNxCxu4O+kZ_gr^w#vH zyq6=VDE8sN{bMJ#Sg{vkms9R5JF|lWKyu)P_x5WN_^OQz^M2rhVS`}(Mvf)PpVbi3 zj4HX1WJ_U^TdepgN@7%z6T1#X&|P@ru}%Qg((6BGARIwNBxw{P646b#Th*4nj!j1g z3DBp_e^37&+BayJ+5@+{It|LxQ&3om`V@6ps&x4cYIu=p&GH|@d8=01Ux_Fa zS*C88Md-!?TNzJT_aH{!pZDq51W?ftQ<$^}MJM4$`d*ta39z=DE7MGaQ;03NJ5e5ZpRYQ4$!| z>6&W^>hDz1^Uh3UL;)-;#76OjqbcQvL}puJw_Q13uY8D>b={%#XEZ8cgQ^#`=ZMY` zOag5yLKhJf;7^FR^7Qi}kt=4u{5ynMa9}fCO90Q0o_|xbw>ojCriCPhFSc0F( zgkGT^itC>?IH_aA|Hl0rjEI=58ZahYa}hJczz7{EgB`aigv z>s?_vKNpY}2xyGs!$5Bo+b(MUU>!5%!*~TSP4H)=XXW!}yc25_YsugqYhoWoM~{rt zd-F_LyDLcJ?n7fUTOdUv9C!d99vw^N*8C#SGn^Fy)iOL80Y4wfjqptOJ}5qO{#rrw z`nJ z{Z>PEphiHY3BppY=LeCbT$)rr1?}ZABY2JsFKMBq9tVJ_RMpe_5C($;$1oYwz`Bv^5tft66Z^KUdHccEtl~Q5RvlsimrFPspeHVa6+ZdVP}4 z_It$U>S?OUfhCgGFtw6;s==>a5`_cI4PRiz5U~=;sUu`BqKKTtDovg3Dp;UG{@Tlh zkbs!FsIy^L62F4y5h3I(lqD`#1C{4{jUrc59g1!A3!g(b6ZV2aAs*<>!k)x|YyuPOW@VOp;+XG~b(O4mpwOyFKZbQKTtIK9`?es%*9U>mq-0}WA#0$1<#tdnslZk$CVDK>m?W) z-IkLOV6we{6!phzppvPI)gT=LUM4k@SF%PhhvuN*%RpNh&mAcgX%61RIku-AC14ze z%MF#xL|83ArT4GvXH0s+XIvwDA;{Ss#P{EG60k2gjo~q9@brvDHy;>TfD$S-WoeO} zGsIJ#&#fWOOXFdm?l_pWSQ5K@R2ZRIc|s@2E3a=$C~ZC-Z7y1~(?kg|R@?ge9tZU7Jo)C}MIEPo7F+0!R76Q4HW;?B5RW zl=QGB&qN};nKB8j5IX&8qK$v!>b%NmRtsO~s4}?cwJDJhdk3)&APbtHCN0@PnEh%j zLWS*)Hx|hbIQKFsMO{47;Kr>=15)J4!^u_yS%6c@f@Kg3)_RO2MklnUy1PaMU4zy4 z6wRMu_dzs8^T7cV2Di?I-^>-$1)8jLrmwyKj(-pn!(d9gX(bhMPlL`xVP9s+xAeym zE8KXnR>|NT=_0Hs*S45eeW|&3G2VtEVHlsmtK}Nn=-aqWc77Me2OMSUBlj#gfg#zz z*yOj+swXhNeskTl48z~Y7Navq=au<3FL#u9Ds2cLbIAkqFyrv3EBTYva}5K6HX46SbRZ8HcIWA$ZN7#N=?raU(pC?o0k* zEwOk@f9BB>dZ|YnN|;Us8xsvRe2AO0@&Gs{`7pK^Jv$^$j`qCZe42Ln%=6$(hDn3@ z*2KM<*!&5*uNAzKloE%bBn43k&HlQE!*w6~9#l>;k*Hdymqz&``C>IMtnV-A&^hc}07Q%}0E{2s|3GN&Uke}i-$kLa z?BCGe|HS_bsfWO*|0nwY-wI&o1p;Km`~N_YV7uZ#bXaVbaMTON+F6b2+)3Si&0GZSlKkm}D^5yl5WJ;alf(aK5$JblLt{V2;wz9jz zF?4JjVdv2k8VhRu6Plma(2{YrPff+cyitL;5CT{soN!M{A!+f7jDtIAt)dY%i;Cs@ zl=n{`?^hxB_w!?&ul_4spRc?1YabSwmHj4`f~$oq)kB~xmGzxs%L*G0X*~wPm+#ni zVVo3^aDAVTl76YDjVb=%n}oNEAfef`Eq$4XrSQaaX5~z)Tk7|WLqC?Ryt=Atd)L;- z37hY?nG_$-*E_JW>5I3Z<&mWC{TiOn_j{EbAr0(o`!NnQhjZN4aoAJxrl#8$?QMOv zOm!&S#opXNy^MxKhljy<%&J1ZZ`CyZW3+xUXIV1Oo!aJe2{6VX>7$rCgA5GK4$-oN zn^8!SR75ItMDk|^GOO|7G7Q(<)QksnMg`z`Ra{(F?cv7J{ne!mjD z130rJ^Vs?WX@)a%|gRwonaCmhx1W=@p&u|Y7a zwSe}eX7_Fto4>ezLlUs0C6#&%$xN@<&P3lREKWVQ|Mqm@c0eBYbMIq2FIYmgIGaEhnC>;3!mqh}GSf>t< zbl_{4m7st*=i;bggVCr1s40WV%BNa&`ZiAWjOm!(o+WR>XF*R}cy(G9#V{|&Kk#j@ z(#osw;_Y0?j_8NRIR~NT$IT=6Op%W7I}?$@UcyCAj4#3=i$q}(f#)W%($NU*k)np% zdsyt{swFQkpDtcDvR!a-QTh9qfmsX3yHkg?#@S3FlDmG5evRj^G6|UFilbESBG|AU zBf#!OOJ_U_Q8cdxgqty4{54J=>*j9hK^ugly!_;f$6l$K6;%`~3w}84>g7%;{KQu; zE4tanM#Vo=RfOUK`I1+8y)qLB!0_fx9PdW1j%z15Qgo86-J8nI`qt~0;tM#3LMjdq zA|BJ_m!!*9X~d^j1?9LiARCp*crs@KNECy=A~y@X0`cNIr_IW6TWJ0)z_0gJF3R-5 zaVEs}Za>>2Y~*^tZR1ZlHFfNoe=bzoOwZYB9(RpGi0YLQWmfwJzOMhlvoQiOtg7^_ zocoiTIHidsyKr@3Laca|j*OsM|Ce7BVwl{T!fUZn3)ijVgwTTdZB(ahPUIE2f!jXz ziPoojo(GfPeEs!rw+w8>Z9dH*)!JcZ!9kRG09gE@5fxv8r$@$?9lt5@Pg)^qxZ3R1 zuhKqQZMIh{b z14_`S5HCdL);nj$5t|)K2#mpzX`fdudNmm)!>YyWDa6PHlG271q+^>+cq#z;|_Ck1@S^1fPlX zsG$Piz{Rr`42MWW9MKB*Di23Xd}SDUTapaDO@-(Nap#fTJTbWNb$d zW{8M72-f{T@UGUy{}(-wuGj#(cJp{92x`w}di%oBUQGktSb;F>*FOqGaUIk~}aj(tR z#6TOU>6PilZvcMzfZ02s`7cjlsE;kb8GUp|S~GXXuSw7^b5cE=B!m4o#54%atbm`qC`;E`S_-6D=Ne zE`Itr1LPeG{Y~tW%JR#nUegAPm%x)%5wt5*x`S%yz-+i7)v=x~&!0$X>A@&Cf~2pUTB8DhvBNFBD-;!YmynCWhUUpQVD~Vb zU;*UJW`EB3!At_73QNhF!xHE^i;g<2IT|#WwU+nvKOV%-P*_rTpvNdp+R!3ZAPsC^ zi$rOH{2);YreX*Hv%Ymp2j0BC1$U@2xNV$s_*>#>wswYJ7~+67CE@A>#BJ$m97P`1 zLL?+x1cu9HO006khDFfd;^rEl>^I8?Hlm@Gdy$(KnZU26z+L8Yu~TJjLg5kRbK(jQ zjRzr#&d2gJ6gX+51un~5*4t`_)ykX7(xzQvrHLII%l?ep@c68y*T+Q)!3+Rsao46- z^amm164HucN%KWm4HZ>K3R<>eF-T1Icb-;M@)Yn(64+Vt>C~g;mSs+&@COCWEKStONjF(q`>v7`Jt;yo;zbnPq!-*b710F_k2yMI@6)JW+Azs!;{u0(-tBI$U z<+80qcnKtfTW$GEE5zyIfbRezZkOjY4Uu*~Us>z-S6Z=Fqx2OWW`IX!@;9}$|uI53QX zzdQeO)KlroR!Ov$=+R*P|uMW9inwsl5_N= z*2#lulWxGQ>(aYcb6n5lwkAh;VZ(rNymBy=GJnugF}>X0(A3o&(UuY24nYH<8EnS zmD=JfTDeMf)hFeHp#;S+ZJs^)%)z@*PosWsTt}Y70552aCNsU+-Z0!uy`9veZa#5m z*UoIsn@VH%H4nx+l~R^UmW2o@SX7%dUcE|B0;1Sj8-Xpef-tz*?zrEAt7KJf-Z@)4 z=h~qNbH%e8o9E^*Ft0k`VGw)$cB$E`%}T^(LTPPZjLWGho7JG>YAu?lW&i4uH9(_! zkN4%Zo2&kOSCAx*yW7Wn+ZO5t`2F0n@HHK&&!WFK%V%aNqv0-9+LxswKSZ!vz{zu) zZ}$=8t1M2T-~HRDHC@Ql$@oT^wy-i6+hRt2FwnUMb7GaCc)o@2vZyLrW9mj_Q$;Jf z#bBV^jiV!U+?`b@B5>$AB~zh{POV+s9!r=*!vBy_*A&sRk;=J&9eCY9<4joV4J^hEdJc6O5|MO?mkttqi~BR&0E8-^0f$m2}kBXhkJ zY82|8XRPyqv~RNRn!(?LUFc%SqM8e?m1(L`V|>_Q_QbyseJD(qitw?FRC_oT z#X>BKRP6qwrs9viZ`xI)*-8@4R>V@Lsb*r!GktW4GqP=Rb@sTR? zfC(z%R`?tmU(zuBvgp$#1bxRliKyNosMcd=m2SXRqcgc8iX2=8#CVIC7fI>OUET|c z76s<+GfGzHsumtIJlpqZ}9|5Fg#Y?72m1bW-zm7=g@AS!?6PI`LFqNJ|bsxV@dR|A! z786Huo+wxP8aGEJ{P&{Z7)WBn>!7+O}_`FsC3W#_g3<~ z{&~F_!{#(B{`PNg%yP?PBlVte$WL#K)82-b0B_ITYzvpKX9?Gbe$2Dq)?O=+-|26I zP^WJeslLP`x*k;pB-~E|b5=`)-jz0J$uBbr69L=4O`)4; znDvHGglrMXE}37Ft1IJCccc{tz{cADjQ-7{c8;Y(7#!OTWV0NICL1dwR}}@^b~en8 zxu&)+lFJom*q(B();V8*M#O`}*2Rif>CUa~&3&B>7HlRX(B5*e&Y^@j7-*E(0#vv% z$Jq;VBOBLg-uz8d+gHyfqJKdKnWdD2I`K=2&5m*#SmLczz-Adr=jQ>e5R}o48EES_ z(ZhbGJgdIucUMcJc8S;42$>OgrJ3!HB<`dHPzxsg_{o~a?hK_y9m?I#MZ*)-O~N3d zc(Vex@KTu6F%~&7j@WUW13cs~W+SE>b;EQ?dPd&f&`YBIr5)IVUn-srVPfG5Ge?9YdusxD~Pt+R8yUA7Z1;lHj;L za^t+^c4zeX&#nCh>cMdMU_zCj3!I$oGs(d0p^80`itRk3|L)_&S-fbL0Hua2xY+Hb}U2-kYQ!{XqHHZuPxH(fj$ z`bsP^Rb>3dU(!T}fN8P0v%G~%xC|7vAyNyC!wo=|rC9+PIw1YMFA5r90e&>*2vM?* zb-hxyT1#gqyd8Q;y{Ql!>Z~S7DCcl!(a3g3)RA7xCH*tbPf3d46qCE?3^@1vC+HVe zLIL(O9VtY@TwD@XJ{=Inm?r!vrnjq&fn%!ouGPj8J3E|3yTgee$HT8+T)8YDZQh8m zNpV;xLZe8j$$f`Yet$_pdjq`O)$kdHHmP{}%nbC-oyYYwd*NAvV&i(Ug1*YV1GL_} z^N1*BpH~S2yi>9CI91|JG$@O=bvys+LG?O?j zMx`09xw^8uyJS16J_^gG`QmA@neA z9Rgb@t(Z6NSc^&YEXKsP1Az>L_fxc=qj-<-G&}$Z2VZz3ECSOrjMC%KV*F)6CEy8c zO>y<(xWjkTm4l3b$r{Mpwt%P21nILo1su`T>J>4y7JwH85pdgHD?1)AG|}A%_Xm=I zAjO#$%$1*$h$EV5*Hgh~a|nNwA%qvO%;5T78a+u=%nm0Y5S>u@Jz{3ZEyD2IdDL@_ zl<7Gpbs6aWpvC7jr;pN=+>??EuauMbN9ru&axQA5@~GCQGm`^!MBx(chGBf?FT#vm9qeW41+p50~VWs9NtkFzxLj@BouKL6FcwTWk%+`EOcXpFaN2+Fc0eI%nJu1y z$28vZAN|%?qfX4Smr0n(TG;e<2~_5QJ3mGf--99UyYL|rc3Tw5OED=TOdjy$C7(l6 zlh6$}0mT;5l^Tzq&Xwm)@P^@@5Fdphj9|-HRJTQcl?1Z`fI3FfXbw#-Ly`J6&QWnp zP}eEZZCKhxDPS={TV}>tK46r=@O_s!;wr)w2FN;p1CE;+4l651Vrii5a2;x&a1<&Q zIwQ9B(+5=DgB0&O_B5lr4_jp<{`sQQcI9BgL5P;!a~f^zDP?xHRLH&NMcT9d_Eee~ z+URSZsPK%-F{I&hN8|EWfe74lt#HC`Ksxt_8M&lqnX<#Du{PWyc+I797zBAVmAJi# zZni|Efr@`0Qa%miT?$gF9{u`^$QH>SVQ@b(j`hidKC!O2rGOCUqOA1~5v z{@6yUe+e%pz8DNMGNh$i2euyLXp|gCm6#=BQlT^Ji!V|o<~pj|LHI)ql_vqP>C zX7*K0$qERQ>J#$0wfNN;=)?*u?TbVhlH+CkJf@IlHK~Oo^`yz1VIX9sRuo(&N#1sW zL{RV43hc{+;T5&NQKFMl(pA5eJSM3dZ<+kvtT_X1omg+2^kux-NTQLCDodE~b6n9q zCmL)Id<}5N=$2JaGKfawNG20$WbD*8xbLCd1O<2t z8p-=oS-MuL?Yc;>XfX*70Xlt|_^?u*oc1d9&DRRDc02(;Egd$d22D@dN`sV5Q-e!S z{ZVRT0{(rm-ob7=ID(`GOc6t43n{@#}f zo?36xoy&4$r62=&$&O}8sbmU1=31Pp96&7v8&;WMSSs(>L^BkjZ&GZf6D_Gf9gPu# zGRfI#iXd2$Y%*xf2tv;46kX5qvB;(vP>re2e|rW`MjdDZmP)>{#Y@uA3Ts&Pd9qt1 z-8m5`8$sDksuUiS2THI~}QDt7{FyCThA?5oxp(SjnXi zjqxVGCMnU(QU@>-0AL>e}qAY_{q>$O#&W6_vbSv{R@qO6UhLdK0@MGvTx$tz5D z6=5X#h7?&zN&a#lqte`JA|_zPL@euaC89a5JvmrSfz6=*QYHues-cjax%iHqxiu+5 ztjz4o)kA**<-_m8IiKKkb-%9OO^ZOc>txogln80J1f7IeRC!GLBD^9RGWmm*tVtMo zRcx$pQGN6Vf37ArJVZM@ZnG;tS`KGTGJr=lv1G_-{J6v;Aaeb<>QLxa7K$qd6LULE z3oftG8%jD{|_)MCQ@L_Ur32e&T%*ofJpl`}NQyPe;o+ZyTOGX!a1(YQAC3Kw{IT_8? z&i1&{xEV{U_^Qyn>I<}xS6G|xErqMG+|->39n3nz0gXz`UfRnoRTVtx(Np;(-$5bf z1c^jfaV^*StnN^7iZ`Y1#x0&~IUHmhndcZy2bz3r(nDPuRdufA4y*1>R-L*XB}Fm! z?1I(9d8YVe#`R*WB*?}~vZIu>mOR=PPAzV$Ec-HZWRfNO%oj&Vy|8$b1l#GZAsU}` zZFULE55JrOI zM`h*JtGr08Gd(#D-J}>%Mz6#0CpkVQF>qyi~++S}XH@lu<_+^}OXfl~L~nhcUY{>g8}&E0#|b zQb!?mXo9+sdbblW&fGM?YB+Uu#os8Uj#BE;N~w2pu+FWNdNrKoi9H#`)KN_RPQ}!_ zEr~d@E2dsuz$_d&S2N0~qn!G&<jy&6uzLyV)O zI!dZPkhG?f>iq)zw7Hd3uZEM+nCzmYI!da)2O;;8>fIKlIlGeT)o?x;^HG#kM@e;* zRL@egrIPC1h7UQrlIqoPA{~+SD5;K;>d+W(DXHG=uuA6^;am^r+pYNkGuxqh+>wa+i#jR8r}gqn@J8>?pcq9W;kjH^$J#C$ zmrM_Uvl_?5B6PPiJSF|nBu3hdNu5b)QYV!eKbfe4N^~o#MB^dHArd=QeH7z$uqS{sBRhGHev5-9DvM( zdb_T3txZbY))`b=2gcyuMLo)STe+k*cU!QM!NZ zMbxYl{lR+;wbD{y^(?#v?j>+aT5ZY=bO}{QUQvhEo<8!`WmrT{- zdj$hiA-Uk2xyIZa+gyHbYiKT%YSVE&^@X`|T_#w|0C)WKV$%&+xw*}9@u;wF%mny- z;Zs`VkRKC|*zn(h-4~AB!;ZQm%(5PyeDD+@iSl(Xbk|Gu(sudEwu1Nf5r@W-py9{0x_2=5?7(SZ0(<+U;YFVE=>Mx!Hj9n@NAIDq{R>{7;IAOHYU?0K;sN8PXado!Q56w}pJc1`(RU6@}r)xxlZf`m3q|n=-+=ez2)4 ztoU#TOt~$KZp*Nn>HsT0HWxM4#g0u8WK$j4)J--Om4jN#rV6tv&}?cqtD4S=9(X_; zTr&#SOv9z}vaZO?OaCYq)Ed4jCd7(+hS^IEWdv=vS0c*SwV~H3iUlE7D7H7#`FoKg zyQ9L|YakUDZE7Sj%xw{64y&d*OH5znl%f<>v2Rr?#k_~X+_ZAaQ8Q?{te%M-3Fkl zCTzT}EJcoBtMG#6D>{Fqdv-F%?_;%&!W7m@)}VY_IDZV} zB4yJl>lTF5B=9YrU3^P!G>2&|mF`8WqL+IgE+39@V0^PE}TJF|L12DD z#5g^0nadWsQ5rK!!b4D+xa|^s!@$ zI<}}|i@HTT+5ExNbC2L^D0T0+{u90SrrqUI5bzV0r;e zw~TL%UTr;I00Rj?2PSr4Vh1L6VB$*y6Q{@=w;nIH;a7JkO^4ESC{2gbJPk@SNiOyE zcufs@nB+1Dm7+oo)*Y;G|$RVKPkQn&qr2Px!| zTW*rhbvC-rQrFq*Ca+y5yqmtmPO?s7*BXG5BaC&ng~Zt)=%RU$e#8Mg;qga>^H^f? z0DQkZF<6?@I&!V(0^_vY-zdz<@d_^5n8%FK+7o(nbT}=Bx!!>cjqle_lAo56;%YOe zTU4yg*XBe7Z6Rx+-eZK_wrsCYQh2 zJ_;C;0M1C}cqAtzaCT6)`AP(5dlGhbjgZ0FPE7?WA)K+O_DEDn;cTPcag`M6uLtEr z3TJC(>5EdR5|JyU5TR?O5ZLy4Da<}h?c9_O#jW?zI3|tD(t0zkqtm!RjdN7orpm=C zj$Y#e)(+W`uU0v6wRcx}eT_HR_=y7#GW(WA-c;woeS%xgkUGT;Gq#cH4y9KFWwy62 ziD5tSY99mT<9AZVK5)huJS#>(+3oP<^*l?3PD{3i(*eZ#�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')} - - - -