From 8a00e7eda76ac7ff91979b8eb38e8240bae0f662 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 11 Oct 2023 10:49:18 +0200 Subject: [PATCH 001/260] Migrated useKeyboardState.js hook to TypeScript. --- ...KeyboardState.js => withKeyboardState.tsx} | 41 +++++++++---------- src/hooks/useKeyboardState.js | 11 ----- src/hooks/useKeyboardState.ts | 10 +++++ 3 files changed, 30 insertions(+), 32 deletions(-) rename src/components/{withKeyboardState.js => withKeyboardState.tsx} (51%) delete mode 100644 src/hooks/useKeyboardState.js create mode 100644 src/hooks/useKeyboardState.ts diff --git a/src/components/withKeyboardState.js b/src/components/withKeyboardState.tsx similarity index 51% rename from src/components/withKeyboardState.js rename to src/components/withKeyboardState.tsx index 8ddf667b4e30..997f5db59619 100755 --- a/src/components/withKeyboardState.js +++ b/src/components/withKeyboardState.tsx @@ -1,20 +1,25 @@ -import React, {forwardRef, createContext, useEffect, useState} from 'react'; +import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useState} from 'react'; import {Keyboard} from 'react-native'; import PropTypes from 'prop-types'; -import getComponentDisplayName from '../libs/getComponentDisplayName'; +import getComponentDisplayName from "../libs/getComponentDisplayName"; -const KeyboardStateContext = createContext(null); -const keyboardStatePropTypes = { - /** Whether or not the keyboard is open */ - isKeyboardShown: PropTypes.bool.isRequired, +type KeyboardStateContextValue = { + /** Whether the keyboard is open */ + isKeyboardShown : boolean }; -const keyboardStateProviderPropTypes = { +type KeyboardStateProviderProps = { /* Actual content wrapped by this component */ - children: PropTypes.node.isRequired, + children: ReactNode }; -function KeyboardStateProvider(props) { +// TODO: Remove - left for backwards compatibility with existing components. +const keyboardStatePropTypes = { + /** Whether the keyboard is open */ + isKeyboardShown: PropTypes.bool.isRequired, +}; +const KeyboardStateContext = createContext(null); +function KeyboardStateProvider(props: KeyboardStateProviderProps) { const {children} = props; const [isKeyboardShown, setIsKeyboardShown] = useState(false); useEffect(() => { @@ -34,14 +39,10 @@ function KeyboardStateProvider(props) { return {children}; } -KeyboardStateProvider.propTypes = keyboardStateProviderPropTypes; -/** - * @param {React.Component} WrappedComponent - * @returns {React.Component} - */ -export default function withKeyboardState(WrappedComponent) { - const WithKeyboardState = forwardRef((props, ref) => ( +// eslint-disable-next-line @typescript-eslint/naming-convention +export default function withKeyboardState(WrappedComponent: ComponentType<{ref: ForwardedRef}>) { + const WithKeyboardState = forwardRef((props: Record, ref: React.ForwardedRef) => ( {(keyboardStateProps) => ( )} - - )); - - WithKeyboardState.displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent)})`; + )); + (WithKeyboardState as unknown as {displayName: string}).displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; return WithKeyboardState; } -export {KeyboardStateProvider, keyboardStatePropTypes, KeyboardStateContext}; +export {KeyboardStateProvider, keyboardStatePropTypes, type KeyboardStateContextValue, KeyboardStateContext}; diff --git a/src/hooks/useKeyboardState.js b/src/hooks/useKeyboardState.js deleted file mode 100644 index 8b57fb60f2b6..000000000000 --- a/src/hooks/useKeyboardState.js +++ /dev/null @@ -1,11 +0,0 @@ -import {useContext} from 'react'; -import {KeyboardStateContext} from '../components/withKeyboardState'; - -/** - * Hook for getting current state of keyboard - * whether or not the keyboard is open - * @returns {Object} - */ -export default function useKeyboardState() { - return useContext(KeyboardStateContext); -} diff --git a/src/hooks/useKeyboardState.ts b/src/hooks/useKeyboardState.ts new file mode 100644 index 000000000000..d7cc8d271503 --- /dev/null +++ b/src/hooks/useKeyboardState.ts @@ -0,0 +1,10 @@ +import {useContext} from 'react'; +import {KeyboardStateContext, KeyboardStateContextValue} from '../components/withKeyboardState'; + +/** + * Hook for getting current state of keyboard + * whether the keyboard is open + */ +export default function useKeyboardState(): KeyboardStateContextValue | null { + return useContext(KeyboardStateContext); +} From d3d050dd3fba985b9b85d25d8bbfa44746c500c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Fa=C5=82at?= Date: Wed, 11 Oct 2023 11:15:46 +0200 Subject: [PATCH 002/260] Added spacing. Co-authored-by: Michael (Mykhailo) Kravchenko --- src/components/withKeyboardState.tsx | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index 997f5db59619..e6853686c9b3 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -18,7 +18,9 @@ const keyboardStatePropTypes = { /** Whether the keyboard is open */ isKeyboardShown: PropTypes.bool.isRequired, }; + const KeyboardStateContext = createContext(null); + function KeyboardStateProvider(props: KeyboardStateProviderProps) { const {children} = props; const [isKeyboardShown, setIsKeyboardShown] = useState(false); From deb4a0842b366ed84a7619001c4fedcdfbc0f677 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Fri, 13 Oct 2023 08:29:39 +0200 Subject: [PATCH 003/260] Prettier applied. --- src/components/withKeyboardState.tsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index 997f5db59619..44e5960aaff2 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -1,16 +1,16 @@ import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useState} from 'react'; import {Keyboard} from 'react-native'; import PropTypes from 'prop-types'; -import getComponentDisplayName from "../libs/getComponentDisplayName"; +import getComponentDisplayName from '../libs/getComponentDisplayName'; type KeyboardStateContextValue = { /** Whether the keyboard is open */ - isKeyboardShown : boolean + isKeyboardShown: boolean; }; type KeyboardStateProviderProps = { /* Actual content wrapped by this component */ - children: ReactNode + children: ReactNode; }; // TODO: Remove - left for backwards compatibility with existing components. @@ -39,7 +39,6 @@ function KeyboardStateProvider(props: KeyboardStateProviderProps) { return {children}; } - // eslint-disable-next-line @typescript-eslint/naming-convention export default function withKeyboardState(WrappedComponent: ComponentType<{ref: ForwardedRef}>) { const WithKeyboardState = forwardRef((props: Record, ref: React.ForwardedRef) => ( @@ -53,7 +52,8 @@ export default function withKeyboardState(WrappedComponent: ComponentType<{ref: ref={ref} /> )} - )); + + )); (WithKeyboardState as unknown as {displayName: string}).displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; return WithKeyboardState; } From e8a054ae0e486142d04760027c74d15a1ff32cf1 Mon Sep 17 00:00:00 2001 From: Roksana Zawilowska Date: Fri, 3 Nov 2023 12:32:59 +0100 Subject: [PATCH 004/260] Fix 29405 cursor displayed before emoji on ios native --- src/components/Composer/index.ios.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js index e5dab3756594..3324cb8f3854 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.js @@ -109,8 +109,6 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. - // On Android the selection prop is required on the TextInput but this prop has issues on IOS - const propsToPass = _.omit(props, 'selection'); return ( ); From 326e443c3f2868cca74f1cec9816f2c8b53c8e13 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 15 Nov 2023 12:42:02 +0100 Subject: [PATCH 005/260] ref: migrating TextInput to TS --- src/components/Icon/index.tsx | 2 + .../{index.native.js => index.native.tsx} | 0 .../BaseTextInput/{index.js => index.tsx} | 95 ++++++++------- .../TextInput/BaseTextInput/types.ts | 109 ++++++++++++++++++ .../TextInputLabel/TextInputLabelPropTypes.js | 25 ---- .../{index.native.js => index.native.tsx} | 16 ++- .../TextInputLabel/{index.js => index.tsx} | 12 +- .../TextInput/TextInputLabel/types.ts | 20 ++++ 8 files changed, 188 insertions(+), 91 deletions(-) rename src/components/TextInput/BaseTextInput/{index.native.js => index.native.tsx} (100%) rename src/components/TextInput/BaseTextInput/{index.js => index.tsx} (85%) create mode 100644 src/components/TextInput/BaseTextInput/types.ts delete mode 100644 src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js rename src/components/TextInput/TextInputLabel/{index.native.js => index.native.tsx} (76%) rename src/components/TextInput/TextInputLabel/{index.js => index.tsx} (67%) create mode 100644 src/components/TextInput/TextInputLabel/types.ts diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 022c740907ea..86e4a592045c 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -100,3 +100,5 @@ class Icon extends PureComponent { } export default Icon; + +export type {SrcProps}; diff --git a/src/components/TextInput/BaseTextInput/index.native.js b/src/components/TextInput/BaseTextInput/index.native.tsx similarity index 100% rename from src/components/TextInput/BaseTextInput/index.native.js rename to src/components/TextInput/BaseTextInput/index.native.tsx diff --git a/src/components/TextInput/BaseTextInput/index.js b/src/components/TextInput/BaseTextInput/index.tsx similarity index 85% rename from src/components/TextInput/BaseTextInput/index.js rename to src/components/TextInput/BaseTextInput/index.tsx index e643c6ff6b4f..8f5b8e4f5320 100644 --- a/src/components/TextInput/BaseTextInput/index.js +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,7 +1,6 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; -import _ from 'underscore'; +import {ActivityIndicator, Animated, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleSheet, TextInput, TextInputFocusEventData, View} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -22,21 +21,22 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; +import BaseTextInputProps from './types'; -function BaseTextInput(props) { - const initialValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput(props: BaseTextInputProps) { + const initialValue = props.value ?? props.defaultValue ?? ''; + const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); - const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [height, setHeight] = useState(variables.componentSizeLarge); + const [width, setWidth] = useState(); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -47,7 +47,7 @@ function BaseTextInput(props) { } if (props.shouldDelayFocus) { - const focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + const focusTimeout = setTimeout(() => input?.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } input.current.focus(); @@ -56,16 +56,16 @@ function BaseTextInput(props) { }, []); const animateLabel = useCallback( - (translateY, scale) => { + (translateY: number, scale: number) => { Animated.parallel([ Animated.spring(labelTranslateY, { toValue: translateY, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), Animated.spring(labelScale, { toValue: scale, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), ]).start(); @@ -74,7 +74,7 @@ function BaseTextInput(props) { ); const activateLabel = useCallback(() => { - const value = props.value || ''; + const value = props.value ?? ''; if (value.length < 0 || isLabelActive.current) { return; @@ -85,9 +85,9 @@ function BaseTextInput(props) { }, [animateLabel, props.value]); const deactivateLabel = useCallback(() => { - const value = props.value || ''; + const value = props.value ?? ''; - if (props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { return; } @@ -95,22 +95,22 @@ function BaseTextInput(props) { isLabelActive.current = false; }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); - const onFocus = (event) => { + const onFocus = (event: NativeSyntheticEvent) => { if (props.onFocus) { props.onFocus(event); } setIsFocused(true); }; - const onBlur = (event) => { + const onBlur = (event: NativeSyntheticEvent) => { if (props.onBlur) { props.onBlur(event); } setIsFocused(false); }; - const onPress = (event) => { - if (props.disabled) { + const onPress = (event?: GestureResponderEvent | KeyboardEvent) => { + if (!!props.disabled || !event) { return; } @@ -118,28 +118,28 @@ function BaseTextInput(props) { props.onPress(event); } - if (!event.isDefaultPrevented()) { - input.current.focus(); + if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { + input.current?.focus(); } }; const onLayout = useCallback( - (event) => { + (event: LayoutChangeEvent) => { if (!props.autoGrowHeight && props.multiline) { return; } const layout = event.nativeEvent.layout; - setWidth((prevWidth) => (props.autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight) => (!props.multiline ? layout.height : prevHeight)); + setWidth((prevWidth: number | undefined) => (props.autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight: number) => (!props.multiline ? layout.height : prevHeight)); }, [props.autoGrowHeight, props.multiline], ); // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value || ''; + const inputValue = props.value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -161,7 +161,7 @@ function BaseTextInput(props) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || !_.isEmpty(props.value)) { + if (props.value === undefined || !props.value) { return; } hasValueRef.current = false; @@ -169,17 +169,14 @@ function BaseTextInput(props) { /** * Set Value & activateLabel - * - * @param {String} value - * @memberof BaseTextInput */ - const setValue = (value) => { + const setValue = (value: string) => { if (props.onInputChange) { props.onInputChange(value); } - - Str.result(props.onChangeText, value); - + if (props.onChangeText) { + Str.result(props.onChangeText, value); + } if (value && value.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: @@ -192,7 +189,7 @@ function BaseTextInput(props) { }; const togglePasswordVisibility = useCallback(() => { - setPasswordHidden((prevPasswordHidden) => !prevPasswordHidden); + setPasswordHidden((prevPasswordHidden: boolean | undefined) => !prevPasswordHidden); }, []); // When adding a new prefix character, adjust this method to add expected character width. @@ -201,7 +198,7 @@ function BaseTextInput(props) { // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix) => { + const getCharacterPadding = (prefix: string) => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -212,20 +209,20 @@ function BaseTextInput(props) { // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label.length); - const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; + const hasLabel = Boolean(props.label?.length); + const isReadOnly = props.readOnly ?? props.disabled; const inputHelpText = props.errorText || props.hint; - const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; + const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; const textInputContainerStyles = StyleSheet.flatten([ styles.textInputContainer, - ...props.textInputContainerStyles, + props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), !props.hideFocusedState && isFocused && styles.borderColorFocus, - (props.hasError || props.errorText) && styles.borderColorDanger, + (!!props.hasError || !!props.errorText) && styles.borderColorDanger, props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, ]); - const isMultiline = props.multiline || props.autoGrowHeight; + const isMultiline = props.multiline ?? props.autoGrowHeight; /* To prevent text jumping caused by virtual DOM calculations on Safari and mobile Chrome, make sure to include the `lineHeight`. @@ -236,8 +233,8 @@ function BaseTextInput(props) { See https://github.com/Expensify/App/issues/13802 */ const lineHeight = useMemo(() => { - if ((Browser.isSafari() || Browser.isMobileChrome()) && _.isArray(props.inputStyle)) { - const lineHeightValue = _.find(props.inputStyle, (f) => f.lineHeight !== undefined); + if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(props.inputStyle)) { + const lineHeightValue = props.inputStyle.find((f) => f?.lineHeight !== undefined); if (lineHeightValue) { return lineHeightValue.lineHeight; } @@ -260,7 +257,7 @@ function BaseTextInput(props) { style={[ props.autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), !isMultiline && styles.componentHeightLarge, - ...props.containerStyles, + props.containerStyles, ]} > } e.preventDefault()} - accessibilityLabel={props.translate('common.visible')} + accessibilityLabel={props.translate?.('common.visible') ?? ''} > { let additionalWidth = 0; if (Browser.isMobileSafari() || Browser.isSafari()) { @@ -423,7 +420,5 @@ function BaseTextInput(props) { } BaseTextInput.displayName = 'BaseTextInput'; -BaseTextInput.propTypes = baseTextInputPropTypes.propTypes; -BaseTextInput.defaultProps = baseTextInputPropTypes.defaultProps; export default withLocalize(BaseTextInput); diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts new file mode 100644 index 000000000000..091a5d18379f --- /dev/null +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -0,0 +1,109 @@ +import React from 'react'; +import {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, TextInputProps, ViewStyle} from 'react-native'; +import {SrcProps} from '@components/Icon'; + +type CustomTextInputProps = { + /** Input label */ + label?: string; + + /** Name attribute for the input */ + name?: string; + + /** Input value */ + value?: string; + + /** Default value - used for non controlled inputs */ + defaultValue?: string; + + /** Input value placeholder */ + placeholder?: string; + + /** Error text to display */ + errorText: string | string[] | Record; + + /** Icon to display in right side of text input */ + icon: (props: SrcProps) => React.ReactNode; + + /** Customize the TextInput container */ + textInputContainerStyles: StyleProp; + + /** Customize the main container */ + containerStyles: StyleProp; + + /** input style */ + inputStyle: StyleProp; + + /** If present, this prop forces the label to remain in a position where it will not collide with input text */ + forceActiveLabel?: boolean; + + /** Should the input auto focus? */ + autoFocus?: boolean; + + /** Disable the virtual keyboard */ + disableKeyboard?: boolean; + + /** + * Autogrow input container length based on the entered text. + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrow?: boolean; + + /** + * Autogrow input container height based on the entered text + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrowHeight?: boolean; + + /** Hide the focus styles on TextInput */ + hideFocusedState?: boolean; + + /** Forward the inner ref */ + innerRef?: React.RefObject; + + /** Maximum characters allowed */ + maxLength?: number; + + /** Hint text to display below the TextInput */ + hint?: string; + + /** Prefix character */ + prefixCharacter?: string; + + /** Whether autoCorrect functionality should enable */ + autoCorrect?: boolean; + + /** Form props */ + /** The ID used to uniquely identify the input in a Form */ + inputID?: string; + + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft?: boolean; + + /** Callback to update the value on Form when input is used in the Form component. */ + onInputChange?: (value: string) => void; + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus?: boolean; + + /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ + submitOnEnter?: boolean; + + /** Indicate whether input is multiline */ + multiline?: boolean; + + /** Set the default value to the input if there is a valid saved value */ + shouldUseDefaultValue?: boolean; + + /** Indicate whether or not the input should prevent swipe actions in tabs */ + shouldInterceptSwipe?: boolean; + + hasError?: boolean; + onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + isLoading?: boolean; + translate?: (key: string) => string; +}; + +type BaseTextInputProps = CustomTextInputProps & TextInputProps; +export default BaseTextInputProps; diff --git a/src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js b/src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js deleted file mode 100644 index 82b98f17d808..000000000000 --- a/src/components/TextInput/TextInputLabel/TextInputLabelPropTypes.js +++ /dev/null @@ -1,25 +0,0 @@ -import PropTypes from 'prop-types'; -import {Animated} from 'react-native'; - -const propTypes = { - /** Label */ - label: PropTypes.string.isRequired, - - /** Label vertical translate */ - labelTranslateY: PropTypes.instanceOf(Animated.Value).isRequired, - - /** Label scale */ - labelScale: PropTypes.instanceOf(Animated.Value).isRequired, - - /** Whether the label is currently active or not */ - isLabelActive: PropTypes.bool.isRequired, - - /** For attribute for label */ - for: PropTypes.string, -}; - -const defaultProps = { - for: '', -}; - -export {propTypes, defaultProps}; diff --git a/src/components/TextInput/TextInputLabel/index.native.js b/src/components/TextInput/TextInputLabel/index.native.tsx similarity index 76% rename from src/components/TextInput/TextInputLabel/index.native.js rename to src/components/TextInput/TextInputLabel/index.native.tsx index 51b231287b1f..7260e7bc9976 100644 --- a/src/components/TextInput/TextInputLabel/index.native.js +++ b/src/components/TextInput/TextInputLabel/index.native.tsx @@ -2,9 +2,9 @@ import React, {useState} from 'react'; import {Animated} from 'react-native'; import * as styleConst from '@components/TextInput/styleConst'; import styles from '@styles/styles'; -import * as TextInputLabelPropTypes from './TextInputLabelPropTypes'; +import TextInputLabelProps from './types'; -function TextInputLabel(props) { +function TextInputLabel({isLabelActive, label, labelScale, labelTranslateY}: TextInputLabelProps) { const [width, setWidth] = useState(0); return ( @@ -16,29 +16,27 @@ function TextInputLabel(props) { style={[ styles.textInputLabel, styles.textInputLabelTransformation( - props.labelTranslateY, - props.labelScale.interpolate({ + labelTranslateY, + labelScale.interpolate({ inputRange: [styleConst.ACTIVE_LABEL_SCALE, styleConst.INACTIVE_LABEL_SCALE], outputRange: [-(width - width * styleConst.ACTIVE_LABEL_SCALE) / 2, 0], }), - props.labelScale, + labelScale, ), // If the label is active but the width is not ready yet, the above translateX value will be 0, // making the label sits at the top center instead of the top left of the input. To solve it // move the label by a percentage value with left style as translateX doesn't support percentage value. width === 0 && - props.isLabelActive && { + isLabelActive && { left: `${-((1 - styleConst.ACTIVE_LABEL_SCALE) * 100) / 2}%`, }, ]} > - {props.label} + {label} ); } -TextInputLabel.propTypes = TextInputLabelPropTypes.propTypes; -TextInputLabel.defaultProps = TextInputLabelPropTypes.defaultProps; TextInputLabel.displayName = 'TextInputLabel'; export default TextInputLabel; diff --git a/src/components/TextInput/TextInputLabel/index.js b/src/components/TextInput/TextInputLabel/index.tsx similarity index 67% rename from src/components/TextInput/TextInputLabel/index.js rename to src/components/TextInput/TextInputLabel/index.tsx index b49635b91d96..1db173c24d0c 100644 --- a/src/components/TextInput/TextInputLabel/index.js +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -1,17 +1,17 @@ import React, {useEffect, useRef} from 'react'; -import {Animated} from 'react-native'; +import {Animated, Text} from 'react-native'; import styles from '@styles/styles'; import CONST from '@src/CONST'; -import {defaultProps, propTypes} from './TextInputLabelPropTypes'; +import TextInputLabelProps from './types'; -function TextInputLabel({for: inputId, label, labelTranslateY, labelScale}) { - const labelRef = useRef(null); +function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { + const labelRef = useRef(null); useEffect(() => { if (!inputId || !labelRef.current) { return; } - labelRef.current.setAttribute('for', inputId); + (labelRef.current as unknown as HTMLFormElement).setAttribute('for', inputId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); @@ -27,7 +27,5 @@ function TextInputLabel({for: inputId, label, labelTranslateY, labelScale}) { } TextInputLabel.displayName = 'TextInputLabel'; -TextInputLabel.propTypes = propTypes; -TextInputLabel.defaultProps = defaultProps; export default React.memo(TextInputLabel); diff --git a/src/components/TextInput/TextInputLabel/types.ts b/src/components/TextInput/TextInputLabel/types.ts new file mode 100644 index 000000000000..6f85eef18f42 --- /dev/null +++ b/src/components/TextInput/TextInputLabel/types.ts @@ -0,0 +1,20 @@ +import {Animated} from 'react-native'; + +type TextInputLabelProps = { + /** Label */ + label: string; + + /** Label vertical translate */ + labelTranslateY: Animated.Value; + + /** Label scale */ + labelScale: Animated.Value; + + /** Whether the label is currently active or not */ + isLabelActive: boolean; + + /** For attribute for label */ + for?: string; +}; + +export default TextInputLabelProps; From 255058f4adf1540438b10807ce5a8eaa2f857318 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 15 Nov 2023 16:39:04 +0100 Subject: [PATCH 006/260] fix: fixing types problems in BaseTextInput for Web --- .../TextInput/BaseTextInput/index.native.tsx | 62 ++++++++++--------- .../TextInput/BaseTextInput/index.tsx | 45 ++++++++++---- .../TextInput/BaseTextInput/types.ts | 18 +++--- 3 files changed, 75 insertions(+), 50 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 5c3e19a2d94c..89c8f67a1026 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,6 +1,6 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, View} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; import _ from 'underscore'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; @@ -22,10 +22,11 @@ import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; +import BaseTextInputProps from './types'; -function BaseTextInput(props) { - const initialValue = props.value || props.defaultValue || ''; - const initialActiveLabel = props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput(props: BaseTextInputProps) { + const initialValue = props.value ?? props.defaultValue ?? ''; + const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); @@ -36,7 +37,7 @@ function BaseTextInput(props) { const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -47,7 +48,7 @@ function BaseTextInput(props) { } if (props.shouldDelayFocus) { - const focusTimeout = setTimeout(() => input.current.focus(), CONST.ANIMATED_TRANSITION); + const focusTimeout = setTimeout(() => input.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } input.current.focus(); @@ -56,16 +57,16 @@ function BaseTextInput(props) { }, []); const animateLabel = useCallback( - (translateY, scale) => { + (translateY: number, scale: number) => { Animated.parallel([ Animated.spring(labelTranslateY, { toValue: translateY, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), Animated.spring(labelScale, { toValue: scale, - duration: styleConst.LABEL_ANIMATION_DURATION, + // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), ]).start(); @@ -85,9 +86,9 @@ function BaseTextInput(props) { }, [animateLabel, props.value]); const deactivateLabel = useCallback(() => { - const value = props.value || ''; + const value = props.value ?? ''; - if (props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { return; } @@ -119,7 +120,7 @@ function BaseTextInput(props) { } if (!event.isDefaultPrevented()) { - input.current.focus(); + input.current?.focus(); } }; @@ -139,7 +140,7 @@ function BaseTextInput(props) { // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value || ''; + const inputValue = props.value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -169,9 +170,6 @@ function BaseTextInput(props) { /** * Set Value & activateLabel - * - * @param {String} value - * @memberof BaseTextInput */ const setValue = (value) => { if (props.onInputChange) { @@ -201,7 +199,7 @@ function BaseTextInput(props) { // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix) => { + const getCharacterPadding = (prefix: string) => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -212,20 +210,21 @@ function BaseTextInput(props) { // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label.length); + const hasLabel = Boolean(props.label?.length); const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; const inputHelpText = props.errorText || props.hint; - const placeholder = props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : null; + const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; + console.log('maxHeight', maxHeight); const textInputContainerStyles = StyleSheet.flatten([ styles.textInputContainer, - ...props.textInputContainerStyles, + props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), !props.hideFocusedState && isFocused && styles.borderColorFocus, - (props.hasError || props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, + (!!props.hasError || props.errorText) && styles.borderColorDanger, + props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = props.multiline || props.autoGrowHeight; + const isMultiline = !!props.multiline || props.autoGrowHeight; return ( <> @@ -241,7 +240,7 @@ function BaseTextInput(props) { style={[ props.autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), !isMultiline && styles.componentHeightLarge, - ...props.containerStyles, + props.containerStyles, ]} > } - {!_.isEmpty(inputHelpText) && ( + {!inputHelpText && ( )} @@ -375,12 +374,17 @@ function BaseTextInput(props) { This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {(props.autoGrow || props.autoGrowHeight) && ( + {(!!props.autoGrow || props.autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 { setTextInputWidth(e.nativeEvent.layout.width); setTextInputHeight(e.nativeEvent.layout.height); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 8f5b8e4f5320..77b2458c6b41 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,6 +1,19 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleSheet, TextInput, TextInputFocusEventData, View} from 'react-native'; +import { + ActivityIndicator, + Animated, + FlexStyle, + GestureResponderEvent, + LayoutChangeEvent, + NativeSyntheticEvent, + StyleProp, + StyleSheet, + TextInput, + TextInputFocusEventData, + View, + ViewStyle, +} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -207,20 +220,19 @@ function BaseTextInput(props: BaseTextInputProps) { } }; - // eslint-disable-next-line react/forbid-foreign-prop-types const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); const hasLabel = Boolean(props.label?.length); const isReadOnly = props.readOnly ?? props.disabled; const inputHelpText = props.errorText || props.hint; const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles = StyleSheet.flatten([ + const textInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), !props.hideFocusedState && isFocused && styles.borderColorFocus, (!!props.hasError || !!props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: 2 * maxHeight}, + props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); const isMultiline = props.multiline ?? props.autoGrowHeight; @@ -234,8 +246,8 @@ function BaseTextInput(props: BaseTextInputProps) { const lineHeight = useMemo(() => { if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(props.inputStyle)) { - const lineHeightValue = props.inputStyle.find((f) => f?.lineHeight !== undefined); - if (lineHeightValue) { + const lineHeightValue = props.inputStyle.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); + if (lineHeightValue && 'lineHeight' in lineHeightValue) { return lineHeightValue.lineHeight; } } @@ -253,7 +265,8 @@ function BaseTextInput(props: BaseTextInputProps) { { if (typeof props.innerRef === 'function') { props.innerRef(ref); - } else if (props.innerRef && _.has(props.innerRef, 'current')) { + } else if (props.innerRef && 'current' in props.innerRef) { // eslint-disable-next-line no-param-reassign props.innerRef.current = ref; } + // @ts-expect-error We need to reassign this ref to the input ref input.current = ref; }} // eslint-disable-next-line @@ -318,7 +332,7 @@ function BaseTextInput(props: BaseTextInputProps) { styles.w100, props.inputStyle, (!hasLabel || isMultiline) && styles.pv0, - props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + !!props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), props.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear @@ -330,7 +344,7 @@ function BaseTextInput(props: BaseTextInputProps) { !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. props.disabled && styles.textInputDisabled, styles.pointerEventsAuto, @@ -383,9 +397,9 @@ function BaseTextInput(props: BaseTextInputProps) { - {!_.isEmpty(inputHelpText) && ( + {!inputHelpText && ( )} @@ -401,7 +415,12 @@ function BaseTextInput(props: BaseTextInputProps) { // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 { let additionalWidth = 0; if (Browser.isMobileSafari() || Browser.isSafari()) { diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 091a5d18379f..de453802b2b6 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,8 +1,9 @@ -import React from 'react'; -import {GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, TextInputProps, ViewStyle} from 'react-native'; +import React, {Component} from 'react'; +import {FlexStyle, GestureResponderEvent, NativeSyntheticEvent, StyleProp, TextInput, TextInputFocusEventData, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import {SrcProps} from '@components/Icon'; -type CustomTextInputProps = { +type CustomBaseTextInputProps = { /** Input label */ label?: string; @@ -25,13 +26,13 @@ type CustomTextInputProps = { icon: (props: SrcProps) => React.ReactNode; /** Customize the TextInput container */ - textInputContainerStyles: StyleProp; + textInputContainerStyles: StyleProp; /** Customize the main container */ - containerStyles: StyleProp; + containerStyles: StyleProp; /** input style */ - inputStyle: StyleProp; + inputStyle: StyleProp; /** If present, this prop forces the label to remain in a position where it will not collide with input text */ forceActiveLabel?: boolean; @@ -60,7 +61,7 @@ type CustomTextInputProps = { hideFocusedState?: boolean; /** Forward the inner ref */ - innerRef?: React.RefObject; + innerRef?: React.ForwardedRef, unknown, unknown> | null>; /** Maximum characters allowed */ maxLength?: number; @@ -105,5 +106,6 @@ type CustomTextInputProps = { translate?: (key: string) => string; }; -type BaseTextInputProps = CustomTextInputProps & TextInputProps; +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; export default BaseTextInputProps; +export type {CustomBaseTextInputProps}; From 7804843840e80f51d15f74adf41fbc86ae4ade70 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 16 Nov 2023 16:26:50 +0700 Subject: [PATCH 007/260] fix incorrect group displayed --- .../BaseCentralPaneNavigator.js | 13 ++++++++++++- src/libs/ReportUtils.js | 4 ++++ src/libs/actions/Report.js | 12 ++++++++++++ 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js index a1646011e560..5e4b916342d5 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js @@ -1,8 +1,10 @@ import {createStackNavigator} from '@react-navigation/stack'; +import lodashGet from 'lodash/get'; import React from 'react'; import ReportScreenWrapper from '@libs/Navigation/AppNavigator/ReportScreenWrapper'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import styles from '@styles/styles'; +import * as Report from '@userActions/Report'; import SCREENS from '@src/SCREENS'; const Stack = createStackNavigator(); @@ -12,7 +14,16 @@ const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : function BaseCentralPaneNavigator() { return ( - + { + const reportID = lodashGet(e, 'data.state.routes[0].params.reportID', ''); + if (reportID) { + Report.updateLastVisitTime(reportID); + } + }, + }} + > { + const lastReadTime = lodashGet(report, 'lastVisitTime') || lodashGet(report, 'lastReadTime'); + return {...report, lastReadTime}; + }) .toArray() .filter((report) => report && report.reportID && report.lastReadTime) .sortBy('lastReadTime') diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 22a1bc5441e6..55d6a5c242d7 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -2503,6 +2503,17 @@ function searchInServer(searchInput) { debouncedSearchInServer(searchInput); } +/** + * @param {String} reportID + */ + +function updateLastVisitTime(reportID) { + if (!ReportUtils.isValidReportIDFromPath(reportID)) { + return; + } + Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {lastVisitTime: DateUtils.getDBTime()}); +} + export { searchInServer, addComment, @@ -2564,4 +2575,5 @@ export { openRoomMembersPage, savePrivateNotesDraft, getDraftPrivateNote, + updateLastVisitTime, }; From b573de9b56fe0cb4516c7d3fd744699324bc4685 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 16 Nov 2023 11:40:08 +0100 Subject: [PATCH 008/260] fix: move inputProps const into native ones --- .../TextInput/BaseTextInput/index.native.tsx | 239 +++++++++++------- .../TextInput/BaseTextInput/index.tsx | 5 +- .../{index.native.js => index.native.tsx} | 0 .../TextInput/{index.js => index.tsx} | 0 .../{styleConst.js => styleConst.ts} | 0 src/components/TextInput/types.ts | 0 src/libs/getSecureEntryKeyboardType/types.ts | 4 +- 7 files changed, 149 insertions(+), 99 deletions(-) rename src/components/TextInput/{index.native.js => index.native.tsx} (100%) rename src/components/TextInput/{index.js => index.tsx} (100%) rename src/components/TextInput/{styleConst.js => styleConst.ts} (100%) create mode 100644 src/components/TextInput/types.ts diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 89c8f67a1026..d696a97232c6 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,7 +1,19 @@ import Str from 'expensify-common/lib/str'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; -import _ from 'underscore'; +import { + ActivityIndicator, + Animated, + FlexStyle, + GestureResponderEvent, + LayoutChangeEvent, + NativeSyntheticEvent, + StyleProp, + StyleSheet, + TextInput, + TextInputFocusEventData, + View, + ViewStyle, +} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -24,16 +36,50 @@ import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; -function BaseTextInput(props: BaseTextInputProps) { - const initialValue = props.value ?? props.defaultValue ?? ''; - const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput({ + label = '', + name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + innerRef = () => {}, + maxLength = null, + hint = '', + shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + translate, + ...inputProps +}: BaseTextInputProps) { + const {hasError = false} = inputProps; + console.log({inputProps}); + const initialValue = value ?? defaultValue ?? ''; + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); const [isFocused, setIsFocused] = useState(false); - const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); + const [passwordHidden, setPasswordHidden] = useState(inputProps.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); - const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [height, setHeight] = useState(variables.componentSizeLarge); + const [width, setWidth] = useState(); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; @@ -43,11 +89,11 @@ function BaseTextInput(props: BaseTextInputProps) { // AutoFocus which only works on mount: useEffect(() => { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 - if (!props.autoFocus || !input.current) { + if (!autoFocus || !input.current) { return; } - if (props.shouldDelayFocus) { + if (shouldDelayFocus) { const focusTimeout = setTimeout(() => input.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } @@ -75,72 +121,72 @@ function BaseTextInput(props: BaseTextInputProps) { ); const activateLabel = useCallback(() => { - const value = props.value || ''; + const inputValue = value ?? ''; - if (value.length < 0 || isLabelActive.current) { + if (inputValue.length < 0 || isLabelActive.current) { return; } animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); isLabelActive.current = true; - }, [animateLabel, props.value]); + }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - const value = props.value ?? ''; + const inputValue = value ?? ''; - if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!forceActiveLabel || inputValue.length !== 0 || prefixCharacter) { return; } animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); + }, [animateLabel, forceActiveLabel, prefixCharacter, value]); - const onFocus = (event) => { - if (props.onFocus) { - props.onFocus(event); + const onFocus = (event: NativeSyntheticEvent) => { + if (inputProps.onFocus) { + inputProps.onFocus(event); } setIsFocused(true); }; - const onBlur = (event) => { - if (props.onBlur) { - props.onBlur(event); + const onBlur = (event: NativeSyntheticEvent) => { + if (inputProps.onBlur) { + inputProps.onBlur(event); } setIsFocused(false); }; - const onPress = (event) => { - if (props.disabled) { + const onPress = (event?: GestureResponderEvent | KeyboardEvent) => { + if (!!inputProps.disabled || !event) { return; } - if (props.onPress) { - props.onPress(event); + if (inputProps.onPress) { + inputProps.onPress(event); } - if (!event.isDefaultPrevented()) { + if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { input.current?.focus(); } }; const onLayout = useCallback( - (event) => { - if (!props.autoGrowHeight && props.multiline) { + (event: LayoutChangeEvent) => { + if (!autoGrowHeight && multiline) { return; } const layout = event.nativeEvent.layout; - setWidth((prevWidth) => (props.autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight) => (!props.multiline ? layout.height : prevHeight)); + setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, - [props.autoGrowHeight, props.multiline], + [autoGrowHeight, multiline], ); // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value ?? ''; + const inputValue = value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -162,26 +208,28 @@ function BaseTextInput(props: BaseTextInputProps) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || !_.isEmpty(props.value)) { + if (value === undefined || value) { return; } hasValueRef.current = false; - }, [props.value]); + }, [value]); /** * Set Value & activateLabel */ - const setValue = (value) => { - if (props.onInputChange) { - props.onInputChange(value); + const setValue = (newValue: string) => { + if (onInputChange) { + onInputChange(newValue); } - Str.result(props.onChangeText, value); + if (inputProps.onChangeText) { + Str.result(inputProps.onChangeText, newValue); + } - if (value && value.length > 0) { + if (newValue && newValue.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: - if (props.value === undefined) { + if (value === undefined) { activateLabel(); } } else { @@ -209,38 +257,38 @@ function BaseTextInput(props: BaseTextInputProps) { }; // eslint-disable-next-line react/forbid-foreign-prop-types - const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label?.length); - const isReadOnly = _.isUndefined(props.readOnly) ? props.disabled : props.readOnly; - const inputHelpText = props.errorText || props.hint; - const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; - const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - console.log('maxHeight', maxHeight); - const textInputContainerStyles = StyleSheet.flatten([ + // const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); + const hasLabel = Boolean(label?.length); + const isReadOnly = inputProps.readOnly ?? inputProps.disabled; + const inputHelpText = errorText || hint; + const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; + const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, - props.textInputContainerStyles, - props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), - !props.hideFocusedState && isFocused && styles.borderColorFocus, - (!!props.hasError || props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, + textInputContainerStyles, + autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !hideFocusedState && isFocused && styles.borderColorFocus, + (!!hasError || errorText) && styles.borderColorDanger, + autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = !!props.multiline || props.autoGrowHeight; + const isMultiline = !!multiline || autoGrowHeight; return ( <> {hasLabel ? ( @@ -261,88 +309,89 @@ function BaseTextInput(props: BaseTextInputProps) { {isMultiline && } ) : null} - {Boolean(props.prefixCharacter) && ( + {Boolean(prefixCharacter) && ( - {props.prefixCharacter} + {prefixCharacter} )} { - if (typeof props.innerRef === 'function') { - props.innerRef(ref); - } else if (props.innerRef && _.has(props.innerRef, 'current')) { + if (typeof innerRef === 'function') { + innerRef(ref); + } else if (innerRef && 'current' in innerRef) { // eslint-disable-next-line no-param-reassign - props.innerRef.current = ref; + innerRef.current = ref; } + // @ts-expect-error We need to reassign this ref to the input ref input.current = ref; }} // eslint-disable-next-line {...inputProps} - autoCorrect={props.secureTextEntry ? false : props.autoCorrect} - placeholder={placeholder} + autoCorrect={inputProps.secureTextEntry ? false : autoCorrect} + placeholder={placeholderValue} placeholderTextColor={themeColors.placeholderText} underlineColorAndroid="transparent" style={[ styles.flex1, styles.w100, - props.inputStyle, + inputStyle, (!hasLabel || isMultiline) && styles.pv0, - props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), - props.secureTextEntry && styles.secureInput, + !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft), + inputProps.secureTextEntry && styles.secureInput, !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), // Add disabled color theme when field is not editable. - props.disabled && styles.textInputDisabled, + inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, ]} multiline={isMultiline} - maxLength={props.maxLength} + maxLength={maxLength} onFocus={onFocus} onBlur={onBlur} onChangeText={setValue} secureTextEntry={passwordHidden} - onPressOut={props.onPress} - showSoftInputOnFocus={!props.disableKeyboard} - keyboardType={getSecureEntryKeyboardType(props.keyboardType, props.secureTextEntry, passwordHidden)} - inputMode={!props.disableKeyboard ? props.inputMode : CONST.INPUT_MODE.NONE} - value={props.value} - selection={props.selection} + onPressOut={inputProps.onPress} + showSoftInputOnFocus={!disableKeyboard} + keyboardType={getSecureEntryKeyboardType(inputProps.keyboardType, inputProps.secureTextEntry ?? false, passwordHidden ?? false)} + inputMode={!disableKeyboard ? inputProps.inputMode : CONST.INPUT_MODE.NONE} + value={value} + selection={inputProps.selection} readOnly={isReadOnly} - defaultValue={props.defaultValue} + defaultValue={defaultValue} // FormSubmit Enter key handler does not have access to direct props. // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}} + dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> - {props.isLoading && ( + {inputProps.isLoading && ( )} - {Boolean(props.secureTextEntry) && ( + {Boolean(inputProps.secureTextEntry) && ( e.preventDefault()} - accessibilityLabel={props.translate('common.visible')} + accessibilityLabel={translate?.('common.visible') ?? ''} > )} - {!props.secureTextEntry && Boolean(props.icon) && ( + {!inputProps.secureTextEntry && Boolean(icon) && ( @@ -363,7 +412,7 @@ function BaseTextInput(props: BaseTextInputProps) { {!inputHelpText && ( )} @@ -374,14 +423,14 @@ function BaseTextInput(props: BaseTextInputProps) { This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {(!!props.autoGrow || props.autoGrowHeight) && ( + {(!!autoGrow || autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */} - {props.value ? `${props.value}${props.value.endsWith('\n') ? '\u200B' : ''}` : props.placeholder} + {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} )} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 77b2458c6b41..547e11b6cbb6 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -3,7 +3,6 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { ActivityIndicator, Animated, - FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, @@ -174,7 +173,7 @@ function BaseTextInput(props: BaseTextInputProps) { // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || !props.value) { + if (props.value === undefined || props.value) { return; } hasValueRef.current = false; @@ -226,7 +225,7 @@ function BaseTextInput(props: BaseTextInputProps) { const inputHelpText = props.errorText || props.hint; const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles: StyleProp = StyleSheet.flatten([ + const textInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, props.textInputContainerStyles, props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), diff --git a/src/components/TextInput/index.native.js b/src/components/TextInput/index.native.tsx similarity index 100% rename from src/components/TextInput/index.native.js rename to src/components/TextInput/index.native.tsx diff --git a/src/components/TextInput/index.js b/src/components/TextInput/index.tsx similarity index 100% rename from src/components/TextInput/index.js rename to src/components/TextInput/index.tsx diff --git a/src/components/TextInput/styleConst.js b/src/components/TextInput/styleConst.ts similarity index 100% rename from src/components/TextInput/styleConst.js rename to src/components/TextInput/styleConst.ts diff --git a/src/components/TextInput/types.ts b/src/components/TextInput/types.ts new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/src/libs/getSecureEntryKeyboardType/types.ts b/src/libs/getSecureEntryKeyboardType/types.ts index fe79440e3109..750c84133ea2 100644 --- a/src/libs/getSecureEntryKeyboardType/types.ts +++ b/src/libs/getSecureEntryKeyboardType/types.ts @@ -1,3 +1,5 @@ -type GetSecureEntryKeyboardType = (keyboardType: string, secureTextEntry: boolean, passwordHidden: boolean) => string; +import {KeyboardTypeOptions} from 'react-native'; + +type GetSecureEntryKeyboardType = (keyboardType: KeyboardTypeOptions | undefined, secureTextEntry: boolean, passwordHidden: boolean) => KeyboardTypeOptions | undefined; export default GetSecureEntryKeyboardType; From b568d32025186f0cd4ce39fcb1d211249662dbc1 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 16 Nov 2023 14:48:31 +0100 Subject: [PATCH 009/260] fix: destructure props --- .../TextInput/BaseTextInput/index.native.tsx | 2 - .../TextInput/BaseTextInput/index.tsx | 44 ++++++++++++++++--- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index d696a97232c6..bde3f880cfbf 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -66,11 +66,9 @@ function BaseTextInput({ autoCorrect = true, prefixCharacter, inputID, - translate, ...inputProps }: BaseTextInputProps) { const {hasError = false} = inputProps; - console.log({inputProps}); const initialValue = value ?? defaultValue ?? ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 547e11b6cbb6..4f83e87084ed 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -35,12 +35,44 @@ import CONST from '@src/CONST'; import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; -function BaseTextInput(props: BaseTextInputProps) { - const initialValue = props.value ?? props.defaultValue ?? ''; - const initialActiveLabel = !!props.forceActiveLabel || initialValue.length > 0 || Boolean(props.prefixCharacter); +function BaseTextInput({ + label = '', + name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + innerRef = () => {}, + maxLength = null, + hint = '', + shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + ...inputProps +}: BaseTextInputProps) { + const {hasError = false} = inputProps; + const initialValue = value ?? defaultValue ?? ''; + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); const [isFocused, setIsFocused] = useState(false); - const [passwordHidden, setPasswordHidden] = useState(props.secureTextEntry); + const [passwordHidden, setPasswordHidden] = useState(inputProps.secureTextEntry); const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); @@ -54,11 +86,11 @@ function BaseTextInput(props: BaseTextInputProps) { // AutoFocus which only works on mount: useEffect(() => { // We are manually managing focus to prevent this issue: https://github.com/Expensify/App/issues/4514 - if (!props.autoFocus || !input.current) { + if (!autoFocus || !input.current) { return; } - if (props.shouldDelayFocus) { + if (shouldDelayFocus) { const focusTimeout = setTimeout(() => input?.current?.focus(), CONST.ANIMATED_TRANSITION); return () => clearTimeout(focusTimeout); } From e90c503a36c75447619c9d8ef26a76108ecbecd0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 17 Nov 2023 10:54:41 +0100 Subject: [PATCH 010/260] fix: fixing types --- .../TextInput/BaseTextInput/index.tsx | 163 +++++++++--------- src/components/TextInput/index.tsx | 52 +++--- 2 files changed, 104 insertions(+), 111 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 4f83e87084ed..f86e33d1eab9 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -3,6 +3,7 @@ import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { ActivityIndicator, Animated, + FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, @@ -32,7 +33,6 @@ import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; function BaseTextInput({ @@ -118,48 +118,48 @@ function BaseTextInput({ ); const activateLabel = useCallback(() => { - const value = props.value ?? ''; + const newValue = value ?? ''; - if (value.length < 0 || isLabelActive.current) { + if (newValue.length < 0 || isLabelActive.current) { return; } animateLabel(styleConst.ACTIVE_LABEL_TRANSLATE_Y, styleConst.ACTIVE_LABEL_SCALE); isLabelActive.current = true; - }, [animateLabel, props.value]); + }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - const value = props.value ?? ''; + const newValue = value ?? ''; - if (!!props.forceActiveLabel || value.length !== 0 || props.prefixCharacter) { + if (!!forceActiveLabel || newValue.length !== 0 || prefixCharacter) { return; } animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, props.forceActiveLabel, props.prefixCharacter, props.value]); + }, [animateLabel, forceActiveLabel,prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { - if (props.onFocus) { - props.onFocus(event); + if (inputProps.onFocus) { + onFocus(event); } setIsFocused(true); }; const onBlur = (event: NativeSyntheticEvent) => { - if (props.onBlur) { - props.onBlur(event); + if (inputProps.onBlur) { + inputProps.onBlur(event); } setIsFocused(false); }; const onPress = (event?: GestureResponderEvent | KeyboardEvent) => { - if (!!props.disabled || !event) { + if (!!inputProps.disabled || !event) { return; } - if (props.onPress) { - props.onPress(event); + if (inputProps.onPress) { + inputProps.onPress(event); } if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { @@ -169,21 +169,21 @@ function BaseTextInput({ const onLayout = useCallback( (event: LayoutChangeEvent) => { - if (!props.autoGrowHeight && props.multiline) { + if (!autoGrowHeight && multiline) { return; } const layout = event.nativeEvent.layout; - setWidth((prevWidth: number | undefined) => (props.autoGrowHeight ? layout.width : prevWidth)); - setHeight((prevHeight: number) => (!props.multiline ? layout.height : prevHeight)); + setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, - [props.autoGrowHeight, props.multiline], + [autoGrowHeight, multiline], ); // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = props.value ?? ''; + const inputValue = value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -205,26 +205,26 @@ function BaseTextInput({ // When the value prop gets cleared externally, we need to keep the ref in sync: useEffect(() => { // Return early when component uncontrolled, or we still have a value - if (props.value === undefined || props.value) { + if (value === undefined || value) { return; } hasValueRef.current = false; - }, [props.value]); + }, [value]); /** * Set Value & activateLabel */ - const setValue = (value: string) => { - if (props.onInputChange) { - props.onInputChange(value); + const setValue = (newValue: string) => { + if (onInputChange) { + onInputChange(newValue); } - if (props.onChangeText) { - Str.result(props.onChangeText, value); + if (inputProps.onChangeText) { + Str.result(inputProps.onChangeText, newValue); } - if (value && value.length > 0) { + if (newValue && newValue.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: - if (props.value === undefined) { + if (value === undefined) { activateLabel(); } } else { @@ -251,21 +251,20 @@ function BaseTextInput({ } }; - const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); - const hasLabel = Boolean(props.label?.length); - const isReadOnly = props.readOnly ?? props.disabled; - const inputHelpText = props.errorText || props.hint; - const placeholder = !!props.prefixCharacter || isFocused || !hasLabel || (hasLabel && props.forceActiveLabel) ? props.placeholder : undefined; - const maxHeight = StyleSheet.flatten(props.containerStyles).maxHeight; - const textInputContainerStyles: StyleProp = StyleSheet.flatten([ + const hasLabel = Boolean(label?.length); + const isReadOnly = inputProps.readOnly ?? inputProps.disabled; + const inputHelpText = errorText || hint; + const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; + const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; + const textInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, - props.textInputContainerStyles, - props.autoGrow && StyleUtils.getWidthStyle(textInputWidth), - !props.hideFocusedState && isFocused && styles.borderColorFocus, - (!!props.hasError || !!props.errorText) && styles.borderColorDanger, - props.autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, + textInputContainerStyles, + autoGrow && StyleUtils.getWidthStyle(textInputWidth), + !hideFocusedState && isFocused && styles.borderColorFocus, + (!!hasError || !!errorText) && styles.borderColorDanger, + autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); - const isMultiline = props.multiline ?? props.autoGrowHeight; + const isMultiline = multiline ?? autoGrowHeight; /* To prevent text jumping caused by virtual DOM calculations on Safari and mobile Chrome, make sure to include the `lineHeight`. @@ -276,32 +275,32 @@ function BaseTextInput({ See https://github.com/Expensify/App/issues/13802 */ const lineHeight = useMemo(() => { - if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(props.inputStyle)) { - const lineHeightValue = props.inputStyle.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); + if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(inputStyle)) { + const lineHeightValue = inputStyle?.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); if (lineHeightValue && 'lineHeight' in lineHeightValue) { return lineHeightValue.lineHeight; } } return undefined; - }, [props.inputStyle]); + }, [inputStyle]); return ( <> {hasLabel ? ( @@ -322,49 +321,49 @@ function BaseTextInput({ {isMultiline && } ) : null} - {Boolean(props.prefixCharacter) && ( + {Boolean(prefixCharacter) && ( - {props.prefixCharacter} + {prefixCharacter} )} { - if (typeof props.innerRef === 'function') { - props.innerRef(ref); - } else if (props.innerRef && 'current' in props.innerRef) { + if (typeof innerRef === 'function') { + innerRef(ref); + } else if (innerRef && 'current' in innerRef) { // eslint-disable-next-line no-param-reassign - props.innerRef.current = ref; + innerRef.current = ref; } // @ts-expect-error We need to reassign this ref to the input ref input.current = ref; }} // eslint-disable-next-line {...inputProps} - autoCorrect={props.secureTextEntry ? false : props.autoCorrect} - placeholder={placeholder} + autoCorrect={inputProps.secureTextEntry ? false : autoCorrect} + placeholder={newPlaceholder} placeholderTextColor={themeColors.placeholderText} underlineColorAndroid="transparent" style={[ styles.flex1, styles.w100, - props.inputStyle, + inputStyle, (!hasLabel || isMultiline) && styles.pv0, - !!props.prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), - props.secureTextEntry && styles.secureInput, + !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + inputProps.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear // once it exceeds the input space (See https://github.com/Expensify/App/issues/13802) @@ -375,41 +374,41 @@ function BaseTextInput({ !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - props.autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. - props.disabled && styles.textInputDisabled, + inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, ]} multiline={isMultiline} - maxLength={props.maxLength} + maxLength={maxLength} onFocus={onFocus} onBlur={onBlur} onChangeText={setValue} secureTextEntry={passwordHidden} - onPressOut={props.onPress} - showSoftInputOnFocus={!props.disableKeyboard} - inputMode={props.inputMode} - value={props.value} - selection={props.selection} + onPressOut={inputProps.onPress} + showSoftInputOnFocus={!disableKeyboard} + inputMode={inputProps.inputMode} + value={value} + selection={inputProps.selection} readOnly={isReadOnly} - defaultValue={props.defaultValue} + defaultValue={defaultValue} // FormSubmit Enter key handler does not have access to direct props. // `dataset.submitOnEnter` is used to indicate that pressing Enter on this input should call the submit callback. - dataSet={{submitOnEnter: isMultiline && props.submitOnEnter}} + dataSet={{submitOnEnter: isMultiline && submitOnEnter}} /> - {props.isLoading && ( + {inputProps.isLoading && ( )} - {Boolean(props.secureTextEntry) && ( + {Boolean(inputProps.secureTextEntry) && ( e.preventDefault()} - accessibilityLabel={props.translate?.('common.visible') ?? ''} + accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} > )} - {!props.secureTextEntry && Boolean(props.icon) && ( + {!inputProps.secureTextEntry && Boolean(icon) && ( @@ -430,7 +429,7 @@ function BaseTextInput({ {!inputHelpText && ( )} @@ -441,14 +440,14 @@ function BaseTextInput({ This text view is used to calculate width or height of the input value given textStyle in this component. This Text component is intentionally positioned out of the screen. */} - {(!!props.autoGrow || props.autoGrowHeight) && ( + {(!!autoGrow || autoGrowHeight) && ( // Add +2 to width on Safari browsers so that text is not cut off due to the cursor or when changing the value // https://github.com/Expensify/App/issues/8158 // https://github.com/Expensify/App/issues/26628 {/* \u200B added to solve the issue of not expanding the text input enough when the value ends with '\n' (https://github.com/Expensify/App/issues/21271) */} - {props.value ? `${props.value}${props.value.endsWith('\n') ? '\u200B' : ''}` : props.placeholder} + {value ? `${value}${value.endsWith('\n') ? '\u200B' : ''}` : placeholder} )} diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 044399ec6e11..c8dc21b97310 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,26 +1,26 @@ -import React, {useEffect, useRef} from 'react'; -import _ from 'underscore'; +import React, {ForwardedRef, useEffect, useRef} from 'react'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import styles from '@styles/styles'; import BaseTextInput from './BaseTextInput'; import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; +import BaseTextInputProps from './BaseTextInput/types'; import * as styleConst from './styleConst'; -function TextInput(props) { - const textInputRef = useRef(null); - const removeVisibilityListenerRef = useRef(null); +function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { + const textInputRef = useRef(null); + const removeVisibilityListenerRef = useRef<() => void>(null); useEffect(() => { if (props.disableKeyboard) { - textInputRef.current.setAttribute('inputmode', 'none'); + textInputRef.current?.setAttribute('inputmode', 'none'); } if (props.name) { - textInputRef.current.setAttribute('name', props.name); + textInputRef.current?.setAttribute('name', props.name); } - + // @ts-expect-error We need to reassign this ref to the input ref removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { if (!Browser.isMobileChrome() || !Visibility.isVisible() || !textInputRef.current || DomUtils.getActiveElement() !== textInputRef.current) { return; @@ -38,33 +38,37 @@ function TextInput(props) { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isLabeledMultiline = Boolean(props.label.length) && props.multiline; + const isLabeledMultiline = Boolean(props.label?.length) && props.multiline; const labelAnimationStyle = { + // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-translate-y': `${styleConst.ACTIVE_LABEL_TRANSLATE_Y}px`, + // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-scale': `${styleConst.ACTIVE_LABEL_SCALE}`, + // eslint-disable-next-line @typescript-eslint/naming-convention '--label-transition-duration': `${styleConst.LABEL_ANIMATION_DURATION}ms`, }; return ( { + innerRef={(el: HTMLFormElement | null) => { + // @ts-expect-error We need to reassign this ref to the input ref textInputRef.current = el; - if (!props.innerRef) { + if (!ref) { return; } - if (_.isFunction(props.innerRef)) { - props.innerRef(el); + if (typeof ref === 'function') { + ref(el); return; } // eslint-disable-next-line no-param-reassign - props.innerRef.current = el; + ref.current = el; }} - inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, ...props.inputStyle]} - textInputContainerStyles={[labelAnimationStyle, ...props.textInputContainerStyles]} + inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, props.inputStyle]} + textInputContainerStyles={[labelAnimationStyle, props.textInputContainerStyles]} + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} /> ); } @@ -73,14 +77,4 @@ TextInput.displayName = 'TextInput'; TextInput.propTypes = baseTextInputPropTypes.propTypes; TextInput.defaultProps = baseTextInputPropTypes.defaultProps; -const TextInputWithRef = React.forwardRef((props, ref) => ( - -)); - -TextInputWithRef.displayName = 'TextInputWithRef'; - -export default TextInputWithRef; +export default React.forwardRef(TextInput); From 6a3a27004030f0d47c2ebff20974beb23190a4e4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 17 Nov 2023 15:38:55 +0100 Subject: [PATCH 011/260] fix: resolving types issues --- .../TextInput/BaseTextInput/index.native.tsx | 101 ++++++++--------- .../TextInput/BaseTextInput/index.tsx | 102 +++++++++--------- .../TextInput/BaseTextInput/types.ts | 15 ++- src/components/TextInput/index.native.tsx | 30 +++--- src/components/TextInput/index.tsx | 27 +++-- src/components/TextInput/types.ts | 0 6 files changed, 143 insertions(+), 132 deletions(-) delete mode 100644 src/components/TextInput/types.ts diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index bde3f880cfbf..d8ecb67517f2 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React, {useCallback, useEffect, useRef, useState} from 'react'; +import React, {Component, ForwardedRef, forwardRef, useCallback, useEffect, useRef, useState} from 'react'; import { ActivityIndicator, Animated, @@ -11,9 +11,11 @@ import { StyleSheet, TextInput, TextInputFocusEventData, + TextInputProps, View, ViewStyle, } from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -33,41 +35,42 @@ import * as StyleUtils from '@styles/StyleUtils'; import themeColors from '@styles/themes/default'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import * as baseTextInputPropTypes from './baseTextInputPropTypes'; import BaseTextInputProps from './types'; -function BaseTextInput({ - label = '', - name = '', - value = undefined, - defaultValue = undefined, - placeholder = '', - errorText = '', - icon = null, - textInputContainerStyles = [], - containerStyles = [], - inputStyle = [], - forceActiveLabel = false, - autoFocus = false, - disableKeyboard = false, - autoGrow = false, - autoGrowHeight = false, - hideFocusedState = false, - innerRef = () => {}, - maxLength = null, - hint = '', - shouldSaveDraft = false, - onInputChange = () => {}, - shouldDelayFocus = false, - submitOnEnter = false, - multiline = false, - shouldUseDefaultValue = false, - shouldInterceptSwipe = false, - autoCorrect = true, - prefixCharacter, - inputID, - ...inputProps -}: BaseTextInputProps) { +function BaseTextInput( + { + label = '', + // name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + maxLength = undefined, + hint = '', + // shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + // shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + ...inputProps + }: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) { const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); @@ -195,7 +198,7 @@ function BaseTextInput({ isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current) + isInputAutoFilled(input.current as unknown as Element) ) { activateLabel(); } else { @@ -266,7 +269,7 @@ function BaseTextInput({ textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), !hideFocusedState && isFocused && styles.borderColorFocus, - (!!hasError || errorText) && styles.borderColorDanger, + (!!hasError || !!errorText) && styles.borderColorDanger, autoGrowHeight && {scrollPaddingTop: typeof maxHeight === 'number' ? 2 * maxHeight : undefined}, ]); const isMultiline = !!multiline || autoGrowHeight; @@ -284,7 +287,7 @@ function BaseTextInput({ accessibilityLabel={label ?? ''} accessible style={[ - autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), + autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, containerStyles, ]} @@ -327,15 +330,15 @@ function BaseTextInput({ )} { - if (typeof innerRef === 'function') { - innerRef(ref); - } else if (innerRef && 'current' in innerRef) { + ref={(el) => { + if (typeof ref === 'function') { + ref(el); + } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - innerRef.current = ref; + ref.current = el; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = ref; + input.current = el; }} // eslint-disable-next-line {...inputProps} @@ -354,7 +357,7 @@ function BaseTextInput({ !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, @@ -389,7 +392,7 @@ function BaseTextInput({ style={[styles.flex1, styles.textInputIconContainer]} onPress={togglePasswordVisibility} onMouseDown={(e) => e.preventDefault()} - accessibilityLabel={translate?.('common.visible') ?? ''} + accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} > )} - {!inputProps.secureTextEntry && Boolean(icon) && ( + {!inputProps.secureTextEntry && icon && ( - {!inputHelpText && ( + {!!inputHelpText && ( {}, - maxLength = null, - hint = '', - shouldSaveDraft = false, - onInputChange = () => {}, - shouldDelayFocus = false, - submitOnEnter = false, - multiline = false, - shouldUseDefaultValue = false, - shouldInterceptSwipe = false, - autoCorrect = true, - prefixCharacter, - inputID, - ...inputProps -}: BaseTextInputProps) { +function BaseTextInput( + { + label = '', + // name = '', + value = undefined, + defaultValue = undefined, + placeholder = '', + errorText = '', + icon = null, + textInputContainerStyles = [], + containerStyles = [], + inputStyle = [], + forceActiveLabel = false, + autoFocus = false, + disableKeyboard = false, + autoGrow = false, + autoGrowHeight = false, + hideFocusedState = false, + maxLength = undefined, + hint = '', + // shouldSaveDraft = false, + onInputChange = () => {}, + shouldDelayFocus = false, + submitOnEnter = false, + multiline = false, + // shouldUseDefaultValue = false, + shouldInterceptSwipe = false, + autoCorrect = true, + prefixCharacter, + inputID, + ...inputProps + }: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) { const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); @@ -137,7 +141,7 @@ function BaseTextInput({ animateLabel(styleConst.INACTIVE_LABEL_TRANSLATE_Y, styleConst.INACTIVE_LABEL_SCALE); isLabelActive.current = false; - }, [animateLabel, forceActiveLabel,prefixCharacter, value]); + }, [animateLabel, forceActiveLabel, prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { if (inputProps.onFocus) { @@ -194,7 +198,7 @@ function BaseTextInput({ isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current) + isInputAutoFilled(input.current as unknown as Element) ) { activateLabel(); } else { @@ -256,7 +260,7 @@ function BaseTextInput({ const inputHelpText = errorText || hint; const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; - const textInputContainerStyles: StyleProp = StyleSheet.flatten([ + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), @@ -298,7 +302,7 @@ function BaseTextInput({ accessibilityLabel={label ?? ''} accessible style={[ - autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, maxHeight), + autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, containerStyles, ]} @@ -308,10 +312,10 @@ function BaseTextInput({ // or if multiline is not supplied we calculate the textinput height, using onLayout. onLayout={onLayout} style={[ - textInputContainerStyles, + newTextInputContainerStyles, // When autoGrow is on and minWidth is not supplied, add a minWidth to allow the input to be focusable. - autoGrow && !textInputContainerStyles?.minWidth && styles.mnw2, + autoGrow && !newTextInputContainerStyles?.minWidth && styles.mnw2, ]} > {hasLabel ? ( @@ -341,15 +345,15 @@ function BaseTextInput({ )} { - if (typeof innerRef === 'function') { - innerRef(ref); - } else if (innerRef && 'current' in innerRef) { + ref={(el) => { + if (typeof ref === 'function') { + ref(el); + } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - innerRef.current = ref; + ref.current = el; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = ref; + input.current = el; }} // eslint-disable-next-line {...inputProps} @@ -362,7 +366,7 @@ function BaseTextInput({ styles.w100, inputStyle, (!hasLabel || isMultiline) && styles.pv0, - !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(props.prefixCharacter) + styles.pl1.paddingLeft), + !!prefixCharacter && StyleUtils.getPaddingLeft(getCharacterPadding(prefixCharacter) + styles.pl1.paddingLeft), inputProps.secureTextEntry && styles.secureInput, // Explicitly remove `lineHeight` from single line inputs so that long text doesn't disappear @@ -416,7 +420,7 @@ function BaseTextInput({ /> )} - {!inputProps.secureTextEntry && Boolean(icon) && ( + {!inputProps.secureTextEntry && icon && ( - {!inputHelpText && ( + {!!inputHelpText && ( ; /** Icon to display in right side of text input */ - icon: (props: SrcProps) => React.ReactNode; + icon: ((props: SrcProps) => React.ReactNode) | null; /** Customize the TextInput container */ textInputContainerStyles: StyleProp; @@ -60,9 +60,6 @@ type CustomBaseTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; - /** Forward the inner ref */ - innerRef?: React.ForwardedRef, unknown, unknown> | null>; - /** Maximum characters allowed */ maxLength?: number; @@ -103,9 +100,9 @@ type CustomBaseTextInputProps = { hasError?: boolean; onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; isLoading?: boolean; - translate?: (key: string) => string; + autoCompleteType?: string; }; -type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; export default BaseTextInputProps; export type {CustomBaseTextInputProps}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index a4d0c76337ab..4ddc64c50de4 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -1,12 +1,17 @@ -import React, {forwardRef, useEffect} from 'react'; -import {AppState, Keyboard} from 'react-native'; +import React, {Component, ForwardedRef, forwardRef, useEffect} from 'react'; +import {AppState, Keyboard, TextInputProps} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import styles from '@styles/styles'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; +import BaseTextInputProps from './BaseTextInput/types'; -const TextInput = forwardRef((props, ref) => { +// eslint-disable-next-line react/function-component-definition +const TextInput = ( + {inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) => { useEffect(() => { - if (!props.disableKeyboard) { + if (!disableKeyboard) { return; } @@ -21,8 +26,7 @@ const TextInput = forwardRef((props, ref) => { return () => { appStateSubscription.remove(); }; - }, [props.disableKeyboard]); - + }, [disableKeyboard]); return ( { // Setting autoCompleteType to new-password throws an error on Android/iOS, so fall back to password in that case // eslint-disable-next-line react/jsx-props-no-multi-spaces autoCompleteType={props.autoCompleteType === 'new-password' ? 'password' : props.autoCompleteType} - innerRef={ref} - inputStyle={[styles.baseTextInput, ...props.inputStyle]} + ref={ref} + inputStyle={[styles.baseTextInput, inputStyle]} /> ); -}); +}; -TextInput.propTypes = baseTextInputPropTypes.propTypes; -TextInput.defaultProps = baseTextInputPropTypes.defaultProps; +// TextInput.propTypes = baseTextInputPropTypes.propTypes; +// TextInput.defaultProps = baseTextInputPropTypes.defaultProps; TextInput.displayName = 'TextInput'; -export default TextInput; +export default forwardRef(TextInput); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index c8dc21b97310..582c35e94958 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,4 +1,6 @@ -import React, {ForwardedRef, useEffect, useRef} from 'react'; +import React, {Component, ForwardedRef, useEffect, useRef} from 'react'; +import {StyleProp, TextInputProps, ViewStyle} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; @@ -8,17 +10,20 @@ import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes' import BaseTextInputProps from './BaseTextInput/types'; import * as styleConst from './styleConst'; -function TextInput(props: BaseTextInputProps, ref: ForwardedRef) { +function TextInput( + {label = '', name = '', textInputContainerStyles, inputStyle, disableKeyboard = false, multiline = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, + ref: ForwardedRef, unknown, unknown>>, +) { const textInputRef = useRef(null); const removeVisibilityListenerRef = useRef<() => void>(null); useEffect(() => { - if (props.disableKeyboard) { + if (disableKeyboard) { textInputRef.current?.setAttribute('inputmode', 'none'); } - if (props.name) { - textInputRef.current?.setAttribute('name', props.name); + if (name) { + textInputRef.current?.setAttribute('name', name); } // @ts-expect-error We need to reassign this ref to the input ref removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { @@ -38,7 +43,7 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isLabeledMultiline = Boolean(props.label?.length) && props.multiline; + const isLabeledMultiline = Boolean(label?.length) && multiline; const labelAnimationStyle = { // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-translate-y': `${styleConst.ACTIVE_LABEL_TRANSLATE_Y}px`, @@ -50,7 +55,9 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef return ( { + // eslint-disable-next-line react/jsx-props-no-spreading + {...props} + ref={(el) => { // @ts-expect-error We need to reassign this ref to the input ref textInputRef.current = el; if (!ref) { @@ -65,10 +72,8 @@ function TextInput(props: BaseTextInputProps, ref: ForwardedRef // eslint-disable-next-line no-param-reassign ref.current = el; }} - inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, props.inputStyle]} - textInputContainerStyles={[labelAnimationStyle, props.textInputContainerStyles]} - // eslint-disable-next-line react/jsx-props-no-spreading - {...props} + inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, inputStyle]} + textInputContainerStyles={[labelAnimationStyle as StyleProp, textInputContainerStyles]} /> ); } diff --git a/src/components/TextInput/types.ts b/src/components/TextInput/types.ts deleted file mode 100644 index e69de29bb2d1..000000000000 From a57d229f1fd88f721cf9662db01636d434b6ae17 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 17 Nov 2023 18:34:18 +0100 Subject: [PATCH 012/260] fix: remove getDisplayNameForTypingIndicator --- src/libs/actions/PersonalDetails.ts | 26 ---------- .../home/report/ReportTypingIndicator.js | 47 +++++++++---------- 2 files changed, 22 insertions(+), 51 deletions(-) diff --git a/src/libs/actions/PersonalDetails.ts b/src/libs/actions/PersonalDetails.ts index 2d51fbb9e8d2..725159c1c7f3 100644 --- a/src/libs/actions/PersonalDetails.ts +++ b/src/libs/actions/PersonalDetails.ts @@ -59,31 +59,6 @@ function getDisplayName(login: string, personalDetail: Pick value?.login === userAccountIDOrLogin)?.[1]; - - // It's possible for displayName to be empty string, so we must use "||" to fallback to userAccountIDOrLogin. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return detailsByLogin?.displayName || userAccountIDOrLogin; - } - - const detailsByAccountID = allPersonalDetails?.[accountID]; - - // It's possible for displayName to be empty string, so we must use "||" to fallback to login or defaultDisplayName. - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - return detailsByAccountID?.displayName || detailsByAccountID?.login || defaultDisplayName; -} - /** * Gets the first and last name from the user's personal details. * If the login is the same as the displayName, then they don't exist, @@ -584,7 +559,6 @@ export { extractFirstAndLastNameFromAvailableDetails, getCountryISO, getDisplayName, - getDisplayNameForTypingIndicator, getPrivatePersonalDetails, openPersonalDetailsPage, openPublicProfilePage, diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 3a2c611ac358..c2c9a0222542 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -2,24 +2,18 @@ import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; import Text from '@components/Text'; import TextWithEllipsis from '@components/TextWithEllipsis'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import compose from '@libs/compose'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; +import * as ReportUtils from '@libs/ReportUtils'; import useThemeStyles from '@styles/useThemeStyles'; -import * as PersonalDetails from '@userActions/PersonalDetails'; import ONYXKEYS from '@src/ONYXKEYS'; const propTypes = { /** Key-value pairs of user accountIDs/logins and whether or not they are typing. Keys are accountIDs or logins. */ userTypingStatuses: PropTypes.objectOf(PropTypes.bool), - - /** Information about the network */ - network: networkPropTypes.isRequired, - - ...withLocalizePropTypes, }; const defaultProps = { @@ -27,13 +21,20 @@ const defaultProps = { }; function ReportTypingIndicator(props) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(props.userTypingStatuses), (loginOrAccountID) => props.userTypingStatuses[loginOrAccountID]), [props.userTypingStatuses]); // If we are offline, the user typing statuses are not up-to-date so do not show them - if (props.network.isOffline) { + if (isOffline) { return null; } + const firstUserTyping = usersTyping[0]; + const firstUserTypingID = Number.isNaN(firstUserTyping) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping; + const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); + const numUsersTyping = _.size(usersTyping); // Decide on the Text element that will hold the display based on the number of users that are typing. @@ -44,8 +45,8 @@ function ReportTypingIndicator(props) { case 1: return ( - {props.translate('reportTypingIndicator.multipleUsers')} - {` ${props.translate('reportTypingIndicator.areTyping')}`} + {translate('reportTypingIndicator.multipleUsers')} + {` ${translate('reportTypingIndicator.areTyping')}`} ); } @@ -69,13 +70,9 @@ ReportTypingIndicator.propTypes = propTypes; ReportTypingIndicator.defaultProps = defaultProps; ReportTypingIndicator.displayName = 'ReportTypingIndicator'; -export default compose( - withLocalize, - withNetwork(), - withOnyx({ - userTypingStatuses: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, - initialValue: {}, - }, - }), -)(ReportTypingIndicator); +export default withOnyx({ + userTypingStatuses: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, + initialValue: {}, + }, +})(ReportTypingIndicator); From 72fc5647fb9969f233acb66d56bc0c5cc599d1bb Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:22:16 -0500 Subject: [PATCH 013/260] extract hardcoded weekStartsOn into a method inside DateUtils --- .../CalendarPicker/generateMonthMatrix.js | 8 ++++-- src/libs/DateUtils.ts | 26 ++++++++++++++++--- 2 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js b/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js index a3497654feec..bb5a66c147a1 100644 --- a/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js +++ b/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js @@ -1,4 +1,5 @@ import {addDays, format, getDay, getDaysInMonth, startOfMonth} from 'date-fns'; +import {getWeekStartsAndEndsOn} from '@libs/DateUtils'; /** * Generates a matrix representation of a month's calendar given the year and month. @@ -24,6 +25,9 @@ export default function generateMonthMatrix(year, month) { throw new Error('Month cannot be greater than 11'); } + // Get the week day for the start and end of week + const {weekStartsOn, weekEndsOn} = getWeekStartsAndEndsOn(); + // Get the number of days in the month and the first day of the month const firstDayOfMonth = startOfMonth(new Date(year, month, 1)); const daysInMonth = getDaysInMonth(firstDayOfMonth); @@ -33,7 +37,7 @@ export default function generateMonthMatrix(year, month) { let currentWeek = []; // Add null values for days before the first day of the month - for (let i = 0; i < getDay(firstDayOfMonth); i++) { + for (let i = weekStartsOn; i < getDay(firstDayOfMonth); i++) { currentWeek.push(null); } @@ -43,7 +47,7 @@ export default function generateMonthMatrix(year, month) { currentWeek.push(Number(format(currentDate, 'd'))); // Start a new row when the current week is full - if (getDay(currentDate) === 6) { + if (getDay(currentDate) === weekEndsOn) { matrix.push(currentWeek); currentWeek = []; } diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 80eae24d9367..6dcfaf758725 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -29,6 +29,11 @@ import * as CurrentDate from './actions/CurrentDate'; import * as Localize from './Localize'; type Locale = ValueOf; +type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6; +type WeekStartsAndEndsOn = { + weekStartsOn: WeekDay; + weekEndsOn: WeekDay; +}; let currentUserAccountID: number | undefined; Onyx.connect({ @@ -60,6 +65,16 @@ Onyx.connect({ }, }); +/** + * Get the day of the week that the week starts and ends on + */ +function getWeekStartsAndEndsOn(): WeekStartsAndEndsOn { + return { + weekStartsOn: 1, // Assuming Monday is the start of the week + weekEndsOn: 0, // Assuming Sunday is the end of the week + }; +} + /** * Gets the locale string and setting default locale for date-fns */ @@ -145,9 +160,10 @@ function datetimeToCalendarTime(locale: Locale, datetime: string, includeTimeZon let tomorrowAt = Localize.translate(locale, 'common.tomorrowAt'); let yesterdayAt = Localize.translate(locale, 'common.yesterdayAt'); const at = Localize.translate(locale, 'common.conjunctionAt'); + const {weekStartsOn} = getWeekStartsAndEndsOn(); - const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week - const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week + const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn}); + const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn}); if (isLowercase) { todayAt = todayAt.toLowerCase(); @@ -284,8 +300,9 @@ function getDaysOfWeek(preferredLocale: Locale): string[] { if (preferredLocale) { setLocale(preferredLocale); } - const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week - const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn: 1}); // Assuming Monday is the start of the week + const {weekStartsOn} = getWeekStartsAndEndsOn(); + const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn}); + const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn}); const daysOfWeek = eachDayOfInterval({start: startOfCurrentWeek, end: endOfCurrentWeek}); // eslint-disable-next-line rulesdir/prefer-underscore-method @@ -413,6 +430,7 @@ const DateUtils = { getMonthNames, getDaysOfWeek, formatWithUTCTimeZone, + weekStartsAndEndsOn: getWeekStartsAndEndsOn, }; export default DateUtils; From 3dd8f85bb79506399093a75a22e558f3d07f3aa7 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Mon, 20 Nov 2023 18:26:56 -0500 Subject: [PATCH 014/260] fixed usage of DateUtils lib --- .../NewDatePicker/CalendarPicker/generateMonthMatrix.js | 4 ++-- src/libs/DateUtils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js b/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js index bb5a66c147a1..600ac0b4fd21 100644 --- a/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js +++ b/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js @@ -1,5 +1,5 @@ import {addDays, format, getDay, getDaysInMonth, startOfMonth} from 'date-fns'; -import {getWeekStartsAndEndsOn} from '@libs/DateUtils'; +import DateUtils from '@libs/DateUtils'; /** * Generates a matrix representation of a month's calendar given the year and month. @@ -26,7 +26,7 @@ export default function generateMonthMatrix(year, month) { } // Get the week day for the start and end of week - const {weekStartsOn, weekEndsOn} = getWeekStartsAndEndsOn(); + const {weekStartsOn, weekEndsOn} = DateUtils.getWeekStartsAndEndsOn(); // Get the number of days in the month and the first day of the month const firstDayOfMonth = startOfMonth(new Date(year, month, 1)); diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 6dcfaf758725..d8803e301cee 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -430,7 +430,7 @@ const DateUtils = { getMonthNames, getDaysOfWeek, formatWithUTCTimeZone, - weekStartsAndEndsOn: getWeekStartsAndEndsOn, + getWeekStartsAndEndsOn, }; export default DateUtils; From 1ff3017f95159fefe0c7318cad05b7ad4f1bd440 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 21 Nov 2023 10:17:04 +0100 Subject: [PATCH 015/260] fix: apply requested changes --- src/pages/home/report/ReportTypingIndicator.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index c2c9a0222542..9b0757668d59 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -26,13 +26,14 @@ function ReportTypingIndicator(props) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(props.userTypingStatuses), (loginOrAccountID) => props.userTypingStatuses[loginOrAccountID]), [props.userTypingStatuses]); + const firstUserTyping = usersTyping[0]; + const firstUserTypingID = useMemo(() => firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping, [firstUserTyping]); + // If we are offline, the user typing statuses are not up-to-date so do not show them if (isOffline) { return null; } - const firstUserTyping = usersTyping[0]; - const firstUserTypingID = Number.isNaN(firstUserTyping) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping; const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); const numUsersTyping = _.size(usersTyping); From b30cb29aa57cf1ede0a2103aa87931fdc46c2138 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Tue, 21 Nov 2023 11:40:10 +0100 Subject: [PATCH 016/260] fix: run prettier --- src/pages/home/report/ReportTypingIndicator.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 9b0757668d59..9e0149e973e4 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -27,7 +27,10 @@ function ReportTypingIndicator(props) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(props.userTypingStatuses), (loginOrAccountID) => props.userTypingStatuses[loginOrAccountID]), [props.userTypingStatuses]); const firstUserTyping = usersTyping[0]; - const firstUserTypingID = useMemo(() => firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping, [firstUserTyping]); + const firstUserTypingID = useMemo( + () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), + [firstUserTyping], + ); // If we are offline, the user typing statuses are not up-to-date so do not show them if (isOffline) { From a13abd6d8c1b3c954a02cfa3b3da6ce7fdd38bd4 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:42:50 -0500 Subject: [PATCH 017/260] moved week starts on into a constant --- src/CONST.ts | 1 + .../CalendarPicker/generateMonthMatrix.js | 3 +- src/libs/DateUtils.ts | 29 ++++++++++--------- 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index c29a7c51e6ef..d1f0ed6a56c2 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -858,6 +858,7 @@ const CONST = { MAX_PENDING_TIME_MS: 10 * 1000, MAX_REQUEST_RETRIES: 10, }, + WEEK_STARTS_ON: 1, // Monday DEFAULT_TIME_ZONE: {automatic: true, selected: 'America/Los_Angeles'}, DEFAULT_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, DEFAULT_CLOSE_ACCOUNT_DATA: {errors: null, success: '', isLoading: false}, diff --git a/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js b/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js index 600ac0b4fd21..9e2ae4195a7c 100644 --- a/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js +++ b/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js @@ -26,7 +26,8 @@ export default function generateMonthMatrix(year, month) { } // Get the week day for the start and end of week - const {weekStartsOn, weekEndsOn} = DateUtils.getWeekStartsAndEndsOn(); + const weekStartsOn = DateUtils.getWeekStartsOn(); + const weekEndsOn = DateUtils.getWeekEndsOn(); // Get the number of days in the month and the first day of the month const firstDayOfMonth = startOfMonth(new Date(year, month, 1)); diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index d8803e301cee..d7b6d9132808 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -30,10 +30,6 @@ import * as Localize from './Localize'; type Locale = ValueOf; type WeekDay = 0 | 1 | 2 | 3 | 4 | 5 | 6; -type WeekStartsAndEndsOn = { - weekStartsOn: WeekDay; - weekEndsOn: WeekDay; -}; let currentUserAccountID: number | undefined; Onyx.connect({ @@ -66,13 +62,19 @@ Onyx.connect({ }); /** - * Get the day of the week that the week starts and ends on + * Get the day of the week that the week starts on + */ +function getWeekStartsOn(): WeekDay { + return CONST.WEEK_STARTS_ON; +} + +/** + * Get the day of the week that the week ends on */ -function getWeekStartsAndEndsOn(): WeekStartsAndEndsOn { - return { - weekStartsOn: 1, // Assuming Monday is the start of the week - weekEndsOn: 0, // Assuming Sunday is the end of the week - }; +function getWeekEndsOn(): WeekDay { + const weekStartsOn = getWeekStartsOn(); + + return weekStartsOn === 0 ? 6 : ((weekStartsOn - 1) as WeekDay); } /** @@ -160,7 +162,7 @@ function datetimeToCalendarTime(locale: Locale, datetime: string, includeTimeZon let tomorrowAt = Localize.translate(locale, 'common.tomorrowAt'); let yesterdayAt = Localize.translate(locale, 'common.yesterdayAt'); const at = Localize.translate(locale, 'common.conjunctionAt'); - const {weekStartsOn} = getWeekStartsAndEndsOn(); + const weekStartsOn = getWeekStartsOn(); const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn}); const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn}); @@ -300,7 +302,7 @@ function getDaysOfWeek(preferredLocale: Locale): string[] { if (preferredLocale) { setLocale(preferredLocale); } - const {weekStartsOn} = getWeekStartsAndEndsOn(); + const weekStartsOn = getWeekStartsOn(); const startOfCurrentWeek = startOfWeek(new Date(), {weekStartsOn}); const endOfCurrentWeek = endOfWeek(new Date(), {weekStartsOn}); const daysOfWeek = eachDayOfInterval({start: startOfCurrentWeek, end: endOfCurrentWeek}); @@ -430,7 +432,8 @@ const DateUtils = { getMonthNames, getDaysOfWeek, formatWithUTCTimeZone, - getWeekStartsAndEndsOn, + getWeekStartsOn, + getWeekEndsOn, }; export default DateUtils; From 5b6b2ff667f574141215f26a87cb8db110a3d9e5 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Tue, 21 Nov 2023 10:43:14 -0500 Subject: [PATCH 018/260] updated generateMonthMatrix test --- tests/unit/generateMonthMatrixTest.js | 62 +++++++++++++-------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/tests/unit/generateMonthMatrixTest.js b/tests/unit/generateMonthMatrixTest.js index 9f565a6e3a78..dc3df145c0b5 100644 --- a/tests/unit/generateMonthMatrixTest.js +++ b/tests/unit/generateMonthMatrixTest.js @@ -3,67 +3,67 @@ import generateMonthMatrix from '../../src/components/NewDatePicker/CalendarPick describe('generateMonthMatrix', () => { it('returns the correct matrix for January 2022', () => { const expected = [ - [null, null, null, null, null, null, 1], - [2, 3, 4, 5, 6, 7, 8], - [9, 10, 11, 12, 13, 14, 15], - [16, 17, 18, 19, 20, 21, 22], - [23, 24, 25, 26, 27, 28, 29], - [30, 31, null, null, null, null, null], + [null, null, null, null, null, 1, 2], + [3, 4, 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23], + [24, 25, 26, 27, 28, 29, 30], + [31, null, null, null, null, null, null], ]; expect(generateMonthMatrix(2022, 0)).toEqual(expected); }); it('returns the correct matrix for February 2022', () => { const expected = [ - [null, null, 1, 2, 3, 4, 5], - [6, 7, 8, 9, 10, 11, 12], - [13, 14, 15, 16, 17, 18, 19], - [20, 21, 22, 23, 24, 25, 26], - [27, 28, null, null, null, null, null], + [null, 1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12, 13], + [14, 15, 16, 17, 18, 19, 20], + [21, 22, 23, 24, 25, 26, 27], + [28, null, null, null, null, null, null], ]; expect(generateMonthMatrix(2022, 1)).toEqual(expected); }); it('returns the correct matrix for leap year February 2020', () => { const expected = [ - [null, null, null, null, null, null, 1], - [2, 3, 4, 5, 6, 7, 8], - [9, 10, 11, 12, 13, 14, 15], - [16, 17, 18, 19, 20, 21, 22], - [23, 24, 25, 26, 27, 28, 29], + [null, null, null, null, null, 1, 2], + [3, 4, 5, 6, 7, 8, 9], + [10, 11, 12, 13, 14, 15, 16], + [17, 18, 19, 20, 21, 22, 23], + [24, 25, 26, 27, 28, 29, null], ]; expect(generateMonthMatrix(2020, 1)).toEqual(expected); }); it('returns the correct matrix for March 2022', () => { const expected = [ - [null, null, 1, 2, 3, 4, 5], - [6, 7, 8, 9, 10, 11, 12], - [13, 14, 15, 16, 17, 18, 19], - [20, 21, 22, 23, 24, 25, 26], - [27, 28, 29, 30, 31, null, null], + [null, 1, 2, 3, 4, 5, 6], + [7, 8, 9, 10, 11, 12, 13], + [14, 15, 16, 17, 18, 19, 20], + [21, 22, 23, 24, 25, 26, 27], + [28, 29, 30, 31, null, null, null], ]; expect(generateMonthMatrix(2022, 2)).toEqual(expected); }); it('returns the correct matrix for April 2022', () => { const expected = [ - [null, null, null, null, null, 1, 2], - [3, 4, 5, 6, 7, 8, 9], - [10, 11, 12, 13, 14, 15, 16], - [17, 18, 19, 20, 21, 22, 23], - [24, 25, 26, 27, 28, 29, 30], + [null, null, null, null, 1, 2, 3], + [4, 5, 6, 7, 8, 9, 10], + [11, 12, 13, 14, 15, 16, 17], + [18, 19, 20, 21, 22, 23, 24], + [25, 26, 27, 28, 29, 30, null], ]; expect(generateMonthMatrix(2022, 3)).toEqual(expected); }); it('returns the correct matrix for December 2022', () => { const expected = [ - [null, null, null, null, 1, 2, 3], - [4, 5, 6, 7, 8, 9, 10], - [11, 12, 13, 14, 15, 16, 17], - [18, 19, 20, 21, 22, 23, 24], - [25, 26, 27, 28, 29, 30, 31], + [null, null, null, 1, 2, 3, 4], + [5, 6, 7, 8, 9, 10, 11], + [12, 13, 14, 15, 16, 17, 18], + [19, 20, 21, 22, 23, 24, 25], + [26, 27, 28, 29, 30, 31, null], ]; expect(generateMonthMatrix(2022, 11)).toEqual(expected); }); From aa015de65f6854c8c585464e7939bb9b33c52c39 Mon Sep 17 00:00:00 2001 From: Carlos Barros <765936+barros001@users.noreply.github.com> Date: Tue, 21 Nov 2023 14:23:54 -0500 Subject: [PATCH 019/260] updated how empty days are added at the beginning of the month --- .../CalendarPicker/generateMonthMatrix.js | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js b/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js index 9e2ae4195a7c..ecf338d36424 100644 --- a/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js +++ b/src/components/NewDatePicker/CalendarPicker/generateMonthMatrix.js @@ -25,8 +25,7 @@ export default function generateMonthMatrix(year, month) { throw new Error('Month cannot be greater than 11'); } - // Get the week day for the start and end of week - const weekStartsOn = DateUtils.getWeekStartsOn(); + // Get the week day for the end of week const weekEndsOn = DateUtils.getWeekEndsOn(); // Get the number of days in the month and the first day of the month @@ -37,11 +36,6 @@ export default function generateMonthMatrix(year, month) { const matrix = []; let currentWeek = []; - // Add null values for days before the first day of the month - for (let i = weekStartsOn; i < getDay(firstDayOfMonth); i++) { - currentWeek.push(null); - } - // Add calendar days to the matrix for (let i = 1; i <= daysInMonth; i++) { const currentDate = addDays(firstDayOfMonth, i - 1); @@ -61,5 +55,11 @@ export default function generateMonthMatrix(year, month) { } matrix.push(currentWeek); } + + // Add null values for days before the first day of the month + for (let i = matrix[0].length; i < 7; i++) { + matrix[0].unshift(null); + } + return matrix; } From 2252f7ca2d08462adca23201c6be65d591de36cd Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 24 Nov 2023 05:51:00 +0700 Subject: [PATCH 020/260] fix merge main --- .../BaseCentralPaneNavigator.js | 13 +------------ src/pages/home/ReportScreen.js | 8 ++++++++ 2 files changed, 9 insertions(+), 12 deletions(-) diff --git a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js index 5e4b916342d5..a1646011e560 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js +++ b/src/libs/Navigation/AppNavigator/Navigators/CentralPaneNavigator/BaseCentralPaneNavigator.js @@ -1,10 +1,8 @@ import {createStackNavigator} from '@react-navigation/stack'; -import lodashGet from 'lodash/get'; import React from 'react'; import ReportScreenWrapper from '@libs/Navigation/AppNavigator/ReportScreenWrapper'; import getCurrentUrl from '@libs/Navigation/currentUrl'; import styles from '@styles/styles'; -import * as Report from '@userActions/Report'; import SCREENS from '@src/SCREENS'; const Stack = createStackNavigator(); @@ -14,16 +12,7 @@ const openOnAdminRoom = url ? new URL(url).searchParams.get('openOnAdminRoom') : function BaseCentralPaneNavigator() { return ( - { - const reportID = lodashGet(e, 'data.state.routes[0].params.reportID', ''); - if (reportID) { - Report.updateLastVisitTime(reportID); - } - }, - }} - > + { + if (!reportID) { + return; + } + Report.updateLastVisitTime(reportID); + }, [reportID]); + const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; From 0c5bc5ec812e70ed87d86f2e17470bdb9139aed3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 10:02:46 +0100 Subject: [PATCH 021/260] Conflicts & requested changes. --- src/components/withKeyboardState.tsx | 4 +--- src/hooks/useKeyboardState.ts | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index 48f2f4c3b8c6..f6ebfc695529 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -1,7 +1,5 @@ -import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useState} from 'react'; -import {Keyboard} from 'react-native'; import PropTypes from 'prop-types'; -import React, {createContext, forwardRef, useEffect, useMemo, useState} from 'react'; +import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; import getComponentDisplayName from '@libs/getComponentDisplayName'; diff --git a/src/hooks/useKeyboardState.ts b/src/hooks/useKeyboardState.ts index d7cc8d271503..edcbaa32cc9e 100644 --- a/src/hooks/useKeyboardState.ts +++ b/src/hooks/useKeyboardState.ts @@ -1,5 +1,5 @@ import {useContext} from 'react'; -import {KeyboardStateContext, KeyboardStateContextValue} from '../components/withKeyboardState'; +import {KeyboardStateContext, KeyboardStateContextValue} from '@components/withKeyboardState'; /** * Hook for getting current state of keyboard From cb0782fcd9b168ca0aef906396baabb416b2a82e Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Mon, 27 Nov 2023 19:19:09 +0700 Subject: [PATCH 022/260] fix does not update timestamp with empty report --- src/pages/home/ReportScreen.js | 10 +++++----- src/pages/home/report/ReportFooter.js | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 92d81a0fae10..3bf8887f1b32 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,3 +1,4 @@ +import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -161,12 +162,13 @@ function ReportScreen({ const reportID = getReportID(route); + const isFocused = useIsFocused(); useEffect(() => { - if (!reportID) { + if (!report.reportID || !isFocused) { return; } - Report.updateLastVisitTime(reportID); - }, [reportID]); + Report.updateLastVisitTime(report.reportID); + }, [report.reportID, isFocused]); const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; @@ -240,7 +242,6 @@ function ReportScreen({ const fetchReportIfNeeded = useCallback(() => { const reportIDFromPath = getReportID(route); - // Report ID will be empty when the reports collection is empty. // This could happen when we are loading the collection for the first time after logging in. if (!ReportUtils.isValidReportIDFromPath(reportIDFromPath)) { @@ -253,7 +254,6 @@ function ReportScreen({ if (report.reportID && report.reportID === getReportID(route) && !isLoadingInitialReportActions) { return; } - Report.openReport(reportIDFromPath); }, [report.reportID, route, isLoadingInitialReportActions]); diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index e5dd5da19ad5..a3bd27cff160 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -72,7 +72,7 @@ function ReportFooter(props) { const isSmallSizeLayout = props.windowWidth - (props.isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = !ReportUtils.canUserPerformWriteAction(props.report); - + return ( <> {hideComposer && ( From b4123e2f7451fb5ad0d2c8cd58eb83244e01df3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 15:32:21 +0100 Subject: [PATCH 023/260] Requested changes. --- src/components/withKeyboardState.tsx | 45 +++++++++++++--------------- 1 file changed, 21 insertions(+), 24 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index f6ebfc695529..f072c99d47f2 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -1,18 +1,14 @@ import PropTypes from 'prop-types'; -import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactNode, useEffect, useMemo, useState} from 'react'; +import React, {ComponentType, createContext, ForwardedRef, forwardRef, ReactElement, RefAttributes, useEffect, useMemo, useState} from 'react'; import {Keyboard} from 'react-native'; import getComponentDisplayName from '@libs/getComponentDisplayName'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; type KeyboardStateContextValue = { /** Whether the keyboard is open */ isKeyboardShown: boolean; }; -type KeyboardStateProviderProps = { - /* Actual content wrapped by this component */ - children: ReactNode; -}; - // TODO: Remove - left for backwards compatibility with existing components. const keyboardStatePropTypes = { /** Whether the keyboard is open */ @@ -21,7 +17,7 @@ const keyboardStatePropTypes = { const KeyboardStateContext = createContext(null); -function KeyboardStateProvider(props: KeyboardStateProviderProps) { +function KeyboardStateProvider(props: ChildrenProps): ReactElement | null { const {children} = props; const [isKeyboardShown, setIsKeyboardShown] = useState(false); useEffect(() => { @@ -47,23 +43,24 @@ function KeyboardStateProvider(props: KeyboardStateProviderProps) { return {children}; } -// eslint-disable-next-line @typescript-eslint/naming-convention -export default function withKeyboardState(WrappedComponent: ComponentType<{ref: ForwardedRef}>) { - const WithKeyboardState = forwardRef((props: Record, ref: React.ForwardedRef) => ( - - {(keyboardStateProps) => ( - - )} - - )); - (WithKeyboardState as unknown as {displayName: string}).displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; - return WithKeyboardState; +export default function withKeyboardState(WrappedComponent: ComponentType>): (props: TProps & React.RefAttributes) => ReactElement | null { + function WithKeyboardState(props: TProps, ref: ForwardedRef) { + return ( + + {(keyboardStateProps) => ( + + )} + + ); + } + WithKeyboardState.displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; + return forwardRef(WithKeyboardState); } export {KeyboardStateProvider, keyboardStatePropTypes, type KeyboardStateContextValue, KeyboardStateContext}; From ab4bc47815a9409a19ff647b60caea50a502eda0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 15:41:12 +0100 Subject: [PATCH 024/260] Requested change. --- src/components/withKeyboardState.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index f072c99d47f2..87b606d4c113 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -17,8 +17,7 @@ const keyboardStatePropTypes = { const KeyboardStateContext = createContext(null); -function KeyboardStateProvider(props: ChildrenProps): ReactElement | null { - const {children} = props; +function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null { const [isKeyboardShown, setIsKeyboardShown] = useState(false); useEffect(() => { const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => { From 7fa124f244182cb2da393cc3b65711380a2cca55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Mon, 27 Nov 2023 16:29:01 +0100 Subject: [PATCH 025/260] Requested changes. --- src/components/withKeyboardState.tsx | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/components/withKeyboardState.tsx b/src/components/withKeyboardState.tsx index 87b606d4c113..fd00ef920399 100755 --- a/src/components/withKeyboardState.tsx +++ b/src/components/withKeyboardState.tsx @@ -19,6 +19,7 @@ const KeyboardStateContext = createContext(nul function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null { const [isKeyboardShown, setIsKeyboardShown] = useState(false); + useEffect(() => { const keyboardDidShowListener = Keyboard.addListener('keyboardDidShow', () => { setIsKeyboardShown(true); @@ -42,8 +43,10 @@ function KeyboardStateProvider({children}: ChildrenProps): ReactElement | null { return {children}; } -export default function withKeyboardState(WrappedComponent: ComponentType>): (props: TProps & React.RefAttributes) => ReactElement | null { - function WithKeyboardState(props: TProps, ref: ForwardedRef) { +export default function withKeyboardState( + WrappedComponent: ComponentType>, +): (props: Omit & React.RefAttributes) => ReactElement | null { + function WithKeyboardState(props: Omit, ref: ForwardedRef) { return ( {(keyboardStateProps) => ( @@ -51,14 +54,14 @@ export default function withKeyboardState(WrappedComponent: Compon // eslint-disable-next-line react/jsx-props-no-spreading {...keyboardStateProps} // eslint-disable-next-line react/jsx-props-no-spreading - {...props} + {...(props as TProps)} ref={ref} /> )} ); } - WithKeyboardState.displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent as ComponentType)})`; + WithKeyboardState.displayName = `withKeyboardState(${getComponentDisplayName(WrappedComponent)})`; return forwardRef(WithKeyboardState); } From 798897370283911fc66b8ee38e3a45083a63f2e6 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 28 Nov 2023 20:50:28 +0700 Subject: [PATCH 026/260] fix lint --- src/pages/home/report/ReportFooter.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportFooter.js b/src/pages/home/report/ReportFooter.js index a3bd27cff160..e5dd5da19ad5 100644 --- a/src/pages/home/report/ReportFooter.js +++ b/src/pages/home/report/ReportFooter.js @@ -72,7 +72,7 @@ function ReportFooter(props) { const isSmallSizeLayout = props.windowWidth - (props.isSmallScreenWidth ? 0 : variables.sideBarWidth) < variables.anonymousReportFooterBreakpoint; const hideComposer = !ReportUtils.canUserPerformWriteAction(props.report); - + return ( <> {hideComposer && ( From 2ffcec2417dd634c342460171de57b2bc847f049 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 30 Nov 2023 08:58:31 +0100 Subject: [PATCH 027/260] fix: destructure props --- src/pages/home/report/ReportTypingIndicator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 9e0149e973e4..afb18a98b350 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -20,12 +20,12 @@ const defaultProps = { userTypingStatuses: {}, }; -function ReportTypingIndicator(props) { +function ReportTypingIndicator({userTypingStatuses}) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); - const usersTyping = useMemo(() => _.filter(_.keys(props.userTypingStatuses), (loginOrAccountID) => props.userTypingStatuses[loginOrAccountID]), [props.userTypingStatuses]); + const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; const firstUserTypingID = useMemo( () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), From 2b40a50d097874d6cd8e0ab65f39af8cebbd6693 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 30 Nov 2023 12:26:35 +0100 Subject: [PATCH 028/260] migrate BlockingView and associated components to ts --- .../{BlockingView.js => BlockingView.tsx} | 71 +++++++------ ...tFoundView.js => FullPageNotFoundView.tsx} | 48 +++++---- .../FullPageOfflineBlockingView.js | 38 ------- .../FullPageOfflineBlockingView.tsx | 31 ++++++ src/components/Text.tsx | 31 +++--- src/components/TextLink.js | 99 ------------------- src/components/TextLink.tsx | 77 +++++++++++++++ 7 files changed, 182 insertions(+), 213 deletions(-) rename src/components/BlockingViews/{BlockingView.js => BlockingView.tsx} (59%) rename src/components/BlockingViews/{FullPageNotFoundView.js => FullPageNotFoundView.tsx} (69%) delete mode 100644 src/components/BlockingViews/FullPageOfflineBlockingView.js create mode 100644 src/components/BlockingViews/FullPageOfflineBlockingView.tsx delete mode 100644 src/components/TextLink.js create mode 100644 src/components/TextLink.tsx diff --git a/src/components/BlockingViews/BlockingView.js b/src/components/BlockingViews/BlockingView.tsx similarity index 59% rename from src/components/BlockingViews/BlockingView.js rename to src/components/BlockingViews/BlockingView.tsx index aec414cdeb74..ede40cfda752 100644 --- a/src/components/BlockingViews/BlockingView.js +++ b/src/components/BlockingViews/BlockingView.tsx @@ -1,6 +1,6 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; +import {SvgProps} from 'react-native-svg'; import AutoEmailLink from '@components/AutoEmailLink'; import Icon from '@components/Icon'; import Text from '@components/Text'; @@ -10,51 +10,52 @@ import Navigation from '@libs/Navigation/Navigation'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; +import {TranslationPaths} from '@src/languages/types'; -const propTypes = { +type BlockingViewProps = { /** Expensicon for the page */ - icon: PropTypes.func.isRequired, + icon: React.FC; /** Color for the icon (should be from theme) */ - iconColor: PropTypes.string, + iconColor?: string; /** Title message below the icon */ - title: PropTypes.string.isRequired, + title: string; /** Subtitle message below the title */ - subtitle: PropTypes.string, + subtitle?: string; /** Link message below the subtitle */ - linkKey: PropTypes.string, + linkKey?: TranslationPaths; /** Whether we should show a link to navigate elsewhere */ - shouldShowLink: PropTypes.bool, + shouldShowLink?: boolean; /** The custom icon width */ - iconWidth: PropTypes.number, + iconWidth?: number; /** The custom icon height */ - iconHeight: PropTypes.number, + iconHeight?: number; /** Function to call when pressing the navigation link */ - onLinkPress: PropTypes.func, + onLinkPress?: () => void; /** Whether we should embed the link with subtitle */ - shouldEmbedLinkWithSubtitle: PropTypes.bool, + shouldEmbedLinkWithSubtitle?: boolean; }; -const defaultProps = { - iconColor: undefined, - subtitle: '', - shouldShowLink: false, - linkKey: 'notFound.goBackHome', - iconWidth: variables.iconSizeSuperLarge, - iconHeight: variables.iconSizeSuperLarge, - onLinkPress: () => Navigation.dismissModal(), - shouldEmbedLinkWithSubtitle: false, -}; - -function BlockingView(props) { +function BlockingView({ + icon, + iconColor, + title, + subtitle = '', + linkKey = 'notFound.goBackHome', + shouldShowLink = false, + iconWidth = variables.iconSizeSuperLarge, + iconHeight = variables.iconSizeSuperLarge, + onLinkPress = () => Navigation.dismissModal, + shouldEmbedLinkWithSubtitle, +}: BlockingViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -63,14 +64,14 @@ function BlockingView(props) { <> - {props.shouldShowLink ? ( + {shouldShowLink ? ( - {translate(props.linkKey)} + {translate(linkKey)} ) : null} @@ -80,14 +81,14 @@ function BlockingView(props) { return ( - {props.title} + {title} - {props.shouldEmbedLinkWithSubtitle ? ( + {shouldEmbedLinkWithSubtitle ? ( {renderContent()} ) : ( {renderContent()} @@ -96,8 +97,6 @@ function BlockingView(props) { ); } -BlockingView.propTypes = propTypes; -BlockingView.defaultProps = defaultProps; BlockingView.displayName = 'BlockingView'; export default BlockingView; diff --git a/src/components/BlockingViews/FullPageNotFoundView.js b/src/components/BlockingViews/FullPageNotFoundView.tsx similarity index 69% rename from src/components/BlockingViews/FullPageNotFoundView.js rename to src/components/BlockingViews/FullPageNotFoundView.tsx index b82474aa0694..3893c83dc45a 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.js +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import HeaderWithBackButton from '@components/HeaderWithBackButton'; @@ -7,52 +6,51 @@ import useLocalize from '@hooks/useLocalize'; import Navigation from '@libs/Navigation/Navigation'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; +import {TranslationPaths} from '@src/languages/types'; import ROUTES from '@src/ROUTES'; import BlockingView from './BlockingView'; -const propTypes = { +type FullPageNotFoundViewProps = { /** Child elements */ - children: PropTypes.node, + children?: React.ReactNode; /** If true, child components are replaced with a blocking "not found" view */ - shouldShow: PropTypes.bool, + shouldShow?: boolean; /** The key in the translations file to use for the title */ - titleKey: PropTypes.string, + titleKey?: TranslationPaths; /** The key in the translations file to use for the subtitle */ - subtitleKey: PropTypes.string, + subtitleKey?: TranslationPaths; /** Whether we should show a link to navigate elsewhere */ - shouldShowLink: PropTypes.bool, + shouldShowLink?: boolean; /** Whether we should show the back button on the header */ - shouldShowBackButton: PropTypes.bool, + shouldShowBackButton?: boolean; /** The key in the translations file to use for the go back link */ - linkKey: PropTypes.string, + linkKey?: TranslationPaths; /** Method to trigger when pressing the back button of the header */ - onBackButtonPress: PropTypes.func, + onBackButtonPress: () => void; /** Function to call when pressing the navigation link */ - onLinkPress: PropTypes.func, -}; - -const defaultProps = { - children: null, - shouldShow: false, - titleKey: 'notFound.notHere', - subtitleKey: 'notFound.pageNotFound', - linkKey: 'notFound.goBackHome', - onBackButtonPress: () => Navigation.goBack(ROUTES.HOME), - shouldShowLink: true, - shouldShowBackButton: true, - onLinkPress: () => Navigation.dismissModal(), + onLinkPress: () => void; }; // eslint-disable-next-line rulesdir/no-negated-variables -function FullPageNotFoundView({children, shouldShow, titleKey, subtitleKey, linkKey, onBackButtonPress, shouldShowLink, shouldShowBackButton, onLinkPress}) { +function FullPageNotFoundView({ + children = null, + shouldShow = false, + titleKey = 'notFound.notHere', + subtitleKey = 'notFound.pageNotFound', + linkKey = 'notFound.goBackHome', + onBackButtonPress = () => Navigation.goBack(ROUTES.HOME), + shouldShowLink = true, + shouldShowBackButton = true, + onLinkPress = () => Navigation.dismissModal, +}: FullPageNotFoundViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); if (shouldShow) { @@ -81,8 +79,6 @@ function FullPageNotFoundView({children, shouldShow, titleKey, subtitleKey, link return children; } -FullPageNotFoundView.propTypes = propTypes; -FullPageNotFoundView.defaultProps = defaultProps; FullPageNotFoundView.displayName = 'FullPageNotFoundView'; export default FullPageNotFoundView; diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.js b/src/components/BlockingViews/FullPageOfflineBlockingView.js deleted file mode 100644 index 5a876ecb8b1c..000000000000 --- a/src/components/BlockingViews/FullPageOfflineBlockingView.js +++ /dev/null @@ -1,38 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import * as Expensicons from '@components/Icon/Expensicons'; -import networkPropTypes from '@components/networkPropTypes'; -import {withNetwork} from '@components/OnyxProvider'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import compose from '@libs/compose'; -import BlockingView from './BlockingView'; - -const propTypes = { - /** Child elements */ - children: PropTypes.node.isRequired, - - /** Props to fetch translation features */ - ...withLocalizePropTypes, - - /** Props to detect online status */ - network: networkPropTypes.isRequired, -}; - -function FullPageOfflineBlockingView(props) { - if (props.network.isOffline) { - return ( - - ); - } - - return props.children; -} - -FullPageOfflineBlockingView.propTypes = propTypes; -FullPageOfflineBlockingView.displayName = 'FullPageOfflineBlockingView'; - -export default compose(withLocalize, withNetwork())(FullPageOfflineBlockingView); diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx new file mode 100644 index 000000000000..c8f5592287f3 --- /dev/null +++ b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import * as Expensicons from '@components/Icon/Expensicons'; +import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; +import BlockingView from './BlockingView'; + +type FullPageOfflineBlockingViewProps = { + /** Child elements */ + children: React.ReactNode; +}; + +function FullPageOfflineBlockingView({children}: FullPageOfflineBlockingViewProps) { + const {translate} = useLocalize(); + const {isOffline} = useNetwork(); + + if (isOffline) { + return ( + + ); + } + + return children; +} + +FullPageOfflineBlockingView.displayName = 'FullPageOfflineBlockingView'; + +export default FullPageOfflineBlockingView; diff --git a/src/components/Text.tsx b/src/components/Text.tsx index 58a5cf300699..a289e224fb72 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -1,28 +1,29 @@ import React, {ForwardedRef} from 'react'; -// eslint-disable-next-line no-restricted-imports import {Text as RNText, TextProps as RNTextProps, StyleSheet} from 'react-native'; import type {TextStyle} from 'react-native'; import fontFamily from '@styles/fontFamily'; import useTheme from '@styles/themes/useTheme'; import variables from '@styles/variables'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; -type TextProps = RNTextProps & { - /** The color of the text */ - color?: string; +type TextProps = RNTextProps & + ChildrenProps & { + /** The color of the text */ + color?: string; - /** The size of the text */ - fontSize?: number; - /** The alignment of the text */ - textAlign?: 'left' | 'right' | 'auto' | 'center' | 'justify'; - /** Any children to display */ - children: React.ReactNode; + /** The size of the text */ + fontSize?: number; - /** The family of the font to use */ - family?: keyof typeof fontFamily; -}; + /** The alignment of the text */ + textAlign?: TextStyle['textAlign']; -function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children = null, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { + /** The family of the font to use */ + family?: keyof typeof fontFamily; + }; + +function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { const theme = useTheme(); + const componentStyle: TextStyle = { color: color ?? theme.text, fontSize, @@ -34,6 +35,7 @@ function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', c if (!componentStyle.lineHeight && componentStyle.fontSize === variables.fontSizeNormal) { componentStyle.lineHeight = variables.fontSizeNormalHeight; } + return ( event.preventDefault(), -}; - -function TextLink(props) { - const styles = useThemeStyles(); - const rest = _.omit(props, _.keys(propTypes)); - const additionalStyles = _.isArray(props.style) ? props.style : [props.style]; - - /** - * @param {Event} event - */ - const openLink = (event) => { - event.preventDefault(); - if (props.onPress) { - props.onPress(); - return; - } - - Link.openExternalLink(props.href); - }; - - /** - * @param {Event} event - */ - const openLinkIfEnterKeyPressed = (event) => { - if (event.key !== 'Enter') { - return; - } - openLink(event); - }; - - return ( - - {props.children} - - ); -} - -TextLink.defaultProps = defaultProps; -TextLink.propTypes = propTypes; -TextLink.displayName = 'TextLink'; - -const TextLinkWithRef = React.forwardRef((props, ref) => ( - -)); - -TextLinkWithRef.displayName = 'TextLinkWithRef'; - -export default TextLinkWithRef; diff --git a/src/components/TextLink.tsx b/src/components/TextLink.tsx new file mode 100644 index 000000000000..fae4a71eeafa --- /dev/null +++ b/src/components/TextLink.tsx @@ -0,0 +1,77 @@ +import React, {ForwardedRef, forwardRef, KeyboardEventHandler, MouseEventHandler} from 'react'; +import {GestureResponderEvent, Text as RNText, StyleProp, TextStyle} from 'react-native'; +import useThemeStyles from '@styles/useThemeStyles'; +import * as Link from '@userActions/Link'; +import CONST from '@src/CONST'; +import Text, {TextProps} from './Text'; + +type LinkProps = { + /** Link to open in new tab */ + href: string; + + onPress?: undefined; +}; + +type PressProps = { + href?: undefined; + + /** Overwrites the default link behavior with a custom callback */ + onPress: () => void; +}; + +type TextLinkProps = (LinkProps | PressProps) & + TextProps & { + /** Additional style props */ + style?: StyleProp; + + /** Callback that is called when mousedown is triggered */ + onMouseDown?: MouseEventHandler; + }; + +function TextLink({href, onPress, children, style, onMouseDown = (event) => event.preventDefault(), ...rest}: TextLinkProps, ref: ForwardedRef) { + const styles = useThemeStyles(); + + const openLink = () => { + if (onPress) { + onPress(); + } else { + Link.openExternalLink(href); + } + }; + + const openLinkOnTap = (event: GestureResponderEvent) => { + event.preventDefault(); + + openLink(); + }; + + const openLinkOnEnterKey: KeyboardEventHandler = (event) => { + if (event.key !== 'Enter') { + return; + } + event.preventDefault(); + + openLink(); + }; + + return ( + + {children} + + ); +} + +TextLink.displayName = 'TextLink'; + +export default forwardRef(TextLink); From 87f5f0e847c0147cd0fe688a70e32b91ad409d75 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 30 Nov 2023 22:46:30 +0700 Subject: [PATCH 029/260] fix add comment --- src/pages/home/ReportScreen.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 09e15db06c02..51707033cce5 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -167,7 +167,10 @@ function ReportScreen({ const reportID = getReportID(route); const isFocused = useIsFocused(); + + // Update the lastVisitTime when the report is focused useEffect(() => { + // We don't want to update the lastVisitTime if the report did not exist or if the report is not focused if (!report.reportID || !isFocused) { return; } From cadc71648498b30cbde5b9beb014cd3728ede69a Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 30 Nov 2023 22:56:23 +0700 Subject: [PATCH 030/260] fix add comment --- src/pages/home/ReportScreen.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 51707033cce5..6b5cd213d916 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -168,9 +168,9 @@ function ReportScreen({ const isFocused = useIsFocused(); - // Update the lastVisitTime when the report is focused + // We need to update the lastVisitTime every time the report is focused + // because lastReadTime does not have up-to-date time stamp every time the user visits a report. useEffect(() => { - // We don't want to update the lastVisitTime if the report did not exist or if the report is not focused if (!report.reportID || !isFocused) { return; } From fe1e320e2fe8270aa45aac8d83548f933a0a9e50 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Dec 2023 09:43:54 +0100 Subject: [PATCH 031/260] fix: apply requested changes --- .../home/report/ReportTypingIndicator.js | 51 ++++++++----------- 1 file changed, 22 insertions(+), 29 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index afb18a98b350..a519afaaa8ba 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -27,47 +27,40 @@ function ReportTypingIndicator({userTypingStatuses}) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; + + // If the user is typing on OldDot, firstUserTyping will be a string (the user's login) const firstUserTypingID = useMemo( () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), [firstUserTyping], ); // If we are offline, the user typing statuses are not up-to-date so do not show them - if (isOffline) { + if (isOffline || !firstUserTyping) { return null; } const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); - const numUsersTyping = _.size(usersTyping); - - // Decide on the Text element that will hold the display based on the number of users that are typing. - switch (numUsersTyping) { - case 0: - return null; - - case 1: - return ( - - ); - - default: - return ( - - {translate('reportTypingIndicator.multipleUsers')} - {` ${translate('reportTypingIndicator.areTyping')}`} - - ); + if (usersTyping.length === 1) { + return ( + + ) } + return ( + + {translate('reportTypingIndicator.multipleUsers')} + {` ${translate('reportTypingIndicator.areTyping')}`} + + ); } ReportTypingIndicator.propTypes = propTypes; From e7d667c2edc8300b5fcb649490785a153c98aaeb Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Dec 2023 09:49:58 +0100 Subject: [PATCH 032/260] fix: run prettier --- src/pages/home/report/ReportTypingIndicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index a519afaaa8ba..c141a4d141e1 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -50,7 +50,7 @@ function ReportTypingIndicator({userTypingStatuses}) { wrapperStyle={[styles.chatItemComposeSecondaryRow, styles.flex1]} leadingTextParentStyle={styles.chatItemComposeSecondaryRowOffset} /> - ) + ); } return ( Date: Fri, 1 Dec 2023 21:29:48 +0700 Subject: [PATCH 033/260] fix add comments to logic --- src/libs/actions/Report.js | 1 - src/pages/home/ReportScreen.js | 2 +- src/types/onyx/Report.ts | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 72f2b79081e1..6dd1fdc57d04 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -2505,7 +2505,6 @@ function searchInServer(searchInput) { /** * @param {String} reportID */ - function updateLastVisitTime(reportID) { if (!ReportUtils.isValidReportIDFromPath(reportID)) { return; diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 392d840528a1..4a99ec0e2bab 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -170,7 +170,7 @@ function ReportScreen({ const isFocused = useIsFocused(); // We need to update the lastVisitTime every time the report is focused - // because lastReadTime does not have up-to-date time stamp every time the user visits a report. + // because lastReadTime is not updated every time the user visits a report. useEffect(() => { if (!report.reportID || !isFocused) { return; diff --git a/src/types/onyx/Report.ts b/src/types/onyx/Report.ts index dd244e9a3754..2a5e91a6de34 100644 --- a/src/types/onyx/Report.ts +++ b/src/types/onyx/Report.ts @@ -34,7 +34,7 @@ type Report = { /** The time of the last read of the report */ lastReadCreated?: string; - /** The last time the report was visited */ + /** The time when user read the last message */ lastReadTime?: string; /** The time of the last visit of the report */ From 8af48118292e47736055ec99cf4f0bbe745f3d3c Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Fri, 1 Dec 2023 15:38:16 +0100 Subject: [PATCH 034/260] fix: minor fix --- src/pages/home/report/ReportTypingIndicator.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index c141a4d141e1..6c6531955115 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -28,7 +28,6 @@ function ReportTypingIndicator({userTypingStatuses}) { const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; - // If the user is typing on OldDot, firstUserTyping will be a string (the user's login) const firstUserTypingID = useMemo( () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), [firstUserTyping], @@ -39,7 +38,8 @@ function ReportTypingIndicator({userTypingStatuses}) { return null; } - const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); + // If the user is typing on OldDot, firstUserTyping will be a string (the user's login) + const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false) || firstUserTyping; if (usersTyping.length === 1) { return ( From 5a89b4118aea1d41ac926222cf435f212989dc2a Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 1 Dec 2023 22:15:23 +0700 Subject: [PATCH 035/260] fix additional space --- src/pages/home/ReportScreen.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 4a99ec0e2bab..631aa4b9808a 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -250,6 +250,7 @@ function ReportScreen({ const fetchReportIfNeeded = useCallback(() => { const reportIDFromPath = getReportID(route); + // Report ID will be empty when the reports collection is empty. // This could happen when we are loading the collection for the first time after logging in. if (!ReportUtils.isValidReportIDFromPath(reportIDFromPath)) { @@ -262,6 +263,7 @@ function ReportScreen({ if (report.reportID && report.reportID === getReportID(route) && !isLoadingInitialReportActions) { return; } + Report.openReport(reportIDFromPath); }, [report.reportID, route, isLoadingInitialReportActions]); From 335527917346b753059ee13867517c078a6ae5bd Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Sat, 2 Dec 2023 00:24:34 +0700 Subject: [PATCH 036/260] fix try to fix performance issue --- src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts index 9fd89cfdf146..d78a26ba52c1 100644 --- a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts +++ b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts @@ -5,5 +5,6 @@ export default function reportWithoutHasDraftSelector(report: OnyxKeyValue<'repo return report; } const {hasDraft, ...reportWithoutHasDraft} = report; - return reportWithoutHasDraft; + const {lastVisitTime, ...reportWithoutHasDraftAndLastVisitTime} = reportWithoutHasDraft; + return reportWithoutHasDraftAndLastVisitTime; } From bddaeb2f30d441548816d2fb435f6525dced5d3d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 4 Dec 2023 09:35:29 +0100 Subject: [PATCH 037/260] fix: types --- src/components/Checkbox.tsx | 4 +- .../BaseTextInput/baseTextInputPropTypes.js | 138 ------------------ .../TextInput/BaseTextInput/index.native.tsx | 16 +- .../TextInput/BaseTextInput/index.tsx | 4 +- src/components/TextInput/index.tsx | 3 - 5 files changed, 13 insertions(+), 152 deletions(-) delete mode 100644 src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 22577ec2b7f9..39c8a484613b 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,4 +1,4 @@ -import React, {ForwardedRef, forwardRef, KeyboardEvent as ReactKeyboardEvent} from 'react'; +import React, {ForwardedRef, forwardRef, MouseEventHandler, KeyboardEvent as ReactKeyboardEvent} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; @@ -29,7 +29,7 @@ type CheckboxProps = ChildrenProps & { containerStyle?: StyleProp; /** Callback that is called when mousedown is triggered. */ - onMouseDown?: () => void; + onMouseDown?: MouseEventHandler; /** The size of the checkbox container */ containerSize?: number; diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js deleted file mode 100644 index 5387d1ff81d1..000000000000 --- a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js +++ /dev/null @@ -1,138 +0,0 @@ -import PropTypes from 'prop-types'; - -const propTypes = { - /** Input label */ - label: PropTypes.string, - - /** Name attribute for the input */ - name: PropTypes.string, - - /** Input value */ - value: PropTypes.string, - - /** Default value - used for non controlled inputs */ - defaultValue: PropTypes.string, - - /** Input value placeholder */ - placeholder: PropTypes.string, - - /** Error text to display */ - errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), - - /** Icon to display in right side of text input */ - icon: PropTypes.func, - - /** Customize the TextInput container */ - textInputContainerStyles: PropTypes.arrayOf(PropTypes.object), - - /** Customize the main container */ - containerStyles: PropTypes.arrayOf(PropTypes.object), - - /** input style */ - inputStyle: PropTypes.arrayOf(PropTypes.object), - - /** If present, this prop forces the label to remain in a position where it will not collide with input text */ - forceActiveLabel: PropTypes.bool, - - /** Should the input auto focus? */ - autoFocus: PropTypes.bool, - - /** Disable the virtual keyboard */ - disableKeyboard: PropTypes.bool, - - /** - * Autogrow input container length based on the entered text. - * Note: If you use this prop, the text input has to be controlled - * by a value prop. - */ - autoGrow: PropTypes.bool, - - /** - * Autogrow input container height based on the entered text - * Note: If you use this prop, the text input has to be controlled - * by a value prop. - */ - autoGrowHeight: PropTypes.bool, - - /** Hide the focus styles on TextInput */ - hideFocusedState: PropTypes.bool, - - /** Forward the inner ref */ - innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), - - /** Maximum characters allowed */ - maxLength: PropTypes.number, - - /** Hint text to display below the TextInput */ - hint: PropTypes.string, - - /** Prefix character */ - prefixCharacter: PropTypes.string, - - /** Whether autoCorrect functionality should enable */ - autoCorrect: PropTypes.bool, - - /** Form props */ - /** The ID used to uniquely identify the input in a Form */ - inputID: PropTypes.string, - - /** Saves a draft of the input value when used in a form */ - shouldSaveDraft: PropTypes.bool, - - /** Callback to update the value on Form when input is used in the Form component. */ - onInputChange: PropTypes.func, - - /** Whether we should wait before focusing the TextInput, useful when using transitions */ - shouldDelayFocus: PropTypes.bool, - - /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ - submitOnEnter: PropTypes.bool, - - /** Indicate whether input is multiline */ - multiline: PropTypes.bool, - - /** Set the default value to the input if there is a valid saved value */ - shouldUseDefaultValue: PropTypes.bool, - - /** Indicate whether or not the input should prevent swipe actions in tabs */ - shouldInterceptSwipe: PropTypes.bool, -}; - -const defaultProps = { - label: '', - name: '', - errorText: '', - placeholder: '', - hasError: false, - containerStyles: [], - textInputContainerStyles: [], - inputStyle: [], - autoFocus: false, - autoCorrect: true, - - /** - * To be able to function as either controlled or uncontrolled component we should not - * assign a default prop value for `value` or `defaultValue` props - */ - value: undefined, - defaultValue: undefined, - forceActiveLabel: false, - disableKeyboard: false, - autoGrow: false, - autoGrowHeight: false, - hideFocusedState: false, - innerRef: () => {}, - shouldSaveDraft: false, - maxLength: null, - hint: '', - prefixCharacter: '', - onInputChange: () => {}, - shouldDelayFocus: false, - submitOnEnter: false, - icon: null, - shouldUseDefaultValue: false, - multiline: false, - shouldInterceptSwipe: false, -}; - -export {propTypes, defaultProps}; diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 1a4108bc1803..c5b57a8bc1f6 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -222,17 +222,17 @@ function BaseTextInput( * Set Value & activateLabel */ const setValue = (newValue: string) => { - const value = isMultiline ? newValue : newValue.replace(/\n/g, ' '); + const formattedValue = isMultiline ? newValue : newValue.replace(/\n/g, ' '); if (onInputChange) { - onInputChange(newValue); + onInputChange(formattedValue); } if (inputProps.onChangeText) { - Str.result(inputProps.onChangeText, newValue); + Str.result(inputProps.onChangeText, formattedValue); } - if (newValue && newValue.length > 0) { + if (formattedValue && formattedValue.length > 0) { hasValueRef.current = true; // When the componment is uncontrolled, we need to manually activate the label: if (value === undefined) { @@ -361,7 +361,7 @@ function BaseTextInput( !isMultiline && {height, lineHeight: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, @@ -395,7 +395,9 @@ function BaseTextInput( e.preventDefault()} + onMouseDown={(e) => { + e.preventDefault(); + }} accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} > diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 365715f588cd..01b727f9875f 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -293,7 +293,7 @@ function BaseTextInput( return ( <> @@ -378,7 +378,7 @@ function BaseTextInput( !isMultiline && Browser.isMobileChrome() && {boxSizing: 'content-box', height: undefined}, // Stop scrollbar flashing when breaking lines with autoGrowHeight enabled. - autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, maxHeight), + autoGrowHeight && StyleUtils.getAutoGrowHeightInputStyle(styles, textInputHeight, typeof maxHeight === 'number' ? maxHeight : 0), // Add disabled color theme when field is not editable. inputProps.disabled && styles.textInputDisabled, styles.pointerEventsAuto, diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 761ad84ae2ca..550bcd0a6e18 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -6,7 +6,6 @@ import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import * as baseTextInputPropTypes from './BaseTextInput/baseTextInputPropTypes'; import BaseTextInputProps from './BaseTextInput/types'; import * as styleConst from './styleConst'; @@ -80,7 +79,5 @@ function TextInput( } TextInput.displayName = 'TextInput'; -TextInput.propTypes = baseTextInputPropTypes.propTypes; -TextInput.defaultProps = baseTextInputPropTypes.defaultProps; export default React.forwardRef(TextInput); From 142c1fac0968385e95f9b1a6d1d21c54639c0341 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 5 Dec 2023 00:23:48 +0100 Subject: [PATCH 038/260] merchant update --- src/components/MoneyRequestConfirmationList.js | 10 ++++++++-- src/libs/actions/IOU.js | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 1b4967a9c54c..43eabe31c79a 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -496,7 +496,7 @@ function MoneyRequestConfirmationList(props) { } const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0; + const shouldDisableButton = selectedParticipants.length === 0 || (props.isPolicyExpenseChat && !props.iouMerchant); const button = shouldShowSettlementButton ? ( )} {shouldShowCategories && ( diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index d9de984ad12c..7e29e46695b3 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -101,7 +101,7 @@ function resetMoneyRequestInfo(id = '') { currency: lodashGet(currentUserPersonalDetails, 'localCurrencyCode', CONST.CURRENCY.USD), comment: '', participants: [], - merchant: CONST.TRANSACTION.DEFAULT_MERCHANT, + merchant: '', category: '', tag: '', created, From 4bc71cd09ed669d7565b81a58aa33a64059f4d31 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Dec 2023 16:16:55 +0100 Subject: [PATCH 039/260] fix: removed commented code --- src/components/TextInput/BaseTextInput/index.native.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 5432d485531c..3aae53046b97 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -111,12 +111,10 @@ function BaseTextInput( Animated.parallel([ Animated.spring(labelTranslateY, { toValue: translateY, - // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), Animated.spring(labelScale, { toValue: scale, - // duration: styleConst.LABEL_ANIMATION_DURATION, useNativeDriver, }), ]).start(); From 6ac934c65b6e841d7a8ea23ed54314dad8dd846c Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 5 Dec 2023 23:15:11 +0700 Subject: [PATCH 040/260] fix try to fix performance regression --- .../OnyxSelectors/reportWithoutHasDraftSelector.ts | 3 +-- src/libs/actions/Report.js | 7 +++++++ src/pages/home/ReportScreen.js | 12 ------------ src/pages/home/report/ReportActionsView.js | 1 + 4 files changed, 9 insertions(+), 14 deletions(-) diff --git a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts index d78a26ba52c1..9fd89cfdf146 100644 --- a/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts +++ b/src/libs/OnyxSelectors/reportWithoutHasDraftSelector.ts @@ -5,6 +5,5 @@ export default function reportWithoutHasDraftSelector(report: OnyxKeyValue<'repo return report; } const {hasDraft, ...reportWithoutHasDraft} = report; - const {lastVisitTime, ...reportWithoutHasDraftAndLastVisitTime} = reportWithoutHasDraft; - return reportWithoutHasDraftAndLastVisitTime; + return reportWithoutHasDraft; } diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index d88a3a0f8495..3f8175c6ca60 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -489,6 +489,13 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p isLoadingNewerReportActions: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, + value: { + lastVisitTime: DateUtils.getDBTime(), + }, + }, ]; const reportSuccessData = [ diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index 92a6a99d16e7..d9ccad121f74 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -1,4 +1,3 @@ -import {useIsFocused} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; @@ -167,17 +166,6 @@ function ReportScreen({ const reportID = getReportID(route); - const isFocused = useIsFocused(); - - // We need to update the lastVisitTime every time the report is focused - // because lastReadTime is not updated every time the user visits a report. - useEffect(() => { - if (!report.reportID || !isFocused) { - return; - } - Report.updateLastVisitTime(report.reportID); - }, [report.reportID, isFocused]); - const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 607d98039070..8ba4bedf197b 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -106,6 +106,7 @@ function ReportActionsView(props) { const createChatError = _.get(props.report, ['errorFields', 'createChat']); // If the report is optimistic (AKA not yet created) we don't need to call openReport again if (props.report.isOptimisticReport || !_.isEmpty(createChatError)) { + Report.updateLastVisitTime(reportID); return; } From bbe0c5df315db8ca83fc7b4d30a6ba902f4b8a58 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Dec 2023 17:16:24 +0100 Subject: [PATCH 041/260] fix: resolve comments --- src/components/Checkbox.tsx | 2 +- .../TextInput/BaseTextInput/index.native.tsx | 12 ++++++------ src/components/TextInput/BaseTextInput/index.tsx | 7 ++----- src/components/TextInput/BaseTextInput/types.ts | 7 +++++++ 4 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/components/Checkbox.tsx b/src/components/Checkbox.tsx index 39c8a484613b..90546296083e 100644 --- a/src/components/Checkbox.tsx +++ b/src/components/Checkbox.tsx @@ -1,4 +1,4 @@ -import React, {ForwardedRef, forwardRef, MouseEventHandler, KeyboardEvent as ReactKeyboardEvent} from 'react'; +import React, {type ForwardedRef, forwardRef, type MouseEventHandler, type KeyboardEvent as ReactKeyboardEvent} from 'react'; import {GestureResponderEvent, StyleProp, View, ViewStyle} from 'react-native'; import * as StyleUtils from '@styles/StyleUtils'; import useTheme from '@styles/themes/useTheme'; diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 3aae53046b97..507882bca9ff 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -4,9 +4,9 @@ import { ActivityIndicator, Animated, FlexStyle, - GestureResponderEvent, - LayoutChangeEvent, - NativeSyntheticEvent, + type GestureResponderEvent, + type LayoutChangeEvent, + type NativeSyntheticEvent, StyleProp, StyleSheet, TextInput, @@ -35,7 +35,7 @@ import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import BaseTextInputProps from './types'; +import type BaseTextInputProps from './types'; function BaseTextInput( { @@ -83,7 +83,7 @@ function BaseTextInput( const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; @@ -180,7 +180,7 @@ function BaseTextInput( const layout = event.nativeEvent.layout; - setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setWidth((prevWidth: number | null) => (autoGrowHeight ? layout.width : prevWidth)); setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, [autoGrowHeight, multiline], diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index bf74d6b1cfc8..2fec81618fd7 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -40,7 +40,6 @@ import BaseTextInputProps from './types'; function BaseTextInput( { label = '', - // name = '', value = undefined, defaultValue = undefined, placeholder = '', @@ -57,12 +56,10 @@ function BaseTextInput( hideFocusedState = false, maxLength = undefined, hint = '', - // shouldSaveDraft = false, onInputChange = () => {}, shouldDelayFocus = false, submitOnEnter = false, multiline = false, - // shouldUseDefaultValue = false, shouldInterceptSwipe = false, autoCorrect = true, prefixCharacter, @@ -82,7 +79,7 @@ function BaseTextInput( const [textInputWidth, setTextInputWidth] = useState(0); const [textInputHeight, setTextInputHeight] = useState(0); const [height, setHeight] = useState(variables.componentSizeLarge); - const [width, setWidth] = useState(); + const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; @@ -179,7 +176,7 @@ function BaseTextInput( const layout = event.nativeEvent.layout; - setWidth((prevWidth: number | undefined) => (autoGrowHeight ? layout.width : prevWidth)); + setWidth((prevWidth: number | null) => (autoGrowHeight ? layout.width : prevWidth)); setHeight((prevHeight: number) => (!multiline ? layout.height : prevHeight)); }, [autoGrowHeight, multiline], diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index e1a123fb0691..99f5d7d728ea 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -97,9 +97,16 @@ type CustomBaseTextInputProps = { /** Indicate whether or not the input should prevent swipe actions in tabs */ shouldInterceptSwipe?: boolean; + /** Should there be an error displayed */ hasError?: boolean; + + /** On Press handler */ onPress?: (event: GestureResponderEvent | KeyboardEvent) => void; + + /** Should loading state should be displayed */ isLoading?: boolean; + + /** Type of autocomplete */ autoCompleteType?: string; }; From 9ae4d5bd07b1929789e043fa73d3a642c1f3549c Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Tue, 5 Dec 2023 23:17:33 +0700 Subject: [PATCH 042/260] fix remove space --- src/pages/home/ReportScreen.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/home/ReportScreen.js b/src/pages/home/ReportScreen.js index d9ccad121f74..e9b79c41dcd7 100644 --- a/src/pages/home/ReportScreen.js +++ b/src/pages/home/ReportScreen.js @@ -165,7 +165,6 @@ function ReportScreen({ const [scrollPosition, setScrollPosition] = useState({}); const reportID = getReportID(route); - const {addWorkspaceRoomOrChatPendingAction, addWorkspaceRoomOrChatErrors} = ReportUtils.getReportOfflinePendingActionAndErrors(report); const screenWrapperStyle = [styles.appContent, styles.flex1, {marginTop: viewportOffsetTop}]; From 8423ed6ab7fe7f5630f2671ff51e12d8b921b080 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 6 Dec 2023 00:02:59 +0700 Subject: [PATCH 043/260] fix update when going back --- src/pages/home/report/ReportActionsView.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8ba4bedf197b..ff00d260accc 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -114,9 +114,12 @@ function ReportActionsView(props) { }; useEffect(() => { + if (!isFocused) { + return; + } openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [isFocused]); useEffect(() => { const prevNetwork = prevNetworkRef.current; From 7cd02ab669c9abe45e76418d3a40380fabdecfc0 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 6 Dec 2023 00:33:03 +0700 Subject: [PATCH 044/260] fix jest test 1 --- src/libs/actions/Report.js | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 3f8175c6ca60..0f8219ebf0db 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -475,9 +475,10 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: reportActionsExist(reportID) - ? {} + ? {lastVisitTime: DateUtils.getDBTime()} : { reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), + lastVisitTime: DateUtils.getDBTime(), }, }, { @@ -489,13 +490,6 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p isLoadingNewerReportActions: false, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, - value: { - lastVisitTime: DateUtils.getDBTime(), - }, - }, ]; const reportSuccessData = [ From 4da05209df8ac70fcd7da57596ec625ff94dd83f Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 6 Dec 2023 00:35:13 +0700 Subject: [PATCH 045/260] fix jest test 1 --- src/libs/actions/Report.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 0f8219ebf0db..0458186b78ae 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -475,10 +475,9 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: reportActionsExist(reportID) - ? {lastVisitTime: DateUtils.getDBTime()} + ? {} : { reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), - lastVisitTime: DateUtils.getDBTime(), }, }, { @@ -490,6 +489,13 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p isLoadingNewerReportActions: false, }, }, + { + onyxMethod: Onyx.METHOD.MERGE, + key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, + value: { + lastVisitTime: DateUtils.getDBTime(), + }, + }, ]; const reportSuccessData = [ From 07f8ea566e40e422cafccf0eaae8facbe987465f Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 6 Dec 2023 00:51:33 +0700 Subject: [PATCH 046/260] fix disable update timestamp when back --- src/pages/home/report/ReportActionsView.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ff00d260accc..8ba4bedf197b 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -114,12 +114,9 @@ function ReportActionsView(props) { }; useEffect(() => { - if (!isFocused) { - return; - } openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isFocused]); + }, []); useEffect(() => { const prevNetwork = prevNetworkRef.current; From 37f11e570f05e35784fd2e96e924a6dc11f205f3 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 6 Dec 2023 01:24:07 +0700 Subject: [PATCH 047/260] fix performance regression --- src/libs/actions/Report.js | 10 ++-------- src/pages/home/report/ReportActionsView.js | 5 ++++- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/libs/actions/Report.js b/src/libs/actions/Report.js index 0458186b78ae..0f8219ebf0db 100644 --- a/src/libs/actions/Report.js +++ b/src/libs/actions/Report.js @@ -475,9 +475,10 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, value: reportActionsExist(reportID) - ? {} + ? {lastVisitTime: DateUtils.getDBTime()} : { reportName: lodashGet(allReports, [reportID, 'reportName'], CONST.REPORT.DEFAULT_REPORT_NAME), + lastVisitTime: DateUtils.getDBTime(), }, }, { @@ -489,13 +490,6 @@ function openReport(reportID, participantLoginList = [], newReportObject = {}, p isLoadingNewerReportActions: false, }, }, - { - onyxMethod: Onyx.METHOD.MERGE, - key: `${ONYXKEYS.COLLECTION.REPORT}${reportID}`, - value: { - lastVisitTime: DateUtils.getDBTime(), - }, - }, ]; const reportSuccessData = [ diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 8ba4bedf197b..ff00d260accc 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -114,9 +114,12 @@ function ReportActionsView(props) { }; useEffect(() => { + if (!isFocused) { + return; + } openReportIfNecessary(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, []); + }, [isFocused]); useEffect(() => { const prevNetwork = prevNetworkRef.current; From b107b9caa20bd2de3545da82190c4bf7671d5820 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Tue, 5 Dec 2023 21:53:36 +0100 Subject: [PATCH 048/260] Fix Composer not positioning caret properly on iOS after substituting emoji for marker --- src/components/Composer/index.ios.js | 4 ++- .../ComposerWithSuggestions.js | 30 +++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/components/Composer/index.ios.js b/src/components/Composer/index.ios.js index b2d7a6e89230..a1b8c1a4ffe6 100644 --- a/src/components/Composer/index.ios.js +++ b/src/components/Composer/index.ios.js @@ -110,6 +110,8 @@ function Composer({shouldClear, onClear, isDisabled, maxLines, forwardedRef, isC // On native layers we like to have the Text Input not focused so the // user can read new chats without the keyboard in the way of the view. + // On Android the selection prop is required on the TextInput but this prop has issues on IOS + const propsToPass = _.omit(props, 'selection'); return ( ); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 663db82a6067..c363fb4e0f72 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -1,7 +1,7 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {findNodeHandle, NativeModules, View} from 'react-native'; +import {findNodeHandle, InteractionManager, NativeModules, Platform, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Composer from '@components/Composer'; @@ -44,6 +44,7 @@ const {RNTextInputReset} = NativeModules; // 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. const isMobileSafari = Browser.isMobileSafari(); +const isIOSNative = Platform.OS === 'ios'; /** * Broadcast that the user is typing. Debounced to limit how often we publish client events. @@ -142,6 +143,8 @@ function ComposerWithSuggestions({ const textInputRef = useRef(null); const insertedEmojisRef = useRef([]); + const syncSelectionWithOnChangeTextRef = useRef(null); + // A flag to indicate whether the onScroll callback is likely triggered by a layout change (caused by text change) or not const isScrollLikelyLayoutTriggered = useRef(false); const suggestions = lodashGet(suggestionsRef, 'current.getSuggestions', () => [])(); @@ -241,6 +244,11 @@ function ComposerWithSuggestions({ setValue(newCommentConverted); if (commentValue !== newComment) { const position = Math.max(selection.end + (newComment.length - commentRef.current.length), cursorPosition || 0); + + if (isIOSNative) { + syncSelectionWithOnChangeTextRef.current = {position, value: newComment}; + } + setSelection({ start: position, end: position, @@ -543,7 +551,25 @@ function ComposerWithSuggestions({ ref={setTextInputRef} placeholder={inputPlaceholder} placeholderTextColor={theme.placeholderText} - onChangeText={(commentValue) => updateComment(commentValue, true)} + onChangeText={(commentValue) => { + if (syncSelectionWithOnChangeTextRef.current !== null) { + setSelection({ + start: syncSelectionWithOnChangeTextRef.current.position, + end: syncSelectionWithOnChangeTextRef.current.position, + }); + } + + updateComment(commentValue, true); + + if (syncSelectionWithOnChangeTextRef.current !== null) { + let positionSnaphsot = syncSelectionWithOnChangeTextRef.current.position; + syncSelectionWithOnChangeTextRef.current = null; + + InteractionManager.runAfterInteractions(() => { + textInputRef.current.setSelection(positionSnaphsot, positionSnaphsot); + }); + } + }} onKeyPress={triggerHotkeyActions} textAlignVertical="top" style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.flex4]} From 34dc79ff7ccda8530fa7b99662baf552acada55e Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 6 Dec 2023 09:19:06 +0700 Subject: [PATCH 049/260] fix reassure error --- src/pages/home/report/ReportActionsView.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index ff00d260accc..60fc38776d2a 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -113,14 +113,6 @@ function ReportActionsView(props) { Report.openReport(reportID); }; - useEffect(() => { - if (!isFocused) { - return; - } - openReportIfNecessary(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isFocused]); - useEffect(() => { const prevNetwork = prevNetworkRef.current; // When returning from offline to online state we want to trigger a request to OpenReport which From 32400c42394560b02363c9324f49d6d2c16789f5 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Wed, 6 Dec 2023 09:27:17 +0700 Subject: [PATCH 050/260] fix reassure regression --- src/pages/home/report/ReportActionsView.js | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pages/home/report/ReportActionsView.js b/src/pages/home/report/ReportActionsView.js index 60fc38776d2a..8ba4bedf197b 100755 --- a/src/pages/home/report/ReportActionsView.js +++ b/src/pages/home/report/ReportActionsView.js @@ -113,6 +113,11 @@ function ReportActionsView(props) { Report.openReport(reportID); }; + useEffect(() => { + openReportIfNecessary(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); + useEffect(() => { const prevNetwork = prevNetworkRef.current; // When returning from offline to online state we want to trigger a request to OpenReport which From c0a9c6a47e613e22ac9df761ab0f8a7c1d062742 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 6 Dec 2023 09:45:15 +0100 Subject: [PATCH 051/260] fix: minor fix --- src/pages/home/report/ReportTypingIndicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 6c6531955115..03d04f947f39 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -38,7 +38,7 @@ function ReportTypingIndicator({userTypingStatuses}) { return null; } - // If the user is typing on OldDot, firstUserTyping will be a string (the user's login) + // If the user is typing on OldDot, firstUserTyping will be a string (the user's displayName) const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false) || firstUserTyping; if (usersTyping.length === 1) { From e6ed623ea26306a6bc7c76f9ca61822acdaf8fd9 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 6 Dec 2023 10:31:24 +0100 Subject: [PATCH 052/260] fix: display name that should be hidden as someone --- src/pages/home/report/ReportTypingIndicator.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index 03d04f947f39..dc3bc98e0c78 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -27,10 +27,11 @@ function ReportTypingIndicator({userTypingStatuses}) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; + const isUserTypingADisplayName = Number.isNaN(Number(firstUserTyping)) const firstUserTypingID = useMemo( - () => (firstUserTyping && Number.isNaN(Number(firstUserTyping)) ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), - [firstUserTyping], + () => (firstUserTyping && isUserTypingADisplayName ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), + [firstUserTyping, isUserTypingADisplayName], ); // If we are offline, the user typing statuses are not up-to-date so do not show them @@ -39,7 +40,7 @@ function ReportTypingIndicator({userTypingStatuses}) { } // If the user is typing on OldDot, firstUserTyping will be a string (the user's displayName) - const firstUserTypingDisplayName = ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false) || firstUserTyping; + const firstUserTypingDisplayName = isUserTypingADisplayName ? firstUserTyping : ReportUtils.getDisplayNameForParticipant(firstUserTypingID, false, false); if (usersTyping.length === 1) { return ( From e186fd3a15fe08861d5bdfbf7b9dbaf02a046eda Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 6 Dec 2023 10:36:20 +0100 Subject: [PATCH 053/260] fix: run prettier --- src/pages/home/report/ReportTypingIndicator.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.js index dc3bc98e0c78..b289d87f2fd1 100755 --- a/src/pages/home/report/ReportTypingIndicator.js +++ b/src/pages/home/report/ReportTypingIndicator.js @@ -27,7 +27,7 @@ function ReportTypingIndicator({userTypingStatuses}) { const styles = useThemeStyles(); const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; - const isUserTypingADisplayName = Number.isNaN(Number(firstUserTyping)) + const isUserTypingADisplayName = Number.isNaN(Number(firstUserTyping)); const firstUserTypingID = useMemo( () => (firstUserTyping && isUserTypingADisplayName ? PersonalDetailsUtils.getAccountIDsByLogins([firstUserTyping])[0] : firstUserTyping), From 1f5cac75d088c89f2e5442d42b5c28ad2a308542 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 6 Dec 2023 11:21:30 +0100 Subject: [PATCH 054/260] fix: address comments --- .../TextInput/BaseTextInput/index.native.tsx | 29 ++++++++----------- .../TextInput/BaseTextInput/index.tsx | 22 +++++++------- .../TextInput/BaseTextInput/types.ts | 1 + .../TextInput/TextInputLabel/index.tsx | 4 +-- src/components/TextInput/index.tsx | 11 +++---- 5 files changed, 32 insertions(+), 35 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 507882bca9ff..f2066a8b8271 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -40,29 +40,26 @@ import type BaseTextInputProps from './types'; function BaseTextInput( { label = '', - // name = '', - value = undefined, - defaultValue = undefined, + value, + defaultValue, placeholder = '', errorText = '', icon = null, - textInputContainerStyles = [], - containerStyles = [], - inputStyle = [], + textInputContainerStyles, + containerStyles, + inputStyle, forceActiveLabel = false, autoFocus = false, disableKeyboard = false, autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = undefined, + maxLength, hint = '', - // shouldSaveDraft = false, onInputChange = () => {}, shouldDelayFocus = false, submitOnEnter = false, multiline = false, - // shouldUseDefaultValue = false, shouldInterceptSwipe = false, autoCorrect = true, prefixCharacter, @@ -75,7 +72,7 @@ function BaseTextInput( const styles = useThemeStyles(); const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; - const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; const isMultiline = multiline || autoGrowHeight; const [isFocused, setIsFocused] = useState(false); @@ -251,7 +248,7 @@ function BaseTextInput( // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix: string) => { + const getCharacterPadding = (prefix: string): number => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -260,8 +257,6 @@ function BaseTextInput( } }; - // eslint-disable-next-line react/forbid-foreign-prop-types - // const inputProps = _.omit(props, _.keys(baseTextInputPropTypes.propTypes)); const hasLabel = Boolean(label?.length); const isReadOnly = inputProps.readOnly ?? inputProps.disabled; const inputHelpText = errorText || hint; @@ -332,15 +327,15 @@ function BaseTextInput( )} { + ref={(element) => { if (typeof ref === 'function') { - ref(el); + ref(element); } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - ref.current = el; + ref.current = element; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = el; + input.current = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 2fec81618fd7..602d7c7cf6de 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -40,8 +40,8 @@ import BaseTextInputProps from './types'; function BaseTextInput( { label = '', - value = undefined, - defaultValue = undefined, + value, + defaultValue, placeholder = '', errorText = '', icon = null, @@ -54,7 +54,7 @@ function BaseTextInput( autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = undefined, + maxLength, hint = '', onInputChange = () => {}, shouldDelayFocus = false, @@ -66,13 +66,13 @@ function BaseTextInput( inputID, ...inputProps }: BaseTextInputProps, - ref: ForwardedRef, unknown, unknown>>, + ref: ForwardedRef>>, ) { const theme = useTheme(); const styles = useThemeStyles(); const {hasError = false} = inputProps; const initialValue = value ?? defaultValue ?? ''; - const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || Boolean(prefixCharacter); + const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; const [isFocused, setIsFocused] = useState(false); const [passwordHidden, setPasswordHidden] = useState(inputProps.secureTextEntry); @@ -243,7 +243,7 @@ function BaseTextInput( // Some characters are wider than the others when rendered, e.g. '@' vs '#'. Chosen font-family and font-size // also have an impact on the width of the character, but as long as there's only one font-family and one font-size, // this method will produce reliable results. - const getCharacterPadding = (prefix: string) => { + const getCharacterPadding = (prefix: string): number => { switch (prefix) { case CONST.POLICY.ROOM_PREFIX: return 10; @@ -276,7 +276,7 @@ function BaseTextInput( const lineHeight = useMemo(() => { if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(inputStyle)) { - const lineHeightValue = inputStyle?.find((f) => f && 'lineHeight' in f && f.lineHeight !== undefined); + const lineHeightValue = inputStyle?.find((style) => style && 'lineHeight' in style && style.lineHeight !== undefined); if (lineHeightValue && 'lineHeight' in lineHeightValue) { return lineHeightValue.lineHeight; } @@ -340,15 +340,15 @@ function BaseTextInput( )} { + ref={(element) => { if (typeof ref === 'function') { - ref(el); + ref(element); } else if (ref && 'current' in ref) { // eslint-disable-next-line no-param-reassign - ref.current = el; + ref.current = element; } // @ts-expect-error We need to reassign this ref to the input ref - input.current = el; + input.current = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 99f5d7d728ea..e2d761d4c067 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -111,5 +111,6 @@ type CustomBaseTextInputProps = { }; type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; + export default BaseTextInputProps; export type {CustomBaseTextInputProps}; diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx index 07ae16b74fd7..086218eb38d0 100644 --- a/src/components/TextInput/TextInputLabel/index.tsx +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -6,13 +6,13 @@ import TextInputLabelProps from './types'; function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { const styles = useThemeStyles(); - const labelRef = useRef(null); + const labelRef = useRef(null); useEffect(() => { if (!inputId || !labelRef.current) { return; } - (labelRef.current as unknown as HTMLFormElement).setAttribute('for', inputId); + labelRef.current.setAttribute('for', inputId); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 550bcd0a6e18..e83d3bd1efca 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -57,20 +57,21 @@ function TextInput( { - // @ts-expect-error We need to reassign this ref to the input ref - textInputRef.current = el; + ref={(element) => { + if (element) { + (textInputRef.current as HTMLElement | Component>) = element; + } if (!ref) { return; } if (typeof ref === 'function') { - ref(el); + ref(element); return; } // eslint-disable-next-line no-param-reassign - ref.current = el; + ref.current = element; }} inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, inputStyle]} textInputContainerStyles={[labelAnimationStyle as StyleProp, textInputContainerStyles]} From e2480e5c4161e7b2b202671622bf1929eb89153e Mon Sep 17 00:00:00 2001 From: artus9033 Date: Wed, 6 Dec 2023 11:42:15 +0100 Subject: [PATCH 055/260] Added comments, additional conditions, refactored fix in ComposerWithSuggestions --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 56184bce73da..471e58dea40c 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -554,11 +554,13 @@ function ComposerWithSuggestions({ updateComment(commentValue, true); - if (syncSelectionWithOnChangeTextRef.current !== null) { - let positionSnaphsot = syncSelectionWithOnChangeTextRef.current.position; + if (isIOSNative && syncSelectionWithOnChangeTextRef.current !== null) { + const positionSnaphsot = syncSelectionWithOnChangeTextRef.current.position; syncSelectionWithOnChangeTextRef.current = null; InteractionManager.runAfterInteractions(() => { + // note: this implementation is only available on non-web RN, thus the wrapping + // 'if' block contains a redundant (since the ref is only used on iOS) platform check textInputRef.current.setSelection(positionSnaphsot, positionSnaphsot); }); } From a0b31b568da56f87f7001f46444fca0e6e3c2254 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Wed, 6 Dec 2023 11:47:37 +0100 Subject: [PATCH 056/260] Fixed typo in ComposerWithSuggestions fix --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 471e58dea40c..490771620e29 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -555,13 +555,13 @@ function ComposerWithSuggestions({ updateComment(commentValue, true); if (isIOSNative && syncSelectionWithOnChangeTextRef.current !== null) { - const positionSnaphsot = syncSelectionWithOnChangeTextRef.current.position; + const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; syncSelectionWithOnChangeTextRef.current = null; InteractionManager.runAfterInteractions(() => { // note: this implementation is only available on non-web RN, thus the wrapping // 'if' block contains a redundant (since the ref is only used on iOS) platform check - textInputRef.current.setSelection(positionSnaphsot, positionSnaphsot); + textInputRef.current.setSelection(positionSnapshot, positionSnapshot); }); } }} From 58f34d4275ca981d33f8fa3eeb54a11c0f60e35a Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Wed, 6 Dec 2023 14:24:30 +0100 Subject: [PATCH 057/260] fix --- src/components/MoneyRequestConfirmationList.js | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 43eabe31c79a..0e09388c4ac7 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -267,6 +267,8 @@ function MoneyRequestConfirmationList(props) { return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]); + const isIOUMerchantPresent = props.isPolicyExpenseChat && !props.iouMerchant; + useEffect(() => { if (shouldDisplayFieldError && props.hasSmartScanFailed) { setFormError('iou.receiptScanningFailed'); @@ -496,7 +498,7 @@ function MoneyRequestConfirmationList(props) { } const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0 || (props.isPolicyExpenseChat && !props.iouMerchant); + const shouldDisableButton = selectedParticipants.length === 0 || isIOUMerchantPresent; const button = shouldShowSettlementButton ? ( )} {shouldShowCategories && ( From ded2e8aa88337af65f25e4e72e746f636e9439dd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 6 Dec 2023 18:39:01 +0100 Subject: [PATCH 058/260] move animation from TabSelector to TabSelectorItem --- src/components/TabSelector/TabSelector.js | 65 +++---------------- src/components/TabSelector/TabSelectorItem.js | 55 +++++++++------- src/styles/styles.ts | 9 --- 3 files changed, 42 insertions(+), 87 deletions(-) diff --git a/src/components/TabSelector/TabSelector.js b/src/components/TabSelector/TabSelector.js index 602a326d6c48..d451bb31a675 100644 --- a/src/components/TabSelector/TabSelector.js +++ b/src/components/TabSelector/TabSelector.js @@ -1,10 +1,9 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import * as Expensicons from '@components/Icon/Expensicons'; import useLocalize from '@hooks/useLocalize'; -import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import TabSelectorItem from './TabSelectorItem'; @@ -53,56 +52,13 @@ const getIconAndTitle = (route, translate) => { } }; -const getOpacity = (position, routesLength, tabIndex, active, affectedTabs) => { - const activeValue = active ? 1 : 0; - const inactiveValue = active ? 0 : 1; - - if (routesLength > 1) { - const inputRange = Array.from({length: routesLength}, (v, i) => i); - - return position.interpolate({ - inputRange, - outputRange: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? activeValue : inactiveValue)), - }); - } - return activeValue; -}; - -function TabSelector({state, navigation, onTabPress, position}) { +function TabSelector({state, navigation, onTabPress}) { const {translate} = useLocalize(); - const theme = useTheme(); const styles = useThemeStyles(); - const defaultAffectedAnimatedTabs = useMemo(() => Array.from({length: state.routes.length}, (v, i) => i), [state.routes.length]); - const [affectedAnimatedTabs, setAffectedAnimatedTabs] = useState(defaultAffectedAnimatedTabs); - - const getBackgroundColor = useCallback( - (routesLength, tabIndex, affectedTabs) => { - if (routesLength > 1) { - const inputRange = Array.from({length: routesLength}, (v, i) => i); - - return position.interpolate({ - inputRange, - outputRange: _.map(inputRange, (i) => (affectedTabs.includes(tabIndex) && i === tabIndex ? theme.border : theme.appBG)), - }); - } - return theme.border; - }, - [theme, position], - ); - React.useEffect(() => { - // It is required to wait transition end to reset affectedAnimatedTabs because tabs style is still animating during transition. - setTimeout(() => { - setAffectedAnimatedTabs(defaultAffectedAnimatedTabs); - }, CONST.ANIMATED_TRANSITION); - }, [defaultAffectedAnimatedTabs, state.index]); - - return ( - - {_.map(state.routes, (route, index) => { - const activeOpacity = getOpacity(position, state.routes.length, index, true, affectedAnimatedTabs); - const inactiveOpacity = getOpacity(position, state.routes.length, index, false, affectedAnimatedTabs); - const backgroundColor = getBackgroundColor(state.routes.length, index, affectedAnimatedTabs); + const tabs = useMemo( + () => + _.map(state.routes, (route, index) => { const isFocused = index === state.index; const {icon, title} = getIconAndTitle(route.name, translate); @@ -111,8 +67,6 @@ function TabSelector({state, navigation, onTabPress, position}) { return; } - setAffectedAnimatedTabs([state.index, index]); - const event = navigation.emit({ type: 'tabPress', target: route.key, @@ -133,15 +87,14 @@ function TabSelector({state, navigation, onTabPress, position}) { icon={icon} title={title} onPress={onPress} - activeOpacity={activeOpacity} - inactiveOpacity={inactiveOpacity} - backgroundColor={backgroundColor} isFocused={isFocused} /> ); - })} - + }), + [navigation, onTabPress, state.index, state.routes, translate], ); + + return {tabs}; } TabSelector.propTypes = propTypes; diff --git a/src/components/TabSelector/TabSelectorItem.js b/src/components/TabSelector/TabSelectorItem.js index 116b5db02d2c..1b6d04405a09 100644 --- a/src/components/TabSelector/TabSelectorItem.js +++ b/src/components/TabSelector/TabSelectorItem.js @@ -1,8 +1,10 @@ import PropTypes from 'prop-types'; -import React from 'react'; +import React, {useCallback, useEffect, useRef} from 'react'; import {Animated, StyleSheet} from 'react-native'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; +import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; +import CONST from '@src/CONST'; import TabIcon from './TabIcon'; import TabLabel from './TabLabel'; @@ -16,18 +18,6 @@ const propTypes = { /** Title of the tab */ title: PropTypes.string, - /** Animated background color value for the tab button */ - // eslint-disable-next-line - backgroundColor: PropTypes.any, - - /** Animated opacity value while the label is inactive state */ - // eslint-disable-next-line - inactiveOpacity: PropTypes.any, - - /** Animated opacity value while the label is in active state */ - // eslint-disable-next-line - activeOpacity: PropTypes.any, - /** Whether this tab is active */ isFocused: PropTypes.bool, }; @@ -36,14 +26,35 @@ const defaultProps = { onPress: () => {}, icon: () => {}, title: '', - backgroundColor: '', - inactiveOpacity: 1, - activeOpacity: 0, isFocused: false, }; -function TabSelectorItem({icon, title, onPress, backgroundColor, activeOpacity, inactiveOpacity, isFocused}) { +function TabSelectorItem({icon, title, onPress, isFocused}) { + const focusValueRef = useRef(new Animated.Value(isFocused ? 1 : 0)); const styles = useThemeStyles(); + const theme = useTheme(); + + useEffect(() => { + Animated.timing(focusValueRef.current, { + toValue: isFocused ? 1 : 0, + duration: CONST.ANIMATED_TRANSITION, + useNativeDriver: true, + }).start(); + }, [isFocused]); + + const getBackgroundColorStyle = useCallback( + (hovered) => { + if (hovered && !isFocused) { + return {backgroundColor: theme.highlightBG}; + } + return {backgroundColor: focusValueRef.current.interpolate({inputRange: [0, 1], outputRange: [theme.appBG, theme.border]})}; + }, + [theme, isFocused], + ); + + const activeOpacityValue = focusValueRef.current; + const inactiveOpacityValue = focusValueRef.current.interpolate({inputRange: [0, 1], outputRange: [1, 0]}); + return ( {({hovered}) => ( - + )} diff --git a/src/styles/styles.ts b/src/styles/styles.ts index b88119beae74..92f4bf561213 100644 --- a/src/styles/styles.ts +++ b/src/styles/styles.ts @@ -3612,15 +3612,6 @@ const styles = (theme: ThemeColors) => fontWeight: isSelected ? fontWeightBold : '400', color: isSelected ? theme.text : theme.textSupporting, } satisfies TextStyle), - - tabBackground: (hovered: boolean, isFocused: boolean, background: string) => ({ - backgroundColor: hovered && !isFocused ? theme.highlightBG : background, - }), - - tabOpacity: (hovered: boolean, isFocused: boolean, activeOpacityValue: number, inactiveOpacityValue: number) => ({ - opacity: hovered && !isFocused ? inactiveOpacityValue : activeOpacityValue, - }), - overscrollSpacer: (backgroundColor: string, height: number) => ({ backgroundColor, From 50a6b8c0514c02a9d4db906e000e310b7524c0fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Wed, 6 Dec 2023 18:42:43 +0100 Subject: [PATCH 059/260] remove unused props --- src/components/TabSelector/TabSelector.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/components/TabSelector/TabSelector.js b/src/components/TabSelector/TabSelector.js index d451bb31a675..a9a1d28e503d 100644 --- a/src/components/TabSelector/TabSelector.js +++ b/src/components/TabSelector/TabSelector.js @@ -21,18 +21,10 @@ const propTypes = { /* Callback fired when tab is pressed */ onTabPress: PropTypes.func, - - /* AnimatedValue for the position of the screen while swiping */ - position: PropTypes.shape({ - interpolate: PropTypes.func.isRequired, - }), }; const defaultProps = { onTabPress: () => {}, - position: { - interpolate: () => {}, - }, }; const getIconAndTitle = (route, translate) => { From 7fcc36ead371dc78b2bedac1b39eaab73fb5c991 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Wed, 6 Dec 2023 21:01:33 +0100 Subject: [PATCH 060/260] fix lint --- src/components/MoneyRequestConfirmationList.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 0e09388c4ac7..d2d8e381bba3 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -545,12 +545,11 @@ function MoneyRequestConfirmationList(props) { }, [ props.isReadOnly, props.iouType, - props.isPolicyExpenseChat, - props.iouMerchant, props.bankAccountRoute, props.iouCurrencyCode, props.policyID, selectedParticipants.length, + isIOUMerchantPresent, confirm, splitOrRequestOptions, formError, From 052610c05133cdf2990323e85dd2cebf6f2b39ac Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Dec 2023 10:03:40 +0100 Subject: [PATCH 061/260] fix: resolve comments --- src/components/RNTextInput.tsx | 5 ++++- .../TextInput/BaseTextInput/index.native.tsx | 9 ++++---- .../TextInput/BaseTextInput/index.tsx | 21 +++++++++---------- .../TextInput/BaseTextInput/types.ts | 7 +++++-- src/components/TextInput/index.native.tsx | 12 ++++------- src/components/TextInput/index.tsx | 20 +++++++++++------- 6 files changed, 40 insertions(+), 34 deletions(-) diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 28555abe3266..5c3a65998e25 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -1,8 +1,9 @@ -import React, {ForwardedRef} from 'react'; +import React, {Component, ForwardedRef} from 'react'; // eslint-disable-next-line no-restricted-imports import {TextInput, TextInputProps} from 'react-native'; import Animated, {AnimatedProps} from 'react-native-reanimated'; +type AnimatedTextInputRef = Component>; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); @@ -27,3 +28,5 @@ function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef, unknown, unknown>>, + ref: BaseTextInputRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -334,8 +335,8 @@ function BaseTextInput( // eslint-disable-next-line no-param-reassign ref.current = element; } - // @ts-expect-error We need to reassign this ref to the input ref - input.current = element; + + (input.current as AnimatedTextInputRef | null) = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 602d7c7cf6de..148d668dbdd4 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React, {Component, ForwardedRef, forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import { ActivityIndicator, Animated, @@ -11,17 +11,15 @@ import { StyleSheet, TextInput, TextInputFocusEventData, - TextInputProps, View, ViewStyle, } from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import RNTextInput from '@components/RNTextInput'; +import RNTextInput, {AnimatedTextInputRef} from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; @@ -35,7 +33,8 @@ import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import BaseTextInputProps from './types'; +import type BaseTextInputProps from './types'; +import type {BaseTextInputRef} from './types'; function BaseTextInput( { @@ -45,9 +44,9 @@ function BaseTextInput( placeholder = '', errorText = '', icon = null, - textInputContainerStyles = [], - containerStyles = [], - inputStyle = [], + textInputContainerStyles, + containerStyles, + inputStyle, forceActiveLabel = false, autoFocus = false, disableKeyboard = false, @@ -66,7 +65,7 @@ function BaseTextInput( inputID, ...inputProps }: BaseTextInputProps, - ref: ForwardedRef>>, + ref: BaseTextInputRef, ) { const theme = useTheme(); const styles = useThemeStyles(); @@ -347,8 +346,8 @@ function BaseTextInput( // eslint-disable-next-line no-param-reassign ref.current = element; } - // @ts-expect-error We need to reassign this ref to the input ref - input.current = element; + + (input.current as AnimatedTextInputRef | null) = element; }} // eslint-disable-next-line {...inputProps} diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index e2d761d4c067..b8171fc91b01 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,5 +1,6 @@ -import React from 'react'; +import React, {Component, ForwardedRef} from 'react'; import {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import {AnimatedProps} from 'react-native-reanimated'; import {SrcProps} from '@components/Icon'; import {LocaleContextProps} from '@components/LocaleContextProvider'; @@ -110,7 +111,9 @@ type CustomBaseTextInputProps = { autoCompleteType?: string; }; +type BaseTextInputRef = ForwardedRef>>; + type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; export default BaseTextInputProps; -export type {CustomBaseTextInputProps}; +export type {CustomBaseTextInputProps, BaseTextInputRef}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index 7b974e1233bc..bf8145b4b2cc 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -1,15 +1,11 @@ -import React, {Component, ForwardedRef, forwardRef, useEffect} from 'react'; -import {AppState, Keyboard, TextInputProps} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; +import React, {forwardRef, useEffect} from 'react'; +import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import BaseTextInputProps from './BaseTextInput/types'; +import BaseTextInputProps, {BaseTextInputRef} from './BaseTextInput/types'; // eslint-disable-next-line react/function-component-definition -const TextInput = ( - {inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, - ref: ForwardedRef, unknown, unknown>>, -) => { +const TextInput = ({inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, ref: BaseTextInputRef) => { const styles = useThemeStyles(); useEffect(() => { diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index e83d3bd1efca..aff41d67cf88 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,4 +1,4 @@ -import React, {Component, ForwardedRef, useEffect, useRef} from 'react'; +import React, {Component, useEffect, useRef} from 'react'; import {StyleProp, TextInputProps, ViewStyle} from 'react-native'; import {AnimatedProps} from 'react-native-reanimated'; import * as Browser from '@libs/Browser'; @@ -6,18 +6,22 @@ import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import BaseTextInputProps from './BaseTextInput/types'; +import type BaseTextInputProps from './BaseTextInput/types'; +import type {BaseTextInputRef} from './BaseTextInput/types'; import * as styleConst from './styleConst'; +type RemoveVisibilityListener = () => void; + function TextInput( {label = '', name = '', textInputContainerStyles, inputStyle, disableKeyboard = false, multiline = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, - ref: ForwardedRef, unknown, unknown>>, + ref: BaseTextInputRef, ) { const styles = useThemeStyles(); const textInputRef = useRef(null); - const removeVisibilityListenerRef = useRef<() => void>(null); + const removeVisibilityListenerRef = useRef(null); useEffect(() => { + let removeVisibilityListener = removeVisibilityListenerRef.current; if (disableKeyboard) { textInputRef.current?.setAttribute('inputmode', 'none'); } @@ -25,8 +29,8 @@ function TextInput( if (name) { textInputRef.current?.setAttribute('name', name); } - // @ts-expect-error We need to reassign this ref to the input ref - removeVisibilityListenerRef.current = Visibility.onVisibilityChange(() => { + + removeVisibilityListener = Visibility.onVisibilityChange(() => { if (!Browser.isMobileChrome() || !Visibility.isVisible() || !textInputRef.current || DomUtils.getActiveElement() !== textInputRef.current) { return; } @@ -35,10 +39,10 @@ function TextInput( }); return () => { - if (!removeVisibilityListenerRef.current) { + if (!removeVisibilityListener) { return; } - removeVisibilityListenerRef.current(); + removeVisibilityListener(); }; // eslint-disable-next-line react-hooks/exhaustive-deps }, []); From 0b22b3b02f2ae18fd630b2f5109393994c468eb2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 7 Dec 2023 10:25:16 +0100 Subject: [PATCH 062/260] fix: lint error --- src/components/TextInput/BaseTextInput/index.native.tsx | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 0bc9de782b92..fe6d483c4acf 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React, {Component, ForwardedRef, forwardRef, useCallback, useEffect, useRef, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react'; import { ActivityIndicator, Animated, @@ -11,11 +11,9 @@ import { StyleSheet, TextInput, TextInputFocusEventData, - TextInputProps, View, ViewStyle, } from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; From 3a81ea41d5c8b1d763e8b1a80c072462e5369ea3 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Thu, 7 Dec 2023 15:01:36 +0100 Subject: [PATCH 063/260] remove require for smart scan --- src/components/MoneyRequestConfirmationList.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index d2d8e381bba3..33c049b45265 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -267,7 +267,7 @@ function MoneyRequestConfirmationList(props) { return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]); - const isIOUMerchantPresent = props.isPolicyExpenseChat && !props.iouMerchant; + const isIOUMerchantPresent = props.isPolicyExpenseChat && !props.isScanRequest && !props.iouMerchant; useEffect(() => { if (shouldDisplayFieldError && props.hasSmartScanFailed) { From 618b9492c7ee0d351e3c582ff7e8320c50e98bf7 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Thu, 7 Dec 2023 15:21:58 +0100 Subject: [PATCH 064/260] fix review suggestions --- src/components/BlockingViews/FullPageNotFoundView.tsx | 1 + .../BlockingViews/FullPageOfflineBlockingView.tsx | 8 ++------ 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 3893c83dc45a..31cc63ef01da 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -53,6 +53,7 @@ function FullPageNotFoundView({ }: FullPageNotFoundViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); + if (shouldShow) { return ( <> diff --git a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx index c8f5592287f3..5f6cd31227fe 100644 --- a/src/components/BlockingViews/FullPageOfflineBlockingView.tsx +++ b/src/components/BlockingViews/FullPageOfflineBlockingView.tsx @@ -2,14 +2,10 @@ import React from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; import BlockingView from './BlockingView'; -type FullPageOfflineBlockingViewProps = { - /** Child elements */ - children: React.ReactNode; -}; - -function FullPageOfflineBlockingView({children}: FullPageOfflineBlockingViewProps) { +function FullPageOfflineBlockingView({children}: ChildrenProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); From 062265efd0de45f311da38c81f0a91333ac8ec80 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Thu, 7 Dec 2023 16:30:03 +0100 Subject: [PATCH 065/260] Refactored ComposerWithSuggestions --- .../ComposerWithSuggestions.js | 52 +++++++++++-------- 1 file changed, 29 insertions(+), 23 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 574ab020d01a..25aab5747f1e 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -1,7 +1,7 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import {findNodeHandle, InteractionManager, NativeModules, Platform, View} from 'react-native'; +import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Composer from '@components/Composer'; @@ -17,6 +17,7 @@ import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; import * as EmojiUtils from '@libs/EmojiUtils'; import focusComposerWithDelay from '@libs/focusComposerWithDelay'; +import getPlatform from '@libs/getPlatform'; import * as KeyDownListener from '@libs/KeyboardShortcut/KeyDownPressListener'; import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; @@ -39,7 +40,7 @@ import {defaultProps, propTypes} from './composerWithSuggestionsProps'; const {RNTextInputReset} = NativeModules; -const isIOSNative = Platform.OS === 'ios'; +const isIOSNative = getPlatform() === CONST.PLATFORM.IOS; /** * Broadcast that the user is typing. Debounced to limit how often we publish client events. @@ -373,6 +374,31 @@ function ComposerWithSuggestions({ [isKeyboardShown, isSmallScreenWidth, parentReportActions, report, reportActions, reportID, handleSendMessage, suggestionsRef, valueRef], ); + const onChangeText = useCallback( + (commentValue) => { + if (syncSelectionWithOnChangeTextRef.current) { + setSelection({ + start: syncSelectionWithOnChangeTextRef.current.position, + end: syncSelectionWithOnChangeTextRef.current.position, + }); + } + + updateComment(commentValue, true); + + if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { + const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; + syncSelectionWithOnChangeTextRef.current = null; + + InteractionManager.runAfterInteractions(() => { + // note: this implementation is only available on non-web RN, thus the wrapping + // 'if' block contains a redundant (since the ref is only used on iOS) platform check + textInputRef.current.setSelection(positionSnapshot, positionSnapshot); + }); + } + }, + [updateComment], + ); + const onSelectionChange = useCallback( (e) => { if (textInputRef.current && textInputRef.current.isFocused() && suggestionsRef.current.onSelectionChange(e)) { @@ -533,27 +559,7 @@ function ComposerWithSuggestions({ ref={setTextInputRef} placeholder={inputPlaceholder} placeholderTextColor={theme.placeholderText} - onChangeText={(commentValue) => { - if (syncSelectionWithOnChangeTextRef.current !== null) { - setSelection({ - start: syncSelectionWithOnChangeTextRef.current.position, - end: syncSelectionWithOnChangeTextRef.current.position, - }); - } - - updateComment(commentValue, true); - - if (isIOSNative && syncSelectionWithOnChangeTextRef.current !== null) { - const positionSnapshot = syncSelectionWithOnChangeTextRef.current.position; - syncSelectionWithOnChangeTextRef.current = null; - - InteractionManager.runAfterInteractions(() => { - // note: this implementation is only available on non-web RN, thus the wrapping - // 'if' block contains a redundant (since the ref is only used on iOS) platform check - textInputRef.current.setSelection(positionSnapshot, positionSnapshot); - }); - } - }} + onChangeText={onChangeText} onKeyPress={triggerHotkeyActions} textAlignVertical="top" style={[styles.textInputCompose, isComposerFullSize ? styles.textInputFullCompose : styles.textInputCollapseCompose]} From 0fc209cbc216f5545db3a97ab4386d5493068c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Fri, 8 Dec 2023 14:05:39 +0100 Subject: [PATCH 066/260] disable animation on web and desktop --- src/components/TabSelector/TabSelector.js | 2 ++ src/components/TabSelector/TabSelectorItem.js | 24 +++++++++++++------ src/libs/Navigation/OnyxTabNavigator.tsx | 15 ++++++++++++ 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/src/components/TabSelector/TabSelector.js b/src/components/TabSelector/TabSelector.js index a9a1d28e503d..f469f0440e2c 100644 --- a/src/components/TabSelector/TabSelector.js +++ b/src/components/TabSelector/TabSelector.js @@ -4,6 +4,7 @@ import {View} from 'react-native'; import _ from 'underscore'; import * as Expensicons from '@components/Icon/Expensicons'; import useLocalize from '@hooks/useLocalize'; +import {TabNavigatorAnimationEnabled} from '@libs/Navigation/OnyxTabNavigator'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; import TabSelectorItem from './TabSelectorItem'; @@ -80,6 +81,7 @@ function TabSelector({state, navigation, onTabPress}) { title={title} onPress={onPress} isFocused={isFocused} + animationEnabled={TabNavigatorAnimationEnabled()} /> ); }), diff --git a/src/components/TabSelector/TabSelectorItem.js b/src/components/TabSelector/TabSelectorItem.js index 1b6d04405a09..1a7328f46a6b 100644 --- a/src/components/TabSelector/TabSelectorItem.js +++ b/src/components/TabSelector/TabSelectorItem.js @@ -20,6 +20,9 @@ const propTypes = { /** Whether this tab is active */ isFocused: PropTypes.bool, + + /** Whether animations should be skipped */ + animationEnabled: PropTypes.bool, }; const defaultProps = { @@ -27,20 +30,27 @@ const defaultProps = { icon: () => {}, title: '', isFocused: false, + animationEnabled: true, }; -function TabSelectorItem({icon, title, onPress, isFocused}) { +function TabSelectorItem({icon, title, onPress, isFocused, animationEnabled}) { const focusValueRef = useRef(new Animated.Value(isFocused ? 1 : 0)); const styles = useThemeStyles(); const theme = useTheme(); useEffect(() => { - Animated.timing(focusValueRef.current, { - toValue: isFocused ? 1 : 0, - duration: CONST.ANIMATED_TRANSITION, - useNativeDriver: true, - }).start(); - }, [isFocused]); + const focusValue = isFocused ? 1 : 0; + + if (animationEnabled) { + return Animated.timing(focusValueRef.current, { + toValue: focusValue, + duration: CONST.ANIMATED_TRANSITION, + useNativeDriver: true, + }).start(); + } + + focusValueRef.current.setValue(focusValue); + }, [animationEnabled, isFocused]); const getBackgroundColorStyle = useCallback( (hovered) => { diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index 1ea57e773323..b8928a88f61e 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -1,12 +1,24 @@ import {createMaterialTopTabNavigator, MaterialTopTabNavigationEventMap} from '@react-navigation/material-top-tabs'; import {EventMapCore, NavigationState, ScreenListeners} from '@react-navigation/native'; import React from 'react'; +import {Platform} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import {OnyxEntry} from 'react-native-onyx/lib/types'; import Tab from '@userActions/Tab'; import ONYXKEYS from '@src/ONYXKEYS'; import ChildrenProps from '@src/types/utils/ChildrenProps'; +const TabNavigatorAnimationEnabled = () => { + switch (Platform.OS) { + case 'macos': + case 'windows': + case 'web': + return false; + default: + return true; + } +}; + type OnyxTabNavigatorOnyxProps = { selectedTab: OnyxEntry; }; @@ -34,6 +46,7 @@ function OnyxTabNavigator({id, selectedTab = '', children, screenListeners, ...r {...rest} id={id} initialRouteName={selectedTab} + screenOptions={{animationEnabled: TabNavigatorAnimationEnabled()}} backBehavior="initialRoute" keyboardDismissMode="none" screenListeners={{ @@ -59,3 +72,5 @@ export default withOnyx({ key: ({id}) => `${ONYXKEYS.COLLECTION.SELECTED_TAB}${id}`, }, })(OnyxTabNavigator); + +export {TabNavigatorAnimationEnabled}; From 12f6197d5e983f38f2775dabdff487ba9009fdf4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 10:10:51 +0100 Subject: [PATCH 067/260] fix: types --- src/components/TextInput/BaseTextInput/index.native.tsx | 1 + src/components/TextInput/BaseTextInput/index.tsx | 1 + src/components/TextInput/BaseTextInput/types.ts | 3 ++- 3 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 8479e30f19ae..1e4671b32e1d 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -71,6 +71,7 @@ function BaseTextInput( const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {hasError = false} = inputProps; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; const isMultiline = multiline || autoGrowHeight; diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index f22a90867dc4..939a385bccf6 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -71,6 +71,7 @@ function BaseTextInput( const styles = useThemeStyles(); const {hasError = false} = inputProps; const StyleUtils = useStyleUtils(); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index b8171fc91b01..dcdfbc4a2c34 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -3,6 +3,7 @@ import {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, import {AnimatedProps} from 'react-native-reanimated'; import {SrcProps} from '@components/Icon'; import {LocaleContextProps} from '@components/LocaleContextProvider'; +import {MaybePhraseKey} from '@libs/Localize'; type CustomBaseTextInputProps = { /** Input label */ @@ -21,7 +22,7 @@ type CustomBaseTextInputProps = { placeholder?: string; /** Error text to display */ - errorText: string | string[] | Record; + errorText: MaybePhraseKey; /** Icon to display in right side of text input */ icon: ((props: SrcProps) => React.ReactNode) | null; From 5c2fffdbcfce87a7f4d38cdfc2d3a1c8715e398d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 11 Dec 2023 11:07:13 +0100 Subject: [PATCH 068/260] tabBar replace inline function with a component --- src/pages/iou/MoneyRequestSelectorPage.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pages/iou/MoneyRequestSelectorPage.js b/src/pages/iou/MoneyRequestSelectorPage.js index af52ea1222ed..e66e383cf3a1 100644 --- a/src/pages/iou/MoneyRequestSelectorPage.js +++ b/src/pages/iou/MoneyRequestSelectorPage.js @@ -112,13 +112,7 @@ function MoneyRequestSelectorPage(props) { ( - - )} + tabBar={TabSelector} > Date: Mon, 11 Dec 2023 12:29:41 +0100 Subject: [PATCH 069/260] fix: bring back proptypes as they are used in different file --- .../BaseTextInput/baseTextInputPropTypes.js | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js diff --git a/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js new file mode 100644 index 000000000000..5387d1ff81d1 --- /dev/null +++ b/src/components/TextInput/BaseTextInput/baseTextInputPropTypes.js @@ -0,0 +1,138 @@ +import PropTypes from 'prop-types'; + +const propTypes = { + /** Input label */ + label: PropTypes.string, + + /** Name attribute for the input */ + name: PropTypes.string, + + /** Input value */ + value: PropTypes.string, + + /** Default value - used for non controlled inputs */ + defaultValue: PropTypes.string, + + /** Input value placeholder */ + placeholder: PropTypes.string, + + /** Error text to display */ + errorText: PropTypes.oneOfType([PropTypes.string, PropTypes.arrayOf(PropTypes.oneOfType([PropTypes.string, PropTypes.object]))]), + + /** Icon to display in right side of text input */ + icon: PropTypes.func, + + /** Customize the TextInput container */ + textInputContainerStyles: PropTypes.arrayOf(PropTypes.object), + + /** Customize the main container */ + containerStyles: PropTypes.arrayOf(PropTypes.object), + + /** input style */ + inputStyle: PropTypes.arrayOf(PropTypes.object), + + /** If present, this prop forces the label to remain in a position where it will not collide with input text */ + forceActiveLabel: PropTypes.bool, + + /** Should the input auto focus? */ + autoFocus: PropTypes.bool, + + /** Disable the virtual keyboard */ + disableKeyboard: PropTypes.bool, + + /** + * Autogrow input container length based on the entered text. + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrow: PropTypes.bool, + + /** + * Autogrow input container height based on the entered text + * Note: If you use this prop, the text input has to be controlled + * by a value prop. + */ + autoGrowHeight: PropTypes.bool, + + /** Hide the focus styles on TextInput */ + hideFocusedState: PropTypes.bool, + + /** Forward the inner ref */ + innerRef: PropTypes.oneOfType([PropTypes.func, PropTypes.object]), + + /** Maximum characters allowed */ + maxLength: PropTypes.number, + + /** Hint text to display below the TextInput */ + hint: PropTypes.string, + + /** Prefix character */ + prefixCharacter: PropTypes.string, + + /** Whether autoCorrect functionality should enable */ + autoCorrect: PropTypes.bool, + + /** Form props */ + /** The ID used to uniquely identify the input in a Form */ + inputID: PropTypes.string, + + /** Saves a draft of the input value when used in a form */ + shouldSaveDraft: PropTypes.bool, + + /** Callback to update the value on Form when input is used in the Form component. */ + onInputChange: PropTypes.func, + + /** Whether we should wait before focusing the TextInput, useful when using transitions */ + shouldDelayFocus: PropTypes.bool, + + /** Indicate whether pressing Enter on multiline input is allowed to submit the form. */ + submitOnEnter: PropTypes.bool, + + /** Indicate whether input is multiline */ + multiline: PropTypes.bool, + + /** Set the default value to the input if there is a valid saved value */ + shouldUseDefaultValue: PropTypes.bool, + + /** Indicate whether or not the input should prevent swipe actions in tabs */ + shouldInterceptSwipe: PropTypes.bool, +}; + +const defaultProps = { + label: '', + name: '', + errorText: '', + placeholder: '', + hasError: false, + containerStyles: [], + textInputContainerStyles: [], + inputStyle: [], + autoFocus: false, + autoCorrect: true, + + /** + * To be able to function as either controlled or uncontrolled component we should not + * assign a default prop value for `value` or `defaultValue` props + */ + value: undefined, + defaultValue: undefined, + forceActiveLabel: false, + disableKeyboard: false, + autoGrow: false, + autoGrowHeight: false, + hideFocusedState: false, + innerRef: () => {}, + shouldSaveDraft: false, + maxLength: null, + hint: '', + prefixCharacter: '', + onInputChange: () => {}, + shouldDelayFocus: false, + submitOnEnter: false, + icon: null, + shouldUseDefaultValue: false, + multiline: false, + shouldInterceptSwipe: false, +}; + +export {propTypes, defaultProps}; From 61deb7f7b41c4720886ffafb9bb1c24fd4d8d3e9 Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 11 Dec 2023 18:40:54 +0700 Subject: [PATCH 070/260] fix: text in code block not vertically centered --- src/components/InlineCodeBlock/WrappedText.js | 52 ++++++++++++------- src/styles/utils/index.ts | 8 +++ 2 files changed, 42 insertions(+), 18 deletions(-) diff --git a/src/components/InlineCodeBlock/WrappedText.js b/src/components/InlineCodeBlock/WrappedText.js index f00ec891116b..4ec8c1f69ad1 100644 --- a/src/components/InlineCodeBlock/WrappedText.js +++ b/src/components/InlineCodeBlock/WrappedText.js @@ -1,8 +1,10 @@ +import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {Fragment} from 'react'; import {View} from 'react-native'; import _ from 'underscore'; import Text from '@components/Text'; +import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; @@ -20,6 +22,16 @@ function getTextMatrix(text) { return _.map(text.split('\n'), (row) => _.without(row.split(CONST.REGEX.SPACE_OR_EMOJI), '')); } +/** + * Validates if the text contains any emoji + * + * @param {String} text + * @returns {Boolean} + */ +function containsEmoji(text) { + return CONST.REGEX.EMOJIS.test(text); +} + const propTypes = { /** Required text */ children: PropTypes.string.isRequired, @@ -40,6 +52,7 @@ const defaultProps = { function WrappedText(props) { const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); if (!_.isString(props.children)) { return null; } @@ -47,25 +60,28 @@ function WrappedText(props) { const textMatrix = getTextMatrix(props.children); return ( <> - {_.map(textMatrix, (rowText, rowIndex) => ( - - {_.map(rowText, (colText, colIndex) => ( - // Outer View is important to vertically center the Text - - - {colText} + {_.map(textMatrix, (rowText, rowIndex) => { + const lineHeight = StyleUtils.getCodeLineHeight(containsEmoji(colText), lodashGet(props.textStyles, 'fontSize', 13)); + return ( + + {_.map(rowText, (colText, colIndex) => ( + // Outer View is important to vertically center the Text + + + {colText} + - - ))} - - ))} + ))} + + ); + })} ); } diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index 8d52c8de200a..b39a3105dc59 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -564,6 +564,13 @@ function getCodeFontSize(isInsideH1: boolean) { return isInsideH1 ? 15 : 13; } +/** + * Returns the line height for the HTML code tag renderer. + */ +function getCodeLineHeight(isEmojiChunk: boolean, fontSize: number): number { + return !isEmojiChunk ? 0 : fontSize; +} + /** * Gives the width for Emoji picker Widget */ @@ -1080,6 +1087,7 @@ const staticStyleUtils = { getEmojiReactionBubbleTextStyle, getFontFamilyMonospace, getCodeFontSize, + getCodeLineHeight, getFontSizeStyle, getLineHeightStyle, getMenuItemTextContainerStyle, From 67bd189a88084c9f52199488f5914dac6a587eaf Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 11 Dec 2023 18:50:46 +0700 Subject: [PATCH 071/260] fix typo --- src/components/InlineCodeBlock/WrappedText.js | 25 ++++++++++--------- src/styles/utils/index.ts | 2 +- 2 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/components/InlineCodeBlock/WrappedText.js b/src/components/InlineCodeBlock/WrappedText.js index 4ec8c1f69ad1..0dea7bf5c024 100644 --- a/src/components/InlineCodeBlock/WrappedText.js +++ b/src/components/InlineCodeBlock/WrappedText.js @@ -6,6 +6,7 @@ import _ from 'underscore'; import Text from '@components/Text'; import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; +import variables from '@styles/variables'; import CONST from '@src/CONST'; /** @@ -60,14 +61,14 @@ function WrappedText(props) { const textMatrix = getTextMatrix(props.children); return ( <> - {_.map(textMatrix, (rowText, rowIndex) => { - const lineHeight = StyleUtils.getCodeLineHeight(containsEmoji(colText), lodashGet(props.textStyles, 'fontSize', 13)); - return ( - - {_.map(rowText, (colText, colIndex) => ( + {_.map(textMatrix, (rowText, rowIndex) => ( + + {_.map(rowText, (colText, colIndex) => { + const lineHeight = StyleUtils.getCodeLineHeight(containsEmoji(colText), lodashGet(props.textStyles, 'fontSize', variables.fontSizeNormal)); + return ( // Outer View is important to vertically center the Text {colText} - ))} - - ); - })} + ); + })} + + ))} ); } diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index b39a3105dc59..bbaa4e16eeac 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -568,7 +568,7 @@ function getCodeFontSize(isInsideH1: boolean) { * Returns the line height for the HTML code tag renderer. */ function getCodeLineHeight(isEmojiChunk: boolean, fontSize: number): number { - return !isEmojiChunk ? 0 : fontSize; + return isEmojiChunk ? 0 : fontSize; } /** From f327d28c594b9b5464604f71b869acc9b63d896b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 15:28:22 +0100 Subject: [PATCH 072/260] fix: tests --- .../TextInput/BaseTextInput/index.native.tsx | 11 +++---- .../TextInput/BaseTextInput/index.tsx | 7 +++-- .../TextInput/BaseTextInput/types.ts | 3 +- src/components/TextInput/index.native.tsx | 13 ++++----- src/components/TextInput/index.tsx | 29 ++++++++----------- 5 files changed, 29 insertions(+), 34 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 1e4671b32e1d..b6db50dfea29 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -24,7 +24,7 @@ import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; import TextInputLabel from '@components/TextInput/TextInputLabel'; -import withLocalize from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import getSecureEntryKeyboardType from '@libs/getSecureEntryKeyboardType'; import isInputAutoFilled from '@libs/isInputAutoFilled'; import useNativeDriver from '@libs/useNativeDriver'; @@ -34,7 +34,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import type BaseTextInputProps from './types'; -import {BaseTextInputRef} from './types'; +import type {BaseTextInputRef} from './types'; function BaseTextInput( { @@ -70,6 +70,7 @@ function BaseTextInput( const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); const {hasError = false} = inputProps; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; @@ -262,7 +263,7 @@ function BaseTextInput( const isReadOnly = inputProps.readOnly ?? inputProps.disabled; const inputHelpText = errorText || hint; const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; - const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; + const maxHeight = StyleSheet.flatten(containerStyles)?.maxHeight; const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, @@ -394,7 +395,7 @@ function BaseTextInput( onMouseDown={(e) => { e.preventDefault(); }} - accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} + accessibilityLabel={translate?.('common.visible') ?? ''} > 0 || !!prefixCharacter; @@ -413,7 +414,7 @@ function BaseTextInput( onMouseDown={(e) => { e.preventDefault(); }} - accessibilityLabel={inputProps.translate?.('common.visible') ?? ''} + accessibilityLabel={translate?.('common.visible') ?? ''} > >>; -type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps & LocaleContextProps; +type BaseTextInputProps = CustomBaseTextInputProps & TextInputProps; export default BaseTextInputProps; export type {CustomBaseTextInputProps, BaseTextInputRef}; diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index bf8145b4b2cc..58d562caa5cd 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -4,12 +4,11 @@ import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; import BaseTextInputProps, {BaseTextInputRef} from './BaseTextInput/types'; -// eslint-disable-next-line react/function-component-definition -const TextInput = ({inputStyle, disableKeyboard = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, ref: BaseTextInputRef) => { +function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); useEffect(() => { - if (!disableKeyboard) { + if (!props.disableKeyboard) { return; } @@ -24,20 +23,20 @@ const TextInput = ({inputStyle, disableKeyboard = false, prefixCharacter, inputI return () => { appStateSubscription.remove(); }; - }, [disableKeyboard]); + }, [props.disableKeyboard]); return ( ); -}; +} TextInput.displayName = 'TextInput'; diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index aff41d67cf88..6e09fb327bf3 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,6 +1,5 @@ -import React, {Component, useEffect, useRef} from 'react'; -import {StyleProp, TextInputProps, ViewStyle} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; +import React, {useEffect, useRef} from 'react'; +import {StyleProp, ViewStyle} from 'react-native'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; @@ -12,22 +11,19 @@ import * as styleConst from './styleConst'; type RemoveVisibilityListener = () => void; -function TextInput( - {label = '', name = '', textInputContainerStyles, inputStyle, disableKeyboard = false, multiline = false, prefixCharacter, inputID, ...props}: BaseTextInputProps, - ref: BaseTextInputRef, -) { +function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); - const textInputRef = useRef(null); + const textInputRef = useRef(null); const removeVisibilityListenerRef = useRef(null); useEffect(() => { let removeVisibilityListener = removeVisibilityListenerRef.current; - if (disableKeyboard) { + if (props.disableKeyboard) { textInputRef.current?.setAttribute('inputmode', 'none'); } - if (name) { - textInputRef.current?.setAttribute('name', name); + if (props.name) { + textInputRef.current?.setAttribute('name', props.name); } removeVisibilityListener = Visibility.onVisibilityChange(() => { @@ -47,7 +43,7 @@ function TextInput( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - const isLabeledMultiline = Boolean(label?.length) && multiline; + const isLabeledMultiline = Boolean(props.label?.length) && props.multiline; const labelAnimationStyle = { // eslint-disable-next-line @typescript-eslint/naming-convention '--active-label-translate-y': `${styleConst.ACTIVE_LABEL_TRANSLATE_Y}px`, @@ -62,9 +58,8 @@ function TextInput( // eslint-disable-next-line react/jsx-props-no-spreading {...props} ref={(element) => { - if (element) { - (textInputRef.current as HTMLElement | Component>) = element; - } + textInputRef.current = element as HTMLElement; + if (!ref) { return; } @@ -77,8 +72,8 @@ function TextInput( // eslint-disable-next-line no-param-reassign ref.current = element; }} - inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, inputStyle]} - textInputContainerStyles={[labelAnimationStyle as StyleProp, textInputContainerStyles]} + inputStyle={[styles.baseTextInput, styles.textInputDesktop, isLabeledMultiline ? styles.textInputMultiline : {}, props.inputStyle]} + textInputContainerStyles={[labelAnimationStyle as StyleProp, props.textInputContainerStyles]} /> ); } From b4597cbe4a921ce6349429a94685292751f1f177 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 16:13:35 +0100 Subject: [PATCH 073/260] fix: import types --- .../TextInput/BaseTextInput/index.native.tsx | 18 +++--------------- .../TextInput/BaseTextInput/index.tsx | 16 ++-------------- .../TextInput/TextInputLabel/index.native.tsx | 2 +- .../TextInput/TextInputLabel/index.tsx | 2 +- src/components/TextInput/index.native.tsx | 5 +++-- src/components/TextInput/index.tsx | 2 +- 6 files changed, 11 insertions(+), 34 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index b6db50dfea29..c0ed78922727 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -1,25 +1,13 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useRef, useState} from 'react'; -import { - ActivityIndicator, - Animated, - FlexStyle, - type GestureResponderEvent, - type LayoutChangeEvent, - type NativeSyntheticEvent, - StyleProp, - StyleSheet, - TextInput, - TextInputFocusEventData, - View, - ViewStyle, -} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; +import type {FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import RNTextInput, {AnimatedTextInputRef} from '@components/RNTextInput'; +import RNTextInput, {type AnimatedTextInputRef} from '@components/RNTextInput'; import SwipeInterceptPanResponder from '@components/SwipeInterceptPanResponder'; import Text from '@components/Text'; import * as styleConst from '@components/TextInput/styleConst'; diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 93c764c330bc..9d42d3b1be78 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,19 +1,7 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import { - ActivityIndicator, - Animated, - FlexStyle, - GestureResponderEvent, - LayoutChangeEvent, - NativeSyntheticEvent, - StyleProp, - StyleSheet, - TextInput, - TextInputFocusEventData, - View, - ViewStyle, -} from 'react-native'; +import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; +import type {FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; diff --git a/src/components/TextInput/TextInputLabel/index.native.tsx b/src/components/TextInput/TextInputLabel/index.native.tsx index 701ac6992d7d..c9ca3dbda7a8 100644 --- a/src/components/TextInput/TextInputLabel/index.native.tsx +++ b/src/components/TextInput/TextInputLabel/index.native.tsx @@ -2,7 +2,7 @@ import React, {useState} from 'react'; import {Animated} from 'react-native'; import * as styleConst from '@components/TextInput/styleConst'; import useThemeStyles from '@styles/useThemeStyles'; -import TextInputLabelProps from './types'; +import type TextInputLabelProps from './types'; function TextInputLabel({isLabelActive, label, labelScale, labelTranslateY}: TextInputLabelProps) { const styles = useThemeStyles(); diff --git a/src/components/TextInput/TextInputLabel/index.tsx b/src/components/TextInput/TextInputLabel/index.tsx index 086218eb38d0..3a441e6ecaa0 100644 --- a/src/components/TextInput/TextInputLabel/index.tsx +++ b/src/components/TextInput/TextInputLabel/index.tsx @@ -2,7 +2,7 @@ import React, {useEffect, useRef} from 'react'; import {Animated, Text} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import TextInputLabelProps from './types'; +import type TextInputLabelProps from './types'; function TextInputLabel({for: inputId = '', label, labelTranslateY, labelScale}: TextInputLabelProps) { const styles = useThemeStyles(); diff --git a/src/components/TextInput/index.native.tsx b/src/components/TextInput/index.native.tsx index 58d562caa5cd..2041f33a641a 100644 --- a/src/components/TextInput/index.native.tsx +++ b/src/components/TextInput/index.native.tsx @@ -2,7 +2,8 @@ import React, {forwardRef, useEffect} from 'react'; import {AppState, Keyboard} from 'react-native'; import useThemeStyles from '@styles/useThemeStyles'; import BaseTextInput from './BaseTextInput'; -import BaseTextInputProps, {BaseTextInputRef} from './BaseTextInput/types'; +import type BaseTextInputProps from './BaseTextInput/types'; +import type {BaseTextInputRef} from './BaseTextInput/types'; function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { const styles = useThemeStyles(); @@ -27,12 +28,12 @@ function TextInput(props: BaseTextInputProps, ref: BaseTextInputRef) { return ( ); diff --git a/src/components/TextInput/index.tsx b/src/components/TextInput/index.tsx index 6e09fb327bf3..94640debf3d4 100644 --- a/src/components/TextInput/index.tsx +++ b/src/components/TextInput/index.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useRef} from 'react'; -import {StyleProp, ViewStyle} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import * as Browser from '@libs/Browser'; import DomUtils from '@libs/DomUtils'; import Visibility from '@libs/Visibility'; From 3abace7c03af33716326c87eb2953d6f59cb5b04 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 11 Dec 2023 16:15:40 +0100 Subject: [PATCH 074/260] fix: import types --- src/components/TextInput/BaseTextInput/types.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 0430644b0357..c71be6dce94e 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,8 +1,9 @@ -import React, {Component, ForwardedRef} from 'react'; -import {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; -import {AnimatedProps} from 'react-native-reanimated'; -import {SrcProps} from '@components/Icon'; -import {MaybePhraseKey} from '@libs/Localize'; +import React from 'react'; +import type {Component, ForwardedRef} from 'react'; +import type {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import type {AnimatedProps} from 'react-native-reanimated'; +import type {SrcProps} from '@components/Icon'; +import type {MaybePhraseKey} from '@libs/Localize'; type CustomBaseTextInputProps = { /** Input label */ From 328621921047beec67f88d69ef30ed34b5330c68 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Mon, 11 Dec 2023 16:44:43 +0100 Subject: [PATCH 075/260] Bump From 62b7acd96d00d23ec524c1daa329641d7e1e64f7 Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Tue, 12 Dec 2023 03:00:23 +0100 Subject: [PATCH 076/260] fix previous month display on calendar after coming back to form --- src/components/DatePicker/CalendarPicker/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DatePicker/CalendarPicker/index.js b/src/components/DatePicker/CalendarPicker/index.js index a404c4746397..7a558bd92990 100644 --- a/src/components/DatePicker/CalendarPicker/index.js +++ b/src/components/DatePicker/CalendarPicker/index.js @@ -50,7 +50,7 @@ class CalendarPicker extends React.PureComponent { if (props.minDate >= props.maxDate) { throw new Error('Minimum date cannot be greater than the maximum date.'); } - let currentDateView = new Date(props.value); + let currentDateView = parseISO(props.value); if (props.maxDate < currentDateView) { currentDateView = props.maxDate; } else if (props.minDate > currentDateView) { From 653790e2d6c895b0c5b4b4397eca907820d0530b Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 12 Dec 2023 11:52:11 +0800 Subject: [PATCH 077/260] remove bg color stuck workaround --- src/pages/home/report/ReportActionItem.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/pages/home/report/ReportActionItem.js b/src/pages/home/report/ReportActionItem.js index 3e4eebffe48f..f2d2870caaee 100644 --- a/src/pages/home/report/ReportActionItem.js +++ b/src/pages/home/report/ReportActionItem.js @@ -157,12 +157,6 @@ function ReportActionItem(props) { // IOUDetails only exists when we are sending money const isSendingMoney = originalMessage.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && _.has(originalMessage, 'IOUDetails'); - // When active action changes, we need to update the `isContextMenuActive` state - const isActiveReportActionForMenu = ReportActionContextMenu.isActiveReportAction(props.action.reportActionID); - useEffect(() => { - setIsContextMenuActive(isActiveReportActionForMenu); - }, [isActiveReportActionForMenu]); - const updateHiddenState = useCallback( (isHiddenValue) => { setIsHidden(isHiddenValue); From a9495bdaf20acdea12c93ad60943ffe08b2c26f1 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 12 Dec 2023 11:52:23 +0800 Subject: [PATCH 078/260] apply a correct fix to the bg color stuck --- src/pages/home/report/ContextMenu/ContextMenuActions.js | 6 +++--- .../report/ContextMenu/PopoverReportActionContextMenu.js | 1 + 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ContextMenu/ContextMenuActions.js b/src/pages/home/report/ContextMenu/ContextMenuActions.js index 2d9faa574ebb..4d7502d77c4a 100644 --- a/src/pages/home/report/ContextMenu/ContextMenuActions.js +++ b/src/pages/home/report/ContextMenu/ContextMenuActions.js @@ -22,7 +22,7 @@ import * as Download from '@userActions/Download'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -import {clearActiveReportAction, hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; +import {hideContextMenu, showDeleteModal} from './ReportActionContextMenu'; /** * Gets the HTML version of the message in an action. @@ -409,12 +409,12 @@ export default [ onPress: (closePopover, {reportID, reportAction}) => { if (closePopover) { // Hide popover, then call showDeleteConfirmModal - hideContextMenu(false, () => showDeleteModal(reportID, reportAction, true, clearActiveReportAction, clearActiveReportAction)); + hideContextMenu(false, () => showDeleteModal(reportID, reportAction)); return; } // No popover to hide, call showDeleteConfirmModal immediately - showDeleteModal(reportID, reportAction, true, clearActiveReportAction, clearActiveReportAction); + showDeleteModal(reportID, reportAction); }, getDescription: () => {}, }, diff --git a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js index 7f60b9d9b4d5..30c68dd52a14 100644 --- a/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js +++ b/src/pages/home/report/ContextMenu/PopoverReportActionContextMenu.js @@ -322,6 +322,7 @@ function PopoverReportActionContextMenu(_props, ref) { onConfirm={confirmDeleteAndHideModal} onCancel={hideDeleteModal} onModalHide={() => { + clearActiveReportAction(); callbackWhenDeleteModalHide.current(); }} prompt={translate('reportActionContextMenu.deleteConfirmation', {action: reportAction})} From 0a7d393635884428b0f75d66715f3d25a3daf707 Mon Sep 17 00:00:00 2001 From: Bernhard Owen Josephus Date: Tue, 12 Dec 2023 12:54:22 +0800 Subject: [PATCH 079/260] use merge for failure data --- src/libs/actions/IOU.js | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/src/libs/actions/IOU.js b/src/libs/actions/IOU.js index 9040d4c1607b..0265fda9aace 100644 --- a/src/libs/actions/IOU.js +++ b/src/libs/actions/IOU.js @@ -2079,17 +2079,28 @@ function editRegularMoneyRequest(transactionID, transactionThreadReportID, trans }, }, { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, - value: transaction, + value: { + ...transaction, + modifiedCreated: transaction.modifiedCreated ? transaction.modifiedCreated : null, + modifiedAmount: transaction.modifiedAmount ? transaction.modifiedCreated : null, + modifiedCurrency: transaction.modifiedCurrency ? transaction.modifiedCurrency : null, + modifiedMerchant: transaction.modifiedMerchant ? transaction.modifiedMerchant : null, + modifiedWaypoints: transaction.modifiedWaypoints ? transaction.modifiedWaypoints : null, + pendingFields: null, + }, }, { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, - value: iouReport, + value: { + ...iouReport, + cachedTotal: iouReport.cachedTotal ? iouReport.cachedTotal : null, + }, }, { - onyxMethod: Onyx.METHOD.SET, + onyxMethod: Onyx.METHOD.MERGE, key: `${ONYXKEYS.COLLECTION.REPORT}${iouReport.chatReportID}`, value: chatReport, }, From 60739a1c01fa1962b9f4dfaba874e928f5d35f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20Miko=C5=82ajczak?= Date: Tue, 12 Dec 2023 10:15:34 +0100 Subject: [PATCH 080/260] fix animation enabled config --- src/components/TabSelector/TabSelector.js | 2 +- src/libs/Navigation/OnyxTabNavigator.tsx | 15 ++++----------- 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/src/components/TabSelector/TabSelector.js b/src/components/TabSelector/TabSelector.js index f469f0440e2c..13cf67f12e29 100644 --- a/src/components/TabSelector/TabSelector.js +++ b/src/components/TabSelector/TabSelector.js @@ -81,7 +81,7 @@ function TabSelector({state, navigation, onTabPress}) { title={title} onPress={onPress} isFocused={isFocused} - animationEnabled={TabNavigatorAnimationEnabled()} + animationEnabled={TabNavigatorAnimationEnabled} /> ); }), diff --git a/src/libs/Navigation/OnyxTabNavigator.tsx b/src/libs/Navigation/OnyxTabNavigator.tsx index b8928a88f61e..e0eefb68032e 100644 --- a/src/libs/Navigation/OnyxTabNavigator.tsx +++ b/src/libs/Navigation/OnyxTabNavigator.tsx @@ -8,16 +8,9 @@ import Tab from '@userActions/Tab'; import ONYXKEYS from '@src/ONYXKEYS'; import ChildrenProps from '@src/types/utils/ChildrenProps'; -const TabNavigatorAnimationEnabled = () => { - switch (Platform.OS) { - case 'macos': - case 'windows': - case 'web': - return false; - default: - return true; - } -}; +const TabNavigatorAnimationEnabled = ['ios', 'android'].includes(Platform.OS); + +const screenOptions = {animationEnabled: TabNavigatorAnimationEnabled}; type OnyxTabNavigatorOnyxProps = { selectedTab: OnyxEntry; @@ -46,7 +39,7 @@ function OnyxTabNavigator({id, selectedTab = '', children, screenListeners, ...r {...rest} id={id} initialRouteName={selectedTab} - screenOptions={{animationEnabled: TabNavigatorAnimationEnabled()}} + screenOptions={screenOptions} backBehavior="initialRoute" keyboardDismissMode="none" screenListeners={{ From cba69d03b7cd37e95affc751d3b3e44408212824 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Dec 2023 10:56:08 +0100 Subject: [PATCH 081/260] fix: resolve comments --- src/components/TextInput/BaseTextInput/types.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index c71be6dce94e..38341c721928 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -1,6 +1,6 @@ import React from 'react'; import type {Component, ForwardedRef} from 'react'; -import type {FlexStyle, GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, StyleProp, TextInputProps, TextStyle, ViewStyle} from 'react-native'; import type {AnimatedProps} from 'react-native-reanimated'; import type {SrcProps} from '@components/Icon'; import type {MaybePhraseKey} from '@libs/Localize'; @@ -22,19 +22,19 @@ type CustomBaseTextInputProps = { placeholder?: string; /** Error text to display */ - errorText: MaybePhraseKey; + errorText?: MaybePhraseKey; /** Icon to display in right side of text input */ icon: ((props: SrcProps) => React.ReactNode) | null; /** Customize the TextInput container */ - textInputContainerStyles: StyleProp; + textInputContainerStyles?: StyleProp; /** Customize the main container */ - containerStyles: StyleProp; + containerStyles?: StyleProp; /** input style */ - inputStyle: StyleProp; + inputStyle?: StyleProp; /** If present, this prop forces the label to remain in a position where it will not collide with input text */ forceActiveLabel?: boolean; From 886aa60d76d7a07710294585c3bd20b9ab9c8c24 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 12 Dec 2023 14:10:56 +0100 Subject: [PATCH 082/260] fix: resolved comments --- src/components/RNTextInput.tsx | 1 - .../TextInput/BaseTextInput/index.native.tsx | 49 +++++++------- .../TextInput/BaseTextInput/index.tsx | 65 ++++++++++--------- .../TextInput/BaseTextInput/types.ts | 2 +- src/libs/isInputAutoFilled.ts | 3 +- 5 files changed, 61 insertions(+), 59 deletions(-) diff --git a/src/components/RNTextInput.tsx b/src/components/RNTextInput.tsx index 5c3a65998e25..db75dc936589 100644 --- a/src/components/RNTextInput.tsx +++ b/src/components/RNTextInput.tsx @@ -7,7 +7,6 @@ type AnimatedTextInputRef = Component>; // Convert the underlying TextInput into an Animated component so that we can take an animated ref and pass it to a worklet const AnimatedTextInput = Animated.createAnimatedComponent(TextInput); -// eslint-disable-next-line @typescript-eslint/no-explicit-any function RNTextInputWithRef(props: TextInputProps, ref: ForwardedRef>>) { return ( {}, shouldDelayFocus = false, @@ -49,17 +53,20 @@ function BaseTextInput( multiline = false, shouldInterceptSwipe = false, autoCorrect = true, - prefixCharacter, + prefixCharacter = '', inputID, - ...inputProps + ...props }: BaseTextInputProps, ref: BaseTextInputRef, ) { + const inputProps = {shouldSaveDraft: false, shouldUseDefaultValue: false, ...props}; const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + const {hasError = false} = inputProps; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; @@ -73,8 +80,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -132,16 +138,12 @@ function BaseTextInput( }, [animateLabel, forceActiveLabel, prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { - if (inputProps.onFocus) { - inputProps.onFocus(event); - } + inputProps.onFocus?.(event); setIsFocused(true); }; const onBlur = (event: NativeSyntheticEvent) => { - if (inputProps.onBlur) { - inputProps.onBlur(event); - } + inputProps.onBlur?.(event); setIsFocused(false); }; @@ -150,9 +152,7 @@ function BaseTextInput( return; } - if (inputProps.onPress) { - inputProps.onPress(event); - } + inputProps.onPress?.(event); if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { input.current?.focus(); @@ -186,7 +186,7 @@ function BaseTextInput( isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current as unknown as Element) + isInputAutoFilled(input.current) ) { activateLabel(); } else { @@ -209,9 +209,7 @@ function BaseTextInput( const setValue = (newValue: string) => { const formattedValue = isMultiline ? newValue : newValue.replace(/\n/g, ' '); - if (onInputChange) { - onInputChange(formattedValue); - } + onInputChange?.(formattedValue); if (inputProps.onChangeText) { Str.result(inputProps.onChangeText, formattedValue); @@ -219,7 +217,7 @@ function BaseTextInput( if (formattedValue && formattedValue.length > 0) { hasValueRef.current = true; - // When the componment is uncontrolled, we need to manually activate the label: + // When the component is uncontrolled, we need to manually activate the label: if (value === undefined) { activateLabel(); } @@ -252,7 +250,7 @@ function BaseTextInput( const inputHelpText = errorText || hint; const placeholderValue = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles)?.maxHeight; - const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), @@ -271,8 +269,7 @@ function BaseTextInput( } {}, shouldDelayFocus = false, @@ -49,8 +53,10 @@ function BaseTextInput( multiline = false, shouldInterceptSwipe = false, autoCorrect = true, - prefixCharacter, + prefixCharacter = '', inputID, + shouldSaveDraft = false, + shouldUseDefaultValue = false, ...inputProps }: BaseTextInputProps, ref: BaseTextInputRef, @@ -60,6 +66,8 @@ function BaseTextInput( const {hasError = false} = inputProps; const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); + + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; @@ -72,8 +80,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -109,7 +116,9 @@ function BaseTextInput( ); const activateLabel = useCallback(() => { - const newValue = value ?? ''; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const newValue = value || ''; if (newValue.length < 0 || isLabelActive.current) { return; @@ -120,7 +129,9 @@ function BaseTextInput( }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - const newValue = value ?? ''; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const newValue = value || ''; if (!!forceActiveLabel || newValue.length !== 0 || prefixCharacter) { return; @@ -131,16 +142,12 @@ function BaseTextInput( }, [animateLabel, forceActiveLabel, prefixCharacter, value]); const onFocus = (event: NativeSyntheticEvent) => { - if (inputProps.onFocus) { - onFocus(event); - } + inputProps.onFocus?.(event); setIsFocused(true); }; const onBlur = (event: NativeSyntheticEvent) => { - if (inputProps.onBlur) { - inputProps.onBlur(event); - } + inputProps.onBlur?.(event); setIsFocused(false); }; @@ -149,9 +156,7 @@ function BaseTextInput( return; } - if (inputProps.onPress) { - inputProps.onPress(event); - } + inputProps.onPress?.(event); if ('isDefaultPrevented' in event && !event?.isDefaultPrevented()) { input.current?.focus(); @@ -174,7 +179,9 @@ function BaseTextInput( // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - const inputValue = value ?? ''; + // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const inputValue = value || ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled @@ -185,7 +192,7 @@ function BaseTextInput( isFocused || // If the text has been supplied by Chrome autofill, the value state is not synced with the value // as Chrome doesn't trigger a change event. When there is autofill text, keep the label activated. - isInputAutoFilled(input.current as unknown as Element) + isInputAutoFilled(input.current) ) { activateLabel(); } else { @@ -206,9 +213,8 @@ function BaseTextInput( * Set Value & activateLabel */ const setValue = (newValue: string) => { - if (onInputChange) { - onInputChange(newValue); - } + onInputChange?.(newValue); + if (inputProps.onChangeText) { Str.result(inputProps.onChangeText, newValue); } @@ -247,7 +253,7 @@ function BaseTextInput( const inputHelpText = errorText || hint; const newPlaceholder = !!prefixCharacter || isFocused || !hasLabel || (hasLabel && forceActiveLabel) ? placeholder : undefined; const maxHeight = StyleSheet.flatten(containerStyles).maxHeight; - const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ + const newTextInputContainerStyles: StyleProp = StyleSheet.flatten([ styles.textInputContainer, textInputContainerStyles, autoGrow && StyleUtils.getWidthStyle(textInputWidth), @@ -265,10 +271,10 @@ function BaseTextInput( See https://github.com/Expensify/App/issues/13802 */ const lineHeight = useMemo(() => { - if ((Browser.isSafari() || Browser.isMobileChrome()) && Array.isArray(inputStyle)) { - const lineHeightValue = inputStyle?.find((style) => style && 'lineHeight' in style && style.lineHeight !== undefined); - if (lineHeightValue && 'lineHeight' in lineHeightValue) { - return lineHeightValue.lineHeight; + if (Browser.isSafari() || Browser.isMobileChrome()) { + const lineHeightValue = StyleSheet.flatten(inputStyle).lineHeight; + if (lineHeightValue !== undefined) { + return lineHeightValue; } } @@ -285,8 +291,7 @@ function BaseTextInput( } Date: Tue, 12 Dec 2023 15:21:54 +0100 Subject: [PATCH 083/260] update ('none') display --- .../MoneyRequestConfirmationList.js | 11 ++++---- .../ReportActionItem/MoneyRequestView.js | 2 +- src/libs/actions/IOU.js | 2 +- src/pages/EditRequestMerchantPage.js | 27 +++++++++++-------- src/pages/EditRequestPage.js | 5 ++++ src/pages/iou/MoneyRequestMerchantPage.js | 4 ++- 6 files changed, 32 insertions(+), 19 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index 33c049b45265..cd2215f0c7fb 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -267,7 +267,8 @@ function MoneyRequestConfirmationList(props) { return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]); - const isIOUMerchantPresent = props.isPolicyExpenseChat && !props.isScanRequest && !props.iouMerchant; + const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || props.iouMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT; + const shouldDisplayMerchantError = props.isPolicyExpenseChat && !props.isScanRequest && isMerchantEmpty; useEffect(() => { if (shouldDisplayFieldError && props.hasSmartScanFailed) { @@ -498,7 +499,7 @@ function MoneyRequestConfirmationList(props) { } const shouldShowSettlementButton = props.iouType === CONST.IOU.TYPE.SEND; - const shouldDisableButton = selectedParticipants.length === 0 || isIOUMerchantPresent; + const shouldDisableButton = selectedParticipants.length === 0 || shouldDisplayMerchantError; const button = shouldShowSettlementButton ? ( )} {shouldShowCategories && ( diff --git a/src/components/ReportActionItem/MoneyRequestView.js b/src/components/ReportActionItem/MoneyRequestView.js index 0249a9f5bb11..f2abcaa06222 100644 --- a/src/components/ReportActionItem/MoneyRequestView.js +++ b/src/components/ReportActionItem/MoneyRequestView.js @@ -232,7 +232,7 @@ function MoneyRequestView({report, parentReport, policyCategories, shouldShowHor { - const errors = {}; - - if (_.isEmpty(value.merchant)) { - errors.merchant = 'common.error.fieldRequired'; - } - - return errors; - }, []); + const validate = useCallback( + (value) => { + const errors = {}; + if (_.isEmpty(value.merchant) && isPolicyExpenseChat) { + errors.merchant = 'common.error.fieldRequired'; + } + return errors; + }, + [isPolicyExpenseChat], + ); return ( { // In case the merchant hasn't been changed, do not make the API request. if (transactionChanges.merchant.trim() === transactionMerchant) { Navigation.dismissModal(); return; } + if (transactionChanges.merchant.trim() === '') { + editMoneyRequest({merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT}); + return; + } editMoneyRequest({merchant: transactionChanges.merchant.trim()}); }} /> diff --git a/src/pages/iou/MoneyRequestMerchantPage.js b/src/pages/iou/MoneyRequestMerchantPage.js index 2c5869dfa7a3..e181e0cce0cd 100644 --- a/src/pages/iou/MoneyRequestMerchantPage.js +++ b/src/pages/iou/MoneyRequestMerchantPage.js @@ -48,11 +48,13 @@ const defaultProps = { }; function MoneyRequestMerchantPage({iou, route}) { + console.log('iou :>> ', iou); const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); const iouType = lodashGet(route, 'params.iouType', ''); const reportID = lodashGet(route, 'params.reportID', ''); + const isEmptyMerchant = iou.merchant === '' || iou.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || iou.merchant === CONST.TRANSACTION.DEFAULT_MERCHANT; useEffect(() => { const moneyRequestId = `${iouType}${reportID}`; @@ -114,7 +116,7 @@ function MoneyRequestMerchantPage({iou, route}) { InputComponent={TextInput} inputID="moneyRequestMerchant" name="moneyRequestMerchant" - defaultValue={iou.merchant} + defaultValue={isEmptyMerchant ? '' : iou.merchant} maxLength={CONST.MERCHANT_NAME_MAX_LENGTH} label={translate('common.merchant')} accessibilityLabel={translate('common.merchant')} From f696242251356d4bccfc2834a4c9df08f95b6c56 Mon Sep 17 00:00:00 2001 From: Artem Makushov Date: Tue, 12 Dec 2023 17:27:22 +0100 Subject: [PATCH 084/260] lint fix --- src/pages/iou/MoneyRequestMerchantPage.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pages/iou/MoneyRequestMerchantPage.js b/src/pages/iou/MoneyRequestMerchantPage.js index e181e0cce0cd..f469992ea074 100644 --- a/src/pages/iou/MoneyRequestMerchantPage.js +++ b/src/pages/iou/MoneyRequestMerchantPage.js @@ -48,7 +48,6 @@ const defaultProps = { }; function MoneyRequestMerchantPage({iou, route}) { - console.log('iou :>> ', iou); const styles = useThemeStyles(); const {translate} = useLocalize(); const {inputCallbackRef} = useAutoFocusInput(); From 0fe0448b794120d495402aa09c5d1ef9170bf6f9 Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Tue, 12 Dec 2023 19:55:56 +0100 Subject: [PATCH 085/260] remove files from another PR --- src/components/Text.tsx | 31 ++++++------ src/components/TextLink.js | 99 +++++++++++++++++++++++++++++++++++++ src/components/TextLink.tsx | 77 ----------------------------- 3 files changed, 113 insertions(+), 94 deletions(-) create mode 100644 src/components/TextLink.js delete mode 100644 src/components/TextLink.tsx diff --git a/src/components/Text.tsx b/src/components/Text.tsx index a289e224fb72..58a5cf300699 100644 --- a/src/components/Text.tsx +++ b/src/components/Text.tsx @@ -1,29 +1,28 @@ import React, {ForwardedRef} from 'react'; +// eslint-disable-next-line no-restricted-imports import {Text as RNText, TextProps as RNTextProps, StyleSheet} from 'react-native'; import type {TextStyle} from 'react-native'; import fontFamily from '@styles/fontFamily'; import useTheme from '@styles/themes/useTheme'; import variables from '@styles/variables'; -import ChildrenProps from '@src/types/utils/ChildrenProps'; -type TextProps = RNTextProps & - ChildrenProps & { - /** The color of the text */ - color?: string; +type TextProps = RNTextProps & { + /** The color of the text */ + color?: string; - /** The size of the text */ - fontSize?: number; + /** The size of the text */ + fontSize?: number; + /** The alignment of the text */ + textAlign?: 'left' | 'right' | 'auto' | 'center' | 'justify'; + /** Any children to display */ + children: React.ReactNode; - /** The alignment of the text */ - textAlign?: TextStyle['textAlign']; + /** The family of the font to use */ + family?: keyof typeof fontFamily; +}; - /** The family of the font to use */ - family?: keyof typeof fontFamily; - }; - -function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { +function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', children = null, family = 'EXP_NEUE', style = {}, ...props}: TextProps, ref: ForwardedRef) { const theme = useTheme(); - const componentStyle: TextStyle = { color: color ?? theme.text, fontSize, @@ -35,7 +34,6 @@ function Text({color, fontSize = variables.fontSizeNormal, textAlign = 'left', c if (!componentStyle.lineHeight && componentStyle.fontSize === variables.fontSizeNormal) { componentStyle.lineHeight = variables.fontSizeNormalHeight; } - return ( event.preventDefault(), +}; + +function TextLink(props) { + const styles = useThemeStyles(); + const rest = _.omit(props, _.keys(propTypes)); + const additionalStyles = _.isArray(props.style) ? props.style : [props.style]; + + /** + * @param {Event} event + */ + const openLink = (event) => { + event.preventDefault(); + if (props.onPress) { + props.onPress(); + return; + } + + Link.openExternalLink(props.href); + }; + + /** + * @param {Event} event + */ + const openLinkIfEnterKeyPressed = (event) => { + if (event.key !== 'Enter') { + return; + } + openLink(event); + }; + + return ( + + {props.children} + + ); +} + +TextLink.defaultProps = defaultProps; +TextLink.propTypes = propTypes; +TextLink.displayName = 'TextLink'; + +const TextLinkWithRef = React.forwardRef((props, ref) => ( + +)); + +TextLinkWithRef.displayName = 'TextLinkWithRef'; + +export default TextLinkWithRef; diff --git a/src/components/TextLink.tsx b/src/components/TextLink.tsx deleted file mode 100644 index fae4a71eeafa..000000000000 --- a/src/components/TextLink.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import React, {ForwardedRef, forwardRef, KeyboardEventHandler, MouseEventHandler} from 'react'; -import {GestureResponderEvent, Text as RNText, StyleProp, TextStyle} from 'react-native'; -import useThemeStyles from '@styles/useThemeStyles'; -import * as Link from '@userActions/Link'; -import CONST from '@src/CONST'; -import Text, {TextProps} from './Text'; - -type LinkProps = { - /** Link to open in new tab */ - href: string; - - onPress?: undefined; -}; - -type PressProps = { - href?: undefined; - - /** Overwrites the default link behavior with a custom callback */ - onPress: () => void; -}; - -type TextLinkProps = (LinkProps | PressProps) & - TextProps & { - /** Additional style props */ - style?: StyleProp; - - /** Callback that is called when mousedown is triggered */ - onMouseDown?: MouseEventHandler; - }; - -function TextLink({href, onPress, children, style, onMouseDown = (event) => event.preventDefault(), ...rest}: TextLinkProps, ref: ForwardedRef) { - const styles = useThemeStyles(); - - const openLink = () => { - if (onPress) { - onPress(); - } else { - Link.openExternalLink(href); - } - }; - - const openLinkOnTap = (event: GestureResponderEvent) => { - event.preventDefault(); - - openLink(); - }; - - const openLinkOnEnterKey: KeyboardEventHandler = (event) => { - if (event.key !== 'Enter') { - return; - } - event.preventDefault(); - - openLink(); - }; - - return ( - - {children} - - ); -} - -TextLink.displayName = 'TextLink'; - -export default forwardRef(TextLink); From cd777131d8c3a843ab36fe866939b0915b58b1b3 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Wed, 13 Dec 2023 13:06:12 +0700 Subject: [PATCH 086/260] mark the latest report action from other users as unread --- src/libs/actions/Report.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index bea4ab8aed77..1f4592c6492e 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -940,7 +940,20 @@ function readNewestAction(reportID: string) { */ function markCommentAsUnread(reportID: string, reportActionCreated: string) { // If no action created date is provided, use the last action's - const actionCreationTime = reportActionCreated || (allReports?.[reportID]?.lastVisibleActionCreated ?? DateUtils.getDBTime(0)); + const reportActions = allReportActions?.[reportID]; + + if (!reportActions) { + return; + } + + // Find the latest report actions from other users + const reportActionsFromOtherUsers = Object.values(reportActions).filter((action) => action.actorAccountID !== currentUserAccountID); + let latestReportActionFromOtherUsers: ReportAction | null = null; + if (reportActionsFromOtherUsers.length !== 0) { + latestReportActionFromOtherUsers = reportActionsFromOtherUsers.reduce((latest, current) => (latest.created > current.created ? latest : current), reportActionsFromOtherUsers[0]); + } + + const actionCreationTime = reportActionCreated || (latestReportActionFromOtherUsers?.created ?? DateUtils.getDBTime(0)); // We subtract 1 millisecond so that the lastReadTime is updated to just before a given reportAction's created date // For example, if we want to mark a report action with ID 100 and created date '2014-04-01 16:07:02.999' unread, we set the lastReadTime to '2014-04-01 16:07:02.998' From 98bdf2a0f187a34823503e0aac2d77dda166d95e Mon Sep 17 00:00:00 2001 From: mkhutornyi Date: Wed, 13 Dec 2023 02:18:29 -0500 Subject: [PATCH 087/260] fix unit test --- src/components/DatePicker/CalendarPicker/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DatePicker/CalendarPicker/index.js b/src/components/DatePicker/CalendarPicker/index.js index 7a558bd92990..078d0dc8fa23 100644 --- a/src/components/DatePicker/CalendarPicker/index.js +++ b/src/components/DatePicker/CalendarPicker/index.js @@ -50,7 +50,7 @@ class CalendarPicker extends React.PureComponent { if (props.minDate >= props.maxDate) { throw new Error('Minimum date cannot be greater than the maximum date.'); } - let currentDateView = parseISO(props.value); + let currentDateView = typeof props.value === 'string' ? parseISO(props.value) : new Date(props.value); if (props.maxDate < currentDateView) { currentDateView = props.maxDate; } else if (props.minDate > currentDateView) { From ac1564eeb8d1c326cf3ed367547e529de5bf3ce2 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 10:49:31 +0100 Subject: [PATCH 088/260] fix: fixed default values for props --- src/components/TextInput/BaseTextInput/index.native.tsx | 2 +- src/components/TextInput/BaseTextInput/index.tsx | 6 ++---- src/components/TextInput/BaseTextInput/types.ts | 3 --- 3 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index e7160b8d631f..49788a4646af 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -45,7 +45,7 @@ function BaseTextInput( autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = null, + maxLength = undefined, hint = '', onInputChange = () => {}, shouldDelayFocus = false, diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index 1e997018b537..cf8c58160c96 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -1,7 +1,7 @@ import Str from 'expensify-common/lib/str'; import React, {forwardRef, useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {ActivityIndicator, Animated, StyleSheet, TextInput, View} from 'react-native'; -import type {FlexStyle, GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; +import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent, StyleProp, TextInputFocusEventData, ViewStyle} from 'react-native'; import Checkbox from '@components/Checkbox'; import FormHelpMessage from '@components/FormHelpMessage'; import Icon from '@components/Icon'; @@ -45,7 +45,7 @@ function BaseTextInput( autoGrow = false, autoGrowHeight = false, hideFocusedState = false, - maxLength = null, + maxLength = undefined, hint = '', onInputChange = () => {}, shouldDelayFocus = false, @@ -55,8 +55,6 @@ function BaseTextInput( autoCorrect = true, prefixCharacter = '', inputID, - shouldSaveDraft = false, - shouldUseDefaultValue = false, ...inputProps }: BaseTextInputProps, ref: BaseTextInputRef, diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index ee3574ee6d04..33addec32784 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -62,9 +62,6 @@ type CustomBaseTextInputProps = { /** Hide the focus styles on TextInput */ hideFocusedState?: boolean; - /** Maximum characters allowed */ - maxLength?: number | null; - /** Hint text to display below the TextInput */ hint?: string; From b2ff88cd3d44870fdf7677ebb5302df9568d2951 Mon Sep 17 00:00:00 2001 From: artus9033 Date: Wed, 13 Dec 2023 12:54:40 +0100 Subject: [PATCH 089/260] Stripped obsolete code from ComposerWithSuggestions.js --- .../ComposerWithSuggestions/ComposerWithSuggestions.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js index 25aab5747f1e..097a2fff8b28 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js @@ -376,13 +376,6 @@ function ComposerWithSuggestions({ const onChangeText = useCallback( (commentValue) => { - if (syncSelectionWithOnChangeTextRef.current) { - setSelection({ - start: syncSelectionWithOnChangeTextRef.current.position, - end: syncSelectionWithOnChangeTextRef.current.position, - }); - } - updateComment(commentValue, true); if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { From d69e08774e3fe2bf9eb4802c7277ed555f401012 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 13 Dec 2023 13:20:42 +0100 Subject: [PATCH 090/260] fix: adress comments --- .../TextInput/BaseTextInput/index.native.tsx | 11 +++++------ src/components/TextInput/BaseTextInput/index.tsx | 15 ++++----------- src/components/TextInput/BaseTextInput/types.ts | 3 +-- src/components/TextInput/index.tsx | 3 +-- src/libs/isInputAutoFilled.ts | 4 ++-- 5 files changed, 13 insertions(+), 23 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.native.tsx b/src/components/TextInput/BaseTextInput/index.native.tsx index 49788a4646af..671b3fdbad5f 100644 --- a/src/components/TextInput/BaseTextInput/index.native.tsx +++ b/src/components/TextInput/BaseTextInput/index.native.tsx @@ -21,8 +21,7 @@ import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type BaseTextInputProps from './types'; -import type {BaseTextInputRef} from './types'; +import type {BaseTextInputProps, BaseTextInputRef} from './types'; function BaseTextInput( { @@ -66,7 +65,7 @@ function BaseTextInput( const {translate} = useLocalize(); const {hasError = false} = inputProps; - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null + // Disabling this line for safeness as nullish coalescing works only if the value is undefined or null // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const initialValue = value || defaultValue || ''; const initialActiveLabel = !!forceActiveLabel || initialValue.length > 0 || !!prefixCharacter; @@ -80,7 +79,7 @@ function BaseTextInput( const [width, setWidth] = useState(null); const labelScale = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_SCALE : styleConst.INACTIVE_LABEL_SCALE)).current; const labelTranslateY = useRef(new Animated.Value(initialActiveLabel ? styleConst.ACTIVE_LABEL_TRANSLATE_Y : styleConst.INACTIVE_LABEL_TRANSLATE_Y)).current; - const input = useRef(null); + const input = useRef(null); const isLabelActive = useRef(initialActiveLabel); // AutoFocus which only works on mount: @@ -377,8 +376,8 @@ function BaseTextInput( { - e.preventDefault(); + onMouseDown={(event) => { + event.preventDefault(); }} accessibilityLabel={translate?.('common.visible') ?? ''} > diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index cf8c58160c96..b2eae4887baf 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -21,8 +21,7 @@ import useStyleUtils from '@styles/useStyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; -import type BaseTextInputProps from './types'; -import type {BaseTextInputRef} from './types'; +import type {BaseTextInputProps, BaseTextInputRef} from './types'; function BaseTextInput( { @@ -114,9 +113,7 @@ function BaseTextInput( ); const activateLabel = useCallback(() => { - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const newValue = value || ''; + const newValue = value ?? ''; if (newValue.length < 0 || isLabelActive.current) { return; @@ -127,9 +124,7 @@ function BaseTextInput( }, [animateLabel, value]); const deactivateLabel = useCallback(() => { - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const newValue = value || ''; + const newValue = value ?? ''; if (!!forceActiveLabel || newValue.length !== 0 || prefixCharacter) { return; @@ -177,9 +172,7 @@ function BaseTextInput( // The ref is needed when the component is uncontrolled and we don't have a value prop const hasValueRef = useRef(initialValue.length > 0); - // Disabling this line for saftiness as nullish coalescing works only if value is undefined or null - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - const inputValue = value || ''; + const inputValue = value ?? ''; const hasValue = inputValue.length > 0 || hasValueRef.current; // Activate or deactivate the label when either focus changes, or for controlled diff --git a/src/components/TextInput/BaseTextInput/types.ts b/src/components/TextInput/BaseTextInput/types.ts index 33addec32784..37dcb0de8f31 100644 --- a/src/components/TextInput/BaseTextInput/types.ts +++ b/src/components/TextInput/BaseTextInput/types.ts @@ -113,5 +113,4 @@ type BaseTextInputRef = ForwardedRef void; diff --git a/src/libs/isInputAutoFilled.ts b/src/libs/isInputAutoFilled.ts index f74b04108cd9..fbe6240def47 100644 --- a/src/libs/isInputAutoFilled.ts +++ b/src/libs/isInputAutoFilled.ts @@ -4,8 +4,8 @@ import isSelectorSupported from './isSelectorSupported'; /** * Check the input is auto filled or not */ -export default function isInputAutoFilled(input: (TextInput & HTMLElement) | null): boolean { - if (!input?.matches) { +export default function isInputAutoFilled(input: (TextInput | HTMLElement) | null): boolean { + if ((!!input && !('matches' in input)) || !input?.matches) { return false; } if (isSelectorSupported(':autofill')) { From ce96a3bf0606f81629b4b2916ab8e92b371da0d5 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Wed, 13 Dec 2023 15:54:31 +0100 Subject: [PATCH 091/260] fix: wip --- src/languages/types.ts | 2 +- src/libs/OptionsListUtils.js | 3 +++ src/libs/ReportUtils.ts | 16 ++++++++++++++-- src/libs/SidebarUtils.ts | 9 +++++++-- 4 files changed, 25 insertions(+), 5 deletions(-) diff --git a/src/languages/types.ts b/src/languages/types.ts index 8e72c700a9cc..6ef47b72ce1a 100644 --- a/src/languages/types.ts +++ b/src/languages/types.ts @@ -103,7 +103,7 @@ type SettleExpensifyCardParams = { type RequestAmountParams = {amount: number}; -type RequestedAmountMessageParams = {formattedAmount: string; comment: string}; +type RequestedAmountMessageParams = {formattedAmount: string; comment?: string}; type SplitAmountParams = {amount: number}; diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index 95a8c52649d3..ee26ab038a5e 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -21,6 +21,7 @@ import * as ReportUtils from './ReportUtils'; import * as TaskUtils from './TaskUtils'; import * as TransactionUtils from './TransactionUtils'; import * as UserUtils from './UserUtils'; +import {getDisplayNameForParticipant} from "./ReportUtils"; /** * OptionsListUtils is used to build a list options passed to the OptionsList component. Several different UI views can @@ -383,6 +384,8 @@ function getAllReportErrors(report, reportActions) { function getLastMessageTextForReport(report) { const lastReportAction = _.find(allSortedReportActions[report.reportID], (reportAction) => ReportActionUtils.shouldReportActionBeVisibleAsLastAction(reportAction)); let lastMessageTextFromReport = ''; + + console.log({lastReportAction}); const lastActionName = lodashGet(lastReportAction, 'actionName', ''); if (ReportActionUtils.isMoneyRequestAction(lastReportAction)) { diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 63037416c923..f4c84c77f783 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -1960,6 +1960,7 @@ function getReportPreviewMessage( policy: OnyxEntry = null, ): string { const reportActionMessage = reportAction?.message?.[0].html ?? ''; + if (isEmptyObject(report) || !report?.reportID) { // The iouReport is not found locally after SignIn because the OpenApp API won't return iouReports if they're settled // As a temporary solution until we know how to solve this the best, we just use the message that returned from BE @@ -1989,7 +1990,8 @@ function getReportPreviewMessage( } const totalAmount = getMoneyRequestReimbursableTotal(report); - const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID, true); + const payerName = isExpenseReport(report) ? getPolicyName(report, false, policy) : getDisplayNameForParticipant(report.managerID, !isPreviewMessageForParentChatReport); + const formattedAmount = CurrencyUtils.convertToDisplayString(totalAmount, report.currency); if (isReportApproved(report) && isGroupPolicy(report)) { @@ -2006,12 +2008,12 @@ function getReportPreviewMessage( return Localize.translateLocal('iou.receiptScanning'); } } + const originalMessage = reportAction?.originalMessage as IOUMessage | undefined; // Show Paid preview message if it's settled or if the amount is paid & stuck at receivers end for only chat reports. if (isSettled(report.reportID) || (report.isWaitingOnBankAccount && isPreviewMessageForParentChatReport)) { // A settled report preview message can come in three formats "paid ... elsewhere" or "paid ... with Expensify" let translatePhraseKey: TranslationPaths = 'iou.paidElsewhereWithAmount'; - const originalMessage = reportAction?.originalMessage as IOUMessage | undefined; if ( [CONST.IOU.PAYMENT_TYPE.VBBA, CONST.IOU.PAYMENT_TYPE.EXPENSIFY].some((paymentType) => paymentType === originalMessage?.paymentType) || !!reportActionMessage.match(/ (with Expensify|using Expensify)$/) || @@ -2028,6 +2030,16 @@ function getReportPreviewMessage( } const containsNonReimbursable = hasNonReimbursableTransactions(report.reportID); + + if (!isPreviewMessageForParentChatReport) { + const lastActorID = reportAction?.actorAccountID + const amount = originalMessage?.amount ?? 0; + const currency = originalMessage?.currency ?? report.currency; + const amountToDisplay = CurrencyUtils.convertToDisplayString(Math.abs(amount), currency ?? ''); + const requestorName = lastActorID && lastActorID !== currentUserAccountID ? getDisplayNameForParticipant(lastActorID, !isPreviewMessageForParentChatReport) : ''; + return `${requestorName ? `${requestorName}: ` : ''}${Localize.translateLocal('iou.requestedAmount', {formattedAmount: amountToDisplay})}` + } + return Localize.translateLocal(containsNonReimbursable ? 'iou.payerSpentAmount' : 'iou.payerOwesAmount', {payer: payerName ?? '', amount: formattedAmount}); } diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index 1813d4f0a795..6cf990dc3489 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -307,7 +307,7 @@ function getOptionData( result.isAllowedToComment = ReportUtils.canUserPerformWriteAction(report); result.chatType = report.chatType; - const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat; + const hasMultipleParticipants = participantPersonalDetailList.length > 1 || result.isChatRoom || result.isPolicyExpenseChat || ReportUtils.isExpenseReport(report); const subtitle = ReportUtils.getChatRoomSubtitle(report); const login = Str.removeSMSDomain(personalDetail?.login ?? ''); @@ -354,8 +354,12 @@ function getOptionData( } } - if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport) && !result.isArchivedRoom) { + console.log({result, lastMessageText}) + + + if ((result.isChatRoom || result.isPolicyExpenseChat || result.isThread || result.isTaskReport || ReportUtils.isExpenseReport(report)) && !result.isArchivedRoom) { const lastAction = visibleReportActionItems[report.reportID]; + console.log(lastAction?.actionName, lastActorDisplayName, lastMessageText); if (lastAction?.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { const newName = lastAction?.originalMessage?.newName ?? ''; @@ -385,6 +389,7 @@ function getOptionData( result.alternateText += `${preposition} ${roomName}`; } } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORTPREVIEW && lastActorDisplayName && lastMessageTextFromReport) { + console.log('hello'); result.alternateText = `${lastActorDisplayName}: ${lastMessageText}`; } else { result.alternateText = lastMessageTextFromReport.length > 0 ? lastMessageText : Localize.translate(preferredLocale, 'report.noActivityYet'); From 53ce40255edc8cb021c759735b2bb010a9d23c2d Mon Sep 17 00:00:00 2001 From: cdOut <88325488+cdOut@users.noreply.github.com> Date: Wed, 13 Dec 2023 19:54:50 +0100 Subject: [PATCH 092/260] add review suggested changes --- src/components/BlockingViews/BlockingView.tsx | 4 ++-- src/components/BlockingViews/FullPageNotFoundView.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/BlockingViews/BlockingView.tsx b/src/components/BlockingViews/BlockingView.tsx index ede40cfda752..e48f2a6917c0 100644 --- a/src/components/BlockingViews/BlockingView.tsx +++ b/src/components/BlockingViews/BlockingView.tsx @@ -53,8 +53,8 @@ function BlockingView({ shouldShowLink = false, iconWidth = variables.iconSizeSuperLarge, iconHeight = variables.iconSizeSuperLarge, - onLinkPress = () => Navigation.dismissModal, - shouldEmbedLinkWithSubtitle, + onLinkPress = () => Navigation.dismissModal(), + shouldEmbedLinkWithSubtitle = false, }: BlockingViewProps) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/components/BlockingViews/FullPageNotFoundView.tsx b/src/components/BlockingViews/FullPageNotFoundView.tsx index 31cc63ef01da..733662251515 100644 --- a/src/components/BlockingViews/FullPageNotFoundView.tsx +++ b/src/components/BlockingViews/FullPageNotFoundView.tsx @@ -49,7 +49,7 @@ function FullPageNotFoundView({ onBackButtonPress = () => Navigation.goBack(ROUTES.HOME), shouldShowLink = true, shouldShowBackButton = true, - onLinkPress = () => Navigation.dismissModal, + onLinkPress = () => Navigation.dismissModal(), }: FullPageNotFoundViewProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); From 04ff7ae972a6159d3e6414d0d26071da440ab863 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Thu, 14 Dec 2023 12:19:55 +0700 Subject: [PATCH 093/260] find the latest report action from other users --- src/libs/actions/Report.ts | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 1f4592c6492e..999aca0ab750 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -939,20 +939,19 @@ function readNewestAction(reportID: string) { * Sets the last read time on a report */ function markCommentAsUnread(reportID: string, reportActionCreated: string) { - // If no action created date is provided, use the last action's const reportActions = allReportActions?.[reportID]; - if (!reportActions) { - return; - } - // Find the latest report actions from other users - const reportActionsFromOtherUsers = Object.values(reportActions).filter((action) => action.actorAccountID !== currentUserAccountID); - let latestReportActionFromOtherUsers: ReportAction | null = null; - if (reportActionsFromOtherUsers.length !== 0) { - latestReportActionFromOtherUsers = reportActionsFromOtherUsers.reduce((latest, current) => (latest.created > current.created ? latest : current), reportActionsFromOtherUsers[0]); - } + const latestReportActionFromOtherUsers = Object.values(reportActions ?? {}).reduce((latest: ReportAction | null, current: ReportAction) => { + if (current.actorAccountID !== currentUserAccountID) { + if (!latest || current.created > latest.created) { + return current; + } + } + return latest; + }, null); + // If no action created date is provided, use the last action's from other user const actionCreationTime = reportActionCreated || (latestReportActionFromOtherUsers?.created ?? DateUtils.getDBTime(0)); // We subtract 1 millisecond so that the lastReadTime is updated to just before a given reportAction's created date From 00ae1e641cc5b82396e8a8ba0d465b624ae4d225 Mon Sep 17 00:00:00 2001 From: Yauheni Pasiukevich Date: Wed, 6 Dec 2023 10:46:34 +0100 Subject: [PATCH 094/260] Migrate 'PopoverWithMeasuredContent.js' component to TypeScript --- src/components/Popover/types.ts | 2 +- ...tent.js => PopoverWithMeasuredContent.tsx} | 120 +++++++----------- 2 files changed, 50 insertions(+), 72 deletions(-) rename src/components/{PopoverWithMeasuredContent.js => PopoverWithMeasuredContent.tsx} (54%) diff --git a/src/components/Popover/types.ts b/src/components/Popover/types.ts index 7f7e2829770c..e76639bd763e 100644 --- a/src/components/Popover/types.ts +++ b/src/components/Popover/types.ts @@ -39,4 +39,4 @@ type PopoverProps = BaseModalProps & { type PopoverWithWindowDimensionsProps = PopoverProps & WindowDimensionsProps; -export type {PopoverProps, PopoverWithWindowDimensionsProps}; +export type {PopoverProps, PopoverWithWindowDimensionsProps, AnchorAlignment, PopoverDimensions}; diff --git a/src/components/PopoverWithMeasuredContent.js b/src/components/PopoverWithMeasuredContent.tsx similarity index 54% rename from src/components/PopoverWithMeasuredContent.js rename to src/components/PopoverWithMeasuredContent.tsx index 7de5be113e53..fec9ac31f8fe 100644 --- a/src/components/PopoverWithMeasuredContent.js +++ b/src/components/PopoverWithMeasuredContent.tsx @@ -1,56 +1,21 @@ -import PropTypes from 'prop-types'; +import isEqual from 'lodash/isEqual'; import React, {useMemo, useState} from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import {LayoutChangeEvent, View} from 'react-native'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import PopoverWithMeasuredContentUtils from '@libs/PopoverWithMeasuredContentUtils'; import CONST from '@src/CONST'; import Popover from './Popover'; -import {defaultProps as defaultPopoverProps, propTypes as popoverPropTypes} from './Popover/popoverPropTypes'; -import {windowDimensionsPropTypes} from './withWindowDimensions'; +import {PopoverProps} from './Popover/types'; -const propTypes = { - // All popover props except: - // 1) anchorPosition (which is overridden for this component) - // 2) windowDimensionsPropTypes - we exclude them because we use useWindowDimensions hook instead - ..._.omit(popoverPropTypes, ['anchorPosition', ..._.keys(windowDimensionsPropTypes)]), - - /** The horizontal and vertical anchors points for the popover */ - anchorPosition: PropTypes.shape({ - horizontal: PropTypes.number.isRequired, - vertical: PropTypes.number.isRequired, - }).isRequired, - - /** How the popover should be aligned. The value you passed will is the part of the component that will be aligned to the - * anchorPosition. ie: vertical:top means the top of the menu will be positioned in the anchorPosition */ - anchorAlignment: PropTypes.shape({ - horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)), - vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)), - }), - - /** Static dimensions for the popover. - * Note: When passed, it will skip dimensions measuring of the popover, and provided dimensions will be used to calculate the anchor position. - */ - popoverDimensions: PropTypes.shape({ - height: PropTypes.number, - width: PropTypes.number, - }), +type AnchorPosition = { + horizontal: number; + vertical: number; }; -const defaultProps = { - ...defaultPopoverProps, - - // Default positioning of the popover - anchorAlignment: { - horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, - vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, - }, - popoverDimensions: { - height: 0, - width: 0, - }, - withoutOverlay: false, +type PopoverWithMeasuredContentProps = Omit & { + /** The horizontal and vertical anchors points for the popover */ + anchorPosition: AnchorPosition; }; /** @@ -60,32 +25,44 @@ const defaultProps = { * anchor position. */ -function PopoverWithMeasuredContent(props) { +function PopoverWithMeasuredContent({ + popoverDimensions = { + height: 0, + width: 0, + }, + anchorPosition, + isVisible, + anchorAlignment = { + horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT, + vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM, + }, + children, + withoutOverlay = false, + ...props +}: PopoverWithMeasuredContentProps) { const styles = useThemeStyles(); const {windowWidth, windowHeight} = useWindowDimensions(); - const [popoverWidth, setPopoverWidth] = useState(props.popoverDimensions.width); - const [popoverHeight, setPopoverHeight] = useState(props.popoverDimensions.height); + const [popoverWidth, setPopoverWidth] = useState(popoverDimensions.width); + const [popoverHeight, setPopoverHeight] = useState(popoverDimensions.height); const [isContentMeasured, setIsContentMeasured] = useState(popoverWidth > 0 && popoverHeight > 0); - const [isVisible, setIsVisible] = useState(false); + const [isCurrentVisible, setIsCurrentVisible] = useState(false); /** * When Popover becomes visible, we need to recalculate the Dimensions. * Skip render on Popover until recalculations have done by setting isContentMeasured false as early as possible. */ - if (!isVisible && props.isVisible) { + if (!isCurrentVisible && isVisible) { // When Popover is shown recalculate - setIsContentMeasured(props.popoverDimensions.width > 0 && props.popoverDimensions.height > 0); - setIsVisible(true); - } else if (isVisible && !props.isVisible) { - setIsVisible(false); + setIsContentMeasured(popoverDimensions.width > 0 && popoverDimensions.height > 0); + setIsCurrentVisible(true); + } else if (isCurrentVisible && !isVisible) { + setIsCurrentVisible(false); } /** * Measure the size of the popover's content. - * - * @param {Object} nativeEvent */ - const measurePopover = ({nativeEvent}) => { + const measurePopover = ({nativeEvent}: LayoutChangeEvent) => { setPopoverWidth(nativeEvent.layout.width); setPopoverHeight(nativeEvent.layout.height); setIsContentMeasured(true); @@ -93,40 +70,40 @@ function PopoverWithMeasuredContent(props) { const adjustedAnchorPosition = useMemo(() => { let horizontalConstraint; - switch (props.anchorAlignment.horizontal) { + switch (anchorAlignment.horizontal) { case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.RIGHT: - horizontalConstraint = {left: props.anchorPosition.horizontal - popoverWidth}; + horizontalConstraint = {left: anchorPosition.horizontal - popoverWidth}; break; case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.CENTER: horizontalConstraint = { - left: Math.floor(props.anchorPosition.horizontal - popoverWidth / 2), + left: Math.floor(anchorPosition.horizontal - popoverWidth / 2), }; break; case CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT: default: - horizontalConstraint = {left: props.anchorPosition.horizontal}; + horizontalConstraint = {left: anchorPosition.horizontal}; } let verticalConstraint; - switch (props.anchorAlignment.vertical) { + switch (anchorAlignment.vertical) { case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.BOTTOM: - verticalConstraint = {top: props.anchorPosition.vertical - popoverHeight}; + verticalConstraint = {top: anchorPosition.vertical - popoverHeight}; break; case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.CENTER: verticalConstraint = { - top: Math.floor(props.anchorPosition.vertical - popoverHeight / 2), + top: Math.floor(anchorPosition.vertical - popoverHeight / 2), }; break; case CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP: default: - verticalConstraint = {top: props.anchorPosition.vertical}; + verticalConstraint = {top: anchorPosition.vertical}; } return { ...horizontalConstraint, ...verticalConstraint, }; - }, [props.anchorPosition, props.anchorAlignment, popoverWidth, popoverHeight]); + }, [anchorPosition, anchorAlignment, popoverWidth, popoverHeight]); const horizontalShift = PopoverWithMeasuredContentUtils.computeHorizontalShift(adjustedAnchorPosition.left, popoverWidth, windowWidth); const verticalShift = PopoverWithMeasuredContentUtils.computeVerticalShift(adjustedAnchorPosition.top, popoverHeight, windowHeight); @@ -136,11 +113,15 @@ function PopoverWithMeasuredContent(props) { }; return isContentMeasured ? ( - {props.children} + {children} ) : ( /* @@ -153,18 +134,15 @@ function PopoverWithMeasuredContent(props) { style={styles.invisiblePopover} onLayout={measurePopover} > - {props.children} + {children} ); } - -PopoverWithMeasuredContent.propTypes = propTypes; -PopoverWithMeasuredContent.defaultProps = defaultProps; PopoverWithMeasuredContent.displayName = 'PopoverWithMeasuredContent'; export default React.memo(PopoverWithMeasuredContent, (prevProps, nextProps) => { if (prevProps.isVisible === nextProps.isVisible && nextProps.isVisible === false) { return true; } - return _.isEqual(prevProps, nextProps); + return isEqual(prevProps, nextProps); }); From fa251dfb5e427b3045790da5e78bb091f0f9641f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 14 Dec 2023 14:04:55 +0100 Subject: [PATCH 095/260] fix: typecheck --- src/components/TextInput/BaseTextInput/index.tsx | 2 +- src/components/TextInput/index.native.tsx | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/TextInput/BaseTextInput/index.tsx b/src/components/TextInput/BaseTextInput/index.tsx index f1bf7c0e6b83..dae61d82b609 100644 --- a/src/components/TextInput/BaseTextInput/index.tsx +++ b/src/components/TextInput/BaseTextInput/index.tsx @@ -286,7 +286,7 @@ function BaseTextInput( style={[ autoGrowHeight && styles.autoGrowHeightInputContainer(textInputHeight, variables.componentSizeLarge, typeof maxHeight === 'number' ? maxHeight : 0), !isMultiline && styles.componentHeightLarge, - ...props.containerStyles, + containerStyles, ]} > Date: Thu, 14 Dec 2023 15:27:48 +0100 Subject: [PATCH 096/260] updates --- src/components/MoneyRequestConfirmationList.js | 2 +- src/pages/EditRequestMerchantPage.js | 2 +- src/pages/EditRequestPage.js | 1 + src/pages/iou/MoneyRequestMerchantPage.js | 2 +- 4 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/components/MoneyRequestConfirmationList.js b/src/components/MoneyRequestConfirmationList.js index cd2215f0c7fb..7d36efa08d1e 100755 --- a/src/components/MoneyRequestConfirmationList.js +++ b/src/components/MoneyRequestConfirmationList.js @@ -267,7 +267,7 @@ function MoneyRequestConfirmationList(props) { return (props.hasSmartScanFailed && TransactionUtils.hasMissingSmartscanFields(transaction)) || (didConfirmSplit && TransactionUtils.areRequiredFieldsEmpty(transaction)); }, [props.isEditingSplitBill, props.hasSmartScanFailed, transaction, didConfirmSplit]); - const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || props.iouMerchant === CONST.TRANSACTION.DEFAULT_MERCHANT; + const isMerchantEmpty = !props.iouMerchant || props.iouMerchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; const shouldDisplayMerchantError = props.isPolicyExpenseChat && !props.isScanRequest && isMerchantEmpty; useEffect(() => { diff --git a/src/pages/EditRequestMerchantPage.js b/src/pages/EditRequestMerchantPage.js index 82ac20979594..d769e69a1c3d 100644 --- a/src/pages/EditRequestMerchantPage.js +++ b/src/pages/EditRequestMerchantPage.js @@ -32,7 +32,7 @@ function EditRequestMerchantPage({defaultMerchant, onSubmit, isPolicyExpenseChat const validate = useCallback( (value) => { const errors = {}; - if (_.isEmpty(value.merchant) && isPolicyExpenseChat) { + if (_.isEmpty(value.merchant) && value.merchant.trim() === '' && isPolicyExpenseChat) { errors.merchant = 'common.error.fieldRequired'; } return errors; diff --git a/src/pages/EditRequestPage.js b/src/pages/EditRequestPage.js index 918337593583..bf6ac9f0c5fc 100644 --- a/src/pages/EditRequestPage.js +++ b/src/pages/EditRequestPage.js @@ -193,6 +193,7 @@ function EditRequestPage({report, route, parentReport, policyCategories, policyT Navigation.dismissModal(); return; } + // This is possible only in case of IOU requests. if (transactionChanges.merchant.trim() === '') { editMoneyRequest({merchant: CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT}); return; diff --git a/src/pages/iou/MoneyRequestMerchantPage.js b/src/pages/iou/MoneyRequestMerchantPage.js index f469992ea074..6aeaa81907c3 100644 --- a/src/pages/iou/MoneyRequestMerchantPage.js +++ b/src/pages/iou/MoneyRequestMerchantPage.js @@ -53,7 +53,7 @@ function MoneyRequestMerchantPage({iou, route}) { const {inputCallbackRef} = useAutoFocusInput(); const iouType = lodashGet(route, 'params.iouType', ''); const reportID = lodashGet(route, 'params.reportID', ''); - const isEmptyMerchant = iou.merchant === '' || iou.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT || iou.merchant === CONST.TRANSACTION.DEFAULT_MERCHANT; + const isEmptyMerchant = iou.merchant === '' || iou.merchant === CONST.TRANSACTION.PARTIAL_TRANSACTION_MERCHANT; useEffect(() => { const moneyRequestId = `${iouType}${reportID}`; From f312cb28d2e32f7b48317f1bb864facca337db13 Mon Sep 17 00:00:00 2001 From: Viktoryia Kliushun Date: Thu, 14 Dec 2023 16:44:23 +0100 Subject: [PATCH 097/260] [TS migration] Migrate 'OptionRow.js' component --- src/components/Icon/index.tsx | 1 + src/components/OfflineWithFeedback.tsx | 6 +- .../{OptionRow.js => OptionRow.tsx} | 239 +++++++++--------- .../Pressable/GenericPressable/types.ts | 2 +- src/libs/ReportUtils.ts | 13 +- src/styles/utils/index.ts | 6 +- src/types/onyx/Report.ts | 2 +- 7 files changed, 137 insertions(+), 132 deletions(-) rename src/components/{OptionRow.js => OptionRow.tsx} (57%) diff --git a/src/components/Icon/index.tsx b/src/components/Icon/index.tsx index 80abe1872c12..9be549efac26 100644 --- a/src/components/Icon/index.tsx +++ b/src/components/Icon/index.tsx @@ -103,3 +103,4 @@ class Icon extends PureComponent { } export default withTheme(withThemeStyles(withStyleUtils(Icon))); +export type {SrcProps}; diff --git a/src/components/OfflineWithFeedback.tsx b/src/components/OfflineWithFeedback.tsx index 5fcf1fe7442b..29fd0c2700dc 100644 --- a/src/components/OfflineWithFeedback.tsx +++ b/src/components/OfflineWithFeedback.tsx @@ -18,13 +18,13 @@ import MessagesRow from './MessagesRow'; type OfflineWithFeedbackProps = ChildrenProps & { /** The type of action that's pending */ - pendingAction: OnyxCommon.PendingAction; + pendingAction?: OnyxCommon.PendingAction; /** Determine whether to hide the component's children if deletion is pending */ shouldHideOnDelete?: boolean; /** The errors to display */ - errors?: OnyxCommon.Errors; + errors?: OnyxCommon.Errors | null; /** Whether we should show the error messages */ shouldShowErrorMessages?: boolean; @@ -56,7 +56,7 @@ type OfflineWithFeedbackProps = ChildrenProps & { type StrikethroughProps = Partial & {style: Array}; -function omitBy(obj: Record | undefined, predicate: (value: T) => boolean) { +function omitBy(obj: Record | undefined | null, predicate: (value: T) => boolean) { // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unused-vars return Object.fromEntries(Object.entries(obj ?? {}).filter(([_, value]) => !predicate(value))); } diff --git a/src/components/OptionRow.js b/src/components/OptionRow.tsx similarity index 57% rename from src/components/OptionRow.js rename to src/components/OptionRow.tsx index 1a8c395ddd8b..d5661d3ef5bf 100644 --- a/src/components/OptionRow.js +++ b/src/components/OptionRow.tsx @@ -1,13 +1,13 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import lodashIsEqual from 'lodash/isEqual'; import React, {useEffect, useRef, useState} from 'react'; -import {InteractionManager, StyleSheet, View} from 'react-native'; -import _ from 'underscore'; +import {InteractionManager, StyleProp, StyleSheet, TextStyle, View, ViewStyle} from 'react-native'; +import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import * as OptionsListUtils from '@libs/OptionsListUtils'; import * as ReportUtils from '@libs/ReportUtils'; +import type {OptionData} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import Button from './Button'; import DisplayNames from './DisplayNames'; @@ -16,156 +16,150 @@ import Icon from './Icon'; import * as Expensicons from './Icon/Expensicons'; import MultipleAvatars from './MultipleAvatars'; import OfflineWithFeedback from './OfflineWithFeedback'; -import optionPropTypes from './optionPropTypes'; import PressableWithFeedback from './Pressable/PressableWithFeedback'; import SelectCircle from './SelectCircle'; import SubscriptAvatar from './SubscriptAvatar'; import Text from './Text'; -import withLocalize, {withLocalizePropTypes} from './withLocalize'; -const propTypes = { +type OptionRowProps = { /** Style for hovered state */ - // eslint-disable-next-line react/forbid-prop-types - hoverStyle: PropTypes.object, + hoverStyle?: StyleProp; /** Option to allow the user to choose from can be type 'report' or 'user' */ - option: optionPropTypes.isRequired, + option: OptionData; /** Whether this option is currently in focus so we can modify its style */ - optionIsFocused: PropTypes.bool, + optionIsFocused?: boolean; /** A function that is called when an option is selected. Selected option is passed as a param */ - onSelectRow: PropTypes.func, + onSelectRow?: (option: OptionData, refElement: View | HTMLDivElement | null) => void | Promise; /** Whether we should show the selected state */ - showSelectedState: PropTypes.bool, + showSelectedState?: boolean; /** Whether to show a button pill instead of a tickbox */ - shouldShowSelectedStateAsButton: PropTypes.bool, + shouldShowSelectedStateAsButton?: boolean; /** Text for button pill */ - selectedStateButtonText: PropTypes.string, + selectedStateButtonText?: string; /** Callback to fire when the multiple selector (tickbox or button) is clicked */ - onSelectedStatePressed: PropTypes.func, + onSelectedStatePressed?: (option: OptionData) => void; /** Whether we highlight selected option */ - highlightSelected: PropTypes.bool, + highlightSelected?: boolean; /** Whether this item is selected */ - isSelected: PropTypes.bool, + isSelected?: boolean; /** Display the text of the option in bold font style */ - boldStyle: PropTypes.bool, + boldStyle?: boolean; /** Whether to show the title tooltip */ - showTitleTooltip: PropTypes.bool, + showTitleTooltip?: boolean; /** Whether this option should be disabled */ - isDisabled: PropTypes.bool, + isDisabled?: boolean; /** Whether to show a line separating options in list */ - shouldHaveOptionSeparator: PropTypes.bool, + shouldHaveOptionSeparator?: boolean; /** Whether to remove the lateral padding and align the content with the margins */ - shouldDisableRowInnerPadding: PropTypes.bool, + shouldDisableRowInnerPadding?: boolean; /** Whether to prevent default focusing on select */ - shouldPreventDefaultFocusOnSelectRow: PropTypes.bool, + shouldPreventDefaultFocusOnSelectRow?: boolean; /** Whether to wrap large text up to 2 lines */ - isMultilineSupported: PropTypes.bool, + isMultilineSupported?: boolean; - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style?: StyleProp; - ...withLocalizePropTypes, + backgroundColor?: string; }; -const defaultProps = { - hoverStyle: undefined, - showSelectedState: false, - shouldShowSelectedStateAsButton: false, - selectedStateButtonText: 'Select', - onSelectedStatePressed: () => {}, - highlightSelected: false, - isSelected: false, - boldStyle: false, - showTitleTooltip: false, - onSelectRow: undefined, - isDisabled: false, - optionIsFocused: false, - isMultilineSupported: false, - style: null, - shouldHaveOptionSeparator: false, - shouldDisableRowInnerPadding: false, - shouldPreventDefaultFocusOnSelectRow: false, -}; - -function OptionRow(props) { +function OptionRow({ + option, + onSelectRow, + style, + hoverStyle, + isDisabled: isOptionDisabled = false, + isMultilineSupported = false, + shouldShowSelectedStateAsButton = false, + highlightSelected = false, + selectedStateButtonText = 'Select', + shouldHaveOptionSeparator = false, + showTitleTooltip = false, + optionIsFocused = false, + boldStyle = false, + onSelectedStatePressed = () => {}, + backgroundColor, + isSelected = false, + showSelectedState = false, + shouldDisableRowInnerPadding = false, + shouldPreventDefaultFocusOnSelectRow = false, +}: OptionRowProps) { const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); - const pressableRef = useRef(null); - const [isDisabled, setIsDisabled] = useState(props.isDisabled); + const {translate} = useLocalize(); + const pressableRef = useRef(null); + const [isDisabled, setIsDisabled] = useState(isOptionDisabled); useEffect(() => { - setIsDisabled(props.isDisabled); - }, [props.isDisabled]); + setIsDisabled(isOptionDisabled); + }, [isOptionDisabled]); - const text = lodashGet(props.option, 'text', ''); - const fullTitle = props.isMultilineSupported ? text.trimStart() : text; + const text = option.text ?? ''; + const fullTitle = isMultilineSupported ? text.trimStart() : text; const indentsLength = text.length - fullTitle.length; const paddingLeft = Math.floor(indentsLength / CONST.INDENTS.length) * styles.ml3.marginLeft; - const textStyle = props.optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; - const textUnreadStyle = props.boldStyle || props.option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; - const displayNameStyle = StyleUtils.combineStyles( + const textStyle = optionIsFocused ? styles.sidebarLinkActiveText : styles.sidebarLinkText; + const textUnreadStyle = boldStyle || option.boldStyle ? [textStyle, styles.sidebarLinkTextBold] : [textStyle]; + const displayNameStyle: StyleProp = [ styles.optionDisplayName, textUnreadStyle, - props.style, + style, styles.pre, isDisabled ? styles.optionRowDisabled : {}, - props.isMultilineSupported ? {paddingLeft} : {}, - ); - const alternateTextStyle = StyleUtils.combineStyles( + isMultilineSupported ? {paddingLeft} : {}, + ]; + const alternateTextStyle: StyleProp = [ textStyle, styles.optionAlternateText, styles.textLabelSupporting, - props.style, - lodashGet(props.option, 'alternateTextMaxLines', 1) === 1 ? styles.pre : styles.preWrap, - ); + style, + (option.alternateTextMaxLines ?? 1) === 1 ? styles.pre : styles.preWrap, + ]; const contentContainerStyles = [styles.flex1]; + const flattenHoverStyle = StyleSheet.flatten(hoverStyle); const sidebarInnerRowStyle = StyleSheet.flatten([styles.chatLinkRowPressable, styles.flexGrow1, styles.optionItemAvatarNameWrapper, styles.optionRow, styles.justifyContentCenter]); - const hoveredBackgroundColor = - (props.hoverStyle || styles.sidebarLinkHover) && (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - ? (props.hoverStyle || styles.sidebarLinkHover).backgroundColor - : props.backgroundColor; + const hoveredStyle = flattenHoverStyle ?? styles.sidebarLinkHover; + const hoveredBackgroundColor = hoveredStyle?.backgroundColor ? (hoveredStyle.backgroundColor as string) : backgroundColor; const focusedBackgroundColor = styles.sidebarLinkActive.backgroundColor; - const isMultipleParticipant = lodashGet(props.option, 'participantsList.length', 0) > 1; - const defaultSubscriptSize = props.option.isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; + const isMultipleParticipant = (option.participantsList?.length ?? 0) > 1; + const defaultSubscriptSize = option.isExpenseRequest ? CONST.AVATAR_SIZE.SMALL_NORMAL : CONST.AVATAR_SIZE.DEFAULT; // We only create tooltips for the first 10 users or so since some reports have hundreds of users, causing performance to degrade. - const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips( - (props.option.participantsList || (props.option.accountID ? [props.option] : [])).slice(0, 10), - isMultipleParticipant, - ); + const displayNamesWithTooltips = ReportUtils.getDisplayNamesWithTooltips((option.participantsList || (option.accountID ? [option] : [])).slice(0, 10), isMultipleParticipant); let subscriptColor = theme.appBG; - if (props.optionIsFocused) { + if (optionIsFocused) { subscriptColor = focusedBackgroundColor; } return ( {(hovered) => ( (pressableRef.current = el)} + ref={pressableRef} onPress={(e) => { - if (!props.onSelectRow) { + if (!onSelectRow) { return; } @@ -173,12 +167,13 @@ function OptionRow(props) { if (e) { e.preventDefault(); } - let result = props.onSelectRow(props.option, pressableRef.current); + let result = onSelectRow(option, pressableRef.current); if (!(result instanceof Promise)) { result = Promise.resolve(); } + InteractionManager.runAfterInteractions(() => { - result.finally(() => setIsDisabled(props.isDisabled)); + result?.finally(() => setIsDisabled(isOptionDisabled)); }); }} disabled={isDisabled} @@ -187,68 +182,64 @@ function OptionRow(props) { styles.alignItemsCenter, styles.justifyContentBetween, styles.sidebarLink, - !props.isDisabled && styles.cursorPointer, - props.shouldDisableRowInnerPadding ? null : styles.sidebarLinkInner, - props.optionIsFocused ? styles.sidebarLinkActive : null, - props.shouldHaveOptionSeparator && styles.borderTop, - !props.onSelectRow && !props.isDisabled ? styles.cursorDefault : null, + !isOptionDisabled && styles.cursorPointer, + shouldDisableRowInnerPadding ? null : styles.sidebarLinkInner, + optionIsFocused ? styles.sidebarLinkActive : null, + shouldHaveOptionSeparator && styles.borderTop, + !onSelectRow && !isOptionDisabled ? styles.cursorDefault : null, ]} - accessibilityLabel={props.option.text} + accessibilityLabel={option.text} role={CONST.ROLE.BUTTON} hoverDimmingValue={1} - hoverStyle={!props.optionIsFocused ? props.hoverStyle || styles.sidebarLinkHover : undefined} - needsOffscreenAlphaCompositing={lodashGet(props.option, 'icons.length', 0) >= 2} - onMouseDown={props.shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} + hoverStyle={!optionIsFocused ? hoverStyle ?? styles.sidebarLinkHover : undefined} + needsOffscreenAlphaCompositing={(option.icons?.length ?? 0) >= 2} + onMouseDown={shouldPreventDefaultFocusOnSelectRow ? (e) => e.preventDefault() : undefined} > - {!_.isEmpty(props.option.icons) && - (props.option.shouldShowSubscript ? ( + {!!option.icons?.length && + (option.shouldShowSubscript ? ( ) : ( ))} - {props.option.alternateText ? ( + {option.alternateText ? ( - {props.option.alternateText} + {option.alternateText} ) : null} - {props.option.descriptiveText ? ( + {option.descriptiveText ? ( - {props.option.descriptiveText} + {option.descriptiveText} ) : null} - {props.option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( + {option.brickRoadIndicator === CONST.BRICK_ROAD_INDICATOR_STATUS.ERROR && ( )} - {props.showSelectedState && ( + {showSelectedState && ( <> - {props.shouldShowSelectedStateAsButton && !props.isSelected ? ( + {shouldShowSelectedStateAsButton && !isSelected ? (