From feb57aed753c1f464331bafe2b27fda03c217dfe Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 11 Jan 2024 21:12:49 +0700 Subject: [PATCH 0001/1096] fix: 34120 --- src/components/DistanceEReceipt.js | 1 + src/components/Image/index.js | 10 +++++-- src/components/Image/index.native.js | 26 ++++++++++++++++++- src/components/ImageWithSizeCalculation.tsx | 5 +++- .../MoneyRequestConfirmationList.js | 1 + ...oraryForRefactorRequestConfirmationList.js | 1 + .../ReportActionItem/ReportActionItemImage.js | 12 ++++++--- src/components/ThumbnailImage.tsx | 4 ++- .../Navigation/AppNavigator/AuthScreens.tsx | 2 +- src/pages/ErrorPage/NotFoundPage.js | 1 + 10 files changed, 53 insertions(+), 10 deletions(-) diff --git a/src/components/DistanceEReceipt.js b/src/components/DistanceEReceipt.js index 0241eea44063..5ffece092770 100644 --- a/src/components/DistanceEReceipt.js +++ b/src/components/DistanceEReceipt.js @@ -72,6 +72,7 @@ function DistanceEReceipt({transaction}) { style={[styles.w100, styles.h100]} isAuthTokenRequired shouldDynamicallyResize={false} + objectPositionTop /> )} diff --git a/src/components/Image/index.js b/src/components/Image/index.js index ef1a69e19c12..58b7ec3383a4 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -1,5 +1,5 @@ import lodashGet from 'lodash/get'; -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {Image as RNImage} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -9,6 +9,7 @@ import RESIZE_MODES from './resizeModes'; function Image(props) { const {source: propsSource, isAuthTokenRequired, onLoad, session} = props; + const [aspectRatio, setAspectRatio] = useState(); /** * Check if the image source is a URL - if so the `encryptedAuthToken` is appended * to the source. @@ -38,8 +39,12 @@ function Image(props) { } RNImage.getSize(source.uri, (width, height) => { onLoad({nativeEvent: {width, height}}); + + if (props.objectPositionTop) { + setAspectRatio(height ? width / height : 'auto'); + } }); - }, [onLoad, source]); + }, [onLoad, source, props.objectPositionTop]); // Omit the props which the underlying RNImage won't use const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']); @@ -49,6 +54,7 @@ function Image(props) { // eslint-disable-next-line react/jsx-props-no-spreading {...forwardedProps} source={source} + style={[forwardedProps.style, aspectRatio !== undefined && {aspectRatio, height: 'auto'}, props.objectPositionTop && !aspectRatio && {opacity: 0}]} /> ); } diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js index f31cfb6936d9..edea86725ae7 100644 --- a/src/components/Image/index.native.js +++ b/src/components/Image/index.native.js @@ -1,11 +1,12 @@ import {Image as ImageComponent} from 'expo-image'; import lodashGet from 'lodash/get'; -import React from 'react'; +import React, {useEffect, useMemo, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {defaultProps, imagePropTypes} from './imagePropTypes'; import RESIZE_MODES from './resizeModes'; +import {Image as RNImage} from 'react-native'; const dimensionsCache = new Map(); @@ -16,6 +17,7 @@ function resolveDimensions(key) { function Image(props) { // eslint-disable-next-line react/destructuring-assignment const {source, isAuthTokenRequired, session, ...rest} = props; + const [aspectRatio, setAspectRatio] = useState(); let imageSource = source; if (source && source.uri && typeof source.uri === 'number') { @@ -33,6 +35,27 @@ function Image(props) { }; } + const newSource = useMemo(() => { + if (isAuthTokenRequired) { + const authToken = lodashGet(session, 'encryptedAuthToken', null); + return {uri: `${source.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`}; + } + return source; + }, [source, isAuthTokenRequired]); + + useEffect(() => { + if (props.onLoad == null) { + return; + } + RNImage.getSize(newSource.uri, (width, height) => { + props.onLoad({nativeEvent: {width, height}}); + + if (props.objectPositionTop) { + setAspectRatio(height ? width / height : 'auto'); + } + }); + }, [props.onLoad, newSource]); + return ( ); } diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index b13d863d97e1..7ebdd41379c1 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -29,6 +29,8 @@ type ImageWithSizeCalculationProps = { /** Whether the image requires an authToken */ isAuthTokenRequired: boolean; + + objectPositionTop: boolean; }; /** @@ -37,7 +39,7 @@ type ImageWithSizeCalculationProps = { * performing some calculation on a network image after fetching dimensions so * it can be appropriately resized. */ -function ImageWithSizeCalculation({url, style, onMeasure, isAuthTokenRequired}: ImageWithSizeCalculationProps) { +function ImageWithSizeCalculation({url, style, onMeasure, isAuthTokenRequired, objectPositionTop}: ImageWithSizeCalculationProps) { const styles = useThemeStyles(); const isLoadedRef = useRef(null); const [isImageCached, setIsImageCached] = useState(true); @@ -88,6 +90,7 @@ function ImageWithSizeCalculation({url, style, onMeasure, isAuthTokenRequired}: }} onError={onError} onLoad={imageLoadedSuccessfully} + objectPositionTop={objectPositionTop} /> {isLoading && !isImageCached && } diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 13dce9337673..aa33ef87a976 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -612,6 +612,7 @@ function MoneyRequestConfirmationList(props) { // but we don't need it to load the blob:// or file:// image when starting a money request / split bill // So if we have a thumbnail, it means we're retrieving the image from the server isAuthTokenRequired={!_.isEmpty(receiptThumbnail)} + objectPositionTop /> )} {props.shouldShowSmartScanFields && ( diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 36d424ea28f2..2888f7cf2cdf 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -646,6 +646,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // but we don't need it to load the blob:// or file:// image when starting a money request / split bill // So if we have a thumbnail, it means we're retrieving the image from the server isAuthTokenRequired={!_.isEmpty(receiptThumbnail)} + objectPositionTop /> )} {shouldShowSmartScanFields && ( diff --git a/src/components/ReportActionItem/ReportActionItemImage.js b/src/components/ReportActionItem/ReportActionItemImage.js index 1495dcbd9111..d8f461aec138 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.js +++ b/src/components/ReportActionItem/ReportActionItemImage.js @@ -72,14 +72,18 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal, transactio style={[styles.w100, styles.h100]} isAuthTokenRequired shouldDynamicallyResize={false} + objectPositionTop /> ); } else { receiptImageComponent = ( - + + + ); } diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index 3c903ee9e78f..b0fecd07093d 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -26,6 +26,7 @@ type ThumbnailImageProps = { /** Should the image be resized on load or just fit container */ shouldDynamicallyResize?: boolean; + objectPositionTop?: boolean; }; type UpdateImageSizeParams = { @@ -71,7 +72,7 @@ function calculateThumbnailImageSize(width: number, height: number, windowHeight return {thumbnailWidth: Math.max(40, thumbnailScreenWidth), thumbnailHeight: Math.max(40, thumbnailScreenHeight)}; } -function ThumbnailImage({previewSourceURL, style, isAuthTokenRequired, imageWidth = 200, imageHeight = 200, shouldDynamicallyResize = true}: ThumbnailImageProps) { +function ThumbnailImage({previewSourceURL, style, isAuthTokenRequired, imageWidth, imageHeight, shouldDynamicallyResize = true, objectPositionTop = false}: ThumbnailImageProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowHeight} = useWindowDimensions(); @@ -103,6 +104,7 @@ function ThumbnailImage({previewSourceURL, style, isAuthTokenRequired, imageWidt url={previewSourceURL} onMeasure={updateImageSize} isAuthTokenRequired={isAuthTokenRequired} + objectPositionTop={objectPositionTop} /> diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index 7286615e6ba6..caeb65fbaa3a 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -291,7 +291,7 @@ function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoom Navigation.navigate(ROUTES.HOME)}/>} /> ); From 0eeedc9bca10abc6554bd2eb5e412a35e9bbe835 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 11 Jan 2024 21:19:21 +0700 Subject: [PATCH 0002/1096] fix: redundant --- src/components/ThumbnailImage.tsx | 2 +- src/libs/Navigation/AppNavigator/AuthScreens.tsx | 2 +- src/pages/ErrorPage/NotFoundPage.js | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index b0fecd07093d..b04a67fbdc7d 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -72,7 +72,7 @@ function calculateThumbnailImageSize(width: number, height: number, windowHeight return {thumbnailWidth: Math.max(40, thumbnailScreenWidth), thumbnailHeight: Math.max(40, thumbnailScreenHeight)}; } -function ThumbnailImage({previewSourceURL, style, isAuthTokenRequired, imageWidth, imageHeight, shouldDynamicallyResize = true, objectPositionTop = false}: ThumbnailImageProps) { +function ThumbnailImage({previewSourceURL, style, isAuthTokenRequired, imageWidth = 200, imageHeight = 200, shouldDynamicallyResize = true, objectPositionTop = false}: ThumbnailImageProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {windowHeight} = useWindowDimensions(); diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index caeb65fbaa3a..7286615e6ba6 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -291,7 +291,7 @@ function AuthScreens({lastUpdateIDAppliedToClient, session, lastOpenedPublicRoom Navigation.navigate(ROUTES.HOME)}/>} + component={NotFoundPage} /> ); From 99c70920e29748b81f8aef54a607c3d9c3cfc51d Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 15 Jan 2024 10:43:47 +0700 Subject: [PATCH 0003/1096] test --- .../ComposerWithSuggestionsEdit.tsx | 49 +++++++++++++++++++ .../report/ReportActionItemMessageEdit.tsx | 3 +- 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx new file mode 100644 index 000000000000..2d742261419d --- /dev/null +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx @@ -0,0 +1,49 @@ +import { ComposerProps } from "@components/Composer/types"; +import Composer from "@components/Composer"; +import React, { ForwardedRef } from 'react'; +import { AnimatedProps } from "react-native-reanimated"; +import { TextInputProps } from "react-native"; + +type ComposerWithSuggestionsEditProps = { + +} + + +function ComposerWithSuggestionsEdit( + { + value, + maxLines = -1, + onKeyPress = () => {}, + style, + numberOfLines: numberOfLinesProp = 0, + onSelectionChange = () => {}, + selection = { + start: 0, + end: 0, + }, + onBlur = () => {}, + onFocus = () => {}, + onChangeText = () => {}, + id = undefined + }: ComposerWithSuggestionsEditProps & ComposerProps, + ref: ForwardedRef>>, +) { + return ( + + ) +} + +export default React.forwardRef(ComposerWithSuggestionsEdit); \ No newline at end of file diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 5934c4c333cb..a4d8d5da4889 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -38,6 +38,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; +import ComposerWithSuggestionsEdit from './ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit'; type ReportActionItemMessageEditProps = { /** All the data of the action */ @@ -410,7 +411,7 @@ function ReportActionItemMessageEdit( - { textInputRef.current = el; From dee95c99adb4d6529d71d3ea2c45748c9ccbfef9 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 15 Jan 2024 12:29:56 +0700 Subject: [PATCH 0004/1096] implement suggestion for edit composer --- src/pages/home/ReportScreen.js | 1 + .../ComposerWithSuggestionsEdit.tsx | 93 +++++++++++++++---- src/pages/home/report/ReportActionItem.js | 1 + .../report/ReportActionItemMessageEdit.tsx | 55 ++++++++++- src/pages/home/report/ReportActionsList.js | 2 + .../report/ReportActionsListItemRenderer.js | 3 + src/pages/home/report/ReportActionsView.js | 1 + 7 files changed, 136 insertions(+), 20 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 64e48ecd5509..59804340547a 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -523,6 +523,7 @@ function ReportScreen({ isLoadingOlderReportActions={reportMetadata.isLoadingOlderReportActions} isComposerFullSize={isComposerFullSize} policy={policy} + listHeight={listHeight} /> )} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx index 2d742261419d..7acdb0c864ab 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx @@ -1,11 +1,25 @@ import { ComposerProps } from "@components/Composer/types"; import Composer from "@components/Composer"; -import React, { ForwardedRef } from 'react'; +import React, { Dispatch, ForwardedRef, MutableRefObject, SetStateAction, useRef, useState } from 'react'; import { AnimatedProps } from "react-native-reanimated"; import { TextInputProps } from "react-native"; +import Suggestions from '../Suggestions'; +import lodashGet from 'lodash/get'; +import * as SuggestionUtils from '@libs/SuggestionUtils'; +import useWindowDimensions from "@hooks/useWindowDimensions"; type ComposerWithSuggestionsEditProps = { - + setValue: Dispatch>, + setSelection: Dispatch>, + resetKeyboardInput: () => void, + isComposerFocused: boolean, + listHeight: number, + suggestionsRef: MutableRefObject, + updateDraft: (newValue: string) => void, + measureParentContainer: (callback: () => void) => void } @@ -24,25 +38,72 @@ function ComposerWithSuggestionsEdit( onBlur = () => {}, onFocus = () => {}, onChangeText = () => {}, + setValue = () => {}, + setSelection = () => {}, + resetKeyboardInput = () => {}, + isComposerFocused, + suggestionsRef, + listHeight, + updateDraft, + measureParentContainer, id = undefined }: ComposerWithSuggestionsEditProps & ComposerProps, ref: ForwardedRef>>, ) { + const {isSmallScreenWidth} = useWindowDimensions(); + + const suggestions = lodashGet(suggestionsRef, 'current.getSuggestions', () => [])(); + + const [composerHeight, setComposerHeight] = useState(0); + + const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions.length); + + console.log(hasEnoughSpaceForLargeSuggestion); + + const isAutoSuggestionPickerLarge = !isSmallScreenWidth || (isSmallScreenWidth && hasEnoughSpaceForLargeSuggestion); + + return ( - + <> + { + const composerLayoutHeight = e.nativeEvent.layout.height; + if (composerHeight === composerLayoutHeight) { + return; + } + setComposerHeight(composerLayoutHeight); + }} + /> + + + + ) } diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index e490c4601d10..496e48ae3519 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -512,6 +512,7 @@ function ReportActionItem(props) { shouldDisableEmojiPicker={ (ReportUtils.chatIncludesConcierge(props.report) && User.isBlockedFromConcierge(props.blockedFromConcierge)) || ReportUtils.isArchivedRoom(props.report) } + listHeight={props.listHeight} /> )} diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index a4d8d5da4889..ba247fc4be6d 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -3,7 +3,7 @@ import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, Keyboard, View} from 'react-native'; +import {InteractionManager, Keyboard, NativeModules, View, findNodeHandle} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; @@ -39,6 +39,11 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import ComposerWithSuggestionsEdit from './ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit'; +import convertToLTRForComposer from '@libs/convertToLTRForComposer'; +import getPlatform from '@libs/getPlatform'; + +const {RNTextInputReset} = NativeModules; + type ReportActionItemMessageEditProps = { /** All the data of the action */ @@ -62,6 +67,8 @@ type ReportActionItemMessageEditProps = { /** Stores user's preferred skin tone */ preferredSkinTone?: number; + + listHeight: number; }; // native ids @@ -69,9 +76,9 @@ const emojiButtonID = 'emojiButton'; const messageEditInput = 'messageEditInput'; const isMobileSafari = Browser.isMobileSafari(); - +const isIOSNative = getPlatform() === CONST.PLATFORM.IOS; function ReportActionItemMessageEdit( - {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, + {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, listHeight}: ReportActionItemMessageEditProps, forwardedRef: ForwardedRef, ) { const theme = useTheme(); @@ -124,6 +131,8 @@ function ReportActionItemMessageEdit( const isFocusedRef = useRef(false); const insertedEmojis = useRef([]); const draftRef = useRef(draft); + const suggestionsRef = useRef(); + const containerRef = useRef(null); useEffect(() => { if (ReportActionsUtils.isDeletedAction(action) || (action.message && draftMessage === action.message[0].html)) { @@ -253,6 +262,9 @@ function ReportActionItemMessageEdit( if (emojis?.length > 0) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (newEmojis?.length > 0) { + if (suggestionsRef.current) { + suggestionsRef.current.resetSuggestions(); + } insertedEmojis.current = [...insertedEmojis.current, ...newEmojis]; debouncedUpdateFrequentlyUsedEmojis(); } @@ -354,6 +366,8 @@ function ReportActionItemMessageEdit( */ const triggerSaveOrCancel = useCallback( (e: NativeSyntheticEvent | KeyboardEvent) => { + suggestionsRef.current.triggerHotkeyActions(e); + if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { return; } @@ -369,6 +383,25 @@ function ReportActionItemMessageEdit( [deleteDraft, isKeyboardShown, isSmallScreenWidth, publishDraft], ); + const resetKeyboardInput = useCallback(() => { + if (!RNTextInputReset) { + return; + } + RNTextInputReset.resetKeyboardInput(findNodeHandle(textInputRef.current)); + }, [textInputRef]); + + const measureContainer = useCallback( + (callback) => { + if (!containerRef.current) { + return; + } + containerRef.current.measureInWindow(callback); + }, + // We added isComposerFullSize in dependencies so that when this value changes, we recalculate the position of the popup + // eslint-disable-next-line react-hooks/exhaustive-deps + [], + ); + /** * Focus the composer text input */ @@ -378,10 +411,13 @@ function ReportActionItemMessageEdit( validateCommentMaxLength(draft); }, [draft, validateCommentMaxLength]); + + return ( <> setSelection(e.nativeEvent.selection)} + onSelectionChange={(e) => { + suggestionsRef.current.onSelectionChange(e) + setSelection(e.nativeEvent.selection) + }} + setValue={setDraft} + setSelection={setSelection} + isComposerFocused={!!textInputRef.current && textInputRef.current.isFocused()} + resetKeyboardInput={resetKeyboardInput} + listHeight={listHeight} + suggestionsRef={suggestionsRef} + updateDraft={updateDraft} + measureParentContainer={measureContainer} /> diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index dba8ef2e11d0..4388e83ee728 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -136,6 +136,7 @@ function ReportActionsList({ loadOlderChats, onLayout, isComposerFullSize, + listHeight }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -417,6 +418,7 @@ function ReportActionsList({ mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} + listHeight={listHeight} /> ), [report, linkedReportActionID, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker], diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index a9ae2b4c73b9..6ebbe22c76f0 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -49,6 +49,7 @@ function ReportActionsListItemRenderer({ shouldHideThreadDividerLine, shouldDisplayNewMarker, linkedReportActionID, + listHeight }) { const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && @@ -62,6 +63,7 @@ function ReportActionsListItemRenderer({ parentReportID={`${report.parentReportID}`} shouldDisplayNewMarker={shouldDisplayNewMarker} index={index} + listHeight={listHeight} /> ) : ( ); } diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 2758437a3962..490b86ada468 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -264,6 +264,7 @@ function ReportActionsView(props) { isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} policy={props.policy} + listHeight={props.listHeight} /> From 5e5adb85b0a05d0d57d4fc3cbc7521d59edc0b59 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 24 Jan 2024 16:17:03 +0700 Subject: [PATCH 0005/1096] fix lint --- src/components/Image/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js index edea86725ae7..1c7b239f6acc 100644 --- a/src/components/Image/index.native.js +++ b/src/components/Image/index.native.js @@ -1,12 +1,12 @@ import {Image as ImageComponent} from 'expo-image'; import lodashGet from 'lodash/get'; import React, {useEffect, useMemo, useState} from 'react'; +import {Image as RNImage} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import {defaultProps, imagePropTypes} from './imagePropTypes'; import RESIZE_MODES from './resizeModes'; -import {Image as RNImage} from 'react-native'; const dimensionsCache = new Map(); From 2e260e62b64f2c0d8601b006a59037b9fbd12fcc Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 24 Jan 2024 17:09:22 +0700 Subject: [PATCH 0006/1096] fix lint --- src/components/Image/index.native.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js index 1c7b239f6acc..f221a3a15019 100644 --- a/src/components/Image/index.native.js +++ b/src/components/Image/index.native.js @@ -41,7 +41,7 @@ function Image(props) { return {uri: `${source.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`}; } return source; - }, [source, isAuthTokenRequired]); + }, [source, isAuthTokenRequired, session]); useEffect(() => { if (props.onLoad == null) { @@ -54,7 +54,7 @@ function Image(props) { setAspectRatio(height ? width / height : 'auto'); } }); - }, [props.onLoad, newSource]); + }, [props.onLoad, newSource, props]); return ( Date: Thu, 25 Jan 2024 17:56:15 +0700 Subject: [PATCH 0007/1096] implement suggestion for edit composer --- src/components/Composer/types.ts | 4 ++- src/pages/home/ReportScreen.js | 1 - .../ComposerWithSuggestionsEdit.tsx | 36 ++++++------------- .../home/report/ReportActionCompose/types.ts | 14 ++++++++ src/pages/home/report/ReportActionItem.js | 1 - .../report/ReportActionItemMessageEdit.tsx | 24 ++++++------- src/pages/home/report/ReportActionsList.js | 2 -- .../report/ReportActionsListItemRenderer.js | 3 -- src/pages/home/report/ReportActionsView.js | 1 - 9 files changed, 38 insertions(+), 48 deletions(-) create mode 100644 src/pages/home/report/ReportActionCompose/types.ts diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index d8d88970ea78..6d4689ab8150 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,4 +1,4 @@ -import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; +import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; type TextSelection = { start: number; @@ -80,6 +80,8 @@ type ComposerProps = { onBlur?: (event: NativeSyntheticEvent) => void; + onLayout?: (event: LayoutChangeEvent) => void; + /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; }; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index e5df63f0ff04..4d043f12351e 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -528,7 +528,6 @@ function ReportScreen({ isLoadingOlderReportActions={reportMetadata.isLoadingOlderReportActions} isComposerFullSize={isComposerFullSize} policy={policy} - listHeight={listHeight} /> )} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx index 7acdb0c864ab..efc1370f7e23 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx @@ -1,12 +1,11 @@ -import { ComposerProps } from "@components/Composer/types"; +import type { ComposerProps } from "@components/Composer/types"; import Composer from "@components/Composer"; -import React, { Dispatch, ForwardedRef, MutableRefObject, SetStateAction, useRef, useState } from 'react'; -import { AnimatedProps } from "react-native-reanimated"; -import { TextInputProps } from "react-native"; -import Suggestions from '../Suggestions'; -import lodashGet from 'lodash/get'; -import * as SuggestionUtils from '@libs/SuggestionUtils'; -import useWindowDimensions from "@hooks/useWindowDimensions"; +import type { Dispatch, ForwardedRef, MutableRefObject, SetStateAction} from 'react'; +import React, { useState } from 'react'; +import type { AnimatedProps } from "react-native-reanimated"; +import type {TextInputProps} from "react-native"; +import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; +import type { SuggestionsRef } from "@pages/home/report/ReportActionCompose/types"; type ComposerWithSuggestionsEditProps = { setValue: Dispatch>, @@ -16,8 +15,7 @@ type ComposerWithSuggestionsEditProps = { }>>, resetKeyboardInput: () => void, isComposerFocused: boolean, - listHeight: number, - suggestionsRef: MutableRefObject, + suggestionsRef: MutableRefObject, updateDraft: (newValue: string) => void, measureParentContainer: (callback: () => void) => void } @@ -29,7 +27,6 @@ function ComposerWithSuggestionsEdit( maxLines = -1, onKeyPress = () => {}, style, - numberOfLines: numberOfLinesProp = 0, onSelectionChange = () => {}, selection = { start: 0, @@ -43,26 +40,14 @@ function ComposerWithSuggestionsEdit( resetKeyboardInput = () => {}, isComposerFocused, suggestionsRef, - listHeight, updateDraft, measureParentContainer, id = undefined }: ComposerWithSuggestionsEditProps & ComposerProps, ref: ForwardedRef>>, ) { - const {isSmallScreenWidth} = useWindowDimensions(); - - const suggestions = lodashGet(suggestionsRef, 'current.getSuggestions', () => [])(); - const [composerHeight, setComposerHeight] = useState(0); - const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions.length); - - console.log(hasEnoughSpaceForLargeSuggestion); - - const isAutoSuggestionPickerLarge = !isSmallScreenWidth || (isSmallScreenWidth && hasEnoughSpaceForLargeSuggestion); - - return ( <> - ) } diff --git a/src/pages/home/report/ReportActionCompose/types.ts b/src/pages/home/report/ReportActionCompose/types.ts new file mode 100644 index 000000000000..8630189d45ec --- /dev/null +++ b/src/pages/home/report/ReportActionCompose/types.ts @@ -0,0 +1,14 @@ +import type { NativeSyntheticEvent, TextInputKeyPressEventData, TextInputSelectionChangeEventData } from "react-native"; + +type SuggestionsRef = { + getSuggestions: () => void; + resetSuggestions: () => void; + triggerHotkeyActions: (event: NativeSyntheticEvent | KeyboardEvent) => void; + onSelectionChange: (event: NativeSyntheticEvent) => void; + updateShouldShowSuggestionMenuToFalse: () => void; + setShouldBlockSuggestionCalc: () => void; +} + + +// eslint-disable-next-line import/prefer-default-export +export type {SuggestionsRef}; \ No newline at end of file diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index f3a02cae06dd..80fb341a2cf8 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -514,7 +514,6 @@ function ReportActionItem(props) { shouldDisableEmojiPicker={ (ReportUtils.chatIncludesConcierge(props.report) && User.isBlockedFromConcierge(props.blockedFromConcierge)) || ReportUtils.isArchivedRoom(props.report) } - listHeight={props.listHeight} /> )} diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index ba247fc4be6d..7d13bd6335ca 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -6,7 +6,6 @@ import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} fr import {InteractionManager, Keyboard, NativeModules, View, findNodeHandle} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import type {Emoji} from '@assets/emojis/types'; -import Composer from '@components/Composer'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; import Icon from '@components/Icon'; @@ -39,8 +38,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import ComposerWithSuggestionsEdit from './ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit'; -import convertToLTRForComposer from '@libs/convertToLTRForComposer'; -import getPlatform from '@libs/getPlatform'; +import type { SuggestionsRef } from './ReportActionCompose/types'; const {RNTextInputReset} = NativeModules; @@ -67,8 +65,6 @@ type ReportActionItemMessageEditProps = { /** Stores user's preferred skin tone */ preferredSkinTone?: number; - - listHeight: number; }; // native ids @@ -76,9 +72,8 @@ const emojiButtonID = 'emojiButton'; const messageEditInput = 'messageEditInput'; const isMobileSafari = Browser.isMobileSafari(); -const isIOSNative = getPlatform() === CONST.PLATFORM.IOS; function ReportActionItemMessageEdit( - {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, listHeight}: ReportActionItemMessageEditProps, + {action, draftMessage, reportID, index, shouldDisableEmojiPicker = false, preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE}: ReportActionItemMessageEditProps, forwardedRef: ForwardedRef, ) { const theme = useTheme(); @@ -131,8 +126,8 @@ function ReportActionItemMessageEdit( const isFocusedRef = useRef(false); const insertedEmojis = useRef([]); const draftRef = useRef(draft); - const suggestionsRef = useRef(); - const containerRef = useRef(null); + const suggestionsRef = useRef(); + const containerRef = useRef(null); useEffect(() => { if (ReportActionsUtils.isDeletedAction(action) || (action.message && draftMessage === action.message[0].html)) { @@ -366,7 +361,9 @@ function ReportActionItemMessageEdit( */ const triggerSaveOrCancel = useCallback( (e: NativeSyntheticEvent | KeyboardEvent) => { - suggestionsRef.current.triggerHotkeyActions(e); + if (suggestionsRef.current) { + suggestionsRef.current.triggerHotkeyActions(e); + } if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { return; @@ -391,7 +388,7 @@ function ReportActionItemMessageEdit( }, [textInputRef]); const measureContainer = useCallback( - (callback) => { + (callback: () => void) => { if (!containerRef.current) { return; } @@ -488,14 +485,15 @@ function ReportActionItemMessageEdit( }} selection={selection} onSelectionChange={(e) => { - suggestionsRef.current.onSelectionChange(e) + if (suggestionsRef.current) { + suggestionsRef.current.onSelectionChange(e) + } setSelection(e.nativeEvent.selection) }} setValue={setDraft} setSelection={setSelection} isComposerFocused={!!textInputRef.current && textInputRef.current.isFocused()} resetKeyboardInput={resetKeyboardInput} - listHeight={listHeight} suggestionsRef={suggestionsRef} updateDraft={updateDraft} measureParentContainer={measureContainer} diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 322d9277af3f..ce8dcb10ef5f 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -136,7 +136,6 @@ function ReportActionsList({ loadOlderChats, onLayout, isComposerFullSize, - listHeight }) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -461,7 +460,6 @@ function ReportActionsList({ mostRecentIOUReportActionID={mostRecentIOUReportActionID} shouldHideThreadDividerLine={shouldHideThreadDividerLine} shouldDisplayNewMarker={shouldDisplayNewMarker(reportAction, index)} - listHeight={listHeight} /> ), [report, linkedReportActionID, sortedReportActions, mostRecentIOUReportActionID, shouldHideThreadDividerLine, shouldDisplayNewMarker], diff --git a/src/pages/home/report/ReportActionsListItemRenderer.js b/src/pages/home/report/ReportActionsListItemRenderer.js index 6ebbe22c76f0..a9ae2b4c73b9 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.js +++ b/src/pages/home/report/ReportActionsListItemRenderer.js @@ -49,7 +49,6 @@ function ReportActionsListItemRenderer({ shouldHideThreadDividerLine, shouldDisplayNewMarker, linkedReportActionID, - listHeight }) { const shouldDisplayParentAction = reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED && @@ -63,7 +62,6 @@ function ReportActionsListItemRenderer({ parentReportID={`${report.parentReportID}`} shouldDisplayNewMarker={shouldDisplayNewMarker} index={index} - listHeight={listHeight} /> ) : ( ); } diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 12179b211e6c..4843a29dde60 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -263,7 +263,6 @@ function ReportActionsView(props) { isLoadingOlderReportActions={props.isLoadingOlderReportActions} isLoadingNewerReportActions={props.isLoadingNewerReportActions} policy={props.policy} - listHeight={props.listHeight} /> From 6939350d98ef2bb731b1973febd417b365a6fffb Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 26 Jan 2024 11:35:09 +0700 Subject: [PATCH 0008/1096] fix crash bug --- src/components/Image/index.js | 4 ++-- src/components/Image/index.native.js | 26 ++++---------------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/components/Image/index.js b/src/components/Image/index.js index 58b7ec3383a4..f4962ad9b324 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -9,7 +9,7 @@ import RESIZE_MODES from './resizeModes'; function Image(props) { const {source: propsSource, isAuthTokenRequired, onLoad, session} = props; - const [aspectRatio, setAspectRatio] = useState(); + const [aspectRatio, setAspectRatio] = useState(null); /** * Check if the image source is a URL - if so the `encryptedAuthToken` is appended * to the source. @@ -54,7 +54,7 @@ function Image(props) { // eslint-disable-next-line react/jsx-props-no-spreading {...forwardedProps} source={source} - style={[forwardedProps.style, aspectRatio !== undefined && {aspectRatio, height: 'auto'}, props.objectPositionTop && !aspectRatio && {opacity: 0}]} + style={[forwardedProps.style, !!aspectRatio && {aspectRatio, height: 'auto'}, props.objectPositionTop && !aspectRatio && {opacity: 0}]} /> ); } diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js index f221a3a15019..7da72c34a012 100644 --- a/src/components/Image/index.native.js +++ b/src/components/Image/index.native.js @@ -35,27 +35,6 @@ function Image(props) { }; } - const newSource = useMemo(() => { - if (isAuthTokenRequired) { - const authToken = lodashGet(session, 'encryptedAuthToken', null); - return {uri: `${source.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`}; - } - return source; - }, [source, isAuthTokenRequired, session]); - - useEffect(() => { - if (props.onLoad == null) { - return; - } - RNImage.getSize(newSource.uri, (width, height) => { - props.onLoad({nativeEvent: {width, height}}); - - if (props.objectPositionTop) { - setAspectRatio(height ? width / height : 'auto'); - } - }); - }, [props.onLoad, newSource, props]); - return ( ); } From 02732408714a4be850731bb3eb86f44960d58dbf Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 26 Jan 2024 15:51:50 +0700 Subject: [PATCH 0009/1096] display suggestion below when composer at the top of the screen --- .../AutoCompleteSuggestions/index.tsx | 9 +++++-- .../AutoCompleteSuggestions/types.ts | 2 +- src/libs/SuggestionUtils.ts | 24 ++++++++++++++++++- src/styles/utils/index.ts | 3 ++- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index baca4011a177..01aa6ea1fc17 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -6,6 +6,7 @@ import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; import type {AutoCompleteSuggestionsProps} from './types'; +import { measureHeightOfSuggestioContainer } from '@libs/SuggestionUtils'; /** * On the mobile-web platform, when long-pressing on auto-complete suggestions, @@ -18,6 +19,7 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} const StyleUtils = useStyleUtils(); const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); + const suggestionContainerHeight = measureHeightOfSuggestioContainer(props.suggestions.length, props.isSuggestionPickerLarge); const [{width, left, bottom}, setContainerState] = React.useState({ width: 0, left: 0, @@ -41,9 +43,12 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} if (!measureParentContainer) { return; } - measureParentContainer((x, y, w) => setContainerState({left: x, bottom: windowHeight - y, width: w})); - }, [measureParentContainer, windowHeight, windowWidth]); + measureParentContainer((x, y, w, h) => { + const currenBottom = y < suggestionContainerHeight ? windowHeight - y - suggestionContainerHeight - h : windowHeight - y; + setContainerState({left: x, bottom: currenBottom, width: w}) + }); + }, [measureParentContainer, windowHeight, windowWidth]); const componentToRender = ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index 61d614dcf2e4..20477af2e365 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -1,6 +1,6 @@ import type {ReactElement} from 'react'; -type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; +type MeasureParentContainerCallback = (x: number, y: number, width: number, height: number) => void; type RenderSuggestionMenuItemProps = { item: TSuggestion; diff --git a/src/libs/SuggestionUtils.ts b/src/libs/SuggestionUtils.ts index 96379ce49ef3..e920678b0ecc 100644 --- a/src/libs/SuggestionUtils.ts +++ b/src/libs/SuggestionUtils.ts @@ -20,4 +20,26 @@ function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight return availableHeight > menuHeight; } -export {trimLeadingSpace, hasEnoughSpaceForLargeSuggestionMenu}; +const measureHeightOfSuggestioContainer = (numRows: number, isSuggestionPickerLarge: boolean): number => { + const borderAndPadding = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + 2; + let suggestionHeight = 0; + + if (isSuggestionPickerLarge) { + if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { + // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available + suggestionHeight = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } else { + suggestionHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + } else { + if (numRows > 2) { + // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible + suggestionHeight = CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } else { + suggestionHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + } + } + return suggestionHeight + borderAndPadding; +}; + +export {trimLeadingSpace, hasEnoughSpaceForLargeSuggestionMenu, measureHeightOfSuggestioContainer}; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index cdefc8bc1c91..289e3430ed40 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -782,7 +782,7 @@ type GetBaseAutoCompleteSuggestionContainerStyleParams = { */ function getBaseAutoCompleteSuggestionContainerStyle({left, bottom, width}: GetBaseAutoCompleteSuggestionContainerStyleParams): ViewStyle { return { - ...positioning.pFixed, + // ...positioning.pFixed, bottom, left, width, @@ -797,6 +797,7 @@ function getAutoCompleteSuggestionContainerStyle(itemsHeight: number): ViewStyle const borderWidth = 2; const height = itemsHeight + 2 * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING; + console.log(height); // The suggester is positioned absolutely within the component that includes the input and RecipientLocalTime view (for non-expanded mode only). To position it correctly, // we need to shift it by the suggester's height plus its padding and, if applicable, the height of the RecipientLocalTime view. From 31bf412c900678781408b37185939d8bf86de4c1 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 26 Jan 2024 16:52:11 +0700 Subject: [PATCH 0010/1096] fix edge case --- .../AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx | 3 ++- src/components/AutoCompleteSuggestions/index.tsx | 3 +++ src/components/AutoCompleteSuggestions/types.ts | 3 +++ src/styles/utils/index.ts | 5 ++--- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 5da9c6981603..f5e6cdc52574 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -39,6 +39,7 @@ function BaseAutoCompleteSuggestions( suggestions, isSuggestionPickerLarge, keyExtractor, + shouldBelowParentContainer = false }: AutoCompleteSuggestionsProps, ref: ForwardedRef, ) { @@ -67,7 +68,7 @@ function BaseAutoCompleteSuggestions( ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; - const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value)); + const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value, shouldBelowParentContainer)); const estimatedListSize = useMemo( () => ({ height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length, diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 01aa6ea1fc17..c6911366f102 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -25,6 +25,7 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} left: 0, bottom: 0, }); + const [shouldBelowContainer, setShouldBelowContainer] = React.useState(false); React.useEffect(() => { const container = containerRef.current; if (!container) { @@ -46,6 +47,7 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} measureParentContainer((x, y, w, h) => { const currenBottom = y < suggestionContainerHeight ? windowHeight - y - suggestionContainerHeight - h : windowHeight - y; + setShouldBelowContainer(y < suggestionContainerHeight); setContainerState({left: x, bottom: currenBottom, width: w}) }); }, [measureParentContainer, windowHeight, windowWidth]); @@ -53,6 +55,7 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} // eslint-disable-next-line react/jsx-props-no-spreading {...props} + shouldBelowParentContainer={shouldBelowContainer} ref={containerRef} /> ); diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index 20477af2e365..2f6f07d51be1 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -33,6 +33,9 @@ type AutoCompleteSuggestionsProps = { /** Meaures the parent container's position and dimensions. */ measureParentContainer?: (callback: MeasureParentContainerCallback) => void; + + /** Whether suggestion should be displayed below the parent container or not */ + shouldBelowParentContainer?: boolean }; export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps}; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 289e3430ed40..d8a616fa34c2 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -792,18 +792,17 @@ function getBaseAutoCompleteSuggestionContainerStyle({left, bottom, width}: GetB /** * Gets the correct position for auto complete suggestion container */ -function getAutoCompleteSuggestionContainerStyle(itemsHeight: number): ViewStyle { +function getAutoCompleteSuggestionContainerStyle(itemsHeight: number, shouldBelowParentContainer: boolean): ViewStyle { 'worklet'; const borderWidth = 2; const height = itemsHeight + 2 * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING; - console.log(height); // The suggester is positioned absolutely within the component that includes the input and RecipientLocalTime view (for non-expanded mode only). To position it correctly, // we need to shift it by the suggester's height plus its padding and, if applicable, the height of the RecipientLocalTime view. return { overflow: 'hidden', - top: -(height + CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + borderWidth), + top: -(height + (shouldBelowParentContainer ? -2 : 1) * (CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + borderWidth)), height, minHeight: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, }; From 7c9c40ff933a7d2d47a39a5f391101863551aa00 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 26 Jan 2024 17:01:55 +0700 Subject: [PATCH 0011/1096] fix lint --- src/components/Image/index.native.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js index 7da72c34a012..862807b909bb 100644 --- a/src/components/Image/index.native.js +++ b/src/components/Image/index.native.js @@ -1,7 +1,6 @@ import {Image as ImageComponent} from 'expo-image'; import lodashGet from 'lodash/get'; -import React, {useEffect, useMemo, useState} from 'react'; -import {Image as RNImage} from 'react-native'; +import React, { useState } from 'react'; import {withOnyx} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; From a153e8a46181c676adfd995ce00759e7b4e6b5cb Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 26 Jan 2024 17:41:00 +0700 Subject: [PATCH 0012/1096] fix lint --- src/components/Image/index.native.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js index 862807b909bb..2791ea40925d 100644 --- a/src/components/Image/index.native.js +++ b/src/components/Image/index.native.js @@ -1,6 +1,6 @@ import {Image as ImageComponent} from 'expo-image'; import lodashGet from 'lodash/get'; -import React, { useState } from 'react'; +import React, {useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; From ee43388d477ef88da7414c15c69ffb1b34221325 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 29 Jan 2024 17:31:42 +0700 Subject: [PATCH 0013/1096] create global ref --- .../BaseAutoCompleteSuggestions.tsx | 2 +- .../AutoCompleteSuggestions/index.tsx | 7 +-- .../AutoCompleteSuggestions/types.ts | 2 +- src/components/EmojiSuggestions.tsx | 2 +- src/libs/SuggestionUtils.ts | 10 ++-- src/libs/actions/SuggestionsAction.ts | 50 +++++++++++++++++++ .../ComposerWithSuggestionsEdit.tsx | 45 +++++++++-------- .../home/report/ReportActionCompose/types.ts | 14 ------ .../report/ReportActionItemMessageEdit.tsx | 28 ++++------- src/pages/home/report/ReportActionsView.js | 2 + 10 files changed, 97 insertions(+), 65 deletions(-) create mode 100644 src/libs/actions/SuggestionsAction.ts delete mode 100644 src/pages/home/report/ReportActionCompose/types.ts diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index f5e6cdc52574..2b7750f732f2 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -39,7 +39,7 @@ function BaseAutoCompleteSuggestions( suggestions, isSuggestionPickerLarge, keyExtractor, - shouldBelowParentContainer = false + shouldBelowParentContainer = false, }: AutoCompleteSuggestionsProps, ref: ForwardedRef, ) { diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index c6911366f102..bda8e0aa7711 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -4,9 +4,9 @@ import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; +import {measureHeightOfSuggestioContainer} from '@libs/SuggestionUtils'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; import type {AutoCompleteSuggestionsProps} from './types'; -import { measureHeightOfSuggestioContainer } from '@libs/SuggestionUtils'; /** * On the mobile-web platform, when long-pressing on auto-complete suggestions, @@ -48,9 +48,10 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} measureParentContainer((x, y, w, h) => { const currenBottom = y < suggestionContainerHeight ? windowHeight - y - suggestionContainerHeight - h : windowHeight - y; setShouldBelowContainer(y < suggestionContainerHeight); - setContainerState({left: x, bottom: currenBottom, width: w}) + setContainerState({left: x, bottom: currenBottom, width: w}); }); - }, [measureParentContainer, windowHeight, windowWidth]); + }, [measureParentContainer, windowHeight, windowWidth, suggestionContainerHeight]); + const componentToRender = ( // eslint-disable-next-line react/jsx-props-no-spreading diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index 2f6f07d51be1..b837901026b2 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -35,7 +35,7 @@ type AutoCompleteSuggestionsProps = { measureParentContainer?: (callback: MeasureParentContainerCallback) => void; /** Whether suggestion should be displayed below the parent container or not */ - shouldBelowParentContainer?: boolean + shouldBelowParentContainer?: boolean; }; export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps}; diff --git a/src/components/EmojiSuggestions.tsx b/src/components/EmojiSuggestions.tsx index 1c0306741048..c95288f43164 100644 --- a/src/components/EmojiSuggestions.tsx +++ b/src/components/EmojiSuggestions.tsx @@ -9,7 +9,7 @@ import getStyledTextArray from '@libs/GetStyledTextArray'; import AutoCompleteSuggestions from './AutoCompleteSuggestions'; import Text from './Text'; -type MeasureParentContainerCallback = (x: number, y: number, width: number) => void; +type MeasureParentContainerCallback = (x: number, y: number, width: number, height: number) => void; type EmojiSuggestionsProps = { /** The index of the highlighted emoji */ diff --git a/src/libs/SuggestionUtils.ts b/src/libs/SuggestionUtils.ts index e920678b0ecc..00039592ba77 100644 --- a/src/libs/SuggestionUtils.ts +++ b/src/libs/SuggestionUtils.ts @@ -31,13 +31,11 @@ const measureHeightOfSuggestioContainer = (numRows: number, isSuggestionPickerLa } else { suggestionHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; } + } else if (numRows > 2) { + // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible + suggestionHeight = CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; } else { - if (numRows > 2) { - // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible - suggestionHeight = CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } else { - suggestionHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; - } + suggestionHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; } return suggestionHeight + borderAndPadding; }; diff --git a/src/libs/actions/SuggestionsAction.ts b/src/libs/actions/SuggestionsAction.ts new file mode 100644 index 000000000000..9fecd18f7d10 --- /dev/null +++ b/src/libs/actions/SuggestionsAction.ts @@ -0,0 +1,50 @@ +import React from 'react'; +import type {NativeSyntheticEvent, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; + +type SuggestionsRef = { + getSuggestions: () => void; + resetSuggestions: () => void; + triggerHotkeyActions: (event: NativeSyntheticEvent | KeyboardEvent) => boolean; + onSelectionChange: (event: NativeSyntheticEvent) => void; + updateShouldShowSuggestionMenuToFalse: () => void; + setShouldBlockSuggestionCalc: () => void; +}; + +const suggestionsRef = React.createRef(); + +function resetSuggestions() { + if (!suggestionsRef.current) { + return; + } + + suggestionsRef.current.resetSuggestions(); +} + +function triggerHotkeyActions(event: NativeSyntheticEvent | KeyboardEvent): boolean { + if (!suggestionsRef.current) { + return false; + } + + return suggestionsRef.current.triggerHotkeyActions(event); +} + +function updateShouldShowSuggestionMenuToFalse() { + if (!suggestionsRef.current) { + return; + } + + suggestionsRef.current.updateShouldShowSuggestionMenuToFalse(); +} + +function onSelectionChange(event: NativeSyntheticEvent) { + if (!suggestionsRef.current) { + return; + } + + suggestionsRef.current.onSelectionChange(event); +} + +export {suggestionsRef, resetSuggestions, triggerHotkeyActions, onSelectionChange, updateShouldShowSuggestionMenuToFalse}; + +// eslint-disable-next-line import/prefer-default-export +export type {SuggestionsRef}; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx index efc1370f7e23..927d160a8b7f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx @@ -1,25 +1,26 @@ -import type { ComposerProps } from "@components/Composer/types"; -import Composer from "@components/Composer"; -import type { Dispatch, ForwardedRef, MutableRefObject, SetStateAction} from 'react'; -import React, { useState } from 'react'; -import type { AnimatedProps } from "react-native-reanimated"; -import type {TextInputProps} from "react-native"; +import type {Dispatch, ForwardedRef, RefObject, SetStateAction} from 'react'; +import React, {useState} from 'react'; +import type {TextInputProps} from 'react-native'; +import type {AnimatedProps} from 'react-native-reanimated'; +import Composer from '@components/Composer'; +import type {ComposerProps} from '@components/Composer/types'; +import type {SuggestionsRef} from '@libs/actions/SuggestionsAction'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; -import type { SuggestionsRef } from "@pages/home/report/ReportActionCompose/types"; type ComposerWithSuggestionsEditProps = { - setValue: Dispatch>, - setSelection: Dispatch>, - resetKeyboardInput: () => void, - isComposerFocused: boolean, - suggestionsRef: MutableRefObject, - updateDraft: (newValue: string) => void, - measureParentContainer: (callback: () => void) => void -} - + setValue: Dispatch>; + setSelection: Dispatch< + SetStateAction<{ + start: number; + end: number; + }> + >; + resetKeyboardInput: () => void; + isComposerFocused: boolean; + suggestionsRef: RefObject; + updateDraft: (newValue: string) => void; + measureParentContainer: (callback: () => void) => void; +}; function ComposerWithSuggestionsEdit( { @@ -42,7 +43,7 @@ function ComposerWithSuggestionsEdit( suggestionsRef, updateDraft, measureParentContainer, - id = undefined + id = undefined, }: ComposerWithSuggestionsEditProps & ComposerProps, ref: ForwardedRef>>, ) { @@ -88,7 +89,7 @@ function ComposerWithSuggestionsEdit( resetKeyboardInput={resetKeyboardInput} /> - ) + ); } -export default React.forwardRef(ComposerWithSuggestionsEdit); \ No newline at end of file +export default React.forwardRef(ComposerWithSuggestionsEdit); diff --git a/src/pages/home/report/ReportActionCompose/types.ts b/src/pages/home/report/ReportActionCompose/types.ts deleted file mode 100644 index 8630189d45ec..000000000000 --- a/src/pages/home/report/ReportActionCompose/types.ts +++ /dev/null @@ -1,14 +0,0 @@ -import type { NativeSyntheticEvent, TextInputKeyPressEventData, TextInputSelectionChangeEventData } from "react-native"; - -type SuggestionsRef = { - getSuggestions: () => void; - resetSuggestions: () => void; - triggerHotkeyActions: (event: NativeSyntheticEvent | KeyboardEvent) => void; - onSelectionChange: (event: NativeSyntheticEvent) => void; - updateShouldShowSuggestionMenuToFalse: () => void; - setShouldBlockSuggestionCalc: () => void; -} - - -// eslint-disable-next-line import/prefer-default-export -export type {SuggestionsRef}; \ No newline at end of file diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 7d13bd6335ca..a6535b83d0f7 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -3,7 +3,7 @@ import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {InteractionManager, Keyboard, NativeModules, View, findNodeHandle} from 'react-native'; +import {findNodeHandle, InteractionManager, Keyboard, NativeModules, View} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import type {Emoji} from '@assets/emojis/types'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; @@ -20,6 +20,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import * as SuggestionsAction from '@libs/actions/SuggestionsAction'; import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; @@ -38,11 +39,10 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import ComposerWithSuggestionsEdit from './ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit'; -import type { SuggestionsRef } from './ReportActionCompose/types'; +import { PortalHost } from '@gorhom/portal'; const {RNTextInputReset} = NativeModules; - type ReportActionItemMessageEditProps = { /** All the data of the action */ action: OnyxTypes.ReportAction; @@ -126,7 +126,6 @@ function ReportActionItemMessageEdit( const isFocusedRef = useRef(false); const insertedEmojis = useRef([]); const draftRef = useRef(draft); - const suggestionsRef = useRef(); const containerRef = useRef(null); useEffect(() => { @@ -182,7 +181,7 @@ function ReportActionItemMessageEdit( // and subsequent programmatic focus shifts (e.g., modal focus trap) to show the blue frame (:focus-visible style), // so we need to ensure that it is only updated after focus. if (isMobileSafari) { - setDraft((prevDraft) => { + setDraft((prevDraft: string) => { setSelection({ start: prevDraft.length, end: prevDraft.length, @@ -257,9 +256,7 @@ function ReportActionItemMessageEdit( if (emojis?.length > 0) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (newEmojis?.length > 0) { - if (suggestionsRef.current) { - suggestionsRef.current.resetSuggestions(); - } + SuggestionsAction.resetSuggestions(); insertedEmojis.current = [...insertedEmojis.current, ...newEmojis]; debouncedUpdateFrequentlyUsedEmojis(); } @@ -361,8 +358,8 @@ function ReportActionItemMessageEdit( */ const triggerSaveOrCancel = useCallback( (e: NativeSyntheticEvent | KeyboardEvent) => { - if (suggestionsRef.current) { - suggestionsRef.current.triggerHotkeyActions(e); + if (SuggestionsAction.triggerHotkeyActions(e)) { + return; } if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { @@ -408,8 +405,6 @@ function ReportActionItemMessageEdit( validateCommentMaxLength(draft); }, [draft, validateCommentMaxLength]); - - return ( <> @@ -423,6 +418,7 @@ function ReportActionItemMessageEdit( hasExceededMaxCommentLength && styles.borderColorDanger, ]} > + { - if (suggestionsRef.current) { - suggestionsRef.current.onSelectionChange(e) - } - setSelection(e.nativeEvent.selection) + SuggestionsAction.onSelectionChange(e); + setSelection(e.nativeEvent.selection); }} setValue={setDraft} setSelection={setSelection} isComposerFocused={!!textInputRef.current && textInputRef.current.isFocused()} resetKeyboardInput={resetKeyboardInput} - suggestionsRef={suggestionsRef} + suggestionsRef={SuggestionsAction.suggestionsRef} updateDraft={updateDraft} measureParentContainer={measureContainer} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 4843a29dde60..1c0719645d2f 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -11,6 +11,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; +import * as SuggestionsAction from '@libs/actions/SuggestionsAction'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; @@ -255,6 +256,7 @@ function ReportActionsView(props) { Date: Tue, 30 Jan 2024 16:29:37 +0700 Subject: [PATCH 0014/1096] fix the case wide image --- src/components/Image/index.js | 4 ++++ src/components/Image/index.native.js | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/src/components/Image/index.js b/src/components/Image/index.js index f4962ad9b324..848e64f60350 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -41,6 +41,10 @@ function Image(props) { onLoad({nativeEvent: {width, height}}); if (props.objectPositionTop) { + if (width > height) { + setAspectRatio(1); + return; + } setAspectRatio(height ? width / height : 'auto'); } }); diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.js index 2791ea40925d..b6d5f5b2dcda 100644 --- a/src/components/Image/index.native.js +++ b/src/components/Image/index.native.js @@ -46,6 +46,10 @@ function Image(props) { props.onLoad({nativeEvent: {width, height}}); } if (props.objectPositionTop) { + if (width > height) { + setAspectRatio(1); + return; + } setAspectRatio(height ? width / height : 'auto'); } }} From 00ccc9bd4e75c8a480313f11ecf986725eb6a83e Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 30 Jan 2024 16:34:02 +0700 Subject: [PATCH 0015/1096] add comment prop --- src/components/Image/imagePropTypes.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/components/Image/imagePropTypes.js b/src/components/Image/imagePropTypes.js index 78bd48ba47ec..cc4c36e32387 100644 --- a/src/components/Image/imagePropTypes.js +++ b/src/components/Image/imagePropTypes.js @@ -34,6 +34,9 @@ const imagePropTypes = { /** Currently logged in user authToken */ authToken: PropTypes.string, }), + + /** Whether we should show the top of the image */ + objectPositionTop: PropTypes.string }; const defaultProps = { @@ -43,6 +46,7 @@ const defaultProps = { }, isAuthTokenRequired: false, resizeMode: RESIZE_MODES.cover, + objectPositionTop: false, onLoadStart: () => {}, onLoadEnd: () => {}, onLoad: () => {}, From c761e1c17e8795c514904e167f23e42c9fa7aa27 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 30 Jan 2024 18:08:45 +0700 Subject: [PATCH 0016/1096] merge main --- src/components/Image/imagePropTypes.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Image/imagePropTypes.js b/src/components/Image/imagePropTypes.js index cc4c36e32387..5d28b1a9f3f4 100644 --- a/src/components/Image/imagePropTypes.js +++ b/src/components/Image/imagePropTypes.js @@ -36,7 +36,7 @@ const imagePropTypes = { }), /** Whether we should show the top of the image */ - objectPositionTop: PropTypes.string + objectPositionTop: PropTypes.string, }; const defaultProps = { From d09ee205845c7b47eb4b6cdc49c803dc8be43918 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 2 Feb 2024 10:41:02 +0700 Subject: [PATCH 0017/1096] centralize logic --- src/components/Image/BaseImage.native.tsx | 16 +++------------- src/components/Image/BaseImage.tsx | 16 +++------------- src/components/Image/index.js | 22 ++++++++++++++++++++-- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/components/Image/BaseImage.native.tsx b/src/components/Image/BaseImage.native.tsx index e912bdda9eba..c517efd04515 100644 --- a/src/components/Image/BaseImage.native.tsx +++ b/src/components/Image/BaseImage.native.tsx @@ -1,10 +1,9 @@ import {Image as ExpoImage} from 'expo-image'; import type {ImageProps as ExpoImageProps, ImageLoadEventData} from 'expo-image'; -import {useCallback, useState} from 'react'; +import {useCallback} from 'react'; import type {BaseImageProps} from './types'; -function BaseImage({onLoad, objectPositionTop = false, style, ...props}: ExpoImageProps & BaseImageProps) { - const [aspectRatio, setAspectRatio] = useState(null); +function BaseImage({onLoad, ...props}: ExpoImageProps & BaseImageProps) { const imageLoadedSuccessfully = useCallback( (event: ImageLoadEventData) => { if (!onLoad) { @@ -14,23 +13,14 @@ function BaseImage({onLoad, objectPositionTop = false, style, ...props}: ExpoIma // We override `onLoad`, so both web and native have the same signature const {width, height} = event.source; onLoad({nativeEvent: {width, height}}); - - if (objectPositionTop) { - if (width > height) { - setAspectRatio(1); - return; - } - setAspectRatio(height ? width / height : 'auto'); - } }, - [onLoad, objectPositionTop], + [onLoad], ); return ( diff --git a/src/components/Image/BaseImage.tsx b/src/components/Image/BaseImage.tsx index 9a3f4668647e..ebdd76840267 100644 --- a/src/components/Image/BaseImage.tsx +++ b/src/components/Image/BaseImage.tsx @@ -1,10 +1,9 @@ -import React, {useCallback, useState} from 'react'; +import React, {useCallback} from 'react'; import {Image as RNImage} from 'react-native'; import type {ImageLoadEventData, ImageProps as WebImageProps} from 'react-native'; import type {BaseImageProps} from './types'; -function BaseImage({onLoad, objectPositionTop = false, style, ...props}: WebImageProps & BaseImageProps) { - const [aspectRatio, setAspectRatio] = useState(null); +function BaseImage({onLoad, ...props}: WebImageProps & BaseImageProps) { const imageLoadedSuccessfully = useCallback( (event: {nativeEvent: ImageLoadEventData}) => { if (!onLoad) { @@ -14,23 +13,14 @@ function BaseImage({onLoad, objectPositionTop = false, style, ...props}: WebImag // We override `onLoad`, so both web and native have the same signature const {width, height} = event.nativeEvent.source; onLoad({nativeEvent: {width, height}}); - - if (objectPositionTop) { - if (width > height) { - setAspectRatio(1); - return; - } - setAspectRatio(height ? width / height : 'auto'); - } }, - [onLoad, objectPositionTop], + [onLoad], ); return ( diff --git a/src/components/Image/index.js b/src/components/Image/index.js index 8cee1cf95e14..2788e85895f6 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -1,5 +1,5 @@ import lodashGet from 'lodash/get'; -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -7,7 +7,9 @@ import BaseImage from './BaseImage'; import {defaultProps, imagePropTypes} from './imagePropTypes'; import RESIZE_MODES from './resizeModes'; -function Image({source: propsSource, isAuthTokenRequired, session, ...forwardedProps}) { +function Image({source: propsSource, isAuthTokenRequired, session, onLoad, ...forwardedProps}) { + const [aspectRatio, setAspectRatio] = useState(null); + // Update the source to include the auth token if required const source = useMemo(() => { if (typeof lodashGet(propsSource, 'uri') === 'number') { @@ -28,11 +30,27 @@ function Image({source: propsSource, isAuthTokenRequired, session, ...forwardedP // eslint-disable-next-line react-hooks/exhaustive-deps }, [propsSource, isAuthTokenRequired]); + const imageLoadedSuccessfully = useCallback((event) => { + const {width, height} = event.nativeEvent; + + onLoad(event); + + if (objectPositionTop) { + if (width > height) { + setAspectRatio(1); + return; + } + setAspectRatio(height ? width / height : 'auto'); + } + }) + return ( ); } From 7cf8fd81cf99cd3a8cfa869f33be36c271081e65 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Fri, 2 Feb 2024 10:56:55 +0700 Subject: [PATCH 0018/1096] fix lint --- src/components/Image/imagePropTypes.js | 2 +- src/components/Image/index.js | 29 ++++++++++++++------------ src/components/Image/types.ts | 3 --- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/src/components/Image/imagePropTypes.js b/src/components/Image/imagePropTypes.js index 5d28b1a9f3f4..deb7d75da5ae 100644 --- a/src/components/Image/imagePropTypes.js +++ b/src/components/Image/imagePropTypes.js @@ -36,7 +36,7 @@ const imagePropTypes = { }), /** Whether we should show the top of the image */ - objectPositionTop: PropTypes.string, + objectPositionTop: PropTypes.bool, }; const defaultProps = { diff --git a/src/components/Image/index.js b/src/components/Image/index.js index 2788e85895f6..6851604be8b1 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.js @@ -1,5 +1,5 @@ import lodashGet from 'lodash/get'; -import React, {useCallback, useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -7,8 +7,8 @@ import BaseImage from './BaseImage'; import {defaultProps, imagePropTypes} from './imagePropTypes'; import RESIZE_MODES from './resizeModes'; -function Image({source: propsSource, isAuthTokenRequired, session, onLoad, ...forwardedProps}) { - const [aspectRatio, setAspectRatio] = useState(null); +function Image({source: propsSource, isAuthTokenRequired, session, onLoad, style, objectPositionTop, ...forwardedProps}) { + const [aspectRatio, setAspectRatio] = useState(null); // Update the source to include the auth token if required const source = useMemo(() => { @@ -30,19 +30,22 @@ function Image({source: propsSource, isAuthTokenRequired, session, onLoad, ...fo // eslint-disable-next-line react-hooks/exhaustive-deps }, [propsSource, isAuthTokenRequired]); - const imageLoadedSuccessfully = useCallback((event) => { - const {width, height} = event.nativeEvent; + const imageLoadedSuccessfully = useCallback( + (event) => { + const {width, height} = event.nativeEvent; - onLoad(event); + onLoad(event); - if (objectPositionTop) { - if (width > height) { - setAspectRatio(1); - return; + if (objectPositionTop) { + if (width > height) { + setAspectRatio(1); + return; + } + setAspectRatio(height ? width / height : 'auto'); } - setAspectRatio(height ? width / height : 'auto'); - } - }) + }, + [onLoad, objectPositionTop], + ); return ( void; - - /** Whether we should show the top of the image */ - objectPositionTop?: boolean; }; export type {BaseImageProps}; From 75656213396603f69e7d93ddc569cedb4ac71181 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Mon, 5 Feb 2024 20:29:51 +0530 Subject: [PATCH 0019/1096] Remove MoneyRequestParticipantsPage.js and copy any changes since Nov 27 into IOURequestStepParticipants.js. Signed-off-by: Krishna Gupta --- .../AppNavigator/ModalStackNavigators.tsx | 1 - src/libs/Navigation/linkingConfig/config.ts | 1 - src/libs/Navigation/types.ts | 4 - .../MoneyRequestParticipantsPage.js | 174 ------------------ 4 files changed, 180 deletions(-) delete mode 100644 src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 4606f867c3fc..a19ffb184cf1 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -95,7 +95,6 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/iou/request/step/IOURequestStepWaypoint').default as React.ComponentType, [SCREENS.MONEY_REQUEST.ROOT]: () => require('../../../pages/iou/MoneyRequestSelectorPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.AMOUNT]: () => require('../../../pages/iou/steps/NewRequestAmountPage').default as React.ComponentType, - [SCREENS.MONEY_REQUEST.PARTICIPANTS]: () => require('../../../pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.CONFIRMATION]: () => require('../../../pages/iou/steps/MoneyRequestConfirmPage').default as React.ComponentType, [SCREENS.MONEY_REQUEST.CURRENCY]: () => require('../../../pages/iou/IOUCurrencySelection').default as React.ComponentType, [SCREENS.MONEY_REQUEST.DATE]: () => require('../../../pages/iou/MoneyRequestDatePage').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index f1c9c316fe93..2bae3ec1f73f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -400,7 +400,6 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.AMOUNT]: ROUTES.MONEY_REQUEST_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_TAX_AMOUNT]: ROUTES.MONEY_REQUEST_STEP_TAX_AMOUNT.route, [SCREENS.MONEY_REQUEST.STEP_TAX_RATE]: ROUTES.MONEY_REQUEST_STEP_TAX_RATE.route, - [SCREENS.MONEY_REQUEST.PARTICIPANTS]: ROUTES.MONEY_REQUEST_PARTICIPANTS.route, [SCREENS.MONEY_REQUEST.CONFIRMATION]: ROUTES.MONEY_REQUEST_CONFIRMATION.route, [SCREENS.MONEY_REQUEST.DATE]: ROUTES.MONEY_REQUEST_DATE.route, [SCREENS.MONEY_REQUEST.CURRENCY]: ROUTES.MONEY_REQUEST_CURRENCY.route, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 3c4cf17853f1..c1b31cc3a864 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -195,10 +195,6 @@ type RoomInviteNavigatorParamList = { type MoneyRequestNavigatorParamList = { [SCREENS.MONEY_REQUEST.ROOT]: undefined; [SCREENS.MONEY_REQUEST.AMOUNT]: undefined; - [SCREENS.MONEY_REQUEST.PARTICIPANTS]: { - iouType: string; - reportID: string; - }; [SCREENS.MONEY_REQUEST.CONFIRMATION]: { iouType: string; reportID: string; diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js deleted file mode 100644 index 216154be9cd4..000000000000 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ /dev/null @@ -1,174 +0,0 @@ -import _ from 'lodash'; -import lodashGet from 'lodash/get'; -import lodashSize from 'lodash/size'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import transactionPropTypes from '@components/transactionPropTypes'; -import useInitialValue from '@hooks/useInitialValue'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import MoneyRequestParticipantsSelector from './MoneyRequestParticipantsSelector'; - -const propTypes = { - /** React Navigation route */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - }), - }).isRequired, - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */ - selectedTab: PropTypes.oneOf(_.values(CONST.TAB_REQUEST)), - - /** Transaction that stores the distance request data */ - transaction: transactionPropTypes, -}; - -const defaultProps = { - iou: iouDefaultProps, - transaction: {}, - selectedTab: undefined, -}; - -function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const prevMoneyRequestId = useRef(iou.id); - const iouType = useInitialValue(() => lodashGet(route, 'params.iouType', '')); - const reportID = useInitialValue(() => lodashGet(route, 'params.reportID', '')); - const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType, selectedTab); - const isSendRequest = iouType === CONST.IOU.TYPE.SEND; - const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab); - const isSplitRequest = iou.id === CONST.IOU.TYPE.SPLIT; - const waypoints = lodashGet(transaction, 'comment.waypoints', {}); - const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints); - const isInvalidWaypoint = lodashSize(validatedWaypoints) < 2; - const headerTitle = useMemo(() => { - if (isDistanceRequest) { - return translate('common.distance'); - } - - if (isSendRequest) { - return translate('common.send'); - } - - if (isScanRequest) { - return translate('tabSelector.scan'); - } - - if (iou.isSplitRequest) { - return translate('iou.split'); - } - - return translate('tabSelector.manual'); - }, [iou, isDistanceRequest, translate, isScanRequest, isSendRequest]); - - const navigateToConfirmationStep = (moneyRequestType) => { - IOU.setMoneyRequestId(moneyRequestType); - IOU.resetMoneyRequestCategory(); - IOU.resetMoneyRequestTag(); - Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(moneyRequestType, reportID)); - }; - - const navigateBack = useCallback((forceFallback = false) => { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), forceFallback); - // eslint-disable-next-line react-hooks/exhaustive-deps -- no deps as we use only initial values - }, []); - - useEffect(() => { - const isInvalidDistanceRequest = !isDistanceRequest || isInvalidWaypoint; - - // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request - if (prevMoneyRequestId.current !== iou.id) { - // The ID is cleared on completing a request. In that case, we will do nothing - if (iou.id && isInvalidDistanceRequest && !isSplitRequest) { - navigateBack(true); - } - return; - } - - // Reset the money request Onyx if the ID in Onyx does not match the ID from params - const moneyRequestId = `${iouType}${reportID}`; - const shouldReset = iou.id !== moneyRequestId && !_.isEmpty(reportID); - if (shouldReset) { - IOU.resetMoneyRequestInfo(moneyRequestId); - } - if (isInvalidDistanceRequest && ((iou.amount === 0 && !iou.receiptPath) || shouldReset)) { - navigateBack(true); - } - - return () => { - prevMoneyRequestId.current = iou.id; - }; - }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest, iouType, reportID, navigateBack, isInvalidWaypoint]); - - return ( - - {({safeAreaPaddingBottomStyle}) => ( - - - navigateToConfirmationStep(iouType)} - navigateToSplit={() => navigateToConfirmationStep(CONST.IOU.TYPE.SPLIT)} - safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} - iouType={iouType} - isDistanceRequest={isDistanceRequest} - isScanRequest={isScanRequest} - /> - - )} - - ); -} - -MoneyRequestParticipantsPage.displayName = 'MoneyRequestParticipantsPage'; -MoneyRequestParticipantsPage.propTypes = propTypes; -MoneyRequestParticipantsPage.defaultProps = defaultProps; - -export default compose( - withOnyx({ - iou: { - key: ONYXKEYS.IOU, - }, - selectedTab: { - key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({iou}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(iou, 'transactionID', 0)}`, - }, - }), -)(MoneyRequestParticipantsPage); From 86190f61d6f9f090ea0714939a15685c57cdc246 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 6 Feb 2024 01:01:01 +0700 Subject: [PATCH 0020/1096] revert hard code --- .../ReportActionItem/ReportActionItemImage.tsx | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index 7dc4cdf07031..ed46e00c7923 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -73,13 +73,11 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr ); } else { receiptImageComponent = ( - - - + ); } From db861f619867edcfad892e05d4c48507223097d1 Mon Sep 17 00:00:00 2001 From: Krishna Gupta Date: Tue, 6 Feb 2024 12:08:45 +0530 Subject: [PATCH 0021/1096] remove MoneyRequestParticipantsPage.js. Signed-off-by: Krishna Gupta --- .../MoneyRequestParticipantsPage.js | 173 ------------------ 1 file changed, 173 deletions(-) delete mode 100644 src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js diff --git a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js b/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js deleted file mode 100644 index ea57d88579ae..000000000000 --- a/src/pages/iou/steps/MoneyRequstParticipantsPage/MoneyRequestParticipantsPage.js +++ /dev/null @@ -1,173 +0,0 @@ -import _ from 'lodash'; -import lodashGet from 'lodash/get'; -import lodashSize from 'lodash/size'; -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import ScreenWrapper from '@components/ScreenWrapper'; -import transactionPropTypes from '@components/transactionPropTypes'; -import useInitialValue from '@hooks/useInitialValue'; -import useLocalize from '@hooks/useLocalize'; -import useThemeStyles from '@hooks/useThemeStyles'; -import compose from '@libs/compose'; -import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import * as MoneyRequestUtils from '@libs/MoneyRequestUtils'; -import Navigation from '@libs/Navigation/Navigation'; -import * as TransactionUtils from '@libs/TransactionUtils'; -import {iouDefaultProps, iouPropTypes} from '@pages/iou/propTypes'; -import * as IOU from '@userActions/IOU'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import MoneyRequestParticipantsSelector from './MoneyRequestParticipantsSelector'; - -const propTypes = { - /** React Navigation route */ - route: PropTypes.shape({ - /** Params from the route */ - params: PropTypes.shape({ - /** The type of IOU report, i.e. bill, request, send */ - iouType: PropTypes.string, - - /** The report ID of the IOU */ - reportID: PropTypes.string, - }), - }).isRequired, - - /** Holds data related to Money Request view state, rather than the underlying Money Request data. */ - iou: iouPropTypes, - - /** The current tab we have navigated to in the request modal. String that corresponds to the request type. */ - selectedTab: PropTypes.oneOf(_.values(CONST.TAB_REQUEST)), - - /** Transaction that stores the distance request data */ - transaction: transactionPropTypes, -}; - -const defaultProps = { - iou: iouDefaultProps, - transaction: {}, - selectedTab: undefined, -}; - -function MoneyRequestParticipantsPage({iou, selectedTab, route, transaction}) { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const prevMoneyRequestId = useRef(iou.id); - const iouType = useInitialValue(() => lodashGet(route, 'params.iouType', '')); - const reportID = useInitialValue(() => lodashGet(route, 'params.reportID', '')); - const isDistanceRequest = MoneyRequestUtils.isDistanceRequest(iouType, selectedTab); - const isSendRequest = iouType === CONST.IOU.TYPE.SEND; - const isScanRequest = MoneyRequestUtils.isScanRequest(selectedTab); - const isSplitRequest = iou.id === CONST.IOU.TYPE.SPLIT; - const waypoints = lodashGet(transaction, 'comment.waypoints', {}); - const validatedWaypoints = TransactionUtils.getValidWaypoints(waypoints); - const isInvalidWaypoint = lodashSize(validatedWaypoints) < 2; - const headerTitle = useMemo(() => { - if (isDistanceRequest) { - return translate('common.distance'); - } - - if (isSendRequest) { - return translate('common.send'); - } - - if (isScanRequest) { - return translate('tabSelector.scan'); - } - - if (iou.isSplitRequest) { - return translate('iou.split'); - } - - return translate('tabSelector.manual'); - }, [iou, isDistanceRequest, translate, isScanRequest, isSendRequest]); - - const navigateToConfirmationStep = (moneyRequestType) => { - IOU.setMoneyRequestId(moneyRequestType); - IOU.resetMoneyRequestCategory(); - Navigation.navigate(ROUTES.MONEY_REQUEST_CONFIRMATION.getRoute(moneyRequestType, reportID)); - }; - - const navigateBack = useCallback((forceFallback = false) => { - Navigation.goBack(ROUTES.MONEY_REQUEST.getRoute(iouType, reportID), forceFallback); - // eslint-disable-next-line react-hooks/exhaustive-deps -- no deps as we use only initial values - }, []); - - useEffect(() => { - const isInvalidDistanceRequest = !isDistanceRequest || isInvalidWaypoint; - - // ID in Onyx could change by initiating a new request in a separate browser tab or completing a request - if (prevMoneyRequestId.current !== iou.id) { - // The ID is cleared on completing a request. In that case, we will do nothing - if (iou.id && isInvalidDistanceRequest && !isSplitRequest) { - navigateBack(true); - } - return; - } - - // Reset the money request Onyx if the ID in Onyx does not match the ID from params - const moneyRequestId = `${iouType}${reportID}`; - const shouldReset = iou.id !== moneyRequestId && !_.isEmpty(reportID); - if (shouldReset) { - IOU.resetMoneyRequestInfo(moneyRequestId); - } - if (isInvalidDistanceRequest && ((iou.amount === 0 && !iou.receiptPath) || shouldReset)) { - navigateBack(true); - } - - return () => { - prevMoneyRequestId.current = iou.id; - }; - }, [iou.amount, iou.id, iou.receiptPath, isDistanceRequest, isSplitRequest, iouType, reportID, navigateBack, isInvalidWaypoint]); - - return ( - - {({safeAreaPaddingBottomStyle}) => ( - - - navigateToConfirmationStep(iouType)} - navigateToSplit={() => navigateToConfirmationStep(CONST.IOU.TYPE.SPLIT)} - safeAreaPaddingBottomStyle={safeAreaPaddingBottomStyle} - iouType={iouType} - isDistanceRequest={isDistanceRequest} - isScanRequest={isScanRequest} - /> - - )} - - ); -} - -MoneyRequestParticipantsPage.displayName = 'MoneyRequestParticipantsPage'; -MoneyRequestParticipantsPage.propTypes = propTypes; -MoneyRequestParticipantsPage.defaultProps = defaultProps; - -export default compose( - withOnyx({ - iou: { - key: ONYXKEYS.IOU, - }, - selectedTab: { - key: `${ONYXKEYS.COLLECTION.SELECTED_TAB}${CONST.TAB.RECEIPT_TAB_ID}`, - }, - }), - // eslint-disable-next-line rulesdir/no-multiple-onyx-in-file - withOnyx({ - transaction: { - key: ({iou}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${lodashGet(iou, 'transactionID', 0)}`, - }, - }), -)(MoneyRequestParticipantsPage); From bd0ccb7e82426ce3ef30b1c8aa5ba6a569ec7eb5 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 6 Feb 2024 23:42:11 +0700 Subject: [PATCH 0022/1096] merge main --- .../ComposerWithSuggestionsEdit.tsx | 5 ++--- src/pages/home/report/ReportActionItemMessageEdit.tsx | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx index 927d160a8b7f..02a629c954e2 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx @@ -1,7 +1,6 @@ import type {Dispatch, ForwardedRef, RefObject, SetStateAction} from 'react'; import React, {useState} from 'react'; -import type {TextInputProps} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; +import type {TextInput} from 'react-native'; import Composer from '@components/Composer'; import type {ComposerProps} from '@components/Composer/types'; import type {SuggestionsRef} from '@libs/actions/SuggestionsAction'; @@ -45,7 +44,7 @@ function ComposerWithSuggestionsEdit( measureParentContainer, id = undefined, }: ComposerWithSuggestionsEditProps & ComposerProps, - ref: ForwardedRef>>, + ref: ForwardedRef, ) { const [composerHeight, setComposerHeight] = useState(0); diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 8dad9fce6003..a89b509a5f9a 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -1,3 +1,4 @@ +import {PortalHost} from '@gorhom/portal'; import ExpensiMark from 'expensify-common/lib/ExpensiMark'; import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; @@ -39,7 +40,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import ComposerWithSuggestionsEdit from './ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit'; -import { PortalHost } from '@gorhom/portal'; const {RNTextInputReset} = NativeModules; From 3088672dda44f8eb7173c0545ef1c1adfebd4498 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 13 Feb 2024 18:44:45 +0500 Subject: [PATCH 0023/1096] perf: use context for shared reports access --- src/App.tsx | 4 + .../LHNOptionsList/LHNOptionsList.tsx | 6 +- src/components/LHNOptionsList/types.ts | 3 - src/hooks/useOrderedReportIDs.tsx | 145 +++++++++++ src/hooks/useReports.tsx | 44 ++++ .../AppNavigator/ReportScreenIDSetter.ts | 13 +- src/libs/SidebarUtils.ts | 57 ++--- src/pages/home/sidebar/SidebarLinksData.js | 228 +----------------- src/pages/workspace/WorkspacesListPage.tsx | 12 +- src/types/onyx/PriorityMode.ts | 6 + 10 files changed, 228 insertions(+), 290 deletions(-) create mode 100644 src/hooks/useOrderedReportIDs.tsx create mode 100644 src/hooks/useReports.tsx create mode 100644 src/types/onyx/PriorityMode.ts diff --git a/src/App.tsx b/src/App.tsx index 7c1ead1d86d3..712b82c21291 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,8 @@ import {KeyboardStateProvider} from './components/withKeyboardState'; import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; +import {OrderedReportIDsContextProvider} from './hooks/useOrderedReportIDs'; +import {ReportsContextProvider} from './hooks/useReports'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import * as Session from './libs/actions/Session'; import * as Environment from './libs/Environment/Environment'; @@ -79,6 +81,8 @@ function App({url}: AppProps) { EnvironmentProvider, CustomStatusBarAndBackgroundContextProvider, ActiveWorkspaceContextProvider, + ReportsContextProvider, + OrderedReportIDsContextProvider, ]} > diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index a55d329f39ee..5ab105eda8df 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -5,6 +5,7 @@ import {StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import withCurrentReportID from '@components/withCurrentReportID'; import usePermissions from '@hooks/usePermissions'; +import {useReports} from '@hooks/useReports'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import variables from '@styles/variables'; @@ -22,7 +23,6 @@ function LHNOptionsList({ onSelectRow, optionMode, shouldDisableFocusOptions = false, - reports = {}, reportActions = {}, policy = {}, preferredLocale = CONST.LOCALES.DEFAULT, @@ -33,6 +33,7 @@ function LHNOptionsList({ transactionViolations = {}, onFirstItemRendered = () => {}, }: LHNOptionsListProps) { + const reports = useReports(); const styles = useThemeStyles(); const {canUseViolations} = usePermissions(); @@ -134,9 +135,6 @@ LHNOptionsList.displayName = 'LHNOptionsList'; export default withCurrentReportID( withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, reportActions: { key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, }, diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 58bea97f04c9..c58770c8383f 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -15,9 +15,6 @@ type LHNOptionsListOnyxProps = { /** The policy which the user has access to and which the report could be tied to */ policy: OnyxCollection; - /** All reports shared with the user */ - reports: OnyxCollection; - /** Array of report actions for this report */ reportActions: OnyxCollection; diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportIDs.tsx new file mode 100644 index 000000000000..3217830054de --- /dev/null +++ b/src/hooks/useOrderedReportIDs.tsx @@ -0,0 +1,145 @@ +import React, {createContext, useContext, useMemo} from 'react'; +import {withOnyx} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {getCurrentUserAccountID} from '@libs/actions/Report'; +import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import SidebarUtils from '@libs/SidebarUtils'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Beta, Policy, PolicyMembers, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type PriorityMode from '@src/types/onyx/PriorityMode'; +import useActiveWorkspace from './useActiveWorkspace'; +import useCurrentReportID from './useCurrentReportID'; +import {useReports} from './useReports'; + +type OnyxProps = { + betas: OnyxEntry; + policies: OnyxCollection; + allReportActions: OnyxCollection; + transactionViolations: OnyxCollection; + policyMembers: OnyxCollection; + priorityMode: OnyxEntry; +}; + +type WithOrderedReportIDsContextProviderProps = OnyxProps & { + children: React.ReactNode; +}; + +const OrderedReportIDsContext = createContext({}); + +function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextProviderProps) { + const chatReports = useReports(); + const currentReportIDValue = useCurrentReportID(); + const {activeWorkspaceID} = useActiveWorkspace(); + + const policyMemberAccountIDs = useMemo( + () => getPolicyMembersByIdWithoutCurrentUser(props.policyMembers, activeWorkspaceID, getCurrentUserAccountID()), + [activeWorkspaceID, props.policyMembers], + ); + + const optionListItems = useMemo( + () => + SidebarUtils.getOrderedReportIDs( + null, + chatReports, + props.betas ?? [], + props.policies, + props.priorityMode, + props.allReportActions, + props.transactionViolations, + activeWorkspaceID, + policyMemberAccountIDs, + ), + [chatReports, props.betas, props.policies, props.priorityMode, props.allReportActions, props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs], + ); + + // We need to make sure the current report is in the list of reports, but we do not want + // to have to re-generate the list every time the currentReportID changes. To do that + // we first generate the list as if there was no current report, then here we check if + // the current report is missing from the list, which should very rarely happen. In this + // case we re-generate the list a 2nd time with the current report included. + const optionListItemsWithCurrentReport = useMemo(() => { + if (currentReportIDValue?.currentReportID && !optionListItems.includes(currentReportIDValue.currentReportID)) { + return SidebarUtils.getOrderedReportIDs( + currentReportIDValue.currentReportID, + chatReports, + props.betas ?? [], + props.policies, + props.priorityMode, + props.allReportActions, + props.transactionViolations, + activeWorkspaceID, + policyMemberAccountIDs, + ); + } + return optionListItems; + }, [ + activeWorkspaceID, + chatReports, + currentReportIDValue?.currentReportID, + optionListItems, + policyMemberAccountIDs, + props.allReportActions, + props.betas, + props.policies, + props.priorityMode, + props.transactionViolations, + ]); + + return {props.children}; +} + +const reportActionsSelector = (reportActions: OnyxEntry) => { + if (!reportActions) { + return []; + } + + return Object.values(reportActions).map((reportAction) => { + const {reportActionID, actionName, originalMessage} = reportAction ?? {}; + const decision = reportAction?.message?.[0]?.moderationDecision?.decision; + return { + reportActionID, + actionName, + originalMessage, + message: [ + { + moderationDecision: {decision}, + }, + ], + }; + }); +}; + +const OrderedReportIDsContextProvider = withOnyx({ + priorityMode: { + key: ONYXKEYS.NVP_PRIORITY_MODE, + initialValue: CONST.PRIORITY_MODE.DEFAULT, + }, + betas: { + key: ONYXKEYS.BETAS, + initialValue: [], + }, + allReportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + // @ts-expect-error Need some help in determining the correct type for this selector + selector: (actions) => reportActionsSelector(actions), + initialValue: {}, + }, + policies: { + key: ONYXKEYS.COLLECTION.POLICY, + initialValue: {}, + }, + policyMembers: { + key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + initialValue: {}, + }, +})(WithOrderedReportIDsContextProvider); + +function useOrderedReportIDs() { + return useContext(OrderedReportIDsContext); +} + +export {OrderedReportIDsContextProvider, OrderedReportIDsContext, useOrderedReportIDs}; diff --git a/src/hooks/useReports.tsx b/src/hooks/useReports.tsx new file mode 100644 index 000000000000..c4082149cbf4 --- /dev/null +++ b/src/hooks/useReports.tsx @@ -0,0 +1,44 @@ +import React, {createContext, useContext, useEffect, useMemo, useState} from 'react'; +import Onyx from 'react-native-onyx'; +import type {OnyxCollection} from 'react-native-onyx'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {Report} from '@src/types/onyx'; + +type Reports = OnyxCollection; +type ReportsContextValue = Reports; + +type ReportsContextProviderProps = { + children: React.ReactNode; +}; + +const ReportsContext = createContext(null); + +function ReportsContextProvider(props: ReportsContextProviderProps) { + const [reports, setReports] = useState(null); + + useEffect(() => { + // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs + const connID = Onyx.connect({ + key: ONYXKEYS.COLLECTION.REPORT, + waitForCollectionCallback: true, + callback: (val) => { + setReports(val); + }, + }); + return () => { + Onyx.disconnect(connID); + }; + }, []); + + const contextValue = useMemo(() => reports ?? {}, [reports]); + + return {props.children}; +} + +function useReports() { + return useContext(ReportsContext); +} + +ReportsContextProvider.displayName = 'ReportsContextProvider'; + +export {ReportsContextProvider, ReportsContext, useReports}; diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts index b4bb56262860..83df18d5492d 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts +++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts @@ -3,6 +3,7 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import usePermissions from '@hooks/usePermissions'; +import {useReports} from '@hooks/useReports'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as App from '@userActions/App'; @@ -11,9 +12,6 @@ import type {Policy, PolicyMembers, Report, ReportMetadata} from '@src/types/ony import type {ReportScreenWrapperProps} from './ReportScreenWrapper'; type ReportScreenIDSetterComponentProps = { - /** Available reports that would be displayed in this navigator */ - reports: OnyxCollection; - /** The policies which the user has access to */ policies: OnyxCollection; @@ -59,9 +57,10 @@ const getLastAccessedReportID = ( }; // This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params -function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { +function ReportScreenIDSetter({route, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); + const reports = useReports(); useEffect(() => { // Don't update if there is a reportID in the params already @@ -83,7 +82,7 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav !canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, - !!reports?.params?.openOnAdminRoom, + !!route?.params?.openOnAdminRoom, reportMetadata, activeWorkspaceID, policyMemberAccountIDs, @@ -106,10 +105,6 @@ function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, nav ReportScreenIDSetter.displayName = 'ReportScreenIDSetter'; export default withOnyx({ - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - allowStaleData: true, - }, policies: { key: ONYXKEYS.COLLECTION.POLICY, allowStaleData: true, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 2347f5b9f5c5..e51276c5c28a 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -2,12 +2,12 @@ import Str from 'expensify-common/lib/str'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; -import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type Policy from '@src/types/onyx/Policy'; +import type PriorityMode from '@src/types/onyx/PriorityMode'; import type Report from '@src/types/onyx/Report'; import type {ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; @@ -78,59 +78,23 @@ function setIsSidebarLoadedReady() { resolveSidebarIsReadyPromise(); } -// Define a cache object to store the memoized results -const reportIDsCache = new Map(); - -// Function to set a key-value pair while maintaining the maximum key limit -function setWithLimit(map: Map, key: TKey, value: TValue) { - if (map.size >= 5) { - // If the map has reached its limit, remove the first (oldest) key-value pair - const firstKey = map.keys().next().value; - map.delete(firstKey); - } - map.set(key, value); -} - -// Variable to verify if ONYX actions are loaded -let hasInitialReportActions = false; - /** * @returns An array of reportIDs sorted in the proper order */ function getOrderedReportIDs( currentReportId: string | null, - allReports: Record, + allReports: OnyxCollection, betas: Beta[], - policies: Record, - priorityMode: ValueOf, + policies: OnyxCollection, + priorityMode: OnyxEntry, allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', policyMemberAccountIDs: number[] = [], ): string[] { - const currentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${currentReportId}`]; - let reportActionCount = currentReportActions?.length ?? 0; - reportActionCount = Math.max(reportActionCount, 1); - - // Generate a unique cache key based on the function arguments - const cachedReportsKey = JSON.stringify( - [currentReportId, allReports, betas, policies, priorityMode, reportActionCount, currentPolicyID, policyMemberAccountIDs], - // Exclude some properties not to overwhelm a cached key value with huge data, which we don't need to store in a cacheKey - (key, value: unknown) => (['participantAccountIDs', 'participants', 'lastMessageText', 'visibleChatMemberAccountIDs'].includes(key) ? undefined : value), - ); - - // Check if the result is already in the cache - const cachedIDs = reportIDsCache.get(cachedReportsKey); - if (cachedIDs && hasInitialReportActions) { - return cachedIDs; - } - - // This is needed to prevent caching when Onyx is empty for a second render - hasInitialReportActions = Object.values(lastReportActions).length > 0; - const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; - const allReportsDictValues = Object.values(allReports); + const allReportsDictValues = Object.values(allReports ?? {}); // Filter out all the reports that shouldn't be displayed let reportsToDisplay = allReportsDictValues.filter((report) => { @@ -173,10 +137,18 @@ function getOrderedReportIDs( const archivedReports: Report[] = []; if (currentPolicyID || policyMemberAccountIDs.length > 0) { - reportsToDisplay = reportsToDisplay.filter((report) => ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID)); + reportsToDisplay = reportsToDisplay.filter((report) => { + if (!report) { + return false; + } + return ReportUtils.doesReportBelongToWorkspace(report, policyMemberAccountIDs, currentPolicyID); + }); } // There are a few properties that need to be calculated for the report which are used when sorting reports. reportsToDisplay.forEach((report) => { + if (!report) { + return; + } // Normally, the spread operator would be used here to clone the report and prevent the need to reassign the params. // However, this code needs to be very performant to handle thousands of reports, so in the interest of speed, we're just going to disable this lint rule and add // the reportDisplayName property to the report object directly. @@ -219,7 +191,6 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); - setWithLimit(reportIDsCache, cachedReportsKey, LHNReports); return LHNReports; } diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 3bd538e8beab..42681fea451b 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -1,11 +1,8 @@ import {deepEqual} from 'fast-equals'; -import lodashGet from 'lodash/get'; -import lodashMap from 'lodash/map'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; import withCurrentReportID from '@components/withCurrentReportID'; @@ -13,13 +10,11 @@ import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalD import withNavigationFocus from '@components/withNavigationFocus'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; +import {useOrderedReportIDs} from '@hooks/useOrderedReportIDs'; import usePrevious from '@hooks/usePrevious'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; -import SidebarUtils from '@libs/SidebarUtils'; -import reportPropTypes from '@pages/reportPropTypes'; import * as Policy from '@userActions/Policy'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -28,107 +23,30 @@ import SidebarLinks, {basePropTypes} from './SidebarLinks'; const propTypes = { ...basePropTypes, - /* Onyx Props */ - /** List of reports */ - chatReports: PropTypes.objectOf(reportPropTypes), - - /** All report actions for all reports */ - - /** Object of report actions for this report */ - allReportActions: PropTypes.objectOf( - PropTypes.arrayOf( - PropTypes.shape({ - error: PropTypes.string, - message: PropTypes.arrayOf( - PropTypes.shape({ - moderationDecision: PropTypes.shape({ - decision: PropTypes.string, - }), - }), - ), - }), - ), - ), - /** Whether the reports are loading. When false it means they are ready to be used. */ isLoadingApp: PropTypes.bool, /** The chat priority mode */ priorityMode: PropTypes.string, - /** Beta features list */ - betas: PropTypes.arrayOf(PropTypes.string), - network: networkPropTypes.isRequired, - /** The policies which the user has access to */ - // eslint-disable-next-line react/forbid-prop-types - policies: PropTypes.object, - - // eslint-disable-next-line react/forbid-prop-types - policyMembers: PropTypes.object, - /** Session info for the currently logged in user. */ session: PropTypes.shape({ /** Currently logged in user accountID */ accountID: PropTypes.number, }), - /** All of the transaction violations */ - transactionViolations: PropTypes.shape({ - violations: PropTypes.arrayOf( - PropTypes.shape({ - /** The transaction ID */ - transactionID: PropTypes.number, - - /** The transaction violation type */ - type: PropTypes.string, - - /** The transaction violation message */ - message: PropTypes.string, - - /** The transaction violation data */ - data: PropTypes.shape({ - /** The transaction violation data field */ - field: PropTypes.string, - - /** The transaction violation data value */ - value: PropTypes.string, - }), - }), - ), - }), }; const defaultProps = { - chatReports: {}, - allReportActions: {}, isLoadingApp: true, priorityMode: CONST.PRIORITY_MODE.DEFAULT, - betas: [], - policies: {}, - policyMembers: {}, session: { accountID: '', }, - transactionViolations: {}, }; -function SidebarLinksData({ - isFocused, - allReportActions, - betas, - chatReports, - currentReportID, - insets, - isLoadingApp, - onLinkClick, - policies, - priorityMode, - network, - policyMembers, - session: {accountID}, - transactionViolations, -}) { +function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, session: {accountID}}) { const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); @@ -141,19 +59,8 @@ function SidebarLinksData({ const reportIDsRef = useRef(null); const isLoading = isLoadingApp; + const reportIDs = useOrderedReportIDs(); const optionListItems = useMemo(() => { - const reportIDs = SidebarUtils.getOrderedReportIDs( - null, - chatReports, - betas, - policies, - priorityMode, - allReportActions, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ); - if (deepEqual(reportIDsRef.current, reportIDs)) { return reportIDsRef.current; } @@ -165,29 +72,7 @@ function SidebarLinksData({ reportIDsRef.current = reportIDs; } return reportIDsRef.current || []; - }, [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, isLoading, network.isOffline, prevPriorityMode]); - - // We need to make sure the current report is in the list of reports, but we do not want - // to have to re-generate the list every time the currentReportID changes. To do that - // we first generate the list as if there was no current report, then here we check if - // the current report is missing from the list, which should very rarely happen. In this - // case we re-generate the list a 2nd time with the current report included. - const optionListItemsWithCurrentReport = useMemo(() => { - if (currentReportID && !_.contains(optionListItems, currentReportID)) { - return SidebarUtils.getOrderedReportIDs( - currentReportID, - chatReports, - betas, - policies, - priorityMode, - allReportActions, - transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - ); - } - return optionListItems; - }, [currentReportID, optionListItems, chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs]); + }, [reportIDs, isLoading, network.isOffline, prevPriorityMode, priorityMode]); const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; @@ -207,8 +92,8 @@ function SidebarLinksData({ // Data props: isActiveReport={isActiveReport} isLoading={isLoading} - optionListItems={optionListItemsWithCurrentReport} activeWorkspaceID={activeWorkspaceID} + optionListItems={optionListItems} /> ); @@ -218,97 +103,12 @@ SidebarLinksData.propTypes = propTypes; SidebarLinksData.defaultProps = defaultProps; SidebarLinksData.displayName = 'SidebarLinksData'; -/** - * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering - * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - * @param {Object} [report] - * @returns {Object|undefined} - */ -const chatReportSelector = (report) => - report && { - reportID: report.reportID, - participantAccountIDs: report.participantAccountIDs, - hasDraft: report.hasDraft, - isPinned: report.isPinned, - isHidden: report.isHidden, - notificationPreference: report.notificationPreference, - errorFields: { - addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, - }, - lastMessageText: report.lastMessageText, - lastVisibleActionCreated: report.lastVisibleActionCreated, - iouReportID: report.iouReportID, - total: report.total, - nonReimbursableTotal: report.nonReimbursableTotal, - hasOutstandingChildRequest: report.hasOutstandingChildRequest, - isWaitingOnBankAccount: report.isWaitingOnBankAccount, - statusNum: report.statusNum, - stateNum: report.stateNum, - chatType: report.chatType, - type: report.type, - policyID: report.policyID, - visibility: report.visibility, - lastReadTime: report.lastReadTime, - // Needed for name sorting: - reportName: report.reportName, - policyName: report.policyName, - oldPolicyName: report.oldPolicyName, - // Other less obvious properites considered for sorting: - ownerAccountID: report.ownerAccountID, - currency: report.currency, - managerID: report.managerID, - // Other important less obivous properties for filtering: - parentReportActionID: report.parentReportActionID, - parentReportID: report.parentReportID, - isDeletedParentAction: report.isDeletedParentAction, - isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }; - -/** - * @param {Object} [reportActions] - * @returns {Object|undefined} - */ -const reportActionsSelector = (reportActions) => - reportActions && - lodashMap(reportActions, (reportAction) => { - const {reportActionID, parentReportActionID, actionName, errors = []} = reportAction; - const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); - - return { - reportActionID, - parentReportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - }, - ], - }; - }); - -/** - * @param {Object} [policy] - * @returns {Object|undefined} - */ -const policySelector = (policy) => - policy && { - type: policy.type, - name: policy.name, - avatar: policy.avatar, - }; - export default compose( withCurrentReportID, withCurrentUserPersonalDetails, withNavigationFocus, withNetwork(), withOnyx({ - chatReports: { - key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, - initialValue: {}, - }, isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, }, @@ -316,26 +116,8 @@ export default compose( key: ONYXKEYS.NVP_PRIORITY_MODE, initialValue: CONST.PRIORITY_MODE.DEFAULT, }, - betas: { - key: ONYXKEYS.BETAS, - initialValue: [], - }, - allReportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - selector: reportActionsSelector, - initialValue: {}, - }, - policies: { - key: ONYXKEYS.COLLECTION.POLICY, - selector: policySelector, - initialValue: {}, - }, policyMembers: { key: ONYXKEYS.COLLECTION.POLICY_MEMBERS, }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - initialValue: {}, - }, }), )(SidebarLinksData); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index aa4f1df1ba7a..5efc94ba5974 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -20,6 +20,7 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import {useReports} from '@hooks/useReports'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -33,7 +34,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; -import type {PolicyMembers, Policy as PolicyType, ReimbursementAccount, Report} from '@src/types/onyx'; +import type {PolicyMembers, Policy as PolicyType, ReimbursementAccount} from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; @@ -72,9 +73,6 @@ type WorkspaceListPageOnyxProps = { /** A collection of objects for all policies which key policy member objects by accountIDs */ allPolicyMembers: OnyxCollection; - - /** All reports shared with the user (coming from Onyx) */ - reports: OnyxCollection; }; type WorkspaceListPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceListPageOnyxProps; @@ -110,7 +108,8 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi throw new Error('Not implemented'); } -function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, reports}: WorkspaceListPageProps) { +function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: WorkspaceListPageProps) { + const reports = useReports(); const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -407,8 +406,5 @@ export default withPolicyAndFullscreenLoading( reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, - reports: { - key: ONYXKEYS.COLLECTION.REPORT, - }, })(WorkspacesListPage), ); diff --git a/src/types/onyx/PriorityMode.ts b/src/types/onyx/PriorityMode.ts new file mode 100644 index 000000000000..5fef300ed014 --- /dev/null +++ b/src/types/onyx/PriorityMode.ts @@ -0,0 +1,6 @@ +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; + +type PriorityMode = ValueOf; + +export default PriorityMode; From 6921051449b209820347247511a943d8b4624ffb Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 15 Feb 2024 19:19:58 +0500 Subject: [PATCH 0024/1096] perf: improve renderItem and use FlatList --- .../LHNOptionsList/LHNOptionsList.tsx | 114 +++--------------- .../LHNOptionsList/OptionRowLHNData.tsx | 51 +------- src/components/LHNOptionsList/types.ts | 3 +- src/components/withCurrentReportID.tsx | 10 +- src/hooks/useOrderedReportIDs.tsx | 38 +++++- src/libs/SidebarUtils.ts | 40 +++++- src/pages/home/sidebar/SidebarLinks.js | 4 +- src/pages/home/sidebar/SidebarLinksData.js | 2 +- src/pages/workspace/WorkspacesListPage.tsx | 4 +- 9 files changed, 111 insertions(+), 155 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index e0a869f5222a..c70e355c46c5 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,41 +1,27 @@ -import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {memo, useCallback} from 'react'; -import {StyleSheet, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; -import withCurrentReportID from '@components/withCurrentReportID'; -import usePermissions from '@hooks/usePermissions'; -import {useReports} from '@hooks/useReports'; +import React, {useCallback, memo} from 'react'; +import {FlatList, StyleSheet, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import * as ReportActionsUtils from '@libs/ReportActionsUtils'; -import variables from '@styles/variables'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; import OptionRowLHNData from './OptionRowLHNData'; -import type {LHNOptionsListOnyxProps, LHNOptionsListProps, RenderItemProps} from './types'; +import type {LHNOptionsListProps, RenderItemProps} from './types'; +import { OrderedReports } from '@libs/SidebarUtils'; +import CONST from '@src/CONST'; +import variables from '@styles/variables'; +import { FlashList } from '@shopify/flash-list'; -const keyExtractor = (item: string) => `report_${item}`; +const keyExtractor = (item: OrderedReports) => `report_${item?.reportID}`; function LHNOptionsList({ style, contentContainerStyles, data, onSelectRow, - optionMode, shouldDisableFocusOptions = false, - reportActions = {}, - policy = {}, - preferredLocale = CONST.LOCALES.DEFAULT, - personalDetails = {}, - transactions = {}, currentReportID = '', - draftComments = {}, - transactionViolations = {}, + optionMode, onFirstItemRendered = () => {}, }: LHNOptionsListProps) { - const reports = useReports(); const styles = useThemeStyles(); - const {canUseViolations} = usePermissions(); // When the first item renders we want to call the onFirstItemRendered callback. // At this point in time we know that the list is actually displaying items. @@ -53,69 +39,29 @@ function LHNOptionsList({ * Function which renders a row in the list */ const renderItem = useCallback( - ({item: reportID}: RenderItemProps): ReactElement => { - const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; - const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; - const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; - const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; - const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : ''; - const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null; - const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; - const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); - const lastReportAction = sortedReportActions[0]; - - // Get the transaction for the last report action - let lastReportActionTransactionID = ''; - - if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { - lastReportActionTransactionID = lastReportAction.originalMessage?.IOUTransactionID ?? ''; - } - const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`] ?? {}; + ({item}: RenderItemProps): ReactElement => { return ( ); }, [ currentReportID, - draftComments, onSelectRow, - optionMode, - personalDetails, - policy, - preferredLocale, - reportActions, - reports, - shouldDisableFocusOptions, - transactions, - transactionViolations, - canUseViolations, onLayoutItem, ], ); return ( - @@ -133,30 +79,6 @@ function LHNOptionsList({ LHNOptionsList.displayName = 'LHNOptionsList'; -export default withCurrentReportID( - withOnyx({ - reportActions: { - key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, - }, - policy: { - key: ONYXKEYS.COLLECTION.POLICY, - }, - preferredLocale: { - key: ONYXKEYS.NVP_PREFERRED_LOCALE, - }, - personalDetails: { - key: ONYXKEYS.PERSONAL_DETAILS_LIST, - }, - transactions: { - key: ONYXKEYS.COLLECTION.TRANSACTION, - }, - draftComments: { - key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, - }, - transactionViolations: { - key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, - }, - })(memo(LHNOptionsList)), -); +export default memo(LHNOptionsList); export type {LHNOptionsListProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index a18d5a8ec1ec..622d7b9d9123 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -16,61 +16,12 @@ import type {OptionRowLHNDataProps} from './types'; */ function OptionRowLHNData({ isFocused = false, - fullReport, - reportActions, - personalDetails = {}, - preferredLocale = CONST.LOCALES.DEFAULT, comment, - policy, - receiptTransactions, - parentReportAction, - transaction, - lastReportActionTransaction = {}, - transactionViolations, - canUseViolations, + optionItem, ...propsToForward }: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; - const optionItemRef = useRef(); - - const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction ?? null); - - const optionItem = useMemo(() => { - // Note: ideally we'd have this as a dependent selector in onyx! - const item = SidebarUtils.getOptionData({ - report: fullReport, - reportActions, - personalDetails, - preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, - policy, - parentReportAction, - hasViolations: !!hasViolations, - }); - if (deepEqual(item, optionItemRef.current)) { - return optionItemRef.current; - } - - optionItemRef.current = item; - - return item; - // Listen parentReportAction to update title of thread report when parentReportAction changed - // Listen to transaction to update title of transaction report when transaction changed - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - fullReport, - lastReportActionTransaction, - reportActions, - personalDetails, - preferredLocale, - policy, - parentReportAction, - transaction, - transactionViolations, - canUseViolations, - receiptTransactions, - ]); - useEffect(() => { if (!optionItem || !!optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { return; diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index c58770c8383f..4c79536571bf 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -8,6 +8,7 @@ import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; +import { OrderedReports } from '@libs/SidebarUtils'; type OptionMode = ValueOf; @@ -134,6 +135,6 @@ type OptionRowLHNProps = { onLayout?: (event: LayoutChangeEvent) => void; }; -type RenderItemProps = {item: string}; +type RenderItemProps = {item: OrderedReports}; export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps}; diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index a452e7565b4e..cc49c44e0e77 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -39,7 +39,15 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro */ const updateCurrentReportID = useCallback( (state: NavigationState) => { - setCurrentReportID(Navigation.getTopmostReportId(state) ?? ''); + const reportID = Navigation.getTopmostReportId(state) ?? ''; + /** + * This is to make sure we don't set the undefined as reportID when + * switching between chat list and settings->workspaces tab. + * and doing so avoid unnecessary re-render of `useOrderedReportIDs`. + */ + if (reportID) { + setCurrentReportID(reportID); + } }, [setCurrentReportID], ); diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportIDs.tsx index 3217830054de..01dab5a3231a 100644 --- a/src/hooks/useOrderedReportIDs.tsx +++ b/src/hooks/useOrderedReportIDs.tsx @@ -1,6 +1,7 @@ import React, {createContext, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import {usePersonalDetails} from '@components/OnyxProvider'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import SidebarUtils from '@libs/SidebarUtils'; @@ -10,6 +11,7 @@ import type {Beta, Policy, PolicyMembers, ReportAction, ReportActions, Transacti import type PriorityMode from '@src/types/onyx/PriorityMode'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; +import usePermissions from './usePermissions'; import {useReports} from './useReports'; type OnyxProps = { @@ -31,6 +33,8 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP const chatReports = useReports(); const currentReportIDValue = useCurrentReportID(); const {activeWorkspaceID} = useActiveWorkspace(); + const personalDetails = usePersonalDetails(); + const {canUseViolations} = usePermissions(); const policyMemberAccountIDs = useMemo( () => getPolicyMembersByIdWithoutCurrentUser(props.policyMembers, activeWorkspaceID, getCurrentUserAccountID()), @@ -49,8 +53,25 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, ), - [chatReports, props.betas, props.policies, props.priorityMode, props.allReportActions, props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs], + [ + chatReports, + props.betas, + props.policies, + props.priorityMode, + props.allReportActions, + props.transactionViolations, + activeWorkspaceID, + policyMemberAccountIDs, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, + ], ); // We need to make sure the current report is in the list of reports, but we do not want @@ -70,6 +91,10 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.transactionViolations, activeWorkspaceID, policyMemberAccountIDs, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, ); } return optionListItems; @@ -84,6 +109,10 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.policies, props.priorityMode, props.transactionViolations, + personalDetails, + props.preferredLocale, + canUseViolations, + props.draftComments, ]); return {props.children}; @@ -136,6 +165,13 @@ const OrderedReportIDsContextProvider = withOnyx, currentPolicyID = '', policyMemberAccountIDs: number[] = [], -): string[] { + personalDetails: OnyxEntry, + preferredLocale: DeepValueOf, + canUseViolations: boolean, + draftComments: OnyxCollection, +): OrderedReports[] { const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports ?? {}); @@ -190,7 +200,33 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); + const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => { + const itemFullReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`] ?? null; + const itemReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? null; + const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; + const itemPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; + const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report?.reportID}`] ?? ''; + + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(itemFullReport, transactionViolations, itemParentReportAction ?? null); + + const item = getOptionData({ + report: itemFullReport, + reportActions: itemReportActions, + personalDetails, + preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, + policy: itemPolicy, + parentReportAction: itemParentReportAction, + hasViolations: !!hasViolations, + }); + + return { + reportID: report.reportID, + optionItem: item, + comment: itemComment, + } + }); + return LHNReports; } diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index e78e656e0b7e..4c05c3d44ae8 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -1,7 +1,7 @@ /* eslint-disable rulesdir/onyx-props-must-have-default */ import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {memo, useCallback, useEffect, useMemo, useRef} from 'react'; import {InteractionManager, StyleSheet, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; @@ -179,5 +179,5 @@ export default withOnyx({ activePolicy: { key: ({activeWorkspaceID}) => `${ONYXKEYS.COLLECTION.POLICY}${activeWorkspaceID}`, }, -})(SidebarLinks); +})(memo(SidebarLinks)); export {basePropTypes}; diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 42681fea451b..87006d3debe2 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -46,7 +46,7 @@ const defaultProps = { }, }; -function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, session: {accountID}}) { +function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, session: {accountID}}) { const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index d778648ea998..968148a8becb 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -107,6 +107,8 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi throw new Error('Not implemented'); } +const stickyHeaderIndices = [0]; + function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: WorkspaceListPageProps) { const reports = useReports(); const theme = useTheme(); @@ -375,7 +377,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: data={workspaces} renderItem={getMenuItem} ListHeaderComponent={listHeaderComponent} - stickyHeaderIndices={[0]} + stickyHeaderIndices={stickyHeaderIndices} /> Date: Mon, 19 Feb 2024 13:35:45 +0700 Subject: [PATCH 0025/1096] fix lint --- .../AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx | 4 ++-- src/components/AutoCompleteSuggestions/index.tsx | 6 +++--- src/components/AutoCompleteSuggestions/types.ts | 2 +- src/libs/SuggestionUtils.ts | 4 ++-- src/pages/home/report/ReportActionItemMessageEdit.tsx | 3 +-- src/styles/utils/index.ts | 4 ++-- 6 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 2b7750f732f2..7b4d23d0f3e7 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -39,7 +39,7 @@ function BaseAutoCompleteSuggestions( suggestions, isSuggestionPickerLarge, keyExtractor, - shouldBelowParentContainer = false, + shouldBeDisplayedBelowParentContainer = false, }: AutoCompleteSuggestionsProps, ref: ForwardedRef, ) { @@ -68,7 +68,7 @@ function BaseAutoCompleteSuggestions( ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; - const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value, shouldBelowParentContainer)); + const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value, shouldBeDisplayedBelowParentContainer)); const estimatedListSize = useMemo( () => ({ height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length, diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index bda8e0aa7711..1f5d87b15962 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -4,7 +4,7 @@ import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; -import {measureHeightOfSuggestioContainer} from '@libs/SuggestionUtils'; +import {measureHeightOfSuggestionsContainer} from '@libs/SuggestionUtils'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; import type {AutoCompleteSuggestionsProps} from './types'; @@ -19,7 +19,7 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} const StyleUtils = useStyleUtils(); const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); - const suggestionContainerHeight = measureHeightOfSuggestioContainer(props.suggestions.length, props.isSuggestionPickerLarge); + const suggestionContainerHeight = measureHeightOfSuggestionsContainer(props.suggestions.length, props.isSuggestionPickerLarge); const [{width, left, bottom}, setContainerState] = React.useState({ width: 0, left: 0, @@ -56,7 +56,7 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} // eslint-disable-next-line react/jsx-props-no-spreading {...props} - shouldBelowParentContainer={shouldBelowContainer} + shouldBeDisplayedBelowParentContainer={shouldBelowContainer} ref={containerRef} /> ); diff --git a/src/components/AutoCompleteSuggestions/types.ts b/src/components/AutoCompleteSuggestions/types.ts index b837901026b2..d9824db1988d 100644 --- a/src/components/AutoCompleteSuggestions/types.ts +++ b/src/components/AutoCompleteSuggestions/types.ts @@ -35,7 +35,7 @@ type AutoCompleteSuggestionsProps = { measureParentContainer?: (callback: MeasureParentContainerCallback) => void; /** Whether suggestion should be displayed below the parent container or not */ - shouldBelowParentContainer?: boolean; + shouldBeDisplayedBelowParentContainer?: boolean; }; export type {AutoCompleteSuggestionsProps, RenderSuggestionMenuItemProps}; diff --git a/src/libs/SuggestionUtils.ts b/src/libs/SuggestionUtils.ts index 00039592ba77..f61989133409 100644 --- a/src/libs/SuggestionUtils.ts +++ b/src/libs/SuggestionUtils.ts @@ -20,7 +20,7 @@ function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight return availableHeight > menuHeight; } -const measureHeightOfSuggestioContainer = (numRows: number, isSuggestionPickerLarge: boolean): number => { +const measureHeightOfSuggestionsContainer = (numRows: number, isSuggestionPickerLarge: boolean): number => { const borderAndPadding = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + 2; let suggestionHeight = 0; @@ -40,4 +40,4 @@ const measureHeightOfSuggestioContainer = (numRows: number, isSuggestionPickerLa return suggestionHeight + borderAndPadding; }; -export {trimLeadingSpace, hasEnoughSpaceForLargeSuggestionMenu, measureHeightOfSuggestioContainer}; +export {trimLeadingSpace, hasEnoughSpaceForLargeSuggestionMenu, measureHeightOfSuggestionsContainer}; diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 1ef9406c6327..bdd41a27baa6 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -4,8 +4,7 @@ import Str from 'expensify-common/lib/str'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {findNodeHandle, InteractionManager, Keyboard, NativeModules, View} from 'react-native'; -import {Keyboard, View} from 'react-native'; +import {findNodeHandle, Keyboard, NativeModules, View} from 'react-native'; import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import type {Emoji} from '@assets/emojis/types'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index ec3f088da537..8ca45907449d 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -793,7 +793,7 @@ function getBaseAutoCompleteSuggestionContainerStyle({left, bottom, width}: GetB /** * Gets the correct position for auto complete suggestion container */ -function getAutoCompleteSuggestionContainerStyle(itemsHeight: number, shouldBelowParentContainer: boolean): ViewStyle { +function getAutoCompleteSuggestionContainerStyle(itemsHeight: number, shouldBeDisplayedBelowParentContainer: boolean): ViewStyle { 'worklet'; const borderWidth = 2; @@ -803,7 +803,7 @@ function getAutoCompleteSuggestionContainerStyle(itemsHeight: number, shouldBelo // we need to shift it by the suggester's height plus its padding and, if applicable, the height of the RecipientLocalTime view. return { overflow: 'hidden', - top: -(height + (shouldBelowParentContainer ? -2 : 1) * (CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + borderWidth)), + top: -(height + (shouldBeDisplayedBelowParentContainer ? -2 : 1) * (CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + borderWidth)), height, minHeight: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, }; From a7bcde62060bef47d9675cea3d300fb156fa170f Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 19 Feb 2024 13:37:25 +0700 Subject: [PATCH 0026/1096] update suggestion --- .../actions/{SuggestionsAction.ts => SuggestionsActions.ts} | 0 .../ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx | 2 +- src/pages/home/report/ReportActionItemMessageEdit.tsx | 2 +- src/pages/home/report/ReportActionsView.js | 2 +- src/styles/utils/index.ts | 2 +- 5 files changed, 4 insertions(+), 4 deletions(-) rename src/libs/actions/{SuggestionsAction.ts => SuggestionsActions.ts} (100%) diff --git a/src/libs/actions/SuggestionsAction.ts b/src/libs/actions/SuggestionsActions.ts similarity index 100% rename from src/libs/actions/SuggestionsAction.ts rename to src/libs/actions/SuggestionsActions.ts diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx index 02a629c954e2..379928e8f28f 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx @@ -3,7 +3,7 @@ import React, {useState} from 'react'; import type {TextInput} from 'react-native'; import Composer from '@components/Composer'; import type {ComposerProps} from '@components/Composer/types'; -import type {SuggestionsRef} from '@libs/actions/SuggestionsAction'; +import type {SuggestionsRef} from '@libs/actions/SuggestionsActions'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; type ComposerWithSuggestionsEditProps = { diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index bdd41a27baa6..226576e8b916 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -21,7 +21,7 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as SuggestionsAction from '@libs/actions/SuggestionsAction'; +import * as SuggestionsAction from '@libs/actions/SuggestionsActions'; import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index a76814caf147..cf23fb76d052 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -11,7 +11,7 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; -import * as SuggestionsAction from '@libs/actions/SuggestionsAction'; +import * as SuggestionsAction from '@libs/actions/SuggestionsActions'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 8ca45907449d..61ef018350df 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -783,7 +783,7 @@ type GetBaseAutoCompleteSuggestionContainerStyleParams = { */ function getBaseAutoCompleteSuggestionContainerStyle({left, bottom, width}: GetBaseAutoCompleteSuggestionContainerStyleParams): ViewStyle { return { - // ...positioning.pFixed, + ...positioning.pFixed, bottom, left, width, From be5faa2661dced9170ec02ff85c302d1f23eb726 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 19 Feb 2024 13:39:21 +0700 Subject: [PATCH 0027/1096] rename variable --- src/libs/SuggestionUtils.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/libs/SuggestionUtils.ts b/src/libs/SuggestionUtils.ts index f61989133409..414244d77e13 100644 --- a/src/libs/SuggestionUtils.ts +++ b/src/libs/SuggestionUtils.ts @@ -20,24 +20,24 @@ function hasEnoughSpaceForLargeSuggestionMenu(listHeight: number, composerHeight return availableHeight > menuHeight; } -const measureHeightOfSuggestionsContainer = (numRows: number, isSuggestionPickerLarge: boolean): number => { +const measureHeightOfSuggestionsContainer = (numRows: number, isSuggestionsPickerLarge: boolean): number => { const borderAndPadding = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + 2; - let suggestionHeight = 0; + let suggestionsHeight = 0; - if (isSuggestionPickerLarge) { + if (isSuggestionsPickerLarge) { if (numRows > CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER) { // On large screens, if there are more than 5 suggestions, we display a scrollable window with a height of 5 items, indicating that there are more items available - suggestionHeight = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + suggestionsHeight = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_VISIBLE_SUGGESTIONS_IN_CONTAINER * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; } else { - suggestionHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + suggestionsHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; } } else if (numRows > 2) { // On small screens, we display a scrollable window with a height of 2.5 items, indicating that there are more items available beyond what is currently visible - suggestionHeight = CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + suggestionsHeight = CONST.AUTO_COMPLETE_SUGGESTER.SMALL_CONTAINER_HEIGHT_FACTOR * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; } else { - suggestionHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; + suggestionsHeight = numRows * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT; } - return suggestionHeight + borderAndPadding; + return suggestionsHeight + borderAndPadding; }; export {trimLeadingSpace, hasEnoughSpaceForLargeSuggestionMenu, measureHeightOfSuggestionsContainer}; From eb09849b6f8d18f52ab18e4942e5ed20b19118ae Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 26 Feb 2024 11:53:05 +0700 Subject: [PATCH 0028/1096] move portal of suggestion to the correct place --- src/pages/home/report/ReportActionItemMessageEdit.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 2c9190926d0d..6a182a6016c3 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -401,9 +401,12 @@ function ReportActionItemMessageEdit( return ( <> - + + - Date: Mon, 26 Feb 2024 17:14:29 +0100 Subject: [PATCH 0029/1096] feat: create new rate field --- .../MoneyRequestConfirmationList.js | 12 ++++++ ...oraryForRefactorRequestConfirmationList.js | 20 ++++++++++ src/languages/en.ts | 1 + src/languages/es.ts | 1 + src/libs/DistanceRequestUtils.ts | 39 ++++++++++++++++--- 5 files changed, 68 insertions(+), 5 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index df2781d3ea89..7bb4bb868c99 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -730,6 +730,18 @@ function MoneyRequestConfirmationList(props) { interactive={!props.isReadOnly} /> )} + {props.isDistanceRequest && ( + Navigation.navigate(ROUTES.MONEY_REQUEST_DISTANCE.getRoute(props.iouType, props.reportID))} + disabled={didConfirm || !isTypeRequest} + interactive={!props.isReadOnly} + /> + )} {shouldShowMerchant && ( {}} + // onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm || !isTypeRequest} + interactive={!isReadOnly} + /> + ), + // TODO: hide when betas ready + shouldShow: isDistanceRequest, + isSupplementary: true, + }, { item: ( Date: Mon, 26 Feb 2024 17:58:22 +0100 Subject: [PATCH 0030/1096] refactor: revert changes to withCurrentReportID --- src/components/withCurrentReportID.tsx | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index cc49c44e0e77..a452e7565b4e 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -39,15 +39,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro */ const updateCurrentReportID = useCallback( (state: NavigationState) => { - const reportID = Navigation.getTopmostReportId(state) ?? ''; - /** - * This is to make sure we don't set the undefined as reportID when - * switching between chat list and settings->workspaces tab. - * and doing so avoid unnecessary re-render of `useOrderedReportIDs`. - */ - if (reportID) { - setCurrentReportID(reportID); - } + setCurrentReportID(Navigation.getTopmostReportId(state) ?? ''); }, [setCurrentReportID], ); From 9ae8701c7ed4a5c98e69d689e63c4ac19694ebf2 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Mon, 26 Feb 2024 18:48:44 +0100 Subject: [PATCH 0031/1096] refactor: remove useReports context & hooks --- src/App.tsx | 2 - src/hooks/useReports.tsx | 44 ------------------- .../AppNavigator/ReportScreenIDSetter.ts | 13 ++++-- src/pages/workspace/WorkspacesListPage.tsx | 10 +++-- 4 files changed, 16 insertions(+), 53 deletions(-) delete mode 100644 src/hooks/useReports.tsx diff --git a/src/App.tsx b/src/App.tsx index 7cbf3478d16e..eb1750a7fe5f 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -30,7 +30,6 @@ import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import {OrderedReportIDsContextProvider} from './hooks/useOrderedReportIDs'; -import {ReportsContextProvider} from './hooks/useReports'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import InitialUrlContext from './libs/InitialUrlContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; @@ -77,7 +76,6 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, - ReportsContextProvider, OrderedReportIDsContextProvider, PlaybackContextProvider, VolumeContextProvider, diff --git a/src/hooks/useReports.tsx b/src/hooks/useReports.tsx deleted file mode 100644 index c4082149cbf4..000000000000 --- a/src/hooks/useReports.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, {createContext, useContext, useEffect, useMemo, useState} from 'react'; -import Onyx from 'react-native-onyx'; -import type {OnyxCollection} from 'react-native-onyx'; -import ONYXKEYS from '@src/ONYXKEYS'; -import type {Report} from '@src/types/onyx'; - -type Reports = OnyxCollection; -type ReportsContextValue = Reports; - -type ReportsContextProviderProps = { - children: React.ReactNode; -}; - -const ReportsContext = createContext(null); - -function ReportsContextProvider(props: ReportsContextProviderProps) { - const [reports, setReports] = useState(null); - - useEffect(() => { - // eslint-disable-next-line rulesdir/prefer-onyx-connect-in-libs - const connID = Onyx.connect({ - key: ONYXKEYS.COLLECTION.REPORT, - waitForCollectionCallback: true, - callback: (val) => { - setReports(val); - }, - }); - return () => { - Onyx.disconnect(connID); - }; - }, []); - - const contextValue = useMemo(() => reports ?? {}, [reports]); - - return {props.children}; -} - -function useReports() { - return useContext(ReportsContext); -} - -ReportsContextProvider.displayName = 'ReportsContextProvider'; - -export {ReportsContextProvider, ReportsContext, useReports}; diff --git a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts index 297313e513f6..529f0f3d31a7 100644 --- a/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts +++ b/src/libs/Navigation/AppNavigator/ReportScreenIDSetter.ts @@ -3,7 +3,6 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import usePermissions from '@hooks/usePermissions'; -import {useReports} from '@hooks/useReports'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -11,6 +10,9 @@ import type {Policy, PolicyMembers, Report, ReportMetadata} from '@src/types/ony import type {ReportScreenWrapperProps} from './ReportScreenWrapper'; type ReportScreenIDSetterComponentProps = { + /** Available reports that would be displayed in this navigator */ + reports: OnyxCollection; + /** The policies which the user has access to */ policies: OnyxCollection; @@ -56,10 +58,9 @@ const getLastAccessedReportID = ( }; // This wrapper is reponsible for opening the last accessed report if there is no reportID specified in the route params -function ReportScreenIDSetter({route, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { +function ReportScreenIDSetter({route, reports, policies, policyMembers = {}, navigation, isFirstTimeNewExpensifyUser = false, reportMetadata, accountID}: ReportScreenIDSetterProps) { const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); - const reports = useReports(); useEffect(() => { // Don't update if there is a reportID in the params already @@ -80,7 +81,7 @@ function ReportScreenIDSetter({route, policies, policyMembers = {}, navigation, !canUseDefaultRooms, policies, isFirstTimeNewExpensifyUser, - !!route?.params?.openOnAdminRoom, + !!reports?.params?.openOnAdminRoom, reportMetadata, activeWorkspaceID, policyMemberAccountIDs, @@ -101,6 +102,10 @@ function ReportScreenIDSetter({route, policies, policyMembers = {}, navigation, ReportScreenIDSetter.displayName = 'ReportScreenIDSetter'; export default withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + allowStaleData: true, + }, policies: { key: ONYXKEYS.COLLECTION.POLICY, allowStaleData: true, diff --git a/src/pages/workspace/WorkspacesListPage.tsx b/src/pages/workspace/WorkspacesListPage.tsx index f646366360eb..a51efd608444 100755 --- a/src/pages/workspace/WorkspacesListPage.tsx +++ b/src/pages/workspace/WorkspacesListPage.tsx @@ -20,7 +20,6 @@ import ScreenWrapper from '@components/ScreenWrapper'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import {useReports} from '@hooks/useReports'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; @@ -73,6 +72,9 @@ type WorkspaceListPageOnyxProps = { /** A collection of objects for all policies which key policy member objects by accountIDs */ allPolicyMembers: OnyxCollection; + + /** All reports shared with the user (coming from Onyx) */ + reports: OnyxCollection; }; type WorkspaceListPageProps = WithPolicyAndFullscreenLoadingProps & WorkspaceListPageOnyxProps; @@ -110,8 +112,7 @@ function dismissWorkspaceError(policyID: string, pendingAction: OnyxCommon.Pendi const stickyHeaderIndices = [0]; -function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount}: WorkspaceListPageProps) { - const reports = useReports(); +function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, reports}: WorkspaceListPageProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -409,5 +410,8 @@ export default withPolicyAndFullscreenLoading( reimbursementAccount: { key: ONYXKEYS.REIMBURSEMENT_ACCOUNT, }, + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, })(WorkspacesListPage), ); From 2c19aa3e06b141a40667b337e8b93a629e62fb0d Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Mon, 26 Feb 2024 19:20:47 +0100 Subject: [PATCH 0032/1096] feat: create rate selection page --- src/ROUTES.ts | 5 ++ src/SCREENS.ts | 1 + ...oraryForRefactorRequestConfirmationList.js | 23 +++---- .../AppNavigator/ModalStackNavigators.tsx | 1 + src/libs/Navigation/linkingConfig/config.ts | 1 + .../iou/request/step/IOURequestStepRate.tsx | 65 +++++++++++++++++++ 6 files changed, 85 insertions(+), 11 deletions(-) create mode 100644 src/pages/iou/request/step/IOURequestStepRate.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index a8786bda3ffb..c01fdac72aef 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -369,6 +369,11 @@ const ROUTES = { getRoute: (iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => getUrlWithBackToParam(`create/${iouType}/distance/${transactionID}/${reportID}`, backTo), }, + MONEY_REQUEST_STEP_RATE: { + route: ':action/:iouType/rate/:transactionID/:reportID', + getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => + getUrlWithBackToParam(`${action}/${iouType}/rate/${transactionID}/${reportID}`, backTo), + }, MONEY_REQUEST_STEP_MERCHANT: { route: ':action/:iouType/merchant/:transactionID/:reportID', getRoute: (action: ValueOf, iouType: ValueOf, transactionID: string, reportID: string, backTo = '') => diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 520895c89c98..ddaf52a03a59 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -143,6 +143,7 @@ const SCREENS = { STEP_DATE: 'Money_Request_Step_Date', STEP_DESCRIPTION: 'Money_Request_Step_Description', STEP_DISTANCE: 'Money_Request_Step_Distance', + STEP_RATE: 'Money_Request_Step_Rate', STEP_MERCHANT: 'Money_Request_Step_Merchant', STEP_PARTICIPANTS: 'Money_Request_Step_Participants', STEP_SCAN: 'Money_Request_Step_Scan', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 6fb0e8bc0f08..f2fcb3ac96eb 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -1,10 +1,10 @@ -import {useIsFocused} from '@react-navigation/native'; -import {format} from 'date-fns'; +import { useIsFocused } from '@react-navigation/native'; +import { format } from 'date-fns'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, {Fragment, useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; -import {View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; +import { View } from 'react-native'; +import { withOnyx } from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; @@ -21,9 +21,9 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import playSound, {SOUNDS} from '@libs/Sound'; +import playSound, { SOUNDS } from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; -import {policyPropTypes} from '@pages/workspace/withPolicy'; +import { policyPropTypes } from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -44,7 +44,8 @@ import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; import Text from './Text'; import transactionPropTypes from './transactionPropTypes'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails, { withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes } from './withCurrentUserPersonalDetails'; + const propTypes = { /** Callback to inform parent modal of success */ @@ -701,9 +702,9 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ description={translate('common.rate')} style={[styles.moneyRequestMenuItem]} titleStyle={styles.flex1} - // TODO: Add the onPress function - onPress={() => {}} - // onPress={() => Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + onPress={() => { + Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + }} disabled={didConfirm || !isTypeRequest} interactive={!isReadOnly} /> diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index 2be262aa5f0f..941900c3bf2c 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -89,6 +89,7 @@ const MoneyRequestModalStackNavigator = createModalStackNavigator require('../../../pages/iou/request/step/IOURequestStepDate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: () => require('../../../pages/iou/request/step/IOURequestStepDescription').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: () => require('../../../pages/iou/request/step/IOURequestStepDistance').default as React.ComponentType, + [SCREENS.MONEY_REQUEST.STEP_RATE]: () => require('../../../pages/iou/request/step/IOURequestStepRate').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: () => require('../../../pages/iou/request/step/IOURequestStepMerchant').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: () => require('../../../pages/iou/request/step/IOURequestStepParticipants').default as React.ComponentType, [SCREENS.MONEY_REQUEST.STEP_SCAN]: () => require('../../../pages/iou/request/step/IOURequestStepScan').default as React.ComponentType, diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 48d649cc4dd9..9163c0b9bfe5 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -416,6 +416,7 @@ const config: LinkingOptions['config'] = { [SCREENS.MONEY_REQUEST.STEP_DATE]: ROUTES.MONEY_REQUEST_STEP_DATE.route, [SCREENS.MONEY_REQUEST.STEP_DESCRIPTION]: ROUTES.MONEY_REQUEST_STEP_DESCRIPTION.route, [SCREENS.MONEY_REQUEST.STEP_DISTANCE]: ROUTES.MONEY_REQUEST_STEP_DISTANCE.route, + [SCREENS.MONEY_REQUEST.STEP_RATE]: ROUTES.MONEY_REQUEST_STEP_RATE.route, [SCREENS.MONEY_REQUEST.HOLD]: ROUTES.MONEY_REQUEST_HOLD_REASON.route, [SCREENS.MONEY_REQUEST.STEP_MERCHANT]: ROUTES.MONEY_REQUEST_STEP_MERCHANT.route, [SCREENS.MONEY_REQUEST.STEP_PARTICIPANTS]: ROUTES.MONEY_REQUEST_STEP_PARTICIPANTS.route, diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx new file mode 100644 index 000000000000..347e627624e9 --- /dev/null +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -0,0 +1,65 @@ +import React from 'react'; +import {withOnyx} from 'react-native-onyx'; +import SelectionList from '@components/SelectionList'; +import RadioListItem from '@components/SelectionList/RadioListItem'; +import Text from '@components/Text'; +import useLocalize from '@hooks/useLocalize'; +import useThemeStyles from '@hooks/useThemeStyles'; +import Navigation from '@libs/Navigation/Navigation'; +import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; +import type {Route} from '@src/ROUTES'; + +type Props = { + // eslint-disable-next-line react/no-unused-prop-types + lastSelectedDistanceRate: string; + /** The route object passed to this screen */ + route: { + /** The params passed to this screen */ + params: { + /** The route to go back to */ + backTo: Route; + }; + }; +}; + +const mockRates = [ + {text: 'Default Rate', alternateText: '0.656/mile', keyForList: 'DefaultRate'}, + {text: 'Custom Rate', alternateText: '0.700/mile', keyForList: 'CustomRate'}, +]; + +function IOURequestStepRate({ + route: { + params: {backTo}, + }, +}: Props) { + const styles = useThemeStyles(); + const {translate} = useLocalize(); + + return ( + Navigation.goBack(backTo)} + shouldShowWrapper={Boolean(backTo)} + testID="rate" + > + {translate('themePage.chooseThemeBelowOrSync')} + + {}} + initiallyFocusedOptionKey="DefaultRate" + /> + + ); +} + +IOURequestStepRate.displayName = 'IOURequestStepRate'; + +// export default withOnyx({ +// lastSelectedDistanceRate: { +// key: 'ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATE', +// }, +// })(IOURequestStepRate); + +export default IOURequestStepRate; From b613e974a2f8e8b7c07e893e64bc6fdf377d08d1 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 15:54:37 +0100 Subject: [PATCH 0033/1096] refactor: use reports from onyx in useOrderedReportIDs --- src/hooks/useOrderedReportIDs.tsx | 69 +++++++++++++++++++++++++++---- 1 file changed, 61 insertions(+), 8 deletions(-) diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportIDs.tsx index 01dab5a3231a..6b3ebd8d9e28 100644 --- a/src/hooks/useOrderedReportIDs.tsx +++ b/src/hooks/useOrderedReportIDs.tsx @@ -4,23 +4,26 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {usePersonalDetails} from '@components/OnyxProvider'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Policy, PolicyMembers, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type {Beta, Locale, Policy, PolicyMembers, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import type PriorityMode from '@src/types/onyx/PriorityMode'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; import usePermissions from './usePermissions'; -import {useReports} from './useReports'; type OnyxProps = { + chatReports: OnyxCollection; betas: OnyxEntry; policies: OnyxCollection; allReportActions: OnyxCollection; transactionViolations: OnyxCollection; policyMembers: OnyxCollection; priorityMode: OnyxEntry; + preferredLocale: OnyxEntry; + draftComments: OnyxCollection; }; type WithOrderedReportIDsContextProviderProps = OnyxProps & { @@ -30,7 +33,6 @@ type WithOrderedReportIDsContextProviderProps = OnyxProps & { const OrderedReportIDsContext = createContext({}); function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextProviderProps) { - const chatReports = useReports(); const currentReportIDValue = useCurrentReportID(); const {activeWorkspaceID} = useActiveWorkspace(); const personalDetails = usePersonalDetails(); @@ -45,7 +47,7 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP () => SidebarUtils.getOrderedReportIDs( null, - chatReports, + props.chatReports, props.betas ?? [], props.policies, props.priorityMode, @@ -59,7 +61,7 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP props.draftComments, ), [ - chatReports, + props.chatReports, props.betas, props.policies, props.priorityMode, @@ -83,7 +85,7 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP if (currentReportIDValue?.currentReportID && !optionListItems.includes(currentReportIDValue.currentReportID)) { return SidebarUtils.getOrderedReportIDs( currentReportIDValue.currentReportID, - chatReports, + props.chatReports, props.betas ?? [], props.policies, props.priorityMode, @@ -100,7 +102,7 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP return optionListItems; }, [ activeWorkspaceID, - chatReports, + props.chatReports, currentReportIDValue?.currentReportID, optionListItems, policyMemberAccountIDs, @@ -118,6 +120,52 @@ function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextP return {props.children}; } +/** + * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering + * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. + * @param {Object} [report] + * @returns {Object|undefined} + */ +const chatReportSelector = (report) => + report && { + reportID: report.reportID, + participantAccountIDs: report.participantAccountIDs, + hasDraft: report.hasDraft, + isPinned: report.isPinned, + isHidden: report.isHidden, + notificationPreference: report.notificationPreference, + errorFields: { + addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, + }, + lastMessageText: report.lastMessageText, + lastVisibleActionCreated: report.lastVisibleActionCreated, + iouReportID: report.iouReportID, + total: report.total, + nonReimbursableTotal: report.nonReimbursableTotal, + hasOutstandingChildRequest: report.hasOutstandingChildRequest, + isWaitingOnBankAccount: report.isWaitingOnBankAccount, + statusNum: report.statusNum, + stateNum: report.stateNum, + chatType: report.chatType, + type: report.type, + policyID: report.policyID, + visibility: report.visibility, + lastReadTime: report.lastReadTime, + // Needed for name sorting: + reportName: report.reportName, + policyName: report.policyName, + oldPolicyName: report.oldPolicyName, + // Other less obvious properites considered for sorting: + ownerAccountID: report.ownerAccountID, + currency: report.currency, + managerID: report.managerID, + // Other important less obivous properties for filtering: + parentReportActionID: report.parentReportActionID, + parentReportID: report.parentReportID, + isDeletedParentAction: report.isDeletedParentAction, + isUnreadWithMention: ReportUtils.isUnreadWithMention(report), + }; + const reportActionsSelector = (reportActions: OnyxEntry) => { if (!reportActions) { return []; @@ -140,6 +188,11 @@ const reportActionsSelector = (reportActions: OnyxEntry) => { }; const OrderedReportIDsContextProvider = withOnyx({ + chatReports: { + key: ONYXKEYS.COLLECTION.REPORT, + selector: chatReportSelector, + initialValue: {}, + }, priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, initialValue: CONST.PRIORITY_MODE.DEFAULT, @@ -151,7 +204,7 @@ const OrderedReportIDsContextProvider = withOnyx reportActionsSelector(actions), + selector: reportActionsSelector, initialValue: {}, }, policies: { From 7f15fbc34bdc512c6c72fe8b15232dc1ba53e756 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 17:32:07 +0100 Subject: [PATCH 0034/1096] refactor: move creating orderedReport objects from getOrderedReportIds to the context itself --- .../LHNOptionsList/LHNOptionsList.tsx | 48 +++--- src/hooks/useOrderedReportIDs.tsx | 140 ++++++++---------- src/libs/SidebarUtils.ts | 46 +----- src/pages/home/sidebar/SidebarLinksData.js | 18 ++- 4 files changed, 100 insertions(+), 152 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index c70e355c46c5..8b18630a9646 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,13 +1,13 @@ +import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {useCallback, memo} from 'react'; -import {FlatList, StyleSheet, View} from 'react-native'; +import React, {memo, useCallback} from 'react'; +import {StyleSheet, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; +import {OrderedReports} from '@libs/SidebarUtils'; +import variables from '@styles/variables'; +import CONST from '@src/CONST'; import OptionRowLHNData from './OptionRowLHNData'; import type {LHNOptionsListProps, RenderItemProps} from './types'; -import { OrderedReports } from '@libs/SidebarUtils'; -import CONST from '@src/CONST'; -import variables from '@styles/variables'; -import { FlashList } from '@shopify/flash-list'; const keyExtractor = (item: OrderedReports) => `report_${item?.reportID}`; @@ -39,29 +39,23 @@ function LHNOptionsList({ * Function which renders a row in the list */ const renderItem = useCallback( - ({item}: RenderItemProps): ReactElement => { - - return ( - - ); - }, - [ - currentReportID, - onSelectRow, - onLayoutItem, - ], + ({item}: RenderItemProps): ReactElement => ( + + ), + [shouldDisableFocusOptions, currentReportID, onSelectRow, onLayoutItem, optionMode], ); return ( - diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportIDs.tsx index 6b3ebd8d9e28..8f071f036fc7 100644 --- a/src/hooks/useOrderedReportIDs.tsx +++ b/src/hooks/useOrderedReportIDs.tsx @@ -1,4 +1,4 @@ -import React, {createContext, useContext, useMemo} from 'react'; +import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {usePersonalDetails} from '@components/OnyxProvider'; @@ -32,101 +32,90 @@ type WithOrderedReportIDsContextProviderProps = OnyxProps & { const OrderedReportIDsContext = createContext({}); -function WithOrderedReportIDsContextProvider(props: WithOrderedReportIDsContextProviderProps) { +function WithOrderedReportIDsContextProvider({ + children, + chatReports, + betas, + policies, + allReportActions, + transactionViolations, + policyMembers, + priorityMode, + preferredLocale, + draftComments, +}: WithOrderedReportIDsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); - const {activeWorkspaceID} = useActiveWorkspace(); const personalDetails = usePersonalDetails(); const {canUseViolations} = usePermissions(); + const {activeWorkspaceID} = useActiveWorkspace(); - const policyMemberAccountIDs = useMemo( - () => getPolicyMembersByIdWithoutCurrentUser(props.policyMembers, activeWorkspaceID, getCurrentUserAccountID()), - [activeWorkspaceID, props.policyMembers], - ); + const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); - const optionListItems = useMemo( - () => + const getOrderedReportIDs = useCallback( + (currentReportID?: string) => SidebarUtils.getOrderedReportIDs( - null, - props.chatReports, - props.betas ?? [], - props.policies, - props.priorityMode, - props.allReportActions, - props.transactionViolations, + currentReportID ?? null, + chatReports, + betas ?? [], + policies, + priorityMode, + allReportActions, + transactionViolations, activeWorkspaceID, policyMemberAccountIDs, - personalDetails, - props.preferredLocale, - canUseViolations, - props.draftComments, ), - [ - props.chatReports, - props.betas, - props.policies, - props.priorityMode, - props.allReportActions, - props.transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - personalDetails, - props.preferredLocale, - canUseViolations, - props.draftComments, - ], + [chatReports, betas, policies, priorityMode, allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs], ); + const orderedReportIDs = useMemo(() => getOrderedReportIDs(), [getOrderedReportIDs]); + // We need to make sure the current report is in the list of reports, but we do not want // to have to re-generate the list every time the currentReportID changes. To do that // we first generate the list as if there was no current report, then here we check if // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. - const optionListItemsWithCurrentReport = useMemo(() => { - if (currentReportIDValue?.currentReportID && !optionListItems.includes(currentReportIDValue.currentReportID)) { - return SidebarUtils.getOrderedReportIDs( - currentReportIDValue.currentReportID, - props.chatReports, - props.betas ?? [], - props.policies, - props.priorityMode, - props.allReportActions, - props.transactionViolations, - activeWorkspaceID, - policyMemberAccountIDs, - personalDetails, - props.preferredLocale, - canUseViolations, - props.draftComments, - ); + const orderedReportIDsWithCurrentReport = useMemo(() => { + if (currentReportIDValue?.currentReportID && !orderedReportIDs.includes(currentReportIDValue.currentReportID)) { + return getOrderedReportIDs(currentReportIDValue.currentReportID); } - return optionListItems; - }, [ - activeWorkspaceID, - props.chatReports, - currentReportIDValue?.currentReportID, - optionListItems, - policyMemberAccountIDs, - props.allReportActions, - props.betas, - props.policies, - props.priorityMode, - props.transactionViolations, - personalDetails, - props.preferredLocale, - canUseViolations, - props.draftComments, - ]); - - return {props.children}; + return orderedReportIDs; + }, [getOrderedReportIDs, currentReportIDValue?.currentReportID, orderedReportIDs]); + + const orderedReportListItems = useMemo( + () => + orderedReportIDsWithCurrentReport.map((reportID) => { + const itemFullReport = chatReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const itemReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; + const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; + const itemPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; + const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; + + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(itemFullReport, transactionViolations, itemParentReportAction ?? null); + + const item = SidebarUtils.getOptionData({ + report: itemFullReport, + reportActions: itemReportActions, + personalDetails, + preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, + policy: itemPolicy, + parentReportAction: itemParentReportAction, + hasViolations: !!hasViolations, + }); + + return {reportID, optionItem: item, comment: itemComment}; + }), + [orderedReportIDsWithCurrentReport, canUseViolations, personalDetails, draftComments, preferredLocale, chatReports, allReportActions, policies, transactionViolations], + ); + + return {children}; } /** * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - * @param {Object} [report] - * @returns {Object|undefined} */ -const chatReportSelector = (report) => +const chatReportSelector = (report: OnyxEntry) => report && { reportID: report.reportID, participantAccountIDs: report.participantAccountIDs, @@ -134,9 +123,7 @@ const chatReportSelector = (report) => isPinned: report.isPinned, isHidden: report.isHidden, notificationPreference: report.notificationPreference, - errorFields: { - addWorkspaceRoom: report.errorFields && report.errorFields.addWorkspaceRoom, - }, + errorFields: {addWorkspaceRoom: report.errorFields?.addWorkspaceRoom}, lastMessageText: report.lastMessageText, lastVisibleActionCreated: report.lastVisibleActionCreated, iouReportID: report.iouReportID, @@ -188,6 +175,7 @@ const reportActionsSelector = (reportActions: OnyxEntry) => { }; const OrderedReportIDsContextProvider = withOnyx({ + // @ts-expect-error Need some help in determining the correct type for this selector chatReports: { key: ONYXKEYS.COLLECTION.REPORT, selector: chatReportSelector, diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index f99384487650..f36becf40715 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -59,30 +59,20 @@ function compareStringDates(a: string, b: string): 0 | 1 | -1 { return 0; } -export type OrderedReports = { - reportID: string; - optionItem: ReportUtils.OptionData | undefined; - comment: string; -}; - /** * @returns An array of reportIDs sorted in the proper order */ function getOrderedReportIDs( currentReportId: string | null, allReports: OnyxCollection, - betas: Beta[], + betas: OnyxEntry, policies: OnyxCollection, priorityMode: OnyxEntry, allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', policyMemberAccountIDs: number[] = [], - personalDetails: OnyxEntry, - preferredLocale: DeepValueOf, - canUseViolations: boolean, - draftComments: OnyxCollection, -): OrderedReports[] { +): string[] { const isInGSDMode = priorityMode === CONST.PRIORITY_MODE.GSD; const isInDefaultMode = !isInGSDMode; const allReportsDictValues = Object.values(allReports ?? {}); @@ -93,12 +83,12 @@ function getOrderedReportIDs( const parentReportActions = allReportActions?.[parentReportActionsKey]; const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID); const doesReportHaveViolations = - betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); + !!betas && betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ report, currentReportId: currentReportId ?? '', isInGSDMode, - betas, + betas: betas ?? [], policies, excludeEmptyChats: true, doesReportHaveViolations, @@ -178,33 +168,7 @@ function getOrderedReportIDs( // Now that we have all the reports grouped and sorted, they must be flattened into an array and only return the reportID. // The order the arrays are concatenated in matters and will determine the order that the groups are displayed in the sidebar. - const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => { - const itemFullReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.reportID}`] ?? null; - const itemReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? null; - const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; - const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; - const itemPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; - const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${report?.reportID}`] ?? ''; - - const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(itemFullReport, transactionViolations, itemParentReportAction ?? null); - - const item = getOptionData({ - report: itemFullReport, - reportActions: itemReportActions, - personalDetails, - preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, - policy: itemPolicy, - parentReportAction: itemParentReportAction, - hasViolations: !!hasViolations, - }); - - return { - reportID: report.reportID, - optionItem: item, - comment: itemComment, - }; - }); - + const LHNReports = [...pinnedAndGBRReports, ...draftReports, ...nonArchivedReports, ...archivedReports].map((report) => report.reportID); return LHNReports; } diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 6d63a8a44fe1..4966a2414e97 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -51,22 +51,24 @@ function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onL // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => Policy.openWorkspace(activeWorkspaceID, policyMemberAccountIDs), [activeWorkspaceID]); - const reportIDsRef = useRef(null); + const orderedReportListItemsRef = useRef(null); const isLoading = isLoadingApp; - const reportIDs = useOrderedReportIDs(); + const orderedReportListItems = useOrderedReportIDs(); + const optionListItems = useMemo(() => { - if (deepEqual(reportIDsRef.current, reportIDs)) { - return reportIDsRef.current; + // this can be very heavy because we are no longer comapring the IDS but the whole objects for the list + if (deepEqual(orderedReportListItemsRef.current, orderedReportListItems)) { + return orderedReportListItemsRef.current; } // 1. We need to update existing reports only once while loading because they are updated several times during loading and causes this regression: https://github.com/Expensify/App/issues/24596#issuecomment-1681679531 // 2. If the user is offline, we need to update the reports unconditionally, since the loading of report data might be stuck in this case. // 3. Changing priority mode to Most Recent will call OpenApp. If there is an existing reports and the priority mode is updated, we want to immediately update the list instead of waiting the OpenApp request to complete - if (!isLoading || !reportIDsRef.current || network.isOffline || (reportIDsRef.current && prevPriorityMode !== priorityMode)) { - reportIDsRef.current = reportIDs; + if (!isLoading || !orderedReportListItemsRef.current || network.isOffline || (orderedReportListItemsRef.current && prevPriorityMode !== priorityMode)) { + orderedReportListItemsRef.current = orderedReportListItems; } - return reportIDsRef.current || []; - }, [reportIDs, isLoading, network.isOffline, prevPriorityMode, priorityMode]); + return orderedReportListItemsRef.current || []; + }, [orderedReportListItems, isLoading, network.isOffline, prevPriorityMode, priorityMode]); const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; From 7fa44fc71ce4eb20d70b4da92094b9a5b6d50394 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 17:34:55 +0100 Subject: [PATCH 0035/1096] Revert "refactor: revert changes to withCurrentReportID" This reverts commit dc88950ac4dbc552e4ca8b705ae4faff05dd70da. --- src/components/withCurrentReportID.tsx | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index a452e7565b4e..cc49c44e0e77 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -39,7 +39,15 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro */ const updateCurrentReportID = useCallback( (state: NavigationState) => { - setCurrentReportID(Navigation.getTopmostReportId(state) ?? ''); + const reportID = Navigation.getTopmostReportId(state) ?? ''; + /** + * This is to make sure we don't set the undefined as reportID when + * switching between chat list and settings->workspaces tab. + * and doing so avoid unnecessary re-render of `useOrderedReportIDs`. + */ + if (reportID) { + setCurrentReportID(reportID); + } }, [setCurrentReportID], ); From ae12b008886272f08668a089342bc9bacbcfb831 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 17:47:45 +0100 Subject: [PATCH 0036/1096] refactor: remove comment in SidebarLinksData --- src/pages/home/sidebar/SidebarLinksData.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 4966a2414e97..5cedc9bbb7fd 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -56,7 +56,6 @@ function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onL const orderedReportListItems = useOrderedReportIDs(); const optionListItems = useMemo(() => { - // this can be very heavy because we are no longer comapring the IDS but the whole objects for the list if (deepEqual(orderedReportListItemsRef.current, orderedReportListItems)) { return orderedReportListItemsRef.current; } From c87f579bda3c94452f6b093edfbc159996a7eb30 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 18:00:43 +0100 Subject: [PATCH 0037/1096] refactor: rename useOrderedReportIDs to useOrderedReportListItems --- src/App.tsx | 4 ++-- src/components/withCurrentReportID.tsx | 2 +- ...tIDs.tsx => useOrderedReportListItems.tsx} | 20 +++++++++---------- src/pages/home/sidebar/SidebarLinksData.js | 4 ++-- 4 files changed, 15 insertions(+), 15 deletions(-) rename src/hooks/{useOrderedReportIDs.tsx => useOrderedReportListItems.tsx} (92%) diff --git a/src/App.tsx b/src/App.tsx index eb1750a7fe5f..5670859e8908 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,7 @@ import {KeyboardStateProvider} from './components/withKeyboardState'; import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; -import {OrderedReportIDsContextProvider} from './hooks/useOrderedReportIDs'; +import {OrderedReportListItemsContextProvider} from './hooks/useOrderedReportListItems'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import InitialUrlContext from './libs/InitialUrlContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; @@ -76,7 +76,7 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, - OrderedReportIDsContextProvider, + OrderedReportListItemsContextProvider, PlaybackContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index cc49c44e0e77..d5c5a6896a9c 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -43,7 +43,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro /** * This is to make sure we don't set the undefined as reportID when * switching between chat list and settings->workspaces tab. - * and doing so avoid unnecessary re-render of `useOrderedReportIDs`. + * and doing so avoid unnecessary re-render of `useOrderedReportListItems`. */ if (reportID) { setCurrentReportID(reportID); diff --git a/src/hooks/useOrderedReportIDs.tsx b/src/hooks/useOrderedReportListItems.tsx similarity index 92% rename from src/hooks/useOrderedReportIDs.tsx rename to src/hooks/useOrderedReportListItems.tsx index 8f071f036fc7..643fcf053012 100644 --- a/src/hooks/useOrderedReportIDs.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -26,13 +26,13 @@ type OnyxProps = { draftComments: OnyxCollection; }; -type WithOrderedReportIDsContextProviderProps = OnyxProps & { +type WithOrderedReportListItemsContextProviderProps = OnyxProps & { children: React.ReactNode; }; -const OrderedReportIDsContext = createContext({}); +const OrderedReportListItemsContext = createContext({}); -function WithOrderedReportIDsContextProvider({ +function WithOrderedReportListItemsContextProvider({ children, chatReports, betas, @@ -43,7 +43,7 @@ function WithOrderedReportIDsContextProvider({ priorityMode, preferredLocale, draftComments, -}: WithOrderedReportIDsContextProviderProps) { +}: WithOrderedReportListItemsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); const personalDetails = usePersonalDetails(); const {canUseViolations} = usePermissions(); @@ -108,7 +108,7 @@ function WithOrderedReportIDsContextProvider({ [orderedReportIDsWithCurrentReport, canUseViolations, personalDetails, draftComments, preferredLocale, chatReports, allReportActions, policies, transactionViolations], ); - return {children}; + return {children}; } /** @@ -174,7 +174,7 @@ const reportActionsSelector = (reportActions: OnyxEntry) => { }); }; -const OrderedReportIDsContextProvider = withOnyx({ +const OrderedReportListItemsContextProvider = withOnyx({ // @ts-expect-error Need some help in determining the correct type for this selector chatReports: { key: ONYXKEYS.COLLECTION.REPORT, @@ -213,10 +213,10 @@ const OrderedReportIDsContextProvider = withOnyx { if (deepEqual(orderedReportListItemsRef.current, orderedReportListItems)) { From df1069c1a1dc695f8faa0dcfd748541bbe1ea8bb Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 18:05:35 +0100 Subject: [PATCH 0038/1096] perf: use memo for extraData in LHNOptionList --- src/components/LHNOptionsList/LHNOptionsList.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 8b18630a9646..8301bc034a74 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -1,6 +1,6 @@ import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; -import React, {memo, useCallback} from 'react'; +import React, {memo, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import {OrderedReports} from '@libs/SidebarUtils'; @@ -53,6 +53,8 @@ function LHNOptionsList({ [shouldDisableFocusOptions, currentReportID, onSelectRow, onLayoutItem, optionMode], ); + const extraData = useMemo(() => [currentReportID], [currentReportID]); + return ( From 1d294bcd188dd768d7c917c48ba19f54bf5a0275 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 19:14:59 +0100 Subject: [PATCH 0039/1096] fix: typescript fixes --- .../LHNOptionsList/LHNOptionsList.tsx | 5 +- .../LHNOptionsList/OptionRowLHNData.tsx | 14 +--- src/components/LHNOptionsList/types.ts | 74 ++++--------------- src/hooks/useOrderedReportListItems.tsx | 3 +- src/libs/SidebarUtils.ts | 3 +- src/pages/home/sidebar/SidebarLinks.js | 2 +- src/pages/home/sidebar/SidebarLinksData.js | 4 + src/types/onyx/index.ts | 2 + 8 files changed, 27 insertions(+), 80 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 8301bc034a74..1884997a58fe 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -3,13 +3,12 @@ import type {ReactElement} from 'react'; import React, {memo, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; -import {OrderedReports} from '@libs/SidebarUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import OptionRowLHNData from './OptionRowLHNData'; -import type {LHNOptionsListProps, RenderItemProps} from './types'; +import type {LHNOptionsListProps, OptionListItem, RenderItemProps} from './types'; -const keyExtractor = (item: OrderedReports) => `report_${item?.reportID}`; +const keyExtractor = (item: OptionListItem) => `report_${item.reportID}`; function LHNOptionsList({ style, diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index 622d7b9d9123..bc1a06ce5f67 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,10 +1,5 @@ -import {deepEqual} from 'fast-equals'; -import React, {useEffect, useMemo, useRef} from 'react'; -import * as ReportUtils from '@libs/ReportUtils'; -import SidebarUtils from '@libs/SidebarUtils'; +import React, {useEffect} from 'react'; import * as Report from '@userActions/Report'; -import CONST from '@src/CONST'; -import type {OptionData} from '@src/libs/ReportUtils'; import OptionRowLHN from './OptionRowLHN'; import type {OptionRowLHNDataProps} from './types'; @@ -14,12 +9,7 @@ import type {OptionRowLHNDataProps} from './types'; * The OptionRowLHN component is memoized, so it will only * re-render if the data really changed. */ -function OptionRowLHNData({ - isFocused = false, - comment, - optionItem, - ...propsToForward -}: OptionRowLHNDataProps) { +function OptionRowLHNData({isFocused = false, comment, optionItem, ...propsToForward}: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; useEffect(() => { diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 4c79536571bf..3f72748ab8a8 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -1,38 +1,22 @@ import type {ContentStyle} from '@shopify/flash-list'; import type {RefObject} from 'react'; import type {LayoutChangeEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; -import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; -import type {EmptyObject} from '@src/types/utils/EmptyObject'; -import { OrderedReports } from '@libs/SidebarUtils'; type OptionMode = ValueOf; -type LHNOptionsListOnyxProps = { - /** The policy which the user has access to and which the report could be tied to */ - policy: OnyxCollection; - - /** Array of report actions for this report */ - reportActions: OnyxCollection; - - /** Indicates which locale the user currently has selected */ - preferredLocale: OnyxEntry; - - /** List of users' personal details */ - personalDetails: OnyxEntry; - - /** The transaction from the parent report action */ - transactions: OnyxCollection; +type OptionListItem = { + /** The reportID of the report */ + reportID: string; - /** List of draft comments */ - draftComments: OnyxCollection; + /** The item that should be rendered */ + optionItem: OptionData | undefined; - /** The list of transaction violations */ - transactionViolations: OnyxCollection; + /** Comment added to report */ + comment: string; }; type CustomLHNOptionsListProps = { @@ -43,7 +27,7 @@ type CustomLHNOptionsListProps = { contentContainerStyles?: StyleProp; /** Sections for the section list */ - data: string[]; + data: OptionListItem[]; /** Callback to fire when a row is selected */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; @@ -58,51 +42,21 @@ type CustomLHNOptionsListProps = { onFirstItemRendered: () => void; }; -type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps; +type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue; type OptionRowLHNDataProps = { /** Whether row should be focused */ isFocused?: boolean; - /** List of users' personal details */ - personalDetails?: PersonalDetailsList; - - /** The preferred language for the app */ - preferredLocale?: OnyxEntry; - - /** The full data of the report */ - fullReport: OnyxEntry; - - /** The policy which the user has access to and which the report could be tied to */ - policy?: OnyxEntry; - - /** The action from the parent report */ - parentReportAction?: OnyxEntry; - - /** The transaction from the parent report action */ - transaction: OnyxEntry; - - /** The transaction linked to the report's last action */ - lastReportActionTransaction?: OnyxEntry; - /** Comment added to report */ comment: string; - /** The receipt transaction from the parent report action */ - receiptTransactions: OnyxCollection; + /** The item that should be rendered */ + optionItem: OptionData | undefined; /** The reportID of the report */ reportID: string; - /** Array of report actions for this report */ - reportActions: OnyxEntry; - - /** List of transaction violation */ - transactionViolations: OnyxCollection; - - /** Whether the user can use violations */ - canUseViolations: boolean | undefined; - /** Toggle between compact and default view */ viewMode?: OptionMode; @@ -130,11 +84,11 @@ type OptionRowLHNProps = { style?: StyleProp; /** The item that should be rendered */ - optionItem?: OptionData; + optionItem: OptionData | undefined; onLayout?: (event: LayoutChangeEvent) => void; }; -type RenderItemProps = {item: OrderedReports}; +type RenderItemProps = {item: OptionListItem}; -export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps}; +export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, OptionListItem, RenderItemProps}; diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index 643fcf053012..970f306c3a80 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -8,8 +8,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Locale, Policy, PolicyMembers, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; -import type PriorityMode from '@src/types/onyx/PriorityMode'; +import type {Beta, Locale, Policy, PolicyMembers, PriorityMode, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; import usePermissions from './usePermissions'; diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 777a61c70733..2e929639270e 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -4,12 +4,11 @@ import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, PersonalDetailsList, TransactionViolation} from '@src/types/onyx'; +import type {PersonalDetails, PersonalDetailsList, ReportActions, TransactionViolation} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type Policy from '@src/types/onyx/Policy'; import type PriorityMode from '@src/types/onyx/PriorityMode'; import type Report from '@src/types/onyx/Report'; -import type {ReportActions} from '@src/types/onyx/ReportAction'; import type ReportAction from '@src/types/onyx/ReportAction'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; import * as CollectionUtils from './CollectionUtils'; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 8cda70d2486a..4c9803e257bc 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -33,7 +33,7 @@ const basePropTypes = { const propTypes = { ...basePropTypes, - optionListItems: PropTypes.arrayOf(PropTypes.string).isRequired, + optionListItems: PropTypes.arrayOf(PropTypes.object).isRequired, isLoading: PropTypes.bool.isRequired, diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 91de05450aae..088899425a0b 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -31,12 +31,16 @@ const propTypes = { network: networkPropTypes.isRequired, + // eslint-disable-next-line react/forbid-prop-types + policyMembers: PropTypes.object, + ...withCurrentUserPersonalDetailsPropTypes, }; const defaultProps = { isLoadingApp: true, priorityMode: CONST.PRIORITY_MODE.DEFAULT, + policyMembers: {}, ...withCurrentUserPersonalDetailsDefaultProps, }; diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index 6846fc302639..f6147a27a49b 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -37,6 +37,7 @@ import type {PolicyMembers} from './PolicyMember'; import type PolicyMember from './PolicyMember'; import type {PolicyReportField, PolicyReportFields} from './PolicyReportField'; import type {PolicyTag, PolicyTagList, PolicyTags} from './PolicyTag'; +import type PriorityMode from './PriorityMode'; import type PrivatePersonalDetails from './PrivatePersonalDetails'; import type RecentlyUsedCategories from './RecentlyUsedCategories'; import type RecentlyUsedReportFields from './RecentlyUsedReportFields'; @@ -111,6 +112,7 @@ export type { PolicyTag, PolicyTags, PolicyTagList, + PriorityMode, PrivatePersonalDetails, RecentWaypoint, RecentlyUsedCategories, From 7dfba4379e6bc48ef256c6363743ea13ad65b614 Mon Sep 17 00:00:00 2001 From: Jakub Romanczyk Date: Wed, 28 Feb 2024 20:08:53 +0100 Subject: [PATCH 0040/1096] fix: update reportActionsSelector to match the one from SidebarLinksData --- src/hooks/useOrderedReportListItems.tsx | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index 970f306c3a80..fa2d18684464 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -1,3 +1,5 @@ +import lodashGet from 'lodash/get'; +import lodashMap from 'lodash/map'; import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; @@ -152,29 +154,27 @@ const chatReportSelector = (report: OnyxEntry) => isUnreadWithMention: ReportUtils.isUnreadWithMention(report), }; -const reportActionsSelector = (reportActions: OnyxEntry) => { - if (!reportActions) { - return []; - } +const reportActionsSelector = (reportActions: OnyxEntry) => + reportActions && + lodashMap(reportActions, (reportAction) => { + const {reportActionID, parentReportActionID, actionName, errors = [], originalMessage} = reportAction; + const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); - return Object.values(reportActions).map((reportAction) => { - const {reportActionID, actionName, originalMessage} = reportAction ?? {}; - const decision = reportAction?.message?.[0]?.moderationDecision?.decision; return { reportActionID, + parentReportActionID, actionName, - originalMessage, + errors, message: [ { moderationDecision: {decision}, }, ], + originalMessage, }; }); -}; const OrderedReportListItemsContextProvider = withOnyx({ - // @ts-expect-error Need some help in determining the correct type for this selector chatReports: { key: ONYXKEYS.COLLECTION.REPORT, selector: chatReportSelector, @@ -190,7 +190,6 @@ const OrderedReportListItemsContextProvider = withOnyx Date: Thu, 29 Feb 2024 10:07:54 +0100 Subject: [PATCH 0041/1096] feat: add translation for the rate page --- src/languages/en.ts | 1 + src/languages/es.ts | 2 ++ src/pages/iou/request/step/IOURequestStepRate.tsx | 6 +++--- 3 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/languages/en.ts b/src/languages/en.ts index 920705883b02..d904a52c1b7a 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -696,6 +696,7 @@ export default { set: 'set', changed: 'changed', removed: 'removed', + chooseARate: 'Choose a rate to use below', }, notificationPreferencesPage: { header: 'Notification preferences', diff --git a/src/languages/es.ts b/src/languages/es.ts index 1b858f707851..842f357639c8 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -691,6 +691,8 @@ export default { set: 'estableció', changed: 'cambió', removed: 'eliminó', + // TODO: check if this is the correct translation + chooseARate: 'Elige una tarifa para utilizar a continuación', }, notificationPreferencesPage: { header: 'Preferencias de avisos', diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 347e627624e9..d82238f2c7ea 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -23,8 +23,8 @@ type Props = { }; const mockRates = [ - {text: 'Default Rate', alternateText: '0.656/mile', keyForList: 'DefaultRate'}, - {text: 'Custom Rate', alternateText: '0.700/mile', keyForList: 'CustomRate'}, + {text: 'Default Rate', alternateText: '$0.656 / mile', keyForList: 'DefaultRate'}, + {text: 'Custom Rate', alternateText: '$0.700 / mile', keyForList: 'CustomRate'}, ]; function IOURequestStepRate({ @@ -42,7 +42,7 @@ function IOURequestStepRate({ shouldShowWrapper={Boolean(backTo)} testID="rate" > - {translate('themePage.chooseThemeBelowOrSync')} + {translate('iou.chooseARate')} Date: Thu, 29 Feb 2024 18:01:07 +0500 Subject: [PATCH 0042/1096] fix: typescript issues --- src/hooks/useOrderedReportListItems.tsx | 70 +------------------------ src/libs/SidebarUtils.ts | 4 +- 2 files changed, 4 insertions(+), 70 deletions(-) diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index fa2d18684464..d368abd897af 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -1,9 +1,7 @@ -import lodashGet from 'lodash/get'; -import lodashMap from 'lodash/map'; import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {usePersonalDetails} from '@components/OnyxProvider'; +import {usePersonalDetails, useReport} from '@components/OnyxProvider'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import * as ReportUtils from '@libs/ReportUtils'; @@ -19,7 +17,7 @@ type OnyxProps = { chatReports: OnyxCollection; betas: OnyxEntry; policies: OnyxCollection; - allReportActions: OnyxCollection; + allReportActions: OnyxCollection; transactionViolations: OnyxCollection; policyMembers: OnyxCollection; priorityMode: OnyxEntry; @@ -112,72 +110,9 @@ function WithOrderedReportListItemsContextProvider({ return {children}; } -/** - * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering - * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - */ -const chatReportSelector = (report: OnyxEntry) => - report && { - reportID: report.reportID, - participantAccountIDs: report.participantAccountIDs, - hasDraft: report.hasDraft, - isPinned: report.isPinned, - isHidden: report.isHidden, - notificationPreference: report.notificationPreference, - errorFields: {addWorkspaceRoom: report.errorFields?.addWorkspaceRoom}, - lastMessageText: report.lastMessageText, - lastVisibleActionCreated: report.lastVisibleActionCreated, - iouReportID: report.iouReportID, - total: report.total, - nonReimbursableTotal: report.nonReimbursableTotal, - hasOutstandingChildRequest: report.hasOutstandingChildRequest, - isWaitingOnBankAccount: report.isWaitingOnBankAccount, - statusNum: report.statusNum, - stateNum: report.stateNum, - chatType: report.chatType, - type: report.type, - policyID: report.policyID, - visibility: report.visibility, - lastReadTime: report.lastReadTime, - // Needed for name sorting: - reportName: report.reportName, - policyName: report.policyName, - oldPolicyName: report.oldPolicyName, - // Other less obvious properites considered for sorting: - ownerAccountID: report.ownerAccountID, - currency: report.currency, - managerID: report.managerID, - // Other important less obivous properties for filtering: - parentReportActionID: report.parentReportActionID, - parentReportID: report.parentReportID, - isDeletedParentAction: report.isDeletedParentAction, - isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }; - -const reportActionsSelector = (reportActions: OnyxEntry) => - reportActions && - lodashMap(reportActions, (reportAction) => { - const {reportActionID, parentReportActionID, actionName, errors = [], originalMessage} = reportAction; - const decision = lodashGet(reportAction, 'message[0].moderationDecision.decision'); - - return { - reportActionID, - parentReportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - }, - ], - originalMessage, - }; - }); - const OrderedReportListItemsContextProvider = withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, - selector: chatReportSelector, initialValue: {}, }, priorityMode: { @@ -190,7 +125,6 @@ const OrderedReportListItemsContextProvider = withOnyx, policies: OnyxCollection, priorityMode: OnyxEntry, - allReportActions: OnyxCollection, + allReportActions: OnyxCollection, transactionViolations: OnyxCollection, currentPolicyID = '', policyMemberAccountIDs: number[] = [], @@ -80,7 +80,7 @@ function getOrderedReportIDs( let reportsToDisplay = allReportsDictValues.filter((report) => { const parentReportActionsKey = `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`; const parentReportActions = allReportActions?.[parentReportActionsKey]; - const parentReportAction = parentReportActions?.find((action) => action && report && action?.reportActionID === report?.parentReportActionID); + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '']; const doesReportHaveViolations = !!betas && betas.includes(CONST.BETAS.VIOLATIONS) && !!parentReportAction && ReportUtils.doesTransactionThreadHaveViolations(report, transactionViolations, parentReportAction); return ReportUtils.shouldReportBeInOptionList({ From 6739b587e93144f2dfa7a2250ac15e3b11488289 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 29 Feb 2024 18:01:30 +0500 Subject: [PATCH 0043/1096] test: fix reassure failing test --- tests/perf-test/SidebarUtils.perf-test.ts | 26 ++++++--------------- tests/utils/LHNTestUtils.tsx | 3 ++- tests/utils/collections/createCollection.ts | 21 +++++++++++++++++ 3 files changed, 30 insertions(+), 20 deletions(-) diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 3aa65331b9c2..6fb63878a832 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -8,7 +8,7 @@ import type {PersonalDetails, TransactionViolation} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; -import createCollection from '../utils/collections/createCollection'; +import createCollection, {createNestedCollection} from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReportAction from '../utils/collections/reportActions'; @@ -27,6 +27,12 @@ const reportActions = createCollection( (index) => createRandomReportAction(index), ); +const allReportActions = createNestedCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (item) => `${item.reportActionID}`, + (index) => createRandomReportAction(index), +); + const personalDetails = createCollection( (item) => item.accountID, (index) => createPersonalDetails(index), @@ -82,24 +88,6 @@ describe('SidebarUtils', () => { (index) => createRandomPolicy(index), ); - const allReportActions = Object.fromEntries( - Object.keys(reportActions).map((key) => [ - key, - [ - { - errors: reportActions[key].errors ?? [], - message: [ - { - moderationDecision: { - decision: reportActions[key].message?.[0]?.moderationDecision?.decision, - }, - }, - ], - }, - ], - ]), - ) as unknown as OnyxCollection; - await waitForBatchedUpdates(); await measureFunction(() => SidebarUtils.getOrderedReportIDs(currentReportId, allReports, betas, policies, CONST.PRIORITY_MODE.DEFAULT, allReportActions, transactionViolations)); }); diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index 80f28002f975..a10dbbdc17f8 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -10,6 +10,7 @@ import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxProvider from '@components/OnyxProvider'; import {CurrentReportIDContextProvider} from '@components/withCurrentReportID'; import {EnvironmentProvider} from '@components/withEnvironment'; +import {OrderedReportListItemsContextProvider} from '@hooks/useOrderedReportListItems'; import DateUtils from '@libs/DateUtils'; import ReportActionItemSingle from '@pages/home/report/ReportActionItemSingle'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; @@ -280,7 +281,7 @@ function getFakeAdvancedReportAction(actionName: ActionName = 'IOU', actor = 'em function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { return ( - + {}} diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index 848ef8f81f47..e62d9769bc53 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -9,3 +9,24 @@ export default function createCollection(createKey: (item: T, index: number) return map; } + +export function createNestedCollection( + createParentKey: (item: T, index: number) => string | number, + createKey: (item: T, index: number) => string | number, + createItem: (index: number) => T, + length = 500, +): Record> { + const map: Record> = {}; + + for (let i = 0; i < length; i++) { + const item = createItem(i); + const itemKey = createKey(item, i); + const itemParentKey = createParentKey(item, i); + map[itemParentKey] = { + ...map[itemParentKey], + [itemKey]: item, + }; + } + + return map; +} From 92fae0c08c3c3ca517fb57313f95e4fd2e77abf0 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 29 Feb 2024 18:02:17 +0500 Subject: [PATCH 0044/1096] revert: move option item data calculation to the renderItem component --- .../LHNOptionsList/LHNOptionsList.tsx | 117 +++++++++++++++--- .../LHNOptionsList/OptionRowLHNData.tsx | 63 +++++++++- src/components/LHNOptionsList/types.ts | 74 +++++++++-- src/hooks/useOrderedReportListItems.tsx | 47 +------ 4 files changed, 226 insertions(+), 75 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 1884997a58fe..5569e53942aa 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -2,13 +2,18 @@ import {FlashList} from '@shopify/flash-list'; import type {ReactElement} from 'react'; import React, {memo, useCallback, useMemo} from 'react'; import {StyleSheet, View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; +import withCurrentReportID from '@components/withCurrentReportID'; +import usePermissions from '@hooks/usePermissions'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import OptionRowLHNData from './OptionRowLHNData'; -import type {LHNOptionsListProps, OptionListItem, RenderItemProps} from './types'; +import type {LHNOptionsListOnyxProps, LHNOptionsListProps, RenderItemProps} from './types'; -const keyExtractor = (item: OptionListItem) => `report_${item.reportID}`; +const keyExtractor = (item: string) => `report_${item}`; function LHNOptionsList({ style, @@ -18,9 +23,18 @@ function LHNOptionsList({ shouldDisableFocusOptions = false, currentReportID = '', optionMode, + reports = {}, + reportActions = {}, + policy = {}, + preferredLocale = CONST.LOCALES.DEFAULT, + personalDetails = {}, + transactions = {}, + draftComments = {}, + transactionViolations = {}, onFirstItemRendered = () => {}, }: LHNOptionsListProps) { const styles = useThemeStyles(); + const {canUseViolations} = usePermissions(); // When the first item renders we want to call the onFirstItemRendered callback. // At this point in time we know that the list is actually displaying items. @@ -38,18 +52,64 @@ function LHNOptionsList({ * Function which renders a row in the list */ const renderItem = useCallback( - ({item}: RenderItemProps): ReactElement => ( - - ), - [shouldDisableFocusOptions, currentReportID, onSelectRow, onLayoutItem, optionMode], + ({item: reportID}: RenderItemProps): ReactElement => { + const itemFullReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const itemReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; + const itemParentReportActions = reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; + const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; + const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; + const transactionID = itemParentReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU ? itemParentReportAction.originalMessage.IOUTransactionID ?? '' : ''; + const itemTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`] ?? null; + const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; + const sortedReportActions = ReportActionsUtils.getSortedReportActionsForDisplay(itemReportActions); + const lastReportAction = sortedReportActions[0]; + + // Get the transaction for the last report action + let lastReportActionTransactionID = ''; + + if (lastReportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { + lastReportActionTransactionID = lastReportAction.originalMessage?.IOUTransactionID ?? ''; + } + const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`] ?? {}; + + return ( + + ); + }, + [ + currentReportID, + draftComments, + onSelectRow, + optionMode, + personalDetails, + policy, + preferredLocale, + reportActions, + reports, + shouldDisableFocusOptions, + transactions, + transactionViolations, + canUseViolations, + onLayoutItem, + ], ); const extraData = useMemo(() => [currentReportID], [currentReportID]); @@ -74,6 +134,33 @@ function LHNOptionsList({ LHNOptionsList.displayName = 'LHNOptionsList'; -export default memo(LHNOptionsList); +export default withCurrentReportID( + withOnyx({ + reports: { + key: ONYXKEYS.COLLECTION.REPORT, + }, + reportActions: { + key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, + }, + policy: { + key: ONYXKEYS.COLLECTION.POLICY, + }, + preferredLocale: { + key: ONYXKEYS.NVP_PREFERRED_LOCALE, + }, + personalDetails: { + key: ONYXKEYS.PERSONAL_DETAILS_LIST, + }, + transactions: { + key: ONYXKEYS.COLLECTION.TRANSACTION, + }, + draftComments: { + key: ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, + }, + transactionViolations: { + key: ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, + }, + })(memo(LHNOptionsList)), +); export type {LHNOptionsListProps}; diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index bc1a06ce5f67..a18d5a8ec1ec 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -1,5 +1,10 @@ -import React, {useEffect} from 'react'; +import {deepEqual} from 'fast-equals'; +import React, {useEffect, useMemo, useRef} from 'react'; +import * as ReportUtils from '@libs/ReportUtils'; +import SidebarUtils from '@libs/SidebarUtils'; import * as Report from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {OptionData} from '@src/libs/ReportUtils'; import OptionRowLHN from './OptionRowLHN'; import type {OptionRowLHNDataProps} from './types'; @@ -9,9 +14,63 @@ import type {OptionRowLHNDataProps} from './types'; * The OptionRowLHN component is memoized, so it will only * re-render if the data really changed. */ -function OptionRowLHNData({isFocused = false, comment, optionItem, ...propsToForward}: OptionRowLHNDataProps) { +function OptionRowLHNData({ + isFocused = false, + fullReport, + reportActions, + personalDetails = {}, + preferredLocale = CONST.LOCALES.DEFAULT, + comment, + policy, + receiptTransactions, + parentReportAction, + transaction, + lastReportActionTransaction = {}, + transactionViolations, + canUseViolations, + ...propsToForward +}: OptionRowLHNDataProps) { const reportID = propsToForward.reportID; + const optionItemRef = useRef(); + + const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(fullReport, transactionViolations, parentReportAction ?? null); + + const optionItem = useMemo(() => { + // Note: ideally we'd have this as a dependent selector in onyx! + const item = SidebarUtils.getOptionData({ + report: fullReport, + reportActions, + personalDetails, + preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, + policy, + parentReportAction, + hasViolations: !!hasViolations, + }); + if (deepEqual(item, optionItemRef.current)) { + return optionItemRef.current; + } + + optionItemRef.current = item; + + return item; + // Listen parentReportAction to update title of thread report when parentReportAction changed + // Listen to transaction to update title of transaction report when transaction changed + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [ + fullReport, + lastReportActionTransaction, + reportActions, + personalDetails, + preferredLocale, + policy, + parentReportAction, + transaction, + transactionViolations, + canUseViolations, + receiptTransactions, + ]); + useEffect(() => { if (!optionItem || !!optionItem.hasDraftComment || !comment || comment.length <= 0 || isFocused) { return; diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 3f72748ab8a8..e09e5cb6c8b5 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -1,22 +1,40 @@ import type {ContentStyle} from '@shopify/flash-list'; import type {RefObject} from 'react'; import type {LayoutChangeEvent, StyleProp, TextStyle, View, ViewStyle} from 'react-native'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {CurrentReportIDContextValue} from '@components/withCurrentReportID'; import type CONST from '@src/CONST'; import type {OptionData} from '@src/libs/ReportUtils'; +import type {Locale, PersonalDetailsList, Policy, Report, ReportAction, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; +import type {EmptyObject} from '@src/types/utils/EmptyObject'; type OptionMode = ValueOf; -type OptionListItem = { - /** The reportID of the report */ - reportID: string; +type LHNOptionsListOnyxProps = { + /** The policy which the user has access to and which the report could be tied to */ + policy: OnyxCollection; - /** The item that should be rendered */ - optionItem: OptionData | undefined; + /** All reports shared with the user */ + reports: OnyxCollection; - /** Comment added to report */ - comment: string; + /** Array of report actions for this report */ + reportActions: OnyxCollection; + + /** Indicates which locale the user currently has selected */ + preferredLocale: OnyxEntry; + + /** List of users' personal details */ + personalDetails: OnyxEntry; + + /** The transaction from the parent report action */ + transactions: OnyxCollection; + + /** List of draft comments */ + draftComments: OnyxCollection; + + /** The list of transaction violations */ + transactionViolations: OnyxCollection; }; type CustomLHNOptionsListProps = { @@ -27,7 +45,7 @@ type CustomLHNOptionsListProps = { contentContainerStyles?: StyleProp; /** Sections for the section list */ - data: OptionListItem[]; + data: string[]; /** Callback to fire when a row is selected */ onSelectRow?: (optionItem: OptionData, popoverAnchor: RefObject) => void; @@ -42,21 +60,51 @@ type CustomLHNOptionsListProps = { onFirstItemRendered: () => void; }; -type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue; +type LHNOptionsListProps = CustomLHNOptionsListProps & CurrentReportIDContextValue & LHNOptionsListOnyxProps; type OptionRowLHNDataProps = { /** Whether row should be focused */ isFocused?: boolean; + /** List of users' personal details */ + personalDetails?: PersonalDetailsList; + + /** The preferred language for the app */ + preferredLocale?: OnyxEntry; + + /** The full data of the report */ + fullReport: OnyxEntry; + + /** The policy which the user has access to and which the report could be tied to */ + policy?: OnyxEntry; + + /** The action from the parent report */ + parentReportAction?: OnyxEntry; + + /** The transaction from the parent report action */ + transaction: OnyxEntry; + + /** The transaction linked to the report's last action */ + lastReportActionTransaction?: OnyxEntry; + /** Comment added to report */ comment: string; - /** The item that should be rendered */ - optionItem: OptionData | undefined; + /** The receipt transaction from the parent report action */ + receiptTransactions: OnyxCollection; /** The reportID of the report */ reportID: string; + /** Array of report actions for this report */ + reportActions: OnyxEntry; + + /** List of transaction violation */ + transactionViolations: OnyxCollection; + + /** Whether the user can use violations */ + canUseViolations: boolean | undefined; + /** Toggle between compact and default view */ viewMode?: OptionMode; @@ -89,6 +137,6 @@ type OptionRowLHNProps = { onLayout?: (event: LayoutChangeEvent) => void; }; -type RenderItemProps = {item: OptionListItem}; +type RenderItemProps = {item: string}; -export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, OptionListItem, RenderItemProps}; +export type {LHNOptionsListProps, OptionRowLHNDataProps, OptionRowLHNProps, LHNOptionsListOnyxProps, RenderItemProps}; diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index d368abd897af..f9aaeaf400c1 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -1,17 +1,14 @@ import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {usePersonalDetails, useReport} from '@components/OnyxProvider'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Locale, Policy, PolicyMembers, PriorityMode, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; -import usePermissions from './usePermissions'; type OnyxProps = { chatReports: OnyxCollection; @@ -21,8 +18,6 @@ type OnyxProps = { transactionViolations: OnyxCollection; policyMembers: OnyxCollection; priorityMode: OnyxEntry; - preferredLocale: OnyxEntry; - draftComments: OnyxCollection; }; type WithOrderedReportListItemsContextProviderProps = OnyxProps & { @@ -40,12 +35,8 @@ function WithOrderedReportListItemsContextProvider({ transactionViolations, policyMembers, priorityMode, - preferredLocale, - draftComments, }: WithOrderedReportListItemsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); - const personalDetails = usePersonalDetails(); - const {canUseViolations} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); @@ -80,34 +71,7 @@ function WithOrderedReportListItemsContextProvider({ return orderedReportIDs; }, [getOrderedReportIDs, currentReportIDValue?.currentReportID, orderedReportIDs]); - const orderedReportListItems = useMemo( - () => - orderedReportIDsWithCurrentReport.map((reportID) => { - const itemFullReport = chatReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; - const itemReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? null; - const itemParentReportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${itemFullReport?.parentReportID}`] ?? null; - const itemParentReportAction = itemParentReportActions?.[itemFullReport?.parentReportActionID ?? ''] ?? null; - const itemPolicy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${itemFullReport?.policyID}`] ?? null; - const itemComment = draftComments?.[`${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`] ?? ''; - - const hasViolations = canUseViolations && ReportUtils.doesTransactionThreadHaveViolations(itemFullReport, transactionViolations, itemParentReportAction ?? null); - - const item = SidebarUtils.getOptionData({ - report: itemFullReport, - reportActions: itemReportActions, - personalDetails, - preferredLocale: preferredLocale ?? CONST.LOCALES.DEFAULT, - policy: itemPolicy, - parentReportAction: itemParentReportAction, - hasViolations: !!hasViolations, - }); - - return {reportID, optionItem: item, comment: itemComment}; - }), - [orderedReportIDsWithCurrentReport, canUseViolations, personalDetails, draftComments, preferredLocale, chatReports, allReportActions, policies, transactionViolations], - ); - - return {children}; + return {children}; } const OrderedReportListItemsContextProvider = withOnyx({ @@ -138,13 +102,6 @@ const OrderedReportListItemsContextProvider = withOnyx Date: Thu, 29 Feb 2024 19:48:44 +0500 Subject: [PATCH 0045/1096] fix: linting --- src/hooks/useOrderedReportListItems.tsx | 2 +- src/pages/home/sidebar/SidebarLinks.js | 2 +- tests/utils/collections/createCollection.ts | 6 +++++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index f9aaeaf400c1..bcd22320d1ca 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -6,7 +6,7 @@ import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportAction, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportActions, TransactionViolation} from '@src/types/onyx'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; diff --git a/src/pages/home/sidebar/SidebarLinks.js b/src/pages/home/sidebar/SidebarLinks.js index 4c9803e257bc..8cda70d2486a 100644 --- a/src/pages/home/sidebar/SidebarLinks.js +++ b/src/pages/home/sidebar/SidebarLinks.js @@ -33,7 +33,7 @@ const basePropTypes = { const propTypes = { ...basePropTypes, - optionListItems: PropTypes.arrayOf(PropTypes.object).isRequired, + optionListItems: PropTypes.arrayOf(PropTypes.string).isRequired, isLoading: PropTypes.bool.isRequired, diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index e62d9769bc53..bcc37c301279 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -10,7 +10,7 @@ export default function createCollection(createKey: (item: T, index: number) return map; } -export function createNestedCollection( +function createNestedCollection( createParentKey: (item: T, index: number) => string | number, createKey: (item: T, index: number) => string | number, createItem: (index: number) => T, @@ -30,3 +30,7 @@ export function createNestedCollection( return map; } + +export { + createNestedCollection, +} \ No newline at end of file From 6e944446670094973945f38b05c5c26416902f70 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 29 Feb 2024 17:59:45 +0100 Subject: [PATCH 0046/1096] feat: wip - display rates for policy --- ...oraryForRefactorRequestConfirmationList.js | 4 +- src/libs/DistanceRequestUtils.ts | 119 +++++++++++++++--- .../iou/request/step/IOURequestStepRate.tsx | 49 +++++--- src/types/onyx/Policy.ts | 4 +- 4 files changed, 139 insertions(+), 37 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 46e4cb3d6733..267662f07157 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -681,7 +681,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ = {}; +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + callback: (policy, key) => { + if (!policy || !key || !policy.name) { + return; + } + + policies[key] = policy; + }, +}); + /** * Retrieves the default mileage rate based on a given policy. * @@ -67,22 +81,21 @@ function convertDistanceUnit(distanceInMeters: number, unit: Unit): number { */ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string { const convertedDistance = convertDistanceUnit(distanceInMeters, unit); - return convertedDistance.toFixed(2); + // TODO: add logic for currencies for which we need to round to 4 decimals + return convertedDistance.toFixed(3); } /** -* @param hasRoute Whether the route exists for the distance request -* @param distanceInMeters Distance traveled -* @param unit Unit that should be used to display the distance -* @param rate Expensable amount allowed per unit -* @param currency The currency associated with the rate -* @param translate Translate function -* @param toLocaleDigit Function to convert to localized digit -* @returns A string that describes the distance traveled and the rate used for expense calculation -*/ + * @param hasRoute Whether the route exists for the distance request + * @param unit Unit that should be used to display the distance + * @param rate Expensable amount allowed per unit + * @param currency The currency associated with the rate + * @param translate Translate function + * @param toLocaleDigit Function to convert to localized digit + * @returns A string that describes the distance traveled and the rate used for expense calculation + */ function getRateForDisplay( hasRoute: boolean, - distanceInMeters: number, unit: Unit, rate: number, currency: string, @@ -101,6 +114,28 @@ function getRateForDisplay( return `${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`; } +// TODO: I wonder if it would be better to rfactor these functions to pass params in an object +/** + * @param hasRoute Whether the route exists for the distance request + * @param distanceInMeters Distance traveled + * @param unit Unit that should be used to display the distance + * @param rate Expensable amount allowed per unit + * @param translate Translate function + * @returns A string that describes the distance traveled + */ +function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, translate: LocaleContextProps['translate']): string { + if (!hasRoute) { + return translate('iou.routePending'); + } + + const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); + const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); + const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); + const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; + + return `${distanceInUnits} ${unitString}`; +} + /** * @param hasRoute Whether the route exists for the distance request * @param distanceInMeters Distance traveled @@ -124,13 +159,52 @@ function getDistanceMerchant( return translate('iou.routePending'); } - const distanceInUnits = getRoundedDistanceInUnits(distanceInMeters, unit); - const distanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.miles') : translate('common.kilometers'); - const singularDistanceUnit = unit === CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES ? translate('common.mile') : translate('common.kilometer'); - const unitString = distanceInUnits === '1' ? singularDistanceUnit : distanceUnit; - const ratePerUnit = getRateForDisplay(hasRoute, distanceInMeters, unit, rate, currency, translate, toLocaleDigit); + const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, translate); + const ratePerUnit = getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit); + + return `${distanceInUnits} @ ${ratePerUnit}`; +} + +/** + * Retrieves the mileage rates for given policy. + * + * @param policyID - The policy ID from which to extract the mileage rates. + * + * @returns An array of mileage rates or an empty array if not found. + */ +function getMileageRates(policyID?: string): MileageRate[] | [] { + if (!policyID) { + return []; + } + + const mileageRates: MileageRate[] = []; + + const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? null; + + if (!policy || !policy?.customUnits) { + return mileageRates; + } + + const distanceUnit = Object.values(policy.customUnits).find((unit) => unit.name === CONST.CUSTOM_UNITS.NAME_DISTANCE); + if (!distanceUnit?.rates) { + return mileageRates; + } + + const rates = Object.values(distanceUnit.rates); + + for (const rate of rates) { + if (rate.enabled) { + mileageRates.push({ + rate: rate.rate ?? 0, + name: rate.name, + currency: rate.currency ?? 'USD', + unit: distanceUnit.attributes.unit, + customUnitRateID: rate.customUnitRateID, + }); + } + } - return `${distanceInUnits} ${unitString} @ ${ratePerUnit}`; + return mileageRates; } /** @@ -147,4 +221,11 @@ function getDistanceRequestAmount(distance: number, unit: Unit, rate: number): n return Math.round(roundedDistance * rate); } -export default {getDefaultMileageRate, getDistanceMerchant, getDistanceRequestAmount, getRateForDisplay}; +export default { + getDefaultMileageRate, + getDistanceMerchant, + getDistanceRequestAmount, + getRateForDisplay, + getMileageRates, + getDistanceForDisplay, +}; diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index d82238f2c7ea..3b7889de9651 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -1,17 +1,27 @@ import React from 'react'; import {withOnyx} from 'react-native-onyx'; +import type {OnyxEntry} from 'react-native-onyx'; import SelectionList from '@components/SelectionList'; import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import compose from '@libs/compose'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; +import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; +import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; +import type {Policy} from '@src/types/onyx'; type Props = { // eslint-disable-next-line react/no-unused-prop-types lastSelectedDistanceRate: string; + + /** Policy details */ + policy: OnyxEntry; + /** The route object passed to this screen */ route: { /** The params passed to this screen */ @@ -22,18 +32,22 @@ type Props = { }; }; -const mockRates = [ - {text: 'Default Rate', alternateText: '$0.656 / mile', keyForList: 'DefaultRate'}, - {text: 'Custom Rate', alternateText: '$0.700 / mile', keyForList: 'CustomRate'}, -]; - function IOURequestStepRate({ + policy, route: { params: {backTo}, }, }: Props) { const styles = useThemeStyles(); - const {translate} = useLocalize(); + const {translate, toLocaleDigit} = useLocalize(); + const rates = DistanceRequestUtils.getMileageRates(policy?.id); + + const data = rates.map((rate) => ({ + text: rate.name ?? '', + alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), + keyForList: rate.name ?? '', + value: rate.customUnitRateID, + })); return ( {translate('iou.chooseARate')} {}} - initiallyFocusedOptionKey="DefaultRate" + // TODO: change for lastSelectedDistanceRates + initiallyFocusedOptionKey="Default Rate" /> ); @@ -56,10 +71,14 @@ function IOURequestStepRate({ IOURequestStepRate.displayName = 'IOURequestStepRate'; -// export default withOnyx({ -// lastSelectedDistanceRate: { -// key: 'ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATE', -// }, -// })(IOURequestStepRate); - -export default IOURequestStepRate; +export default compose( + withWritableReportOrNotFound, + withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, + }, + // lastSelectedDistanceRates: { + // key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + // }, + }), +)(IOURequestStepRate); diff --git a/src/types/onyx/Policy.ts b/src/types/onyx/Policy.ts index 1eece2d3a1e0..8e408951a84f 100644 --- a/src/types/onyx/Policy.ts +++ b/src/types/onyx/Policy.ts @@ -19,8 +19,10 @@ type Attributes = { type MileageRate = { unit: Unit; - rate?: number; currency: string; + customUnitRateID?: string; + rate?: number; + name?: string; }; type CustomUnit = OnyxCommon.OnyxValueWithOfflineFeedback<{ From e735658f12826766791c2bfa1d3bb7b82b4559d6 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 1 Mar 2024 13:30:58 +0500 Subject: [PATCH 0047/1096] test: fix failing test --- .../LHNOptionsList/LHNOptionsList.tsx | 2 +- src/components/withCurrentReportID.tsx | 2 +- src/hooks/useOrderedReportListItems.tsx | 18 +++++++-- tests/unit/SidebarOrderTest.ts | 6 +-- tests/utils/LHNTestUtils.tsx | 37 ++++++++++++------- 5 files changed, 44 insertions(+), 21 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 5569e53942aa..d5b422122144 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -112,7 +112,7 @@ function LHNOptionsList({ ], ); - const extraData = useMemo(() => [currentReportID], [currentReportID]); + const extraData = useMemo(() => [reportActions, reports, policy, personalDetails], [reportActions, reports, policy, personalDetails]); return ( diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index d5c5a6896a9c..0d052f759f81 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -45,7 +45,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro * switching between chat list and settings->workspaces tab. * and doing so avoid unnecessary re-render of `useOrderedReportListItems`. */ - if (reportID) { + if (reportID !== undefined) { setCurrentReportID(reportID); } }, diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useOrderedReportListItems.tsx index bcd22320d1ca..b160c372679a 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useOrderedReportListItems.tsx @@ -22,6 +22,7 @@ type OnyxProps = { type WithOrderedReportListItemsContextProviderProps = OnyxProps & { children: React.ReactNode; + currentReportIDForTests?: string; }; const OrderedReportListItemsContext = createContext({}); @@ -35,8 +36,19 @@ function WithOrderedReportListItemsContextProvider({ transactionViolations, policyMembers, priorityMode, + /** + * Only required to make unit tests work, since we + * explicitly pass the currentReportID in LHNTestUtils + * to SidebarLinksData, so this context doesn't have an + * access to currentReportID in that case. + * + * So this is a work around to have currentReportID available + * only in testing environment. + */ + currentReportIDForTests, }: WithOrderedReportListItemsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); + const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportID; const {activeWorkspaceID} = useActiveWorkspace(); const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); @@ -65,11 +77,11 @@ function WithOrderedReportListItemsContextProvider({ // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. const orderedReportIDsWithCurrentReport = useMemo(() => { - if (currentReportIDValue?.currentReportID && !orderedReportIDs.includes(currentReportIDValue.currentReportID)) { - return getOrderedReportIDs(currentReportIDValue.currentReportID); + if (derivedCurrentReportID && !orderedReportIDs.includes(derivedCurrentReportID)) { + return getOrderedReportIDs(derivedCurrentReportID); } return orderedReportIDs; - }, [getOrderedReportIDs, currentReportIDValue?.currentReportID, orderedReportIDs]); + }, [getOrderedReportIDs, derivedCurrentReportID, orderedReportIDs]); return {children}; } diff --git a/tests/unit/SidebarOrderTest.ts b/tests/unit/SidebarOrderTest.ts index 27da8348f43d..10d30e4c6dc8 100644 --- a/tests/unit/SidebarOrderTest.ts +++ b/tests/unit/SidebarOrderTest.ts @@ -298,7 +298,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, ...reportCollectionDataSet, }), ) @@ -362,7 +362,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, ...reportCollectionDataSet, }), ) @@ -429,7 +429,7 @@ describe('Sidebar', () => { Onyx.multiSet({ [ONYXKEYS.NVP_PRIORITY_MODE]: CONST.PRIORITY_MODE.DEFAULT, [ONYXKEYS.PERSONAL_DETAILS_LIST]: LHNTestUtils.fakePersonalDetails, - [ONYXKEYS.IS_LOADING_REPORT_DATA]: false, + [ONYXKEYS.IS_LOADING_APP]: false, [`${ONYXKEYS.COLLECTION.POLICY}${fakeReport.policyID}`]: fakePolicy, ...reportCollectionDataSet, }), diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index a10dbbdc17f8..c8c3f145d951 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -281,19 +281,30 @@ function getFakeAdvancedReportAction(actionName: ActionName = 'IOU', actor = 'em function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { return ( - - {}} - insets={{ - top: 0, - left: 0, - right: 0, - bottom: 0, - }} - isSmallScreenWidth={false} - currentReportID={currentReportID} - /> + + {/* + * Only required to make unit tests work, since we + * explicitly pass the currentReportID in LHNTestUtils + * to SidebarLinksData, so this context doesn't have an + * access to currentReportID in that case. + * + * So this is a work around to have currentReportID available + * only in testing environment. + * */} + + {}} + insets={{ + top: 0, + left: 0, + right: 0, + bottom: 0, + }} + isSmallScreenWidth={false} + currentReportID={currentReportID} + /> + ); } From 94fcc48f89f95ba50fc22a20ac04183dbd25cb8a Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 1 Mar 2024 13:31:12 +0500 Subject: [PATCH 0048/1096] fix: prettier --- tests/utils/collections/createCollection.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index bcc37c301279..4a2a1fa4eb78 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -31,6 +31,4 @@ function createNestedCollection( return map; } -export { - createNestedCollection, -} \ No newline at end of file +export {createNestedCollection}; From cde1d19d37cdd0203ca6023612dfe12ea0f46537 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Mar 2024 16:12:00 +0100 Subject: [PATCH 0049/1096] feat: wip - get rate for p2p --- ...oraryForRefactorRequestConfirmationList.js | 52 ++++++++++++++++--- src/libs/DistanceRequestUtils.ts | 13 +++-- ...yForRefactorRequestParticipantsSelector.js | 4 +- 3 files changed, 55 insertions(+), 14 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 267662f07157..5a7019e63428 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -239,6 +239,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ session: {accountID}, shouldShowSmartScanFields, transaction, + lastSelectedDistanceRate = {}, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -250,6 +251,12 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeSend = iouType === CONST.IOU.TYPE.SEND; const {unit, rate, currency} = mileageRate; + + // const rate = transaction.comment.customUnit.customUnitRateID === '_FAKE_P2P_ID_' + + // const {unit, rate, currency} = lastSelectedDistanceRate || mileageRate; + + const distance = lodashGet(transaction, 'routes.route0.distance', 0); const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; const taxRates = lodashGet(policy, 'taxRates', {}); @@ -266,6 +273,8 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowDate = shouldShowSmartScanFields || isDistanceRequest; const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest; + console.log({transaction}); + const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); // A flag for showing the tags field @@ -622,6 +631,10 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ); }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); + // TODO: change for a value from usePermissions [will be added in this PR https://github.com/Expensify/App/pull/37185] + // change for true for development + const canUseP2PDistanceRequests = true; + // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. const classifiedFields = [ @@ -681,7 +694,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ), - shouldShow: isDistanceRequest, + shouldShow: isDistanceRequest && !canUseP2PDistanceRequests, isSupplementary: true, }, { item: ( Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_DISTANCE.getRoute(iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()))} + disabled={didConfirm || !isTypeRequest} + interactive={!isReadOnly} + /> + ), + shouldShow: isDistanceRequest && canUseP2PDistanceRequests, + isSupplementary: true, + }, + { + item: ( + { - Navigation.navigate(ROUTES.MONEY_REQUEST_STEP_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams())); + Navigation.navigate( + ROUTES.MONEY_REQUEST_STEP_RATE.getRoute(CONST.IOU.ACTION.CREATE, iouType, transaction.transactionID, reportID, Navigation.getActiveRouteWithoutParams()), + ); }} disabled={didConfirm || !isTypeRequest} - interactive={!isReadOnly} + interactive={!isReadOnly && isPolicyExpenseChat} /> ), - // TODO: hide when betas ready - shouldShow: isDistanceRequest, + shouldShow: isDistanceRequest && canUseP2PDistanceRequests, isSupplementary: true, }, { @@ -950,12 +982,16 @@ export default compose( policyTags: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, }, + // TODO: add NVP_LAST_SELECTED_DISTANCE_RATES + // lastSelectedDistanceRates: { + // key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + // }, mileageRate: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getDefaultMileageRate, }, policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - }, + } }), )(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index a605ace8da87..191edd93bfb9 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -12,6 +12,8 @@ type DefaultMileageRate = { rate?: number; currency?: string; unit: Unit; + name?: string; + customUnitRateID?: string; }; const policies: OnyxCollection = {}; @@ -55,6 +57,8 @@ function getDefaultMileageRate(policy: OnyxEntry): DefaultMileageRate | rate: distanceRate.rate, currency: distanceRate.currency, unit: distanceUnit.attributes.unit, + name: distanceRate.name, + customUnitRateID: distanceRate.customUnitRateID, }; } @@ -91,6 +95,7 @@ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string return convertedDistance.toFixed(3); } +// TODO: I wonder if it would be better to refactor these functions to pass params in an object /** * @param hasRoute Whether the route exists for the distance request * @param unit Unit that should be used to display the distance @@ -120,7 +125,7 @@ function getRateForDisplay( return `${currencySymbol}${ratePerUnit} / ${singularDistanceUnit}`; } -// TODO: I wonder if it would be better to rfactor these functions to pass params in an object +// TODO: this function will be added in https://github.com/Expensify/App/pull/37185, remove it to avoid conflicts /** * @param hasRoute Whether the route exists for the distance request * @param distanceInMeters Distance traveled @@ -129,8 +134,8 @@ function getRateForDisplay( * @param translate Translate function * @returns A string that describes the distance traveled */ -function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, translate: LocaleContextProps['translate']): string { - if (!hasRoute) { +function getDistanceForDisplay(hasRoute: boolean, distanceInMeters: number, unit: Unit, rate: number, translate: LocaleContextProps['translate']): string { + if (!hasRoute || !rate) { return translate('iou.routePending'); } @@ -165,7 +170,7 @@ function getDistanceMerchant( return translate('iou.routePending'); } - const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, translate); + const distanceInUnits = getDistanceForDisplay(hasRoute, distanceInMeters, unit, rate, translate); const ratePerUnit = getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit); return `${distanceInUnits} @ ${ratePerUnit}`; diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index 2865316b7fd5..b42b94afc686 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -121,7 +121,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ iouType === CONST.IOU.TYPE.REQUEST, // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + true, false, {}, [], @@ -131,7 +131,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, + true, false, ); From c3cb80bf507de6c4922b28e170893cdcaa126ebb Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Mar 2024 17:38:59 +0100 Subject: [PATCH 0050/1096] feat: wip - new rate field logic --- src/CONST.ts | 7 ++++ ...oraryForRefactorRequestConfirmationList.js | 40 +++++++++---------- src/libs/DistanceRequestUtils.ts | 39 +++++++++--------- src/libs/actions/IOU.ts | 13 ++++++ .../iou/request/step/IOURequestStepRate.tsx | 12 +++++- 5 files changed, 69 insertions(+), 42 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 8d4eaac44a38..2bd047f6bf51 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1398,6 +1398,13 @@ const CONST = { RATE_DECIMALS: 3, }, + // TODO: remove this mock when https://github.com/Expensify/App/issues/36982 is done + CURRENCY_TO_DEFAULT_MILEAGE_RATE: { + USD: { unit: "mile", rate: 0.5 }, + EUR: { unit: "kilometer", rate: 0.8 }, + GBP: { unit: "mile", rate: 0.45 }, + }, + TERMS: { CFPB_PREPAID: 'cfpb.gov/prepaid', CFPB_COMPLAINT: 'cfpb.gov/complaint', diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 5a7019e63428..9c2d77239015 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -1,10 +1,10 @@ -import { useIsFocused } from '@react-navigation/native'; -import { format } from 'date-fns'; +import {useIsFocused} from '@react-navigation/native'; +import {format} from 'date-fns'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; -import React, { Fragment, useCallback, useEffect, useMemo, useReducer, useRef, useState } from 'react'; -import { View } from 'react-native'; -import { withOnyx } from 'react-native-onyx'; +import React, {useCallback, useEffect, useMemo, useReducer, useRef, useState} from 'react'; +import {View} from 'react-native'; +import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import useLocalize from '@hooks/useLocalize'; import usePermissions from '@hooks/usePermissions'; @@ -21,9 +21,9 @@ import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; -import playSound, { SOUNDS } from '@libs/Sound'; +import playSound, {SOUNDS} from '@libs/Sound'; import * as TransactionUtils from '@libs/TransactionUtils'; -import { policyPropTypes } from '@pages/workspace/withPolicy'; +import {policyPropTypes} from '@pages/workspace/withPolicy'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -44,8 +44,7 @@ import Switch from './Switch'; import tagPropTypes from './tagPropTypes'; import Text from './Text'; import transactionPropTypes from './transactionPropTypes'; -import withCurrentUserPersonalDetails, { withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes } from './withCurrentUserPersonalDetails'; - +import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from './withCurrentUserPersonalDetails'; const propTypes = { /** Callback to inform parent modal of success */ @@ -239,7 +238,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ session: {accountID}, shouldShowSmartScanFields, transaction, - lastSelectedDistanceRate = {}, + mileageRates, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -250,12 +249,11 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeSplit = iouType === CONST.IOU.TYPE.SPLIT; const isTypeSend = iouType === CONST.IOU.TYPE.SEND; + // TODO: uncomment after Splits and P2P are enabled https://github.com/Expensify/App/pull/37185, mileageRate prop should be be removed + // const mileageRate = transaction.comment.customUnit.customUnitRateID === '_FAKE_P2P_ID_' ? DistanceRequestUtils.getRateForP2P(policy.outputCurrency) : mileageRates[transaction.comment.customUnit.customUnitRateID]; const {unit, rate, currency} = mileageRate; - // const rate = transaction.comment.customUnit.customUnitRateID === '_FAKE_P2P_ID_' - - // const {unit, rate, currency} = lastSelectedDistanceRate || mileageRate; - + // will hardcoded rates will have a currency property? If not we have to get currency other way const distance = lodashGet(transaction, 'routes.route0.distance', 0); const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; @@ -273,8 +271,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const shouldShowDate = shouldShowSmartScanFields || isDistanceRequest; const shouldShowMerchant = shouldShowSmartScanFields && !isDistanceRequest; - console.log({transaction}); - const policyTagLists = useMemo(() => PolicyUtils.getTagLists(policyTags), [policyTags]); // A flag for showing the tags field @@ -633,7 +629,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // TODO: change for a value from usePermissions [will be added in this PR https://github.com/Expensify/App/pull/37185] // change for true for development - const canUseP2PDistanceRequests = true; + const canUseP2PDistanceRequests = false; // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. @@ -982,16 +978,16 @@ export default compose( policyTags: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY_TAGS}${policyID}`, }, - // TODO: add NVP_LAST_SELECTED_DISTANCE_RATES - // lastSelectedDistanceRates: { - // key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, - // }, mileageRate: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, selector: DistanceRequestUtils.getDefaultMileageRate, }, + mileageRates: { + key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, + selector: (policy) => DistanceRequestUtils.getMileageRates(policy ? policy.id : ''), + }, policy: { key: ({policyID}) => `${ONYXKEYS.COLLECTION.POLICY}${policyID}`, - } + }, }), )(MoneyTemporaryForRefactorRequestConfirmationList); diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index 191edd93bfb9..dd95612af3e6 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -1,4 +1,4 @@ -import type {OnyxEntry, OnyxCollection} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {LocaleContextProps} from '@components/LocaleContextProvider'; import CONST from '@src/CONST'; @@ -183,13 +183,13 @@ function getDistanceMerchant( * * @returns An array of mileage rates or an empty array if not found. */ -function getMileageRates(policyID?: string): DefaultMileageRate[] | [] { +function getMileageRates(policyID?: string): Record { + const mileageRates = {}; + if (!policyID) { - return []; + return mileageRates; } - const mileageRates: DefaultMileageRate[] = []; - const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${policyID}`] ?? null; if (!policy || !policy?.customUnits) { @@ -201,23 +201,25 @@ function getMileageRates(policyID?: string): DefaultMileageRate[] | [] { return mileageRates; } - const rates = Object.values(distanceUnit.rates); - - for (const rate of rates) { - if (rate.enabled) { - mileageRates.push({ - rate: rate.rate ?? 0, - name: rate.name, - currency: rate.currency ?? 'USD', - unit: distanceUnit.attributes.unit, - customUnitRateID: rate.customUnitRateID, - }); - } - } + Object.entries(distanceUnit.rates).forEach(([rateID, rate]) => { + // TODO: fix TS error + mileageRates[rateID] = { + rate: rate.rate, + currency: rate.currency, + unit: distanceUnit.attributes.unit, + name: rate.name, + customUnitRateID: rate.customUnitRateID, + }; + }); return mileageRates; } +// TODO: probably will need to be changed +function getRateForP2P(currency) { + return CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE[currency] ?? CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE.USD; +} + /** * Calculates the request amount based on distance, unit, and rate. * @@ -239,4 +241,5 @@ export default { getRateForDisplay, getMileageRates, getDistanceForDisplay, + getRateForP2P, }; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 5f9657755b02..91dd95b12592 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -355,6 +355,17 @@ function setMoneyRequestReceipt(transactionID: string, source: string, filename: }); } +/** Set the last selected distance rate for policy */ +// TODO: probably need to be changed +function setLastSelectedDistanceRates(policyID: string, rateID: string) { + Onyx.merge('lastSelectedDistanceRates', {[policyID]: rateID}); +} + +/** Update transaction distance rate */ +function updateDistanceRequestRate(transactionID: string, rateID: string) { + Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION_DRAFT}${transactionID}`, {comment: {customUnit: {customUnitRateID: rateID}}}); +} + /** Reset money request info from the store with its initial value */ function resetMoneyRequestInfo(id = '') { // Disabling this line since currentDate can be an empty string @@ -4327,4 +4338,6 @@ export { cancelPayment, navigateToStartStepIfScanFileCannotBeRead, savePreferredPaymentMethod, + setLastSelectedDistanceRates, + updateDistanceRequestRate, }; diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 3b7889de9651..a2cf7a0af1c8 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -9,6 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as IOU from '@libs/actions/IOU'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -42,13 +43,20 @@ function IOURequestStepRate({ const {translate, toLocaleDigit} = useLocalize(); const rates = DistanceRequestUtils.getMileageRates(policy?.id); - const data = rates.map((rate) => ({ + const data = Object.values(rates).map((rate) => ({ text: rate.name ?? '', alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), keyForList: rate.name ?? '', value: rate.customUnitRateID, })); + const selectDistanceRate = (customUnitRateID) => { + IOU.setLastSelectedDistanceRates(policy?.id ?? '', customUnitRateID); + // TODO: get a proper transaction ID + IOU.updateDistanceRequestRate('1', customUnitRateID); + Navigation.goBack(backTo); + } + return ( {}} + onSelectRow={({value}) => selectDistanceRate(value)} // TODO: change for lastSelectedDistanceRates initiallyFocusedOptionKey="Default Rate" /> From 5bf399af20bdb7816bedf1ed40ee37fa83e3fffa Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Mar 2024 17:51:11 +0100 Subject: [PATCH 0051/1096] fix: revert filtering changes --- .../MoneyTemporaryForRefactorRequestParticipantsSelector.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index b42b94afc686..2865316b7fd5 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -121,7 +121,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ iouType === CONST.IOU.TYPE.REQUEST, // We don't want to include any P2P options like personal details or reports that are not workspace chats for certain features. - true, + iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, {}, [], @@ -131,7 +131,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ // We don't want the user to be able to invite individuals when they are in the "Distance request" flow for now. // This functionality is being built here: https://github.com/Expensify/App/issues/23291 - true, + iouRequestType !== CONST.IOU.REQUEST_TYPE.DISTANCE, false, ); From 930eb47f5222088e8f687d1be63707d83ec239e2 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Mon, 4 Mar 2024 17:18:45 +0700 Subject: [PATCH 0052/1096] remove global ref and create suggestion context --- src/App.tsx | 2 + src/libs/actions/SuggestionsActions.ts | 50 ------------------- .../ComposerWithSuggestionsEdit.tsx | 25 +++++----- .../SuggestionsContext.tsx | 50 +++++++++++++++++++ .../report/ReportActionItemMessageEdit.tsx | 21 +++++--- src/pages/home/report/ReportActionsView.js | 10 +++- 6 files changed, 87 insertions(+), 71 deletions(-) delete mode 100644 src/libs/actions/SuggestionsActions.ts create mode 100644 src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx diff --git a/src/App.tsx b/src/App.tsx index cbe5948f8d4e..03dc3c328488 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -31,6 +31,7 @@ import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import InitialUrlContext from './libs/InitialUrlContext'; +import {SuggestionsContextProvider} from './pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; import type {Route} from './ROUTES'; @@ -76,6 +77,7 @@ function App({url}: AppProps) { ActiveElementRoleProvider, ActiveWorkspaceContextProvider, PlaybackContextProvider, + SuggestionsContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, ]} diff --git a/src/libs/actions/SuggestionsActions.ts b/src/libs/actions/SuggestionsActions.ts deleted file mode 100644 index 9fecd18f7d10..000000000000 --- a/src/libs/actions/SuggestionsActions.ts +++ /dev/null @@ -1,50 +0,0 @@ -import React from 'react'; -import type {NativeSyntheticEvent, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; - -type SuggestionsRef = { - getSuggestions: () => void; - resetSuggestions: () => void; - triggerHotkeyActions: (event: NativeSyntheticEvent | KeyboardEvent) => boolean; - onSelectionChange: (event: NativeSyntheticEvent) => void; - updateShouldShowSuggestionMenuToFalse: () => void; - setShouldBlockSuggestionCalc: () => void; -}; - -const suggestionsRef = React.createRef(); - -function resetSuggestions() { - if (!suggestionsRef.current) { - return; - } - - suggestionsRef.current.resetSuggestions(); -} - -function triggerHotkeyActions(event: NativeSyntheticEvent | KeyboardEvent): boolean { - if (!suggestionsRef.current) { - return false; - } - - return suggestionsRef.current.triggerHotkeyActions(event); -} - -function updateShouldShowSuggestionMenuToFalse() { - if (!suggestionsRef.current) { - return; - } - - suggestionsRef.current.updateShouldShowSuggestionMenuToFalse(); -} - -function onSelectionChange(event: NativeSyntheticEvent) { - if (!suggestionsRef.current) { - return; - } - - suggestionsRef.current.onSelectionChange(event); -} - -export {suggestionsRef, resetSuggestions, triggerHotkeyActions, onSelectionChange, updateShouldShowSuggestionMenuToFalse}; - -// eslint-disable-next-line import/prefer-default-export -export type {SuggestionsRef}; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx index 379928e8f28f..ce8cfa1dc356 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit.tsx @@ -1,24 +1,26 @@ import type {Dispatch, ForwardedRef, RefObject, SetStateAction} from 'react'; import React, {useState} from 'react'; -import type {TextInput} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, TextInput} from 'react-native'; import Composer from '@components/Composer'; import type {ComposerProps} from '@components/Composer/types'; -import type {SuggestionsRef} from '@libs/actions/SuggestionsActions'; +import type {SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; -type ComposerWithSuggestionsEditProps = { +type Selection = { + start: number; + end: number; +}; + +type ComposerWithSuggestionsEditProps = ComposerProps & { setValue: Dispatch>; - setSelection: Dispatch< - SetStateAction<{ - start: number; - end: number; - }> - >; + setSelection: Dispatch>; resetKeyboardInput: () => void; isComposerFocused: boolean; suggestionsRef: RefObject; updateDraft: (newValue: string) => void; - measureParentContainer: (callback: () => void) => void; + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + value: string; + selection: Selection; }; function ComposerWithSuggestionsEdit( @@ -43,7 +45,7 @@ function ComposerWithSuggestionsEdit( updateDraft, measureParentContainer, id = undefined, - }: ComposerWithSuggestionsEditProps & ComposerProps, + }: ComposerWithSuggestionsEditProps, ref: ForwardedRef, ) { const [composerHeight, setComposerHeight] = useState(0); @@ -74,7 +76,6 @@ function ComposerWithSuggestionsEdit( ; + updateCurrentActiveSuggestionsRef: (ref: SuggestionsRef | null, id: string) => void; + clearActiveSuggestionsRef: () => void; +}; + +const SuggestionsContext = createContext({ + currentActiveSuggestionsRef: {current: null}, + updateCurrentActiveSuggestionsRef: () => {}, + clearActiveSuggestionsRef: () => {}, +}); + +function SuggestionsContextProvider({children}: SuggestionsContextProviderProps) { + const currentActiveSuggestionsRef = useRef(null); + const [activeID, setActiveID] = useState(null); + + const updateCurrentActiveSuggestionsRef = useCallback((ref: SuggestionsRef | null, id: string) => { + currentActiveSuggestionsRef.current = ref; + setActiveID(id); + }, []); + + const clearActiveSuggestionsRef = useCallback(() => { + currentActiveSuggestionsRef.current = null; + setActiveID(null); + }, []); + + const contextValue = useMemo( + () => ({activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef}), + [activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef], + ); + + return {children}; +} + +function useSuggestionsContext() { + const context = useContext(SuggestionsContext); + return context; +} + +SuggestionsContextProvider.displayName = 'PlaybackContextProvider'; + +export {SuggestionsContextProvider, useSuggestionsContext}; diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 6a182a6016c3..ad516946fecf 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -5,7 +5,7 @@ import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {findNodeHandle, Keyboard, NativeModules, View} from 'react-native'; -import type {NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputKeyPressEventData} from 'react-native'; import type {Emoji} from '@assets/emojis/types'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; @@ -21,7 +21,6 @@ import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; -import * as SuggestionsAction from '@libs/actions/SuggestionsActions'; import * as Browser from '@libs/Browser'; import * as ComposerUtils from '@libs/ComposerUtils'; import * as EmojiUtils from '@libs/EmojiUtils'; @@ -42,6 +41,8 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import * as ReportActionContextMenu from './ContextMenu/ReportActionContextMenu'; import ComposerWithSuggestionsEdit from './ReportActionCompose/ComposerWithSuggestionsEdit/ComposerWithSuggestionsEdit'; +import {useSuggestionsContext} from './ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; +import type {SuggestionsRef} from './ReportActionCompose/ReportActionCompose'; const {RNTextInputReset} = NativeModules; @@ -81,6 +82,7 @@ function ReportActionItemMessageEdit( const {translate, preferredLocale} = useLocalize(); const {isKeyboardShown} = useKeyboardState(); const {isSmallScreenWidth} = useWindowDimensions(); + const {updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef} = useSuggestionsContext(); const getInitialDraft = () => { if (draftMessage === action?.message?.[0].html) { @@ -125,6 +127,7 @@ function ReportActionItemMessageEdit( const insertedEmojis = useRef([]); const draftRef = useRef(draft); const containerRef = useRef(null); + const suggestionsRef = useRef(null); useEffect(() => { if (ReportActionsUtils.isDeletedAction(action) || (action.message && draftMessage === action.message[0].html)) { @@ -257,7 +260,7 @@ function ReportActionItemMessageEdit( if (emojis?.length > 0) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (newEmojis?.length > 0) { - SuggestionsAction.resetSuggestions(); + suggestionsRef.current?.resetSuggestions(); insertedEmojis.current = [...insertedEmojis.current, ...newEmojis]; debouncedUpdateFrequentlyUsedEmojis(); } @@ -352,7 +355,7 @@ function ReportActionItemMessageEdit( */ const triggerSaveOrCancel = useCallback( (e: NativeSyntheticEvent | KeyboardEvent) => { - if (SuggestionsAction.triggerHotkeyActions(e)) { + if (suggestionsRef.current?.triggerHotkeyActions(e as KeyboardEvent)) { return; } @@ -379,7 +382,7 @@ function ReportActionItemMessageEdit( }, [textInputRef]); const measureContainer = useCallback( - (callback: () => void) => { + (callback: MeasureInWindowOnSuccessCallback) => { if (!containerRef.current) { return; } @@ -465,11 +468,15 @@ function ReportActionItemMessageEdit( if (!ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { ReportActionContextMenu.clearActiveReportAction(); } + + updateCurrentActiveSuggestionsRef(suggestionsRef.current, action.reportActionID); }} onBlur={(event: NativeSyntheticEvent) => { setIsFocused(false); // @ts-expect-error TODO: TextInputFocusEventData doesn't contain relatedTarget. const relatedTargetId = event.nativeEvent?.relatedTarget?.id; + suggestionsRef.current?.resetSuggestions(); + clearActiveSuggestionsRef(); if (relatedTargetId && [messageEditInput, emojiButtonID].includes(relatedTargetId)) { return; } @@ -477,14 +484,14 @@ function ReportActionItemMessageEdit( }} selection={selection} onSelectionChange={(e) => { - SuggestionsAction.onSelectionChange(e); + suggestionsRef.current?.onSelectionChange?.(e); setSelection(e.nativeEvent.selection); }} setValue={setDraft} setSelection={setSelection} isComposerFocused={!!textInputRef.current && textInputRef.current.isFocused()} resetKeyboardInput={resetKeyboardInput} - suggestionsRef={SuggestionsAction.suggestionsRef} + suggestionsRef={suggestionsRef} updateDraft={updateDraft} measureParentContainer={measureContainer} /> diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 5a167dbb376f..a7584a3570dd 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -11,7 +11,6 @@ import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withW import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useInitialValue from '@hooks/useInitialValue'; import usePrevious from '@hooks/usePrevious'; -import * as SuggestionsAction from '@libs/actions/SuggestionsActions'; import compose from '@libs/compose'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; import Performance from '@libs/Performance'; @@ -25,6 +24,7 @@ import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import PopoverReactionList from './ReactionList/PopoverReactionList'; +import {useSuggestionsContext} from './ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; import reportActionPropTypes from './reportActionPropTypes'; import ReportActionsList from './ReportActionsList'; @@ -88,6 +88,7 @@ const defaultProps = { function ReportActionsView(props) { useCopySelectionHelper(); const reactionListRef = useContext(ReactionListContext); + const {currentActiveSuggestionsRef} = useSuggestionsContext(); const didLayout = useRef(false); const didSubscribeToReportTypingEvents = useRef(false); const isFirstRender = useRef(true); @@ -261,7 +262,12 @@ function ReportActionsView(props) { report={props.report} parentReportAction={props.parentReportAction} onLayout={recordTimeToMeasureItemLayout} - onScroll={SuggestionsAction.updateShouldShowSuggestionMenuToFalse} + onScroll={() => { + if (!currentActiveSuggestionsRef.current) { + return; + } + currentActiveSuggestionsRef.current.updateShouldShowSuggestionMenuToFalse(); + }} sortedReportActions={props.reportActions} mostRecentIOUReportActionID={mostRecentIOUReportActionID} loadOlderChats={loadOlderChats} From e0673e0baf75e6610c39c234cd758e8958b6157c Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 14:50:54 +0500 Subject: [PATCH 0053/1096] refactor: rename to useReportIDs --- src/App.tsx | 4 +- ...edReportListItems.tsx => useReportIDs.tsx} | 66 +++++++++++-------- src/pages/home/sidebar/SidebarLinksData.js | 4 +- tests/utils/LHNTestUtils.tsx | 6 +- 4 files changed, 46 insertions(+), 34 deletions(-) rename src/hooks/{useOrderedReportListItems.tsx => useReportIDs.tsx} (72%) diff --git a/src/App.tsx b/src/App.tsx index 3a294757e149..6cfc2587074b 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -29,7 +29,7 @@ import {KeyboardStateProvider} from './components/withKeyboardState'; import {WindowDimensionsProvider} from './components/withWindowDimensions'; import Expensify from './Expensify'; import useDefaultDragAndDrop from './hooks/useDefaultDragAndDrop'; -import {OrderedReportListItemsContextProvider} from './hooks/useOrderedReportListItems'; +import {ReportIDsContextProvider} from './hooks/useReportIDs'; import OnyxUpdateManager from './libs/actions/OnyxUpdateManager'; import InitialUrlContext from './libs/InitialUrlContext'; import {ReportAttachmentsProvider} from './pages/home/report/ReportAttachmentsContext'; @@ -76,7 +76,7 @@ function App({url}: AppProps) { CustomStatusBarAndBackgroundContextProvider, ActiveElementRoleProvider, ActiveWorkspaceContextProvider, - OrderedReportListItemsContextProvider, + ReportIDsContextProvider, PlaybackContextProvider, VolumeContextProvider, VideoPopoverMenuContextProvider, diff --git a/src/hooks/useOrderedReportListItems.tsx b/src/hooks/useReportIDs.tsx similarity index 72% rename from src/hooks/useOrderedReportListItems.tsx rename to src/hooks/useReportIDs.tsx index 4580df82ed34..651e21f08b24 100644 --- a/src/hooks/useOrderedReportListItems.tsx +++ b/src/hooks/useReportIDs.tsx @@ -1,7 +1,7 @@ +import _ from 'lodash'; import React, {createContext, useCallback, useContext, useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import _ from 'underscore'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; @@ -9,6 +9,7 @@ import SidebarUtils from '@libs/SidebarUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportActions, Transaction, TransactionViolation} from '@src/types/onyx'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; import usePermissions from './usePermissions'; @@ -24,14 +25,22 @@ type OnyxProps = { allTransactions: OnyxCollection; }; -type WithOrderedReportListItemsContextProviderProps = OnyxProps & { +type WithReportIDsContextProviderProps = OnyxProps & { children: React.ReactNode; currentReportIDForTests?: string; }; -const OrderedReportListItemsContext = createContext({}); +type ReportIDsContextValue = { + orderedReportIDs: string[]; + reportIDsWithErrors: Record; +}; + +const ReportIDsContext = createContext({ + orderedReportIDs: [], + reportIDsWithErrors: {}, +}); -function WithOrderedReportListItemsContextProvider({ +function WithReportIDsContextProvider({ children, chatReports, betas, @@ -51,7 +60,7 @@ function WithOrderedReportListItemsContextProvider({ * only in testing environment. */ currentReportIDForTests, -}: WithOrderedReportListItemsContextProviderProps) { +}: WithReportIDsContextProviderProps) { const currentReportIDValue = useCurrentReportID(); const derivedCurrentReportID = currentReportIDForTests ?? currentReportIDValue?.currentReportID; const {activeWorkspaceID} = useActiveWorkspace(); @@ -59,22 +68,25 @@ function WithOrderedReportListItemsContextProvider({ const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); - const chatReportsKeys = useMemo(() => _.keys(chatReports), [chatReports]); - const reportIDsWithErrors = useMemo(() => { - return _.reduce( - chatReportsKeys, - (errorsMap, reportKey) => { - const report = chatReports && chatReports[reportKey]; - const allReportsActions = allReportActions && allReportActions[reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.REPORT_ACTIONS)]; - const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions, allTransactions) || {}; - if (_.size(errors) === 0) { - return errorsMap; - } - return {...errorsMap, [reportKey.replace(ONYXKEYS.COLLECTION.REPORT, '')]: errors}; - }, - {}, - ); - }, [chatReportsKeys, allReportActions, allTransactions, chatReports]); + const chatReportsKeys = useMemo(() => Object.keys(chatReports ?? {}), [chatReports]); + // eslint-disable-next-line you-dont-need-lodash-underscore/reduce + const reportIDsWithErrors = useMemo( + () => + _.reduce( + chatReportsKeys, + (errorsMap, reportKey) => { + const report = chatReports?.[reportKey] ?? null; + const allReportsActions = allReportActions?.[reportKey.replace(ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.COLLECTION.REPORT_ACTIONS)] ?? null; + const errors = OptionsListUtils.getAllReportErrors(report, allReportsActions, allTransactions) || {}; + if (Object.values(errors).length === 0) { + return errorsMap; + } + return {...errorsMap, [reportKey.replace(ONYXKEYS.COLLECTION.REPORT, '')]: errors}; + }, + {}, + ), + [chatReportsKeys, allReportActions, allTransactions, chatReports], + ); const getOrderedReportIDs = useCallback( (currentReportID?: string) => @@ -116,10 +128,10 @@ function WithOrderedReportListItemsContextProvider({ [orderedReportIDsWithCurrentReport, reportIDsWithErrors], ); - return {children}; + return {children}; } -const OrderedReportListItemsContextProvider = withOnyx({ +const ReportIDsContextProvider = withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, initialValue: {}, @@ -151,10 +163,10 @@ const OrderedReportListItemsContextProvider = withOnyx { if (deepEqual(orderedReportIDsRef.current, orderedReportIDs)) { diff --git a/tests/utils/LHNTestUtils.tsx b/tests/utils/LHNTestUtils.tsx index c8c3f145d951..04344ba71184 100644 --- a/tests/utils/LHNTestUtils.tsx +++ b/tests/utils/LHNTestUtils.tsx @@ -10,7 +10,7 @@ import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OnyxProvider from '@components/OnyxProvider'; import {CurrentReportIDContextProvider} from '@components/withCurrentReportID'; import {EnvironmentProvider} from '@components/withEnvironment'; -import {OrderedReportListItemsContextProvider} from '@hooks/useOrderedReportListItems'; +import {ReportIDsContextProvider} from '@hooks/useReportIDs'; import DateUtils from '@libs/DateUtils'; import ReportActionItemSingle from '@pages/home/report/ReportActionItemSingle'; import SidebarLinksData from '@pages/home/sidebar/SidebarLinksData'; @@ -291,7 +291,7 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { * So this is a work around to have currentReportID available * only in testing environment. * */} - + {}} @@ -304,7 +304,7 @@ function MockedSidebarLinks({currentReportID = ''}: MockedSidebarLinksProps) { isSmallScreenWidth={false} currentReportID={currentReportID} /> - + ); } From 30ca3030ee04390772e1a03bc69305fbf4160600 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 14:51:43 +0500 Subject: [PATCH 0054/1096] refactor: don't set currentReportID if it's on workspaces screen --- src/components/withCurrentReportID.tsx | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index 0d052f759f81..54cdc84f127a 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -4,6 +4,7 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {createContext, forwardRef, useCallback, useMemo, useState} from 'react'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import Navigation from '@libs/Navigation/Navigation'; +import SCREENS from '@src/SCREENS'; type CurrentReportIDContextValue = { updateCurrentReportID: (state: NavigationState) => void; @@ -40,14 +41,17 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro const updateCurrentReportID = useCallback( (state: NavigationState) => { const reportID = Navigation.getTopmostReportId(state) ?? ''; + /** * This is to make sure we don't set the undefined as reportID when * switching between chat list and settings->workspaces tab. - * and doing so avoid unnecessary re-render of `useOrderedReportListItems`. + * and doing so avoid unnecessary re-render of `useReportIDs`. */ - if (reportID !== undefined) { - setCurrentReportID(reportID); + const params = state.routes[state.index].params; + if (params && 'screen' in params && params.screen === SCREENS.SETTINGS.WORKSPACES) { + return; } + setCurrentReportID(reportID); }, [setCurrentReportID], ); From 121b5378606b353ecc014df082c140dfe5737d4f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 14:51:53 +0500 Subject: [PATCH 0055/1096] refactor: remove dead code --- tests/perf-test/SidebarUtils.perf-test.ts | 8 +------ tests/utils/collections/createCollection.ts | 23 --------------------- 2 files changed, 1 insertion(+), 30 deletions(-) diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 8672d7c0cafe..2b2bdbc6b57a 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -10,7 +10,7 @@ import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import createCollection, {createNestedCollection} from '../utils/collections/createCollection'; +import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReportAction from '../utils/collections/reportActions'; @@ -29,12 +29,6 @@ const reportActions = createCollection( (index) => createRandomReportAction(index), ); -// const allReportActions = createNestedCollection( -// (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, -// (item) => `${item.reportActionID}`, -// (index) => createRandomReportAction(index), -// ); - const personalDetails = createCollection( (item) => item.accountID, (index) => createPersonalDetails(index), diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index 4a2a1fa4eb78..848ef8f81f47 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -9,26 +9,3 @@ export default function createCollection(createKey: (item: T, index: number) return map; } - -function createNestedCollection( - createParentKey: (item: T, index: number) => string | number, - createKey: (item: T, index: number) => string | number, - createItem: (index: number) => T, - length = 500, -): Record> { - const map: Record> = {}; - - for (let i = 0; i < length; i++) { - const item = createItem(i); - const itemKey = createKey(item, i); - const itemParentKey = createParentKey(item, i); - map[itemParentKey] = { - ...map[itemParentKey], - [itemKey]: item, - }; - } - - return map; -} - -export {createNestedCollection}; From 9e41603f9da5a3eb38873a8ab24476c2eadacdaf Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 15:13:57 +0500 Subject: [PATCH 0056/1096] fix: linting --- src/hooks/useReportIDs.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index 651e21f08b24..2e7c63182646 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -67,11 +67,10 @@ function WithReportIDsContextProvider({ const {canUseViolations} = usePermissions(); const policyMemberAccountIDs = useMemo(() => getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, getCurrentUserAccountID()), [activeWorkspaceID, policyMembers]); - const chatReportsKeys = useMemo(() => Object.keys(chatReports ?? {}), [chatReports]); - // eslint-disable-next-line you-dont-need-lodash-underscore/reduce const reportIDsWithErrors = useMemo( () => + // eslint-disable-next-line you-dont-need-lodash-underscore/reduce _.reduce( chatReportsKeys, (errorsMap, reportKey) => { From 4a3284161b02ea616cc40a1bc741221672e6e777 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Tue, 5 Mar 2024 15:17:33 +0500 Subject: [PATCH 0057/1096] refactor: remove irrelevant changes --- src/components/LHNOptionsList/LHNOptionsList.tsx | 4 ++-- src/components/LHNOptionsList/types.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 31af022a12aa..be8ce677b641 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -20,15 +20,15 @@ function LHNOptionsList({ contentContainerStyles, data, onSelectRow, - shouldDisableFocusOptions = false, - currentReportID = '', optionMode, + shouldDisableFocusOptions = false, reports = {}, reportActions = {}, policy = {}, preferredLocale = CONST.LOCALES.DEFAULT, personalDetails = {}, transactions = {}, + currentReportID = '', draftComments = {}, transactionViolations = {}, onFirstItemRendered = () => {}, diff --git a/src/components/LHNOptionsList/types.ts b/src/components/LHNOptionsList/types.ts index 0d1bda775255..c122ab018392 100644 --- a/src/components/LHNOptionsList/types.ts +++ b/src/components/LHNOptionsList/types.ts @@ -139,7 +139,7 @@ type OptionRowLHNProps = { style?: StyleProp; /** The item that should be rendered */ - optionItem: OptionData | undefined; + optionItem?: OptionData; onLayout?: (event: LayoutChangeEvent) => void; }; From 581d09d7a3dec1f1c301c7dccfba4877e60cb7c4 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Mar 2024 11:33:46 +0500 Subject: [PATCH 0058/1096] fix: comments --- src/components/withCurrentReportID.tsx | 2 +- src/hooks/useReportIDs.tsx | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index 54cdc84f127a..bb3283a21d25 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -45,7 +45,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro /** * This is to make sure we don't set the undefined as reportID when * switching between chat list and settings->workspaces tab. - * and doing so avoid unnecessary re-render of `useReportIDs`. + * and doing so avoids an unnecessary re-render of `useReportIDs`. */ const params = state.routes[state.index].params; if (params && 'screen' in params && params.screen === SCREENS.SETTINGS.WORKSPACES) { diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index 2e7c63182646..547975ae1cd0 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -56,8 +56,7 @@ function WithReportIDsContextProvider({ * to SidebarLinksData, so this context doesn't have an * access to currentReportID in that case. * - * So this is a work around to have currentReportID available - * only in testing environment. + * This is a workaround to have currentReportID available in testing environment. */ currentReportIDForTests, }: WithReportIDsContextProviderProps) { @@ -109,7 +108,7 @@ function WithReportIDsContextProvider({ // We need to make sure the current report is in the list of reports, but we do not want // to have to re-generate the list every time the currentReportID changes. To do that - // we first generate the list as if there was no current report, then here we check if + // we first generate the list as if there was no current report, then we check if // the current report is missing from the list, which should very rarely happen. In this // case we re-generate the list a 2nd time with the current report included. const orderedReportIDsWithCurrentReport = useMemo(() => { From fffe41ac7ed346c542da74cbe7952e581be0eb1c Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Mar 2024 11:58:40 +0500 Subject: [PATCH 0059/1096] fix: safely check the nested properties --- src/components/withCurrentReportID.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index bb3283a21d25..22f68de9f57a 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -47,7 +47,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro * switching between chat list and settings->workspaces tab. * and doing so avoids an unnecessary re-render of `useReportIDs`. */ - const params = state.routes[state.index].params; + const params = state?.routes?.[state.index]?.params; if (params && 'screen' in params && params.screen === SCREENS.SETTINGS.WORKSPACES) { return; } From 6b41bf7819af297e0c3b7b32ced187b119c68193 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Mon, 11 Mar 2024 12:11:17 +0500 Subject: [PATCH 0060/1096] feat: add comments for extraData prop --- src/components/LHNOptionsList/LHNOptionsList.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index be8ce677b641..d141a5bbb3f4 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -129,6 +129,12 @@ function LHNOptionsList({ keyExtractor={keyExtractor} renderItem={renderItem} estimatedItemSize={optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight} + // Previously, we were passing `extraData={[currentReportID]}`, which upon every render, was causing the + // re-render because of the new array reference. FlashList's children actually don't depend on the + // `currentReportID` prop but they depend on the `reportActions`, `reports`, `policy`, `personalDetails`. + // Previously it was working for us because of the new array reference. Even if you only pass an empty + // array, it will still work because of the new reference. But it's better to pass the actual dependencies + // to avoid unnecessary re-renders. extraData={extraData} showsVerticalScrollIndicator={false} /> From ef7b30e6a95bc95ed2ead63f27e25636398f7c6b Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 13 Mar 2024 10:19:52 +0100 Subject: [PATCH 0061/1096] refactor: use canUseP2PDistanceRequests from usePermissions --- .../MoneyTemporaryForRefactorRequestConfirmationList.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 182aa5464772..7333d405363b 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -664,10 +664,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ ); }, [isReadOnly, iouType, selectedParticipants.length, confirm, bankAccountRoute, iouCurrencyCode, policyID, splitOrRequestOptions, formError, styles.ph1, styles.mb2]); - // TODO: change for a value from usePermissions [will be added in this PR https://github.com/Expensify/App/pull/37185] - // change for true for development - const canUseP2PDistanceRequests = false; - // An intermediate structure that helps us classify the fields as "primary" and "supplementary". // The primary fields are always shown to the user, while an extra action is needed to reveal the supplementary ones. const classifiedFields = [ From 64127110f19b7df5f74d3a6cb207303fd190cd4d Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 13 Mar 2024 16:44:48 +0500 Subject: [PATCH 0062/1096] fix: reassure tests --- tests/perf-test/SidebarUtils.perf-test.ts | 25 +++++---------------- tests/utils/collections/createCollection.ts | 25 +++++++++++++++++++++ 2 files changed, 31 insertions(+), 19 deletions(-) diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 8566abb97c7f..a30c298f7471 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -9,7 +9,7 @@ import type {PersonalDetails, TransactionViolation} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; -import createCollection from '../utils/collections/createCollection'; +import createCollection, {createNestedCollection} from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReportAction, {getRandomDate} from '../utils/collections/reportActions'; @@ -51,24 +51,11 @@ const policies = createCollection( const mockedBetas = Object.values(CONST.BETAS); -const allReportActions = Object.fromEntries( - Object.keys(reportActions).map((key) => [ - `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${key}`, - [ - { - errors: reportActions[key].errors ?? [], - message: [ - { - moderationDecision: { - decision: reportActions[key].message?.[0]?.moderationDecision?.decision, - }, - }, - ], - reportActionID: reportActions[key].reportActionID, - }, - ], - ]), -) as unknown as OnyxCollection; +const allReportActions = createNestedCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (item) => `${item.reportActionID}`, + (index) => createRandomReportAction(index), +); const currentReportId = '1'; const transactionViolations = {} as OnyxCollection; diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index 848ef8f81f47..ddb0b68742a4 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -9,3 +9,28 @@ export default function createCollection(createKey: (item: T, index: number) return map; } + +function createNestedCollection( + createParentKey: (item: T, index: number) => string | number, + createKey: (item: T, index: number) => string | number, + createItem: (index: number) => T, + length = 500, +): Record> { + const map: Record> = {}; + + for (let i = 0; i < length; i++) { + const item = createItem(i); + const itemKey = createKey(item, i); + const itemParentKey = createParentKey(item, i); + map[itemParentKey] = { + ...map[itemParentKey], + [itemKey]: item, + }; + } + + return map; +} + +export { + createNestedCollection, +}; \ No newline at end of file From e3477b2e751fe03702029de13aa8cee9f0ef68c6 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Wed, 13 Mar 2024 16:51:56 +0500 Subject: [PATCH 0063/1096] fix: apply prettier --- tests/utils/collections/createCollection.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index ddb0b68742a4..4a2a1fa4eb78 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -31,6 +31,4 @@ function createNestedCollection( return map; } -export { - createNestedCollection, -}; \ No newline at end of file +export {createNestedCollection}; From 6f3afddef1d036658a48b38ca8f0caf6c696c85f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 13 Mar 2024 17:28:24 +0100 Subject: [PATCH 0064/1096] fix: remove mock rates --- src/CONST.ts | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 9b2502cb17d1..d4fbd0ff6ef3 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1431,13 +1431,6 @@ const CONST = { FAKE_P2P_ID: '_FAKE_P2P_ID_', }, - // TODO: remove this mock when https://github.com/Expensify/App/issues/36982 is done - CURRENCY_TO_DEFAULT_MILEAGE_RATE: { - USD: { unit: "mile", rate: 0.5 }, - EUR: { unit: "kilometer", rate: 0.8 }, - GBP: { unit: "mile", rate: 0.45 }, - }, - TERMS: { CFPB_PREPAID: 'cfpb.gov/prepaid', CFPB_COMPLAINT: 'cfpb.gov/complaint', From 50174f38629f23ef3363e51613755b05e30ec3d0 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 13 Mar 2024 17:29:24 +0100 Subject: [PATCH 0065/1096] feat: create function for getting personal policy --- src/libs/PolicyUtils.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/libs/PolicyUtils.ts b/src/libs/PolicyUtils.ts index 4689fd03ebd0..6a3829b10934 100644 --- a/src/libs/PolicyUtils.ts +++ b/src/libs/PolicyUtils.ts @@ -1,5 +1,6 @@ import Str from 'expensify-common/lib/str'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; +import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -12,6 +13,14 @@ import Navigation from './Navigation/Navigation'; type MemberEmailsToAccountIDs = Record; type UnitRate = {rate: number}; +let allPolicies: OnyxCollection; + +Onyx.connect({ + key: ONYXKEYS.COLLECTION.POLICY, + waitForCollectionCallback: true, + callback: (value) => (allPolicies = value), +}); + /** * Filter out the active policies, which will exclude policies with pending deletion * These are policies that we can use to create reports with in NewDot. @@ -265,6 +274,10 @@ function goBackFromInvalidPolicy() { Navigation.navigateWithSwitchPolicyID({route: ROUTES.ALL_SETTINGS}); } +function getPersonalPolicy() { + return Object.values(allPolicies ?? {}).find((policy) => policy?.type === CONST.POLICY.TYPE.PERSONAL); +} + export { getActivePolicies, hasAccountingConnections, @@ -295,6 +308,7 @@ export { getPathWithoutPolicyID, getPolicyMembersByIdWithoutCurrentUser, goBackFromInvalidPolicy, + getPersonalPolicy }; export type {MemberEmailsToAccountIDs}; From 30b319f5e5b6ec940e9101bf11fcb5bfc28eef77 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 14 Mar 2024 13:41:19 +0500 Subject: [PATCH 0066/1096] perf: check for the settings tab existence in screen params and early return --- src/components/withCurrentReportID.tsx | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index 22f68de9f57a..55b542ccacb7 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -4,7 +4,6 @@ import type {ComponentType, ForwardedRef, RefAttributes} from 'react'; import React, {createContext, forwardRef, useCallback, useMemo, useState} from 'react'; import getComponentDisplayName from '@libs/getComponentDisplayName'; import Navigation from '@libs/Navigation/Navigation'; -import SCREENS from '@src/SCREENS'; type CurrentReportIDContextValue = { updateCurrentReportID: (state: NavigationState) => void; @@ -44,11 +43,14 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro /** * This is to make sure we don't set the undefined as reportID when - * switching between chat list and settings->workspaces tab. - * and doing so avoids an unnecessary re-render of `useReportIDs`. + * switching between chat list and settings tab. The settings tab + * includes multiple screens and we don't want to set the reportID + * to falsy value when switching between them. + * + * Doing so avoids an unnecessary re-render of `useReportIDs`. */ const params = state?.routes?.[state.index]?.params; - if (params && 'screen' in params && params.screen === SCREENS.SETTINGS.WORKSPACES) { + if (params && 'screen' in params && typeof params.screen === 'string' && params.screen.indexOf('Settings_') !== -1) { return; } setCurrentReportID(reportID); From eb3aff8ee97f1f055d22376ea7004ba575ff1cb9 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 14 Mar 2024 19:15:31 +0100 Subject: [PATCH 0067/1096] feat: add rate field to MoneyRequestView --- .../ReportActionItem/MoneyRequestView.tsx | 68 +++++++++++++++---- src/libs/DistanceRequestUtils.ts | 17 ++--- 2 files changed, 63 insertions(+), 22 deletions(-) diff --git a/src/components/ReportActionItem/MoneyRequestView.tsx b/src/components/ReportActionItem/MoneyRequestView.tsx index 70c65d1d66ce..bcaa7f830925 100644 --- a/src/components/ReportActionItem/MoneyRequestView.tsx +++ b/src/components/ReportActionItem/MoneyRequestView.tsx @@ -21,6 +21,7 @@ import type {ViolationField} from '@hooks/useViolations'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as CardUtils from '@libs/CardUtils'; import * as CurrencyUtils from '@libs/CurrencyUtils'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as PolicyUtils from '@libs/PolicyUtils'; import * as ReceiptUtils from '@libs/ReceiptUtils'; @@ -89,8 +90,8 @@ function MoneyRequestView({ const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {isSmallScreenWidth} = useWindowDimensions(); - const {translate} = useLocalize(); - const {canUseViolations} = usePermissions(); + const {translate, toLocaleDigit} = useLocalize(); + const {canUseViolations, canUseP2PDistanceRequests} = usePermissions(); const parentReportAction = parentReportActions?.[report.parentReportActionID ?? ''] ?? null; const moneyRequestReport = parentReport; const { @@ -151,6 +152,20 @@ function MoneyRequestView({ let amountDescription = `${translate('iou.amount')}`; + const hasRoute = TransactionUtils.hasRoute(transaction); + const distance = transaction?.routes?.route0?.distance ?? 0; + const rateID = transaction?.comment.customUnit?.customUnitRateID ?? '0'; + + const rates = DistanceRequestUtils.getMileageRates(policy?.id); + const {unit, currency, rate} = rates[rateID as string] ?? { + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + currency: CONST.CURRENCY.USD, + rate: 0, + }; + + const rateToDisplay = DistanceRequestUtils.getRateForDisplay(hasRoute, unit, rate, currency, translate, toLocaleDigit); + const distanceToDisplay = DistanceRequestUtils.getDistanceForDisplay(hasRoute, distance, unit, rate, translate); + const saveBillable = useCallback( (newBillable: boolean) => { // If the value hasn't changed, don't request to save changes on the server and just close the modal @@ -236,6 +251,44 @@ function MoneyRequestView({ [transactionAmount, isSettled, isCancelled, isPolicyExpenseChat, isEmptyMerchant, transactionDate, hasErrors, canUseViolations, hasViolations, translate, getViolationsForField], ); + const distanceRequestFields = canUseP2PDistanceRequests ? ( + <> + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + /> + + + {}} + /> + + + ) : ( + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + /> + + ) + return ( @@ -320,16 +373,7 @@ function MoneyRequestView({ /> {isDistanceRequest ? ( - - Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} - /> - + distanceRequestFields ) : ( { - const mileageRates = {}; + const mileageRates: Record = {}; if (!policyID) { return mileageRates; @@ -202,7 +201,6 @@ function getMileageRates(policyID?: string): Record } Object.entries(distanceUnit.rates).forEach(([rateID, rate]) => { - // TODO: fix TS error mileageRates[rateID] = { rate: rate.rate, currency: rate.currency, @@ -215,8 +213,7 @@ function getMileageRates(policyID?: string): Record return mileageRates; } -// TODO: probably will need to be changed -function getRateForP2P(currency) { +function getRateForP2P(currency: string) { return CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE[currency] ?? CONST.CURRENCY_TO_DEFAULT_MILEAGE_RATE.USD; } From 8dba89bbcad90b6e13b9f55732376d8fe24fee4a Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 15 Mar 2024 13:29:59 +0500 Subject: [PATCH 0068/1096] feat: add selector from SidebarLinksData --- src/hooks/useReportIDs.tsx | 114 +++++++++++++++++++++++++++++++++---- 1 file changed, 103 insertions(+), 11 deletions(-) diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index 918a459e300a..f9b59be8e14c 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -3,21 +3,37 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; +import type {PolicySelector} from '@pages/home/sidebar/SidebarLinksData'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportActions, TransactionViolation} from '@src/types/onyx'; +import type * as OnyxTypes from '@src/types/onyx'; +import type {Message} from '@src/types/onyx/ReportAction'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; type OnyxProps = { - chatReports: OnyxCollection; - betas: OnyxEntry; - policies: OnyxCollection; - allReportActions: OnyxCollection; - transactionViolations: OnyxCollection; - policyMembers: OnyxCollection; - priorityMode: PriorityMode; + /** List of reports */ + chatReports: OnyxCollection; + + /** Beta features list */ + betas: OnyxEntry; + + /** The policies which the user has access to */ + policies: OnyxCollection; + + /** All report actions for all reports */ + allReportActions: OnyxCollection; + + /** All of the transaction violations */ + transactionViolations: OnyxCollection; + + /** All policy members */ + policyMembers: OnyxCollection; + + /** The chat priority mode */ + priorityMode: OnyxTypes.PriorityMode; }; type WithReportIDsContextProviderProps = OnyxProps & { @@ -63,10 +79,10 @@ function WithReportIDsContextProvider({ SidebarUtils.getOrderedReportIDs( currentReportID ?? null, chatReports, - betas ?? [], - policies, + betas, + policies as OnyxCollection, priorityMode, - allReportActions, + allReportActions as OnyxCollection, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -98,10 +114,84 @@ function WithReportIDsContextProvider({ return {children}; } +type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; +type ReportActionsSelector = Array>; + +/** + * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering + * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. + */ +const chatReportSelector = (report: OnyxEntry): ChatReportSelector => + (report && { + reportID: report.reportID, + participantAccountIDs: report.participantAccountIDs, + hasDraft: report.hasDraft, + isPinned: report.isPinned, + isHidden: report.isHidden, + notificationPreference: report.notificationPreference, + errorFields: { + addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, + }, + lastMessageText: report.lastMessageText, + lastVisibleActionCreated: report.lastVisibleActionCreated, + iouReportID: report.iouReportID, + total: report.total, + nonReimbursableTotal: report.nonReimbursableTotal, + hasOutstandingChildRequest: report.hasOutstandingChildRequest, + isWaitingOnBankAccount: report.isWaitingOnBankAccount, + statusNum: report.statusNum, + stateNum: report.stateNum, + chatType: report.chatType, + type: report.type, + policyID: report.policyID, + visibility: report.visibility, + lastReadTime: report.lastReadTime, + // Needed for name sorting: + reportName: report.reportName, + policyName: report.policyName, + oldPolicyName: report.oldPolicyName, + // Other less obvious properites considered for sorting: + ownerAccountID: report.ownerAccountID, + currency: report.currency, + managerID: report.managerID, + // Other important less obivous properties for filtering: + parentReportActionID: report.parentReportActionID, + parentReportID: report.parentReportID, + isDeletedParentAction: report.isDeletedParentAction, + isUnreadWithMention: ReportUtils.isUnreadWithMention(report), + }) as ChatReportSelector; + +const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => + (reportActions && + Object.values(reportActions).map((reportAction) => { + const {reportActionID, actionName, errors = [], originalMessage} = reportAction; + const decision = reportAction.message?.[0].moderationDecision?.decision; + + return { + reportActionID, + actionName, + errors, + message: [ + { + moderationDecision: {decision}, + }, + ] as Message[], + originalMessage, + }; + })) as ReportActionsSelector; + +const policySelector = (policy: OnyxEntry): PolicySelector => + (policy && { + type: policy.type, + name: policy.name, + avatar: policy.avatar, + }) as PolicySelector; + const ReportIDsContextProvider = withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, initialValue: {}, + selector: chatReportSelector, }, priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, @@ -114,10 +204,12 @@ const ReportIDsContextProvider = withOnyx Date: Fri, 15 Mar 2024 16:39:49 +0700 Subject: [PATCH 0069/1096] fix the suggestion padding for edit composer --- src/CONST.ts | 1 + .../BaseAutoCompleteSuggestions.tsx | 4 +++- .../ComposerWithSuggestionsEdit/SuggestionsContext.tsx | 10 ++++++++-- src/pages/home/report/ReportActionItemMessageEdit.tsx | 5 ++++- src/styles/utils/index.ts | 5 +++-- 5 files changed, 19 insertions(+), 6 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index fa44cda20720..68fb72541845 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1065,6 +1065,7 @@ const CONST = { EMOJI_PICKER_HEADER_HEIGHT: 32, RECIPIENT_LOCAL_TIME_HEIGHT: 25, AUTO_COMPLETE_SUGGESTER: { + EDIT_SUGGESTER_PADDING: 8, SUGGESTER_PADDING: 6, SUGGESTER_INNER_PADDING: 8, SUGGESTION_ROW_HEIGHT: 40, diff --git a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx index 6c65afa9c2a8..a11fb2a01cc8 100644 --- a/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx +++ b/src/components/AutoCompleteSuggestions/BaseAutoCompleteSuggestions.tsx @@ -10,6 +10,7 @@ import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; +import {useSuggestionsContext} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import viewForwardedRef from '@src/types/utils/viewForwardedRef'; @@ -48,6 +49,7 @@ function BaseAutoCompleteSuggestions( const StyleUtils = useStyleUtils(); const rowHeight = useSharedValue(0); const scrollRef = useRef>(null); + const {activeID} = useSuggestionsContext(); /** * Render a suggestion menu item component. */ @@ -69,7 +71,7 @@ function BaseAutoCompleteSuggestions( ); const innerHeight = CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length; - const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value, shouldBeDisplayedBelowParentContainer)); + const animatedStyles = useAnimatedStyle(() => StyleUtils.getAutoCompleteSuggestionContainerStyle(rowHeight.value, shouldBeDisplayedBelowParentContainer, Boolean(activeID))); const estimatedListSize = useMemo( () => ({ height: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT * suggestions.length, diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx index 9118e80ef230..36ed962010ab 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx @@ -7,15 +7,19 @@ type SuggestionsContextProviderProps = { }; type SuggestionsContextProps = { + activeID: string | null; currentActiveSuggestionsRef: MutableRefObject; updateCurrentActiveSuggestionsRef: (ref: SuggestionsRef | null, id: string) => void; clearActiveSuggestionsRef: () => void; + isActiveSuggestions: (id: string) => boolean; }; const SuggestionsContext = createContext({ + activeID: null, currentActiveSuggestionsRef: {current: null}, updateCurrentActiveSuggestionsRef: () => {}, clearActiveSuggestionsRef: () => {}, + isActiveSuggestions: () => false, }); function SuggestionsContextProvider({children}: SuggestionsContextProviderProps) { @@ -32,9 +36,11 @@ function SuggestionsContextProvider({children}: SuggestionsContextProviderProps) setActiveID(null); }, []); + const isActiveSuggestions = useCallback((id: string) => id === activeID, [activeID]); + const contextValue = useMemo( - () => ({activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef}), - [activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef], + () => ({activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef, isActiveSuggestions}), + [activeID, currentActiveSuggestionsRef, updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef, isActiveSuggestions], ); return {children}; diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 858f33a5a8dc..294f7e83d66e 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -83,7 +83,7 @@ function ReportActionItemMessageEdit( const {translate, preferredLocale} = useLocalize(); const {isKeyboardShown} = useKeyboardState(); const {isSmallScreenWidth} = useWindowDimensions(); - const {updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef} = useSuggestionsContext(); + const {updateCurrentActiveSuggestionsRef, clearActiveSuggestionsRef, isActiveSuggestions} = useSuggestionsContext(); const getInitialDraft = () => { if (draftMessage === action?.message?.[0].html) { @@ -212,6 +212,9 @@ function ReportActionItemMessageEdit( if (ReportActionContextMenu.isActiveReportAction(action.reportActionID)) { ReportActionContextMenu.clearActiveReportAction(); } + if (isActiveSuggestions(action.reportActionID)) { + clearActiveSuggestionsRef(); + } // Show the main composer when the focused message is deleted from another client // to prevent the main composer stays hidden until we swtich to another chat. diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 2a05278dec6c..1fde098c4aeb 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -844,17 +844,18 @@ const shouldPreventScroll = shouldPreventScrollOnAutoCompleteSuggestion(); /** * Gets the correct position for auto complete suggestion container */ -function getAutoCompleteSuggestionContainerStyle(itemsHeight: number, shouldBeDisplayedBelowParentContainer: boolean): ViewStyle { +function getAutoCompleteSuggestionContainerStyle(itemsHeight: number, shouldBeDisplayedBelowParentContainer: boolean, isEditComposer: boolean): ViewStyle { 'worklet'; const borderWidth = 2; const height = itemsHeight + 2 * CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_INNER_PADDING + (shouldPreventScroll ? borderWidth : 0); + const suggestionsPadding = isEditComposer ? CONST.AUTO_COMPLETE_SUGGESTER.EDIT_SUGGESTER_PADDING : CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING; // The suggester is positioned absolutely within the component that includes the input and RecipientLocalTime view (for non-expanded mode only). To position it correctly, // we need to shift it by the suggester's height plus its padding and, if applicable, the height of the RecipientLocalTime view. return { overflow: 'hidden', - top: -(height + (shouldBeDisplayedBelowParentContainer ? -2 : 1) * (CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTER_PADDING + (shouldPreventScroll ? 0 : borderWidth))), + top: -(height + (shouldBeDisplayedBelowParentContainer ? -2 : 1) * (suggestionsPadding + (shouldPreventScroll ? 0 : borderWidth))), height, minHeight: CONST.AUTO_COMPLETE_SUGGESTER.SUGGESTION_ROW_HEIGHT, }; From f333232781b6b64566801dc1958c05284c90081d Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 15 Mar 2024 18:40:10 +0500 Subject: [PATCH 0070/1096] fix: reassure tests --- tests/perf-test/SidebarUtils.perf-test.ts | 26 +++++---------------- tests/utils/collections/createCollection.ts | 23 ++++++++++++++++++ 2 files changed, 29 insertions(+), 20 deletions(-) diff --git a/tests/perf-test/SidebarUtils.perf-test.ts b/tests/perf-test/SidebarUtils.perf-test.ts index 8566abb97c7f..4bd04e823f63 100644 --- a/tests/perf-test/SidebarUtils.perf-test.ts +++ b/tests/perf-test/SidebarUtils.perf-test.ts @@ -9,7 +9,7 @@ import type {PersonalDetails, TransactionViolation} from '@src/types/onyx'; import type Policy from '@src/types/onyx/Policy'; import type Report from '@src/types/onyx/Report'; import type ReportAction from '@src/types/onyx/ReportAction'; -import createCollection from '../utils/collections/createCollection'; +import createCollection, {createNestedCollection} from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomPolicy from '../utils/collections/policies'; import createRandomReportAction, {getRandomDate} from '../utils/collections/reportActions'; @@ -51,25 +51,11 @@ const policies = createCollection( const mockedBetas = Object.values(CONST.BETAS); -const allReportActions = Object.fromEntries( - Object.keys(reportActions).map((key) => [ - `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${key}`, - [ - { - errors: reportActions[key].errors ?? [], - message: [ - { - moderationDecision: { - decision: reportActions[key].message?.[0]?.moderationDecision?.decision, - }, - }, - ], - reportActionID: reportActions[key].reportActionID, - }, - ], - ]), -) as unknown as OnyxCollection; - +const allReportActions = createNestedCollection( + (item) => `${ONYXKEYS.COLLECTION.REPORT}${item.reportID}`, + (item) => `${item.reportActionID}`, + (index) => createRandomReportAction(index), +); const currentReportId = '1'; const transactionViolations = {} as OnyxCollection; diff --git a/tests/utils/collections/createCollection.ts b/tests/utils/collections/createCollection.ts index 848ef8f81f47..4a2a1fa4eb78 100644 --- a/tests/utils/collections/createCollection.ts +++ b/tests/utils/collections/createCollection.ts @@ -9,3 +9,26 @@ export default function createCollection(createKey: (item: T, index: number) return map; } + +function createNestedCollection( + createParentKey: (item: T, index: number) => string | number, + createKey: (item: T, index: number) => string | number, + createItem: (index: number) => T, + length = 500, +): Record> { + const map: Record> = {}; + + for (let i = 0; i < length; i++) { + const item = createItem(i); + const itemKey = createKey(item, i); + const itemParentKey = createParentKey(item, i); + map[itemParentKey] = { + ...map[itemParentKey], + [itemKey]: item, + }; + } + + return map; +} + +export {createNestedCollection}; From 3557027d7ccadd1dfa89cb3d2042e1c5ae927523 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Fri, 15 Mar 2024 18:40:36 +0500 Subject: [PATCH 0071/1096] fix: resolve merge conflicts --- src/hooks/useReportIDs.tsx | 114 ++++--------------------------------- 1 file changed, 11 insertions(+), 103 deletions(-) diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index f9b59be8e14c..422febfca140 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -3,37 +3,21 @@ import {withOnyx} from 'react-native-onyx'; import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {getCurrentUserAccountID} from '@libs/actions/Report'; import {getPolicyMembersByIdWithoutCurrentUser} from '@libs/PolicyUtils'; -import * as ReportUtils from '@libs/ReportUtils'; import SidebarUtils from '@libs/SidebarUtils'; -import type {PolicySelector} from '@pages/home/sidebar/SidebarLinksData'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type * as OnyxTypes from '@src/types/onyx'; -import type {Message} from '@src/types/onyx/ReportAction'; +import type {Beta, Policy, PolicyMembers, PriorityMode, Report, ReportActions, TransactionViolation} from '@src/types/onyx'; import useActiveWorkspace from './useActiveWorkspace'; import useCurrentReportID from './useCurrentReportID'; type OnyxProps = { - /** List of reports */ - chatReports: OnyxCollection; - - /** Beta features list */ - betas: OnyxEntry; - - /** The policies which the user has access to */ - policies: OnyxCollection; - - /** All report actions for all reports */ - allReportActions: OnyxCollection; - - /** All of the transaction violations */ - transactionViolations: OnyxCollection; - - /** All policy members */ - policyMembers: OnyxCollection; - - /** The chat priority mode */ - priorityMode: OnyxTypes.PriorityMode; + chatReports: OnyxCollection; + betas: OnyxEntry; + policies: OnyxCollection; + allReportActions: OnyxCollection; + transactionViolations: OnyxCollection; + policyMembers: OnyxCollection; + priorityMode: OnyxEntry; }; type WithReportIDsContextProviderProps = OnyxProps & { @@ -79,10 +63,10 @@ function WithReportIDsContextProvider({ SidebarUtils.getOrderedReportIDs( currentReportID ?? null, chatReports, - betas, - policies as OnyxCollection, + betas ?? [], + policies, priorityMode, - allReportActions as OnyxCollection, + allReportActions, transactionViolations, activeWorkspaceID, policyMemberAccountIDs, @@ -114,84 +98,10 @@ function WithReportIDsContextProvider({ return {children}; } -type ChatReportSelector = OnyxTypes.Report & {isUnreadWithMention: boolean}; -type ReportActionsSelector = Array>; - -/** - * This function (and the few below it), narrow down the data from Onyx to just the properties that we want to trigger a re-render of the component. This helps minimize re-rendering - * and makes the entire component more performant because it's not re-rendering when a bunch of properties change which aren't ever used in the UI. - */ -const chatReportSelector = (report: OnyxEntry): ChatReportSelector => - (report && { - reportID: report.reportID, - participantAccountIDs: report.participantAccountIDs, - hasDraft: report.hasDraft, - isPinned: report.isPinned, - isHidden: report.isHidden, - notificationPreference: report.notificationPreference, - errorFields: { - addWorkspaceRoom: report.errorFields?.addWorkspaceRoom, - }, - lastMessageText: report.lastMessageText, - lastVisibleActionCreated: report.lastVisibleActionCreated, - iouReportID: report.iouReportID, - total: report.total, - nonReimbursableTotal: report.nonReimbursableTotal, - hasOutstandingChildRequest: report.hasOutstandingChildRequest, - isWaitingOnBankAccount: report.isWaitingOnBankAccount, - statusNum: report.statusNum, - stateNum: report.stateNum, - chatType: report.chatType, - type: report.type, - policyID: report.policyID, - visibility: report.visibility, - lastReadTime: report.lastReadTime, - // Needed for name sorting: - reportName: report.reportName, - policyName: report.policyName, - oldPolicyName: report.oldPolicyName, - // Other less obvious properites considered for sorting: - ownerAccountID: report.ownerAccountID, - currency: report.currency, - managerID: report.managerID, - // Other important less obivous properties for filtering: - parentReportActionID: report.parentReportActionID, - parentReportID: report.parentReportID, - isDeletedParentAction: report.isDeletedParentAction, - isUnreadWithMention: ReportUtils.isUnreadWithMention(report), - }) as ChatReportSelector; - -const reportActionsSelector = (reportActions: OnyxEntry): ReportActionsSelector => - (reportActions && - Object.values(reportActions).map((reportAction) => { - const {reportActionID, actionName, errors = [], originalMessage} = reportAction; - const decision = reportAction.message?.[0].moderationDecision?.decision; - - return { - reportActionID, - actionName, - errors, - message: [ - { - moderationDecision: {decision}, - }, - ] as Message[], - originalMessage, - }; - })) as ReportActionsSelector; - -const policySelector = (policy: OnyxEntry): PolicySelector => - (policy && { - type: policy.type, - name: policy.name, - avatar: policy.avatar, - }) as PolicySelector; - const ReportIDsContextProvider = withOnyx({ chatReports: { key: ONYXKEYS.COLLECTION.REPORT, initialValue: {}, - selector: chatReportSelector, }, priorityMode: { key: ONYXKEYS.NVP_PRIORITY_MODE, @@ -204,12 +114,10 @@ const ReportIDsContextProvider = withOnyx Date: Mon, 18 Mar 2024 19:57:19 -0300 Subject: [PATCH 0072/1096] feat: change customRateID when participant changes, other improvements --- ...oraryForRefactorRequestConfirmationList.js | 25 ++++--- .../ReportActionItem/MoneyRequestView.tsx | 72 +++++++++---------- src/libs/DistanceRequestUtils.ts | 3 +- src/libs/TransactionUtils.ts | 9 +++ src/libs/actions/IOU.ts | 6 +- ...yForRefactorRequestParticipantsSelector.js | 2 +- .../step/IOURequestStepParticipants.js | 35 ++++++++- .../iou/request/step/IOURequestStepRate.tsx | 28 +++++--- 8 files changed, 116 insertions(+), 64 deletions(-) diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 93831606416e..ce5668924271 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -224,7 +224,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ isReadOnly, isScanRequest, listStyles, - mileageRate, + mileageRates, onConfirm, onSelectParticipant, onSendMoney, @@ -242,7 +242,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ session: {accountID}, shouldShowSmartScanFields, transaction, - mileageRates, }) { const theme = useTheme(); const styles = useThemeStyles(); @@ -254,14 +253,19 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const isTypeSend = iouType === CONST.IOU.TYPE.SEND; const canEditDistance = isTypeRequest || (canUseP2PDistanceRequests && isTypeSplit); - // TODO: uncomment after Splits and P2P are enabled https://github.com/Expensify/App/pull/37185, mileageRate prop should be be removed - // const mileageRate = transaction.comment.customUnit.customUnitRateID === '_FAKE_P2P_ID_' ? DistanceRequestUtils.getRateForP2P(policy.outputCurrency) : mileageRates[transaction.comment.customUnit.customUnitRateID]; - const {unit, rate, currency} = mileageRate; + const personalPolicy = policyID === CONST.POLICY.ID_FAKE ? PolicyUtils.getPersonalPolicy() : policy; + const mileageRate = TransactionUtils.isCustomUnitRateIDForP2P(transaction) + ? DistanceRequestUtils.getRateForP2P(personalPolicy.outputCurrency) + : mileageRates[transaction.comment.customUnit.customUnitRateID]; - // will hardcoded rates will have a currency property? If not we have to get currency other way + const {unit, rate} = mileageRate || { + unit: CONST.CUSTOM_UNITS.DISTANCE_UNIT_MILES, + rate: CONST.CUSTOM_UNITS.MILEAGE_IRS_RATE * 100, + }; + + const currency = personalPolicy.outputCurrency; const distance = lodashGet(transaction, 'routes.route0.distance', 0); - const shouldCalculateDistanceAmount = isDistanceRequest && iouAmount === 0; const taxRates = lodashGet(policy, 'taxRates', {}); // A flag for showing the categories field @@ -292,7 +296,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ const formattedAmount = isDistanceRequestWithPendingRoute ? '' : CurrencyUtils.convertToDisplayString( - shouldCalculateDistanceAmount ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount, + isDistanceRequest && iouAmount === 0 ? DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate) : iouAmount, isDistanceRequest ? currency : iouCurrencyCode, ); const formattedTaxAmount = CurrencyUtils.convertToDisplayString(transaction.taxAmount, iouCurrencyCode); @@ -359,13 +363,13 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ }, [isFocused, transaction, shouldDisplayFieldError, hasSmartScanFailed, didConfirmSplit, isMerchantRequired, merchantError]); useEffect(() => { - if (!shouldCalculateDistanceAmount) { + if (!isDistanceRequest) { return; } const amount = DistanceRequestUtils.getDistanceRequestAmount(distance, unit, rate); IOU.setMoneyRequestAmount_temporaryForRefactor(transaction.transactionID, amount, currency); - }, [shouldCalculateDistanceAmount, distance, rate, unit, transaction, currency]); + }, [isDistanceRequest, distance, rate, unit, transaction, currency]); /** * Returns the participants with amount @@ -757,7 +761,6 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ item: ( - - Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} - /> - - - {}} - /> - - - ) : ( - - Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} - /> - - ) + <> + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + /> + + + {}} + /> + + + ) : ( + + Navigation.navigate(ROUTES.EDIT_REQUEST.getRoute(report.reportID, CONST.EDIT_REQUEST_FIELD.DISTANCE))} + /> + + ); return ( diff --git a/src/libs/DistanceRequestUtils.ts b/src/libs/DistanceRequestUtils.ts index e954ce4d266d..28714a92ed84 100644 --- a/src/libs/DistanceRequestUtils.ts +++ b/src/libs/DistanceRequestUtils.ts @@ -91,8 +91,7 @@ function convertDistanceUnit(distanceInMeters: number, unit: Unit): number { */ function getRoundedDistanceInUnits(distanceInMeters: number, unit: Unit): string { const convertedDistance = convertDistanceUnit(distanceInMeters, unit); - // TODO: add logic for currencies for which we need to round to 4 decimals - return convertedDistance.toFixed(3); + return convertedDistance.toFixed(2); } // TODO: I wonder if it would be better to refactor these functions to pass params in an object diff --git a/src/libs/TransactionUtils.ts b/src/libs/TransactionUtils.ts index bc94c8fee8fc..f694199fbeb4 100644 --- a/src/libs/TransactionUtils.ts +++ b/src/libs/TransactionUtils.ts @@ -584,6 +584,14 @@ function getEnabledTaxRateCount(options: TaxRates) { return Object.values(options).filter((option: TaxRate) => !option.isDisabled).length; } +/** + * Check if the customUnitRateID has a value default for P2P distance requests + */ + +function isCustomUnitRateIDForP2P(transaction: Transaction): boolean { + return transaction?.comment?.customUnit?.customUnitRateID === CONST.CUSTOM_UNITS.FAKE_P2P_ID; +} + export { buildOptimisticTransaction, calculateTaxAmount, @@ -633,6 +641,7 @@ export { waypointHasValidAddress, getRecentTransactions, hasViolation, + isCustomUnitRateIDForP2P, }; export type {TransactionChanges}; diff --git a/src/libs/actions/IOU.ts b/src/libs/actions/IOU.ts index 477fb99900b8..31cd49269e2c 100644 --- a/src/libs/actions/IOU.ts +++ b/src/libs/actions/IOU.ts @@ -253,8 +253,9 @@ function initMoneyRequest(reportID: string, policy: OnyxEntry, waypoint1: {}, }; const report = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? null; + const parentReport = allReports?.[`${ONYXKEYS.COLLECTION.REPORT}${report?.parentReportID}`] ?? null; let customUnitRateID: string = CONST.CUSTOM_UNITS.FAKE_P2P_ID; - if (ReportUtils.isPolicyExpenseChat(report)) { + if (ReportUtils.isPolicyExpenseChat(report) || ReportUtils.isPolicyExpenseChat(parentReport)) { customUnitRateID = lastSelectedDistanceRates?.[policy?.id ?? ''] ?? DistanceRequestUtils.getDefaultMileageRate(policy)?.customUnitRateID ?? ''; } comment.customUnit = {customUnitRateID}; @@ -373,9 +374,8 @@ function setMoneyRequestReceipt(transactionID: string, source: string, filename: } /** Set the last selected distance rate for policy */ -// TODO: probably need to be changed function setLastSelectedDistanceRates(policyID: string, rateID: string) { - Onyx.merge('lastSelectedDistanceRates', {[policyID]: rateID}); + Onyx.merge(ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, {[policyID]: rateID}); } /** Update transaction distance rate */ diff --git a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js index cdc3e72f98f4..7bdcafef61e6 100644 --- a/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js +++ b/src/pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector.js @@ -190,7 +190,7 @@ function MoneyTemporaryForRefactorRequestParticipantsSelector({ const addSingleParticipant = (option) => { onParticipantsAdded([ { - ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText'), + ..._.pick(option, 'accountID', 'login', 'isPolicyExpenseChat', 'reportID', 'searchText', 'policyID'), selected: true, }, ]); diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 5ca465d8fb78..8c92961a28c4 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -1,16 +1,21 @@ import {useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; +import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import transactionPropTypes from '@components/transactionPropTypes'; import useLocalize from '@hooks/useLocalize'; import compose from '@libs/compose'; +import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import * as IOUUtils from '@libs/IOUUtils'; import Navigation from '@libs/Navigation/Navigation'; +import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import MoneyRequestParticipantsSelector from '@pages/iou/request/MoneyTemporaryForRefactorRequestParticipantsSelector'; import * as IOU from '@userActions/IOU'; import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import IOURequestStepRoutePropTypes from './IOURequestStepRoutePropTypes'; import StepScreenWrapper from './StepScreenWrapper'; @@ -24,6 +29,9 @@ const propTypes = { /* Onyx Props */ /** The transaction object being modified in Onyx */ transaction: transactionPropTypes, + + // eslint-disable-next-line + lastSelectedDistanceRates: PropTypes.object, }; const defaultProps = { @@ -36,6 +44,7 @@ function IOURequestStepParticipants({ }, transaction, transaction: {participants = []}, + lastSelectedDistanceRates = {}, }) { const {translate} = useLocalize(); const navigation = useNavigation(); @@ -100,6 +109,22 @@ function IOURequestStepParticipants({ } IOU.setMoneyRequestParticipants_temporaryForRefactor(transactionID, val); + + // change customUnitRateID when choosing + if (val[0].isPolicyExpenseChat && TransactionUtils.isCustomUnitRateIDForP2P(transaction)) { + let customUnitRateID = ''; + if (val[0].policyID && lastSelectedDistanceRates[val[0].policyID]) { + customUnitRateID = lastSelectedDistanceRates[val[0].policyID]; + } else { + const policy = ReportUtils.getPolicy(val[0].policyID); + const defaultRate = DistanceRequestUtils.getDefaultMileageRate(policy); + customUnitRateID = defaultRate ? defaultRate.customUnitRateID : ''; + } + IOU.updateDistanceRequestRate(transactionID, customUnitRateID); + } else if (!TransactionUtils.isCustomUnitRateIDForP2P(transaction)) { + IOU.updateDistanceRequestRate(transactionID, CONST.CUSTOM_UNITS.FAKE_P2P_ID); + } + numberOfParticipants.current = val.length; // When multiple participants are selected, the reportID is generated at the end of the confirmation step. @@ -163,4 +188,12 @@ IOURequestStepParticipants.displayName = 'IOURequestStepParticipants'; IOURequestStepParticipants.propTypes = propTypes; IOURequestStepParticipants.defaultProps = defaultProps; -export default compose(withWritableReportOrNotFound, withFullTransactionOrNotFound)(IOURequestStepParticipants); +export default compose( + withWritableReportOrNotFound, + withFullTransactionOrNotFound, + withOnyx({ + lastSelectedDistanceRates: { + key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + }, + }), +)(IOURequestStepParticipants); diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index a2cf7a0af1c8..10513f7a44ca 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -6,19 +6,21 @@ import RadioListItem from '@components/SelectionList/RadioListItem'; import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; +import * as IOU from '@libs/actions/IOU'; import compose from '@libs/compose'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; -import * as IOU from '@libs/actions/IOU'; import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; + type Props = { - // eslint-disable-next-line react/no-unused-prop-types - lastSelectedDistanceRate: string; + /** Object of last selected rates for the policies */ + lastSelectedDistanceRates: Record; /** Policy details */ policy: OnyxEntry; @@ -38,11 +40,14 @@ function IOURequestStepRate({ route: { params: {backTo}, }, + lastSelectedDistanceRates = {}, }: Props) { const styles = useThemeStyles(); const {translate, toLocaleDigit} = useLocalize(); const rates = DistanceRequestUtils.getMileageRates(policy?.id); + const lastSelectedRate = lastSelectedDistanceRates[policy?.id ?? '0'] ?? '0'; + const data = Object.values(rates).map((rate) => ({ text: rate.name ?? '', alternateText: DistanceRequestUtils.getRateForDisplay(true, rate.unit, rate.rate, rate.currency, translate, toLocaleDigit), @@ -50,9 +55,10 @@ function IOURequestStepRate({ value: rate.customUnitRateID, })); - const selectDistanceRate = (customUnitRateID) => { + const initiallyFocusedOption = rates[lastSelectedRate]?.name ?? CONST.CUSTOM_UNITS.DEFAULT_RATE; + + function selectDistanceRate(customUnitRateID = '0') { IOU.setLastSelectedDistanceRates(policy?.id ?? '', customUnitRateID); - // TODO: get a proper transaction ID IOU.updateDistanceRequestRate('1', customUnitRateID); Navigation.goBack(backTo); } @@ -70,8 +76,7 @@ function IOURequestStepRate({ sections={[{data}]} ListItem={RadioListItem} onSelectRow={({value}) => selectDistanceRate(value)} - // TODO: change for lastSelectedDistanceRates - initiallyFocusedOptionKey="Default Rate" + initiallyFocusedOptionKey={initiallyFocusedOption} /> ); @@ -82,11 +87,14 @@ IOURequestStepRate.displayName = 'IOURequestStepRate'; export default compose( withWritableReportOrNotFound, withOnyx({ + // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS policy: { + // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report ? report.policyID : '0'}`, }, - // lastSelectedDistanceRates: { - // key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, - // }, + // @ts-expect-error TODO: fix when withWritableReportOrNotFound will be migrated to TS + lastSelectedDistanceRates: { + key: ONYXKEYS.NVP_LAST_SELECTED_DISTANCE_RATES, + }, }), )(IOURequestStepRate); From 07db2abc4a3a1d5e6d64ce12cc0ec21274946d96 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 19 Mar 2024 17:48:30 +0700 Subject: [PATCH 0073/1096] update variable name --- src/components/AutoCompleteSuggestions/index.tsx | 14 +++++++------- .../home/report/ReportActionItemMessageEdit.tsx | 1 - 2 files changed, 7 insertions(+), 8 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/index.tsx b/src/components/AutoCompleteSuggestions/index.tsx index 1f5d87b15962..66ea0de6f9f3 100644 --- a/src/components/AutoCompleteSuggestions/index.tsx +++ b/src/components/AutoCompleteSuggestions/index.tsx @@ -19,13 +19,13 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} const StyleUtils = useStyleUtils(); const containerRef = React.useRef(null); const {windowHeight, windowWidth} = useWindowDimensions(); - const suggestionContainerHeight = measureHeightOfSuggestionsContainer(props.suggestions.length, props.isSuggestionPickerLarge); + const suggestionsContainerHeight = measureHeightOfSuggestionsContainer(props.suggestions.length, props.isSuggestionPickerLarge); const [{width, left, bottom}, setContainerState] = React.useState({ width: 0, left: 0, bottom: 0, }); - const [shouldBelowContainer, setShouldBelowContainer] = React.useState(false); + const [shouldShowBelowContainer, setShouldShowBelowContainer] = React.useState(false); React.useEffect(() => { const container = containerRef.current; if (!container) { @@ -46,17 +46,17 @@ function AutoCompleteSuggestions({measureParentContainer = () => {} } measureParentContainer((x, y, w, h) => { - const currenBottom = y < suggestionContainerHeight ? windowHeight - y - suggestionContainerHeight - h : windowHeight - y; - setShouldBelowContainer(y < suggestionContainerHeight); - setContainerState({left: x, bottom: currenBottom, width: w}); + const currentBottom = y < suggestionsContainerHeight ? windowHeight - y - suggestionsContainerHeight - h : windowHeight - y; + setShouldShowBelowContainer(y < suggestionsContainerHeight); + setContainerState({left: x, bottom: currentBottom, width: w}); }); - }, [measureParentContainer, windowHeight, windowWidth, suggestionContainerHeight]); + }, [measureParentContainer, windowHeight, windowWidth, suggestionsContainerHeight]); const componentToRender = ( // eslint-disable-next-line react/jsx-props-no-spreading {...props} - shouldBeDisplayedBelowParentContainer={shouldBelowContainer} + shouldBeDisplayedBelowParentContainer={shouldShowBelowContainer} ref={containerRef} /> ); diff --git a/src/pages/home/report/ReportActionItemMessageEdit.tsx b/src/pages/home/report/ReportActionItemMessageEdit.tsx index 294f7e83d66e..85f206cb94b0 100644 --- a/src/pages/home/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/home/report/ReportActionItemMessageEdit.tsx @@ -392,7 +392,6 @@ function ReportActionItemMessageEdit( } containerRef.current.measureInWindow(callback); }, - // We added isComposerFullSize in dependencies so that when this value changes, we recalculate the position of the popup // eslint-disable-next-line react-hooks/exhaustive-deps [], ); From a0227e07854c76bbc9039d5df2971de4c5294a5f Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 19 Mar 2024 09:42:20 -0300 Subject: [PATCH 0074/1096] fix: run prettier --- src/pages/iou/request/step/IOURequestStepRate.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 10513f7a44ca..583fa8c055c7 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -17,7 +17,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; - type Props = { /** Object of last selected rates for the policies */ lastSelectedDistanceRates: Record; From eb350eec27ba9858929864cdc29aada4453b88e1 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 19 Mar 2024 09:53:29 -0300 Subject: [PATCH 0075/1096] fix: lint fix --- src/pages/iou/request/step/IOURequestStepParticipants.js | 2 +- src/pages/iou/request/step/IOURequestStepRate.tsx | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pages/iou/request/step/IOURequestStepParticipants.js b/src/pages/iou/request/step/IOURequestStepParticipants.js index 8c92961a28c4..a66387709f61 100644 --- a/src/pages/iou/request/step/IOURequestStepParticipants.js +++ b/src/pages/iou/request/step/IOURequestStepParticipants.js @@ -137,7 +137,7 @@ function IOURequestStepParticipants({ // When a participant is selected, the reportID needs to be saved because that's the reportID that will be used in the confirmation step. selectedReportID.current = lodashGet(val, '[0].reportID', reportID); }, - [reportID, transactionID, iouType, participants, updateRouteParams], + [iouType, participants, transactionID, transaction, reportID, updateRouteParams, lastSelectedDistanceRates], ); const goToNextStep = useCallback( diff --git a/src/pages/iou/request/step/IOURequestStepRate.tsx b/src/pages/iou/request/step/IOURequestStepRate.tsx index 583fa8c055c7..de570ca3c67f 100644 --- a/src/pages/iou/request/step/IOURequestStepRate.tsx +++ b/src/pages/iou/request/step/IOURequestStepRate.tsx @@ -10,12 +10,12 @@ import * as IOU from '@libs/actions/IOU'; import compose from '@libs/compose'; import DistanceRequestUtils from '@libs/DistanceRequestUtils'; import Navigation from '@libs/Navigation/Navigation'; -import StepScreenWrapper from '@pages/iou/request/step/StepScreenWrapper'; -import withWritableReportOrNotFound from '@pages/iou/request/step/withWritableReportOrNotFound'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type {Route} from '@src/ROUTES'; import type {Policy} from '@src/types/onyx'; +import StepScreenWrapper from './StepScreenWrapper'; +import withWritableReportOrNotFound from './withWritableReportOrNotFound'; type Props = { /** Object of last selected rates for the policies */ From 54d0bb9d340926b8997c8b1d584321554d49ecec Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 20 Mar 2024 12:08:37 +0700 Subject: [PATCH 0076/1096] fix lint --- src/components/Image/index.tsx | 36 +++++++++++++++++++-- src/components/Image/types.ts | 7 ++-- src/components/ImageWithSizeCalculation.tsx | 5 +-- 3 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index f4e5bf4834e0..c8c78b54de06 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -1,11 +1,39 @@ -import React, {useMemo} from 'react'; +import React, {useCallback, useMemo, useState} from 'react'; import {withOnyx} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import BaseImage from './BaseImage'; -import type {ImageOnyxProps, ImageOwnProps, ImageProps} from './types'; +import type {ImageOnLoadEvent, ImageOnyxProps, ImageOwnProps, ImageProps} from './types'; -function Image({source: propsSource, isAuthTokenRequired = false, session, ...forwardedProps}: ImageProps) { +function Image({source: propsSource, isAuthTokenRequired = false, session, onLoad, objectPositionTop, style, ...forwardedProps}: ImageProps) { + const [aspectRatio, setAspectRatio] = useState(null); + + const updateAspectRatio = useCallback( + (width: number, height: number) => { + if (!objectPositionTop) { + return; + } + + if (width > height) { + setAspectRatio(1); + return; + } + + setAspectRatio(height ? width / height : 'auto'); + }, + [objectPositionTop], + ); + + const handleLoad = useCallback( + (event: ImageOnLoadEvent) => { + const {width, height} = event.nativeEvent; + + onLoad?.(event); + + updateAspectRatio(width, height); + }, + [onLoad, updateAspectRatio], + ); /** * Check if the image source is a URL - if so the `encryptedAuthToken` is appended * to the source. @@ -34,6 +62,8 @@ function Image({source: propsSource, isAuthTokenRequired = false, session, ...fo ); diff --git a/src/components/Image/types.ts b/src/components/Image/types.ts index 2a5fcbb19324..c2f73e74ee34 100644 --- a/src/components/Image/types.ts +++ b/src/components/Image/types.ts @@ -23,12 +23,12 @@ type BaseImageProps = { /** Event for when the image is fully loaded and returns the natural dimensions of the image */ onLoad?: (event: ImageOnLoadEvent) => void; -}; -type ImageOwnProps = BaseImageProps & { /** Styles for the Image */ style?: StyleProp; +}; +type ImageOwnProps = BaseImageProps & { /** Should an auth token be included in the image request */ isAuthTokenRequired?: boolean; @@ -46,6 +46,9 @@ type ImageOwnProps = BaseImageProps & { /** Progress events while the image is downloading */ onProgress?: () => void; + + /** Whether we should show the top of the image */ + objectPositionTop?: boolean; }; type ImageProps = ImageOnyxProps & ImageOwnProps; diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index 46e1308692ba..ffa9517299cc 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -33,7 +33,8 @@ type ImageWithSizeCalculationProps = { /** Whether the image requires an authToken */ isAuthTokenRequired: boolean; - objectPositionTop: boolean; + /** Whether we should show the top of the image */ + objectPositionTop?: boolean; }; /** @@ -42,7 +43,7 @@ type ImageWithSizeCalculationProps = { * performing some calculation on a network image after fetching dimensions so * it can be appropriately resized. */ -function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthTokenRequired, objectPositionTop}: ImageWithSizeCalculationProps) { +function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthTokenRequired, objectPositionTop = false}: ImageWithSizeCalculationProps) { const styles = useThemeStyles(); const isLoadedRef = useRef(null); const [isImageCached, setIsImageCached] = useState(true); From c093a99e4fcbf720603f836be8cadca5d71217c1 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 20 Mar 2024 14:54:49 +0700 Subject: [PATCH 0077/1096] fix portal on native --- src/components/AutoCompleteSuggestions/index.native.tsx | 4 +++- .../home/report/ReportActionCompose/ReportActionCompose.tsx | 2 +- src/pages/home/report/ReportActionItemMessageEdit.tsx | 2 +- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/AutoCompleteSuggestions/index.native.tsx b/src/components/AutoCompleteSuggestions/index.native.tsx index fbfa7d953581..526954d370e3 100644 --- a/src/components/AutoCompleteSuggestions/index.native.tsx +++ b/src/components/AutoCompleteSuggestions/index.native.tsx @@ -1,11 +1,13 @@ import {Portal} from '@gorhom/portal'; import React from 'react'; +import {useSuggestionsContext} from '@pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext'; import BaseAutoCompleteSuggestions from './BaseAutoCompleteSuggestions'; import type {AutoCompleteSuggestionsProps} from './types'; function AutoCompleteSuggestions({measureParentContainer, ...props}: AutoCompleteSuggestionsProps) { + const {activeID} = useSuggestionsContext(); return ( - + {/* eslint-disable-next-line react/jsx-props-no-spreading */} {...props} /> diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 1e0e322be258..4c767eeb7966 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -364,7 +364,7 @@ function ReportActionCompose({ {shouldShowReportRecipientLocalTime && hasReportRecipient && } - + - + Date: Wed, 20 Mar 2024 15:02:00 +0700 Subject: [PATCH 0078/1096] rename --- .../ComposerWithSuggestionsEdit/SuggestionsContext.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx index 36ed962010ab..ceecb56af450 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestionsEdit/SuggestionsContext.tsx @@ -51,6 +51,6 @@ function useSuggestionsContext() { return context; } -SuggestionsContextProvider.displayName = 'PlaybackContextProvider'; +SuggestionsContextProvider.displayName = 'SuggestionsContextProvider'; export {SuggestionsContextProvider, useSuggestionsContext}; From 61ef58859587485c314e8a97d7116c8e2f75373f Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 21 Mar 2024 15:13:38 +0500 Subject: [PATCH 0079/1096] refactor: use currentReportID from useReportIDs --- src/hooks/useReportIDs.tsx | 5 ++++- src/pages/home/sidebar/SidebarLinksData.js | 6 ++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index 422febfca140..bdb9bbf9ada3 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -27,10 +27,12 @@ type WithReportIDsContextProviderProps = OnyxProps & { type ReportIDsContextValue = { orderedReportIDs: string[]; + currentReportID: string; }; const ReportIDsContext = createContext({ orderedReportIDs: [], + currentReportID: '', }); function WithReportIDsContextProvider({ @@ -91,8 +93,9 @@ function WithReportIDsContextProvider({ const contextValue = useMemo( () => ({ orderedReportIDs: orderedReportIDsWithCurrentReport, + currentReportID: derivedCurrentReportID ?? '', }), - [orderedReportIDsWithCurrentReport], + [orderedReportIDsWithCurrentReport, derivedCurrentReportID], ); return {children}; diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 35c4a2c59cd2..1f4a8cf343d4 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -5,7 +5,6 @@ import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import networkPropTypes from '@components/networkPropTypes'; import {withNetwork} from '@components/OnyxProvider'; -import withCurrentReportID from '@components/withCurrentReportID'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withNavigationFocus from '@components/withNavigationFocus'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; @@ -44,7 +43,7 @@ const defaultProps = { ...withCurrentUserPersonalDetailsDefaultProps, }; -function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, currentUserPersonalDetails}) { +function SidebarLinksData({isFocused, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, currentUserPersonalDetails}) { const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); @@ -57,7 +56,7 @@ function SidebarLinksData({isFocused, currentReportID, insets, isLoadingApp, onL const orderedReportIDsRef = useRef(null); const isLoading = isLoadingApp; - const {orderedReportIDs} = useReportIDs(); + const {orderedReportIDs, currentReportID} = useReportIDs(); const optionListItems = useMemo(() => { if (deepEqual(orderedReportIDsRef.current, orderedReportIDs)) { @@ -103,7 +102,6 @@ SidebarLinksData.defaultProps = defaultProps; SidebarLinksData.displayName = 'SidebarLinksData'; export default compose( - withCurrentReportID, withCurrentUserPersonalDetails, withNavigationFocus, withNetwork(), From 5b1c8835707f9c7c8daf6ed6283a2e97f6d75707 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 21 Mar 2024 17:33:55 +0700 Subject: [PATCH 0080/1096] replace objectPositionTop with objectPosition --- src/CONST.ts | 5 +++++ src/components/DistanceEReceipt.tsx | 2 +- src/components/Image/index.tsx | 9 +++++---- src/components/Image/types.ts | 10 +++++++--- src/components/ImageWithSizeCalculation.tsx | 10 ++++++---- src/components/MoneyRequestConfirmationList.tsx | 2 +- ...MoneyTemporaryForRefactorRequestConfirmationList.js | 2 +- .../ReportActionItem/ReportActionItemImage.tsx | 4 ++-- src/components/ThumbnailImage.tsx | 10 +++++++--- 9 files changed, 35 insertions(+), 19 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index bb191ac5e028..3cd473f2f451 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1095,6 +1095,11 @@ const CONST = { JPEG: 'image/jpeg', }, + IMAGE_OBJECT_POSITION: { + TOP: 'top', + INITIAL: 'initial', + }, + FILE_TYPE_REGEX: { // Image MimeTypes allowed by iOS photos app. IMAGE: /\.(jpg|jpeg|png|webp|gif|tiff|bmp|heic|heif)$/, diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx index 864dc831f74c..03ae1cdef5ad 100644 --- a/src/components/DistanceEReceipt.tsx +++ b/src/components/DistanceEReceipt.tsx @@ -63,7 +63,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) { style={[styles.w100, styles.h100]} isAuthTokenRequired shouldDynamicallyResize={false} - objectPositionTop + objectPosition /> )} diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx index c8c78b54de06..e6cecdc0d5ec 100644 --- a/src/components/Image/index.tsx +++ b/src/components/Image/index.tsx @@ -5,12 +5,13 @@ import ONYXKEYS from '@src/ONYXKEYS'; import BaseImage from './BaseImage'; import type {ImageOnLoadEvent, ImageOnyxProps, ImageOwnProps, ImageProps} from './types'; -function Image({source: propsSource, isAuthTokenRequired = false, session, onLoad, objectPositionTop, style, ...forwardedProps}: ImageProps) { +function Image({source: propsSource, isAuthTokenRequired = false, session, onLoad, objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL, style, ...forwardedProps}: ImageProps) { const [aspectRatio, setAspectRatio] = useState(null); + const isObjectPositionTop = objectPosition === CONST.IMAGE_OBJECT_POSITION.TOP; const updateAspectRatio = useCallback( (width: number, height: number) => { - if (!objectPositionTop) { + if (!isObjectPositionTop) { return; } @@ -21,7 +22,7 @@ function Image({source: propsSource, isAuthTokenRequired = false, session, onLoa setAspectRatio(height ? width / height : 'auto'); }, - [objectPositionTop], + [isObjectPositionTop], ); const handleLoad = useCallback( @@ -63,7 +64,7 @@ function Image({source: propsSource, isAuthTokenRequired = false, session, onLoa // eslint-disable-next-line react/jsx-props-no-spreading {...forwardedProps} onLoad={handleLoad} - style={[style, aspectRatio ? {aspectRatio, height: 'auto'} : {}, objectPositionTop && !aspectRatio && {opacity: 0}]} + style={[style, aspectRatio ? {aspectRatio, height: 'auto'} : {}, isObjectPositionTop && !aspectRatio && {opacity: 0}]} source={source} /> ); diff --git a/src/components/Image/types.ts b/src/components/Image/types.ts index c2f73e74ee34..27964d8a6764 100644 --- a/src/components/Image/types.ts +++ b/src/components/Image/types.ts @@ -1,10 +1,14 @@ import type {ImageSource} from 'expo-image'; import type {ImageRequireSource, ImageResizeMode, ImageStyle, ImageURISource, StyleProp} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; +import type {ValueOf} from 'type-fest'; +import type CONST from '@src/CONST'; import type {Session} from '@src/types/onyx'; type ExpoImageSource = ImageSource | number | ImageSource[]; +type ImageObjectPosition = ValueOf; + type ImageOnyxProps = { /** Session info for the currently logged in user. */ session: OnyxEntry; @@ -47,10 +51,10 @@ type ImageOwnProps = BaseImageProps & { /** Progress events while the image is downloading */ onProgress?: () => void; - /** Whether we should show the top of the image */ - objectPositionTop?: boolean; + /** The object position of image */ + objectPosition?: ImageObjectPosition; }; type ImageProps = ImageOnyxProps & ImageOwnProps; -export type {BaseImageProps, ImageOwnProps, ImageOnyxProps, ImageProps, ExpoImageSource, ImageOnLoadEvent}; +export type {BaseImageProps, ImageOwnProps, ImageOnyxProps, ImageProps, ExpoImageSource, ImageOnLoadEvent, ImageObjectPosition}; diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index ffa9517299cc..eac5c676370b 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -5,9 +5,11 @@ import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import Log from '@libs/Log'; +import CONST from '@src/CONST'; import FullscreenLoadingIndicator from './FullscreenLoadingIndicator'; import Image from './Image'; import RESIZE_MODES from './Image/resizeModes'; +import type {ImageObjectPosition} from './Image/types'; type OnMeasure = (args: {width: number; height: number}) => void; @@ -33,8 +35,8 @@ type ImageWithSizeCalculationProps = { /** Whether the image requires an authToken */ isAuthTokenRequired: boolean; - /** Whether we should show the top of the image */ - objectPositionTop?: boolean; + /** The object position of image */ + objectPosition?: ImageObjectPosition; }; /** @@ -43,7 +45,7 @@ type ImageWithSizeCalculationProps = { * performing some calculation on a network image after fetching dimensions so * it can be appropriately resized. */ -function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthTokenRequired, objectPositionTop = false}: ImageWithSizeCalculationProps) { +function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthTokenRequired, objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL}: ImageWithSizeCalculationProps) { const styles = useThemeStyles(); const isLoadedRef = useRef(null); const [isImageCached, setIsImageCached] = useState(true); @@ -104,7 +106,7 @@ function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthT }} onError={onError} onLoad={imageLoadedSuccessfully} - objectPositionTop={objectPositionTop} + objectPosition={objectPosition} /> {isLoading && !isImageCached && } diff --git a/src/components/MoneyRequestConfirmationList.tsx b/src/components/MoneyRequestConfirmationList.tsx index b2ea75cae8b0..75d156e58eaa 100755 --- a/src/components/MoneyRequestConfirmationList.tsx +++ b/src/components/MoneyRequestConfirmationList.tsx @@ -614,7 +614,7 @@ function MoneyRequestConfirmationList({ // but we don't need it to load the blob:// or file:// image when starting a money request / split bill // So if we have a thumbnail, it means we're retrieving the image from the server isAuthTokenRequired={!!receiptThumbnail} - objectPositionTop + objectPosition={CONST.IMAGE_OBJECT_POSITION.TOP} /> ) : ( // The empty receipt component should only show for IOU Requests of a paid policy ("Team" or "Corporate") diff --git a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js index 08957c96128a..c32241d92096 100755 --- a/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js +++ b/src/components/MoneyTemporaryForRefactorRequestConfirmationList.js @@ -906,7 +906,7 @@ function MoneyTemporaryForRefactorRequestConfirmationList({ // but we don't need it to load the blob:// or file:// image when starting a money request / split bill // So if we have a thumbnail, it means we're retrieving the image from the server isAuthTokenRequired={!_.isEmpty(receiptThumbnail)} - objectPositionTop + objectPosition={CONST.IMAGE_OBJECT_POSITION.TOP} /> ), [receiptFilename, receiptImage, styles, receiptThumbnail, isLocalFile, isAttachmentInvalid], diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index b39cabf4e74e..3c6f6e7e2421 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -85,7 +85,7 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr fallbackIcon={Expensicons.Receipt} fallbackIconSize={isSingleImage ? variables.iconSizeSuperLarge : variables.iconSizeExtraLarge} shouldDynamicallyResize={false} - objectPositionTop + objectPosition={CONST.IMAGE_OBJECT_POSITION.TOP} /> ); } else if (isLocalFile && filename && Str.isPDF(filename) && typeof attachmentModalSource === 'string') { @@ -100,7 +100,7 @@ function ReportActionItemImage({thumbnail, image, enablePreviewModal = false, tr ); } diff --git a/src/components/ThumbnailImage.tsx b/src/components/ThumbnailImage.tsx index d1bdfdf3ca9f..8b8da81c727a 100644 --- a/src/components/ThumbnailImage.tsx +++ b/src/components/ThumbnailImage.tsx @@ -6,9 +6,11 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useThumbnailDimensions from '@hooks/useThumbnailDimensions'; import variables from '@styles/variables'; +import CONST from '@src/CONST'; import type IconAsset from '@src/types/utils/IconAsset'; import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; +import type {ImageObjectPosition} from './Image/types'; import ImageWithSizeCalculation from './ImageWithSizeCalculation'; type ThumbnailImageProps = { @@ -35,7 +37,9 @@ type ThumbnailImageProps = { /** Should the image be resized on load or just fit container */ shouldDynamicallyResize?: boolean; - objectPositionTop?: boolean; + + /** The object position of image */ + objectPosition?: ImageObjectPosition; }; type UpdateImageSizeParams = { @@ -52,7 +56,7 @@ function ThumbnailImage({ shouldDynamicallyResize = true, fallbackIcon = Expensicons.Gallery, fallbackIconSize = variables.iconSizeSuperLarge, - objectPositionTop = false, + objectPosition = CONST.IMAGE_OBJECT_POSITION.INITIAL, }: ThumbnailImageProps) { const styles = useThemeStyles(); const theme = useTheme(); @@ -104,7 +108,7 @@ function ThumbnailImage({ onMeasure={updateImageSize} onLoadFailure={() => setFailedToLoad(true)} isAuthTokenRequired={isAuthTokenRequired} - objectPositionTop={objectPositionTop} + objectPosition={objectPosition} /> From 115ec923108600f1ad167783d5acb23417effe5c Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 21 Mar 2024 17:37:50 +0700 Subject: [PATCH 0081/1096] update objectPosition prop --- src/components/DistanceEReceipt.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx index 03ae1cdef5ad..3d2a6a3e57b6 100644 --- a/src/components/DistanceEReceipt.tsx +++ b/src/components/DistanceEReceipt.tsx @@ -18,6 +18,7 @@ import PendingMapView from './MapView/PendingMapView'; import ScrollView from './ScrollView'; import Text from './Text'; import ThumbnailImage from './ThumbnailImage'; +import CONST from '@src/CONST'; type DistanceEReceiptProps = { /** The transaction for the distance request */ @@ -63,7 +64,7 @@ function DistanceEReceipt({transaction}: DistanceEReceiptProps) { style={[styles.w100, styles.h100]} isAuthTokenRequired shouldDynamicallyResize={false} - objectPosition + objectPosition={CONST.IMAGE_OBJECT_POSITION.TOP} /> )} From 1562a88172728730d3ad58cad1abc0e1529f7a28 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 21 Mar 2024 18:05:42 +0700 Subject: [PATCH 0082/1096] fix order import --- src/components/DistanceEReceipt.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DistanceEReceipt.tsx b/src/components/DistanceEReceipt.tsx index 3d2a6a3e57b6..2d6e3e56210f 100644 --- a/src/components/DistanceEReceipt.tsx +++ b/src/components/DistanceEReceipt.tsx @@ -8,6 +8,7 @@ import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportUtils from '@libs/ReportUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; +import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import type {Transaction} from '@src/types/onyx'; import type {WaypointCollection} from '@src/types/onyx/Transaction'; @@ -18,7 +19,6 @@ import PendingMapView from './MapView/PendingMapView'; import ScrollView from './ScrollView'; import Text from './Text'; import ThumbnailImage from './ThumbnailImage'; -import CONST from '@src/CONST'; type DistanceEReceiptProps = { /** The transaction for the distance request */ From 84aa5ac37af2faf3eeb547b600627ef9c40fa920 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 21 Mar 2024 16:10:24 +0500 Subject: [PATCH 0083/1096] refactor: remove unnecessary ref --- src/pages/home/sidebar/SidebarLinksData.js | 29 +++------------------- 1 file changed, 3 insertions(+), 26 deletions(-) diff --git a/src/pages/home/sidebar/SidebarLinksData.js b/src/pages/home/sidebar/SidebarLinksData.js index 1f4a8cf343d4..207eed55586a 100644 --- a/src/pages/home/sidebar/SidebarLinksData.js +++ b/src/pages/home/sidebar/SidebarLinksData.js @@ -1,15 +1,11 @@ -import {deepEqual} from 'fast-equals'; import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useMemo, useRef} from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; import withNavigationFocus from '@components/withNavigationFocus'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; -import usePrevious from '@hooks/usePrevious'; import {useReportIDs} from '@hooks/useReportIDs'; import useThemeStyles from '@hooks/useThemeStyles'; import compose from '@libs/compose'; @@ -28,8 +24,6 @@ const propTypes = { /** The chat priority mode */ priorityMode: PropTypes.string, - network: networkPropTypes.isRequired, - // eslint-disable-next-line react/forbid-prop-types policyMembers: PropTypes.object, @@ -43,35 +37,19 @@ const defaultProps = { ...withCurrentUserPersonalDetailsDefaultProps, }; -function SidebarLinksData({isFocused, insets, isLoadingApp, onLinkClick, priorityMode, network, policyMembers, currentUserPersonalDetails}) { +function SidebarLinksData({isFocused, insets, isLoadingApp, onLinkClick, priorityMode, policyMembers, currentUserPersonalDetails}) { const styles = useThemeStyles(); const {activeWorkspaceID} = useActiveWorkspace(); const {translate} = useLocalize(); - const prevPriorityMode = usePrevious(priorityMode); const policyMemberAccountIDs = getPolicyMembersByIdWithoutCurrentUser(policyMembers, activeWorkspaceID, currentUserPersonalDetails.accountID); // eslint-disable-next-line react-hooks/exhaustive-deps useEffect(() => Policy.openWorkspace(activeWorkspaceID, policyMemberAccountIDs), [activeWorkspaceID]); - const orderedReportIDsRef = useRef(null); const isLoading = isLoadingApp; const {orderedReportIDs, currentReportID} = useReportIDs(); - const optionListItems = useMemo(() => { - if (deepEqual(orderedReportIDsRef.current, orderedReportIDs)) { - return orderedReportIDsRef.current; - } - - // 1. We need to update existing reports only once while loading because they are updated several times during loading and causes this regression: https://github.com/Expensify/App/issues/24596#issuecomment-1681679531 - // 2. If the user is offline, we need to update the reports unconditionally, since the loading of report data might be stuck in this case. - // 3. Changing priority mode to Most Recent will call OpenApp. If there is an existing reports and the priority mode is updated, we want to immediately update the list instead of waiting the OpenApp request to complete - if (!isLoading || !orderedReportIDsRef.current || network.isOffline || (orderedReportIDsRef.current && prevPriorityMode !== priorityMode)) { - orderedReportIDsRef.current = orderedReportIDs; - } - return orderedReportIDsRef.current || []; - }, [orderedReportIDs, isLoading, network.isOffline, prevPriorityMode, priorityMode]); - const currentReportIDRef = useRef(currentReportID); currentReportIDRef.current = currentReportID; const isActiveReport = useCallback((reportID) => currentReportIDRef.current === reportID, []); @@ -91,7 +69,7 @@ function SidebarLinksData({isFocused, insets, isLoadingApp, onLinkClick, priorit isActiveReport={isActiveReport} isLoading={isLoading} activeWorkspaceID={activeWorkspaceID} - optionListItems={optionListItems} + optionListItems={orderedReportIDs} /> ); @@ -104,7 +82,6 @@ SidebarLinksData.displayName = 'SidebarLinksData'; export default compose( withCurrentUserPersonalDetails, withNavigationFocus, - withNetwork(), withOnyx({ isLoadingApp: { key: ONYXKEYS.IS_LOADING_APP, From 8532c0864d9b6a2ffb88ae3759801781d6bf23af Mon Sep 17 00:00:00 2001 From: Muhammad Hur Ali Date: Thu, 21 Mar 2024 16:16:02 +0500 Subject: [PATCH 0084/1096] update comment is updateCurrentReportID function Co-authored-by: Rory Abraham <47436092+roryabraham@users.noreply.github.com> --- src/components/withCurrentReportID.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index 55b542ccacb7..27a965a151de 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -42,7 +42,7 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro const reportID = Navigation.getTopmostReportId(state) ?? ''; /** - * This is to make sure we don't set the undefined as reportID when + * This is to make sure we don't set the reportID as undefined when * switching between chat list and settings tab. The settings tab * includes multiple screens and we don't want to set the reportID * to falsy value when switching between them. From ac465f3976aef5d891f7ba4e7eb4bc2043ca13a3 Mon Sep 17 00:00:00 2001 From: Muhammad Hur Ali Date: Thu, 21 Mar 2024 16:16:46 +0500 Subject: [PATCH 0085/1096] update comment in useReportIDs hook Co-authored-by: Rory Abraham <47436092+roryabraham@users.noreply.github.com> --- src/hooks/useReportIDs.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useReportIDs.tsx b/src/hooks/useReportIDs.tsx index bdb9bbf9ada3..4d7474905a2e 100644 --- a/src/hooks/useReportIDs.tsx +++ b/src/hooks/useReportIDs.tsx @@ -47,7 +47,7 @@ function WithReportIDsContextProvider({ /** * Only required to make unit tests work, since we * explicitly pass the currentReportID in LHNTestUtils - * to SidebarLinksData, so this context doesn't have an + * to SidebarLinksData, so this context doesn't have * access to currentReportID in that case. * * This is a workaround to have currentReportID available in testing environment. From 886622e7940a7b28e11e299d0aa89a6e34030cc6 Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 21 Mar 2024 16:20:12 +0500 Subject: [PATCH 0086/1096] refactor: make comment less verbose --- src/components/withCurrentReportID.tsx | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/components/withCurrentReportID.tsx b/src/components/withCurrentReportID.tsx index 27a965a151de..a72063913283 100644 --- a/src/components/withCurrentReportID.tsx +++ b/src/components/withCurrentReportID.tsx @@ -41,13 +41,9 @@ function CurrentReportIDContextProvider(props: CurrentReportIDContextProviderPro (state: NavigationState) => { const reportID = Navigation.getTopmostReportId(state) ?? ''; - /** - * This is to make sure we don't set the reportID as undefined when - * switching between chat list and settings tab. The settings tab - * includes multiple screens and we don't want to set the reportID - * to falsy value when switching between them. - * - * Doing so avoids an unnecessary re-render of `useReportIDs`. + /* + * Make sure we don't make the reportID undefined when switching between the chat list and settings tab. + * This helps prevent unnecessary re-renders. */ const params = state?.routes?.[state.index]?.params; if (params && 'screen' in params && typeof params.screen === 'string' && params.screen.indexOf('Settings_') !== -1) { From 6d17116c0e2af4fbadbe79b366fb16c7434b51be Mon Sep 17 00:00:00 2001 From: hurali97 Date: Thu, 21 Mar 2024 16:20:46 +0500 Subject: [PATCH 0087/1096] refactor: remove irrelevant comment --- src/components/LHNOptionsList/LHNOptionsList.tsx | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index c9052567ac36..fa4c89216d08 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -122,12 +122,6 @@ function LHNOptionsList({ keyExtractor={keyExtractor} renderItem={renderItem} estimatedItemSize={optionMode === CONST.OPTION_MODE.COMPACT ? variables.optionRowHeightCompact : variables.optionRowHeight} - // Previously, we were passing `extraData={[currentReportID]}`, which upon every render, was causing the - // re-render because of the new array reference. FlashList's children actually don't depend on the - // `currentReportID` prop but they depend on the `reportActions`, `reports`, `policy`, `personalDetails`. - // Previously it was working for us because of the new array reference. Even if you only pass an empty - // array, it will still work because of the new reference. But it's better to pass the actual dependencies - // to avoid unnecessary re-renders. extraData={extraData} showsVerticalScrollIndicator={false} /> From d296c84255fe8a96ff0f43e8b22aff1d95608f08 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Thu, 21 Mar 2024 18:12:58 +0100 Subject: [PATCH 0088/1096] Turn on Onboarding Flow Stage 1 --- src/ROUTES.ts | 4 - src/SCREENS.ts | 7 - src/languages/en.ts | 22 --- src/languages/es.ts | 22 --- .../AppNavigator/ModalStackNavigators.tsx | 8 - .../Navigators/RightModalNavigator.tsx | 4 - .../BottomTabBar.tsx | 12 +- src/libs/Navigation/linkingConfig/config.ts | 7 - src/libs/Navigation/types.ts | 8 - src/libs/actions/Welcome.ts | 6 +- .../ExpensifyClassicPage.tsx | 75 --------- .../ManageTeamsExpensesPage.tsx | 122 -------------- .../PurposeForUsingExpensifyPage.tsx | 155 ------------------ src/styles/theme/themes/dark.ts | 8 - src/styles/theme/themes/light.ts | 8 - 15 files changed, 2 insertions(+), 466 deletions(-) delete mode 100644 src/pages/OnboardEngagement/ExpensifyClassicPage.tsx delete mode 100644 src/pages/OnboardEngagement/ManageTeamsExpensesPage.tsx delete mode 100644 src/pages/OnboardEngagement/PurposeForUsingExpensifyPage.tsx diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c326b1e69e08..83aff058a41a 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -420,10 +420,6 @@ const ROUTES = { NEW_TASK_TITLE: 'new/task/title', NEW_TASK_DESCRIPTION: 'new/task/description', - ONBOARD: 'onboard', - ONBOARD_MANAGE_EXPENSES: 'onboard/manage-expenses', - ONBOARD_EXPENSIFY_CLASSIC: 'onboard/expensify-classic', - TEACHERS_UNITE: 'teachersunite', I_KNOW_A_TEACHER: 'teachersunite/i-know-a-teacher', I_AM_A_TEACHER: 'teachersunite/i-am-a-teacher', diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3cf747aa3859..45d4c79ca764 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -109,7 +109,6 @@ const SCREENS = { PARTICIPANTS: 'Participants', MONEY_REQUEST: 'MoneyRequest', NEW_TASK: 'NewTask', - ONBOARD_ENGAGEMENT: 'Onboard_Engagement', TEACHERS_UNITE: 'TeachersUnite', TASK_DETAILS: 'Task_Details', ENABLE_PAYMENTS: 'EnablePayments', @@ -274,12 +273,6 @@ const SCREENS = { PURPOSE: 'Onboarding_Purpose', }, - ONBOARD_ENGAGEMENT: { - ROOT: 'Onboard_Engagement_Root', - MANAGE_TEAMS_EXPENSES: 'Manage_Teams_Expenses', - EXPENSIFY_CLASSIC: 'Expenisfy_Classic', - }, - WELCOME_VIDEO: { ROOT: 'Welcome_Video_Root', }, diff --git a/src/languages/en.ts b/src/languages/en.ts index 92522b51d1f6..37b4b26ea45b 100755 --- a/src/languages/en.ts +++ b/src/languages/en.ts @@ -2512,28 +2512,6 @@ export default { }, copyReferralLink: 'Copy invite link', }, - purposeForExpensify: { - [CONST.INTRO_CHOICES.TRACK]: 'Track business spend for taxes', - [CONST.INTRO_CHOICES.SUBMIT]: 'Get paid back by my employer', - [CONST.INTRO_CHOICES.MANAGE_TEAM]: "Manage my team's expenses", - [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chat and split bills with friends', - welcomeMessage: 'Welcome to Expensify', - welcomeSubtitle: 'What would you like to do?', - }, - manageTeams: { - [CONST.MANAGE_TEAMS_CHOICE.MULTI_LEVEL]: 'Multi level approval', - [CONST.MANAGE_TEAMS_CHOICE.CUSTOM_EXPENSE]: 'Custom expense coding', - [CONST.MANAGE_TEAMS_CHOICE.CARD_TRACKING]: 'Company card tracking', - [CONST.MANAGE_TEAMS_CHOICE.ACCOUNTING]: 'Accounting integrations', - [CONST.MANAGE_TEAMS_CHOICE.RULE]: 'Rule enforcement', - title: 'Do you require any of the following features?', - }, - expensifyClassic: { - title: "Expensify Classic has everything you'll need", - firstDescription: "While we're busy working on New Expensify, it currently doesn't support some of the features you're looking for.", - secondDescription: "Don't worry, Expensify Classic has everything you need.", - buttonText: 'Take me to Expensify Classic', - }, violations: { allTagLevelsRequired: 'All tags required', autoReportedRejectedExpense: ({rejectReason, rejectedBy}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rejected this expense with the comment "${rejectReason}"`, diff --git a/src/languages/es.ts b/src/languages/es.ts index 95a84bcf59a0..f4f7b02124eb 100644 --- a/src/languages/es.ts +++ b/src/languages/es.ts @@ -3004,28 +3004,6 @@ export default { }, copyReferralLink: 'Copiar enlace de invitación', }, - purposeForExpensify: { - [CONST.INTRO_CHOICES.TRACK]: 'Seguimiento de los gastos de empresa para fines fiscales', - [CONST.INTRO_CHOICES.SUBMIT]: 'Reclamar gastos a mi empleador', - [CONST.INTRO_CHOICES.MANAGE_TEAM]: 'Gestionar los gastos de mi equipo', - [CONST.INTRO_CHOICES.CHAT_SPLIT]: 'Chatea y divide gastos con tus amigos', - welcomeMessage: 'Bienvenido a Expensify', - welcomeSubtitle: '¿Qué te gustaría hacer?', - }, - manageTeams: { - [CONST.MANAGE_TEAMS_CHOICE.MULTI_LEVEL]: 'Aprobación multinivel', - [CONST.MANAGE_TEAMS_CHOICE.CUSTOM_EXPENSE]: 'Codificación personalizada de gastos', - [CONST.MANAGE_TEAMS_CHOICE.CARD_TRACKING]: 'Seguimiento de tarjetas corporativas', - [CONST.MANAGE_TEAMS_CHOICE.ACCOUNTING]: 'Integraciones de contaduría', - [CONST.MANAGE_TEAMS_CHOICE.RULE]: 'Aplicación de reglas', - title: '¿Necesitas alguna de las siguientes funciones?', - }, - expensifyClassic: { - title: 'Expensify Classic tiene todo lo que necesitas', - firstDescription: 'Aunque estamos ocupados trabajando en el Nuevo Expensify, actualmente no soporta algunas de las funciones que estás buscando.', - secondDescription: 'No te preocupes, Expensify Classic tiene todo lo que necesitas.', - buttonText: 'Llévame a Expensify Classic', - }, violations: { allTagLevelsRequired: 'Todas las etiquetas son obligatorias', autoReportedRejectedExpense: ({rejectedBy, rejectReason}: ViolationsAutoReportedRejectedExpenseParams) => `${rejectedBy} rechazó la solicitud y comentó "${rejectReason}"`, diff --git a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx index bd5bfc46134a..370324602a0a 100644 --- a/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx +++ b/src/libs/Navigation/AppNavigator/ModalStackNavigators.tsx @@ -12,7 +12,6 @@ import type { MoneyRequestNavigatorParamList, NewChatNavigatorParamList, NewTaskNavigatorParamList, - OnboardEngagementNavigatorParamList, ParticipantsNavigatorParamList, PrivateNotesNavigatorParamList, ProfileNavigatorParamList, @@ -172,12 +171,6 @@ const NewTaskModalStackNavigator = createModalStackNavigator require('../../../pages/tasks/NewTaskDescriptionPage').default as React.ComponentType, }); -const OnboardEngagementModalStackNavigator = createModalStackNavigator({ - [SCREENS.ONBOARD_ENGAGEMENT.ROOT]: () => require('../../../pages/OnboardEngagement/PurposeForUsingExpensifyPage').default as React.ComponentType, - [SCREENS.ONBOARD_ENGAGEMENT.MANAGE_TEAMS_EXPENSES]: () => require('../../../pages/OnboardEngagement/ManageTeamsExpensesPage').default as React.ComponentType, - [SCREENS.ONBOARD_ENGAGEMENT.EXPENSIFY_CLASSIC]: () => require('../../../pages/OnboardEngagement/ExpensifyClassicPage').default as React.ComponentType, -}); - const NewTeachersUniteNavigator = createModalStackNavigator({ [SCREENS.SAVE_THE_WORLD.ROOT]: () => require('../../../pages/TeachersUnite/SaveTheWorldPage').default as React.ComponentType, [SCREENS.I_KNOW_A_TEACHER]: () => require('../../../pages/TeachersUnite/KnowATeacherPage').default as React.ComponentType, @@ -336,7 +329,6 @@ const ProcessMoneyRequestHoldStackNavigator = createModalStackNavigator({ export { AddPersonalBankAccountModalStackNavigator, DetailsModalStackNavigator, - OnboardEngagementModalStackNavigator, EditRequestStackNavigator, EnablePaymentsStackNavigator, FlagCommentStackNavigator, diff --git a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx index c421bdc82028..93d2f8fba989 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/RightModalNavigator.tsx @@ -85,10 +85,6 @@ function RightModalNavigator({navigation}: RightModalNavigatorProps) { name={SCREENS.RIGHT_MODAL.NEW_TASK} component={ModalStackNavigators.NewTaskModalStackNavigator} /> - Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS)}); - Welcome.isOnboardingFlowCompleted({ - onNotCompleted: () => - Navigation.navigate( - // Uncomment once Stage 1 Onboarding Flow is ready - // - // ROUTES.ONBOARDING_PERSONAL_DETAILS - // - ROUTES.ONBOARD, - ), - }); + Welcome.isOnboardingFlowCompleted({onNotCompleted: () => Navigation.navigate(ROUTES.ONBOARDING_PERSONAL_DETAILS)}); // eslint-disable-next-line react-hooks/exhaustive-deps }, [isLoadingApp]); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index dafce762116e..1982afaa149f 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -451,13 +451,6 @@ const config: LinkingOptions['config'] = { [SCREENS.NEW_TASK.DESCRIPTION]: ROUTES.NEW_TASK_DESCRIPTION, }, }, - [SCREENS.RIGHT_MODAL.ONBOARD_ENGAGEMENT]: { - screens: { - [SCREENS.ONBOARD_ENGAGEMENT.ROOT]: ROUTES.ONBOARD, - [SCREENS.ONBOARD_ENGAGEMENT.MANAGE_TEAMS_EXPENSES]: ROUTES.ONBOARD_MANAGE_EXPENSES, - [SCREENS.ONBOARD_ENGAGEMENT.EXPENSIFY_CLASSIC]: ROUTES.ONBOARD_EXPENSIFY_CLASSIC, - }, - }, [SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: { screens: { [SCREENS.SAVE_THE_WORLD.ROOT]: ROUTES.TEACHERS_UNITE, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index 41e4602ea2d9..7dc862ed437d 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -412,12 +412,6 @@ type NewTaskNavigatorParamList = { [SCREENS.NEW_TASK.DESCRIPTION]: undefined; }; -type OnboardEngagementNavigatorParamList = { - [SCREENS.ONBOARD_ENGAGEMENT.ROOT]: undefined; - [SCREENS.ONBOARD_ENGAGEMENT.MANAGE_TEAMS_EXPENSES]: undefined; - [SCREENS.ONBOARD_ENGAGEMENT.EXPENSIFY_CLASSIC]: undefined; -}; - type TeachersUniteNavigatorParamList = { [SCREENS.SAVE_THE_WORLD.ROOT]: undefined; [SCREENS.I_KNOW_A_TEACHER]: undefined; @@ -525,7 +519,6 @@ type RightModalNavigatorParamList = { [SCREENS.RIGHT_MODAL.ROOM_INVITE]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.MONEY_REQUEST]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.NEW_TASK]: NavigatorScreenParams; - [SCREENS.RIGHT_MODAL.ONBOARD_ENGAGEMENT]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TEACHERS_UNITE]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.TASK_DETAILS]: NavigatorScreenParams; [SCREENS.RIGHT_MODAL.ENABLE_PAYMENTS]: NavigatorScreenParams; @@ -737,7 +730,6 @@ export type { ReimbursementAccountNavigatorParamList, State, WorkspaceSwitcherNavigatorParamList, - OnboardEngagementNavigatorParamList, SwitchPolicyIDParams, FullScreenNavigatorParamList, WorkspacesCentralPaneNavigatorParamList, diff --git a/src/libs/actions/Welcome.ts b/src/libs/actions/Welcome.ts index a93da266e622..95b879fc7485 100644 --- a/src/libs/actions/Welcome.ts +++ b/src/libs/actions/Welcome.ts @@ -63,11 +63,7 @@ function isOnboardingFlowCompleted({onCompleted, onNotCompleted}: HasCompletedOn return; } - // Uncomment once Stage 1 Onboarding Flow is ready - // - // const onboardingFlowCompleted = hasProvidedPersonalDetails && hasSelectedPurpose; - // - const onboardingFlowCompleted = hasSelectedPurpose; + const onboardingFlowCompleted = hasProvidedPersonalDetails && hasSelectedPurpose; if (onboardingFlowCompleted) { onCompleted?.(); diff --git a/src/pages/OnboardEngagement/ExpensifyClassicPage.tsx b/src/pages/OnboardEngagement/ExpensifyClassicPage.tsx deleted file mode 100644 index 7b242dd4aefa..000000000000 --- a/src/pages/OnboardEngagement/ExpensifyClassicPage.tsx +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import Button from '@components/Button'; -import HeaderPageLayout from '@components/HeaderPageLayout'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; -import Text from '@components/Text'; -import useLocalize from '@hooks/useLocalize'; -import useTheme from '@hooks/useTheme'; -import useThemeStyles from '@hooks/useThemeStyles'; -import useWindowDimensions from '@hooks/useWindowDimensions'; -import Navigation from '@libs/Navigation/Navigation'; -import variables from '@styles/variables'; -import * as Link from '@userActions/Link'; -import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; -import SCREENS from '@src/SCREENS'; - -function ExpensifyClassicModal() { - const styles = useThemeStyles(); - const {translate} = useLocalize(); - const {isExtraSmallScreenHeight} = useWindowDimensions(); - const theme = useTheme(); - - const navigateBack = () => { - Navigation.goBack(ROUTES.ONBOARD_MANAGE_EXPENSES); - }; - - const navigateToOldDot = () => { - Link.openOldDotLink(`${CONST.OLDDOT_URLS.INBOX}${CONST.OLDDOT_URLS.DISMMISSED_REASON}`); - }; - - return ( - - } - footer={ -