From 8a00e7eda76ac7ff91979b8eb38e8240bae0f662 Mon Sep 17 00:00:00 2001 From: Kacper Falat Date: Wed, 11 Oct 2023 10:49:18 +0200 Subject: [PATCH 001/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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/526] 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 4810696d937d92af414c59fba2160537b801fde0 Mon Sep 17 00:00:00 2001 From: dukenv0307 Date: Tue, 21 Nov 2023 23:58:29 +0700 Subject: [PATCH 019/526] Update all selectors to use new format for selected participant --- src/libs/OptionsListUtils.js | 4 +++- src/pages/RoomInvitePage.js | 19 +++++++++++-------- src/pages/workspace/WorkspaceInvitePage.js | 18 ++++++++++-------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index b97ae6daed11..f1e97151a062 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -1595,15 +1595,17 @@ function formatMemberForList(member, config = {}) { * @param {Array} betas * @param {String} searchValue * @param {Array} excludeLogins + * @param {Boolean} includeSelectedOptions * @returns {Object} */ -function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', excludeLogins = []) { +function getMemberInviteOptions(personalDetails, betas = [], searchValue = '', excludeLogins = [], includeSelectedOptions = false) { return getOptions([], personalDetails, { betas, searchInputValue: searchValue.trim(), includePersonalDetails: true, excludeLogins, sortPersonalDetailsByAlphaAsc: true, + includeSelectedOptions, }); } diff --git a/src/pages/RoomInvitePage.js b/src/pages/RoomInvitePage.js index a1f7d22c3dc3..cacf603f8836 100644 --- a/src/pages/RoomInvitePage.js +++ b/src/pages/RoomInvitePage.js @@ -84,7 +84,7 @@ function RoomInvitePage(props) { }, []); useEffect(() => { - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails information const detailsMap = {}; @@ -104,13 +104,16 @@ function RoomInvitePage(props) { const sections = []; let indexOffset = 0; - sections.push({ - title: undefined, - data: selectedOptions, - shouldShow: true, - indexOffset, - }); - indexOffset += selectedOptions.length; + // Only show the selected participants if the search is empty + if (searchTerm === '') { + sections.push({ + title: undefined, + data: selectedOptions, + shouldShow: true, + indexOffset, + }); + indexOffset += selectedOptions.length; + } // Filtering out selected users from the search results const selectedLogins = _.map(selectedOptions, ({login}) => login); diff --git a/src/pages/workspace/WorkspaceInvitePage.js b/src/pages/workspace/WorkspaceInvitePage.js index 4bef69c82414..eeffb35b4d23 100644 --- a/src/pages/workspace/WorkspaceInvitePage.js +++ b/src/pages/workspace/WorkspaceInvitePage.js @@ -90,7 +90,7 @@ function WorkspaceInvitePage(props) { const newPersonalDetailsDict = {}; const newSelectedOptionsDict = {}; - const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers); + const inviteOptions = OptionsListUtils.getMemberInviteOptions(props.personalDetails, props.betas, searchTerm, excludedUsers, true); // Update selectedOptions with the latest personalDetails and policyMembers information const detailsMap = {}; @@ -130,13 +130,15 @@ function WorkspaceInvitePage(props) { const sections = []; let indexOffset = 0; - sections.push({ - title: undefined, - data: selectedOptions, - shouldShow: true, - indexOffset, - }); - indexOffset += selectedOptions.length; + if (searchTerm === '') { + sections.push({ + title: undefined, + data: selectedOptions, + shouldShow: true, + indexOffset, + }); + indexOffset += selectedOptions.length; + } // Filtering out selected users from the search results const selectedLogins = _.map(selectedOptions, ({login}) => login); From 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 020/526] 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 45c6f7878ea5edb01435409fa2f28bea06b94a00 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 22 Nov 2023 11:40:38 +0700 Subject: [PATCH 021/526] fix: 30726 A blank spot appears after deleting a money request --- src/pages/home/report/ReportActionsList.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index e1230d7219db..e9ecbd1bdeab 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -21,6 +21,7 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; +import usePrevious from '@hooks/usePrevious'; import FloatingMessageCounter from './FloatingMessageCounter'; import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; import reportActionPropTypes from './reportActionPropTypes'; @@ -160,6 +161,8 @@ function ReportActionsList({ const reportActionSize = useRef(sortedReportActions.length); const lastReadTimeRef = useRef(report.lastReadTime); + const previousLastIndex = usePrevious(sortedReportActions[0].reportActionID) + const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); // This state is used to force a re-render when the user manually marks a message as unread @@ -174,6 +177,14 @@ function ReportActionsList({ opacity.value = withTiming(1, {duration: 100}); }, [opacity]); + useEffect(()=>{ + if(previousLastIndex === sortedReportActions[0].reportActionID || reportActionSize.current <= sortedReportActions.length){ + return; + } + reportScrollManager.scrollToBottom(); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [previousLastIndex, sortedReportActions[0].reportActionID]) + useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because // the parent component is sending the previous reportID even when the user isn't active From fd105ab93551e48ae230fcf2f988566180e6bce2 Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 22 Nov 2023 14:30:54 +0700 Subject: [PATCH 022/526] format --- src/pages/home/report/ReportActionsList.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index e9ecbd1bdeab..81a481e667cb 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -11,6 +11,7 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import usePrevious from '@hooks/usePrevious'; import useReportScrollManager from '@hooks/useReportScrollManager'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; @@ -21,7 +22,6 @@ import useThemeStyles from '@styles/useThemeStyles'; import variables from '@styles/variables'; import * as Report from '@userActions/Report'; import CONST from '@src/CONST'; -import usePrevious from '@hooks/usePrevious'; import FloatingMessageCounter from './FloatingMessageCounter'; import ListBoundaryLoader from './ListBoundaryLoader/ListBoundaryLoader'; import reportActionPropTypes from './reportActionPropTypes'; @@ -161,7 +161,7 @@ function ReportActionsList({ const reportActionSize = useRef(sortedReportActions.length); const lastReadTimeRef = useRef(report.lastReadTime); - const previousLastIndex = usePrevious(sortedReportActions[0].reportActionID) + const previousLastIndex = usePrevious(sortedReportActions[0].reportActionID); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); @@ -177,14 +177,14 @@ function ReportActionsList({ opacity.value = withTiming(1, {duration: 100}); }, [opacity]); - useEffect(()=>{ - if(previousLastIndex === sortedReportActions[0].reportActionID || reportActionSize.current <= sortedReportActions.length){ + useEffect(() => { + if (previousLastIndex === sortedReportActions[0].reportActionID || reportActionSize.current <= sortedReportActions.length) { return; } reportScrollManager.scrollToBottom(); // eslint-disable-next-line react-hooks/exhaustive-deps - }, [previousLastIndex, sortedReportActions[0].reportActionID]) - + }, [previousLastIndex, sortedReportActions[0].reportActionID]); + useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because // the parent component is sending the previous reportID even when the user isn't active From 7dc7bfa3d4a60f4d95e1ebc2f9abfa69e203bbed Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 22 Nov 2023 15:38:39 +0700 Subject: [PATCH 023/526] refactor --- src/pages/home/report/ReportActionsList.js | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 81a481e667cb..c02c1c162946 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -11,7 +11,6 @@ import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultPro import withWindowDimensions, {windowDimensionsPropTypes} from '@components/withWindowDimensions'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; -import usePrevious from '@hooks/usePrevious'; import useReportScrollManager from '@hooks/useReportScrollManager'; import compose from '@libs/compose'; import DateUtils from '@libs/DateUtils'; @@ -161,7 +160,9 @@ function ReportActionsList({ const reportActionSize = useRef(sortedReportActions.length); const lastReadTimeRef = useRef(report.lastReadTime); - const previousLastIndex = usePrevious(sortedReportActions[0].reportActionID); + const lastActionIndex = lodashGet(sortedReportActions, [0, 'reportActionID']); + + const previousLastIndex = useRef(lastActionIndex); const linkedReportActionID = lodashGet(route, 'params.reportActionID', ''); @@ -178,12 +179,12 @@ function ReportActionsList({ }, [opacity]); useEffect(() => { - if (previousLastIndex === sortedReportActions[0].reportActionID || reportActionSize.current <= sortedReportActions.length) { + if (previousLastIndex.current === lastActionIndex || reportActionSize.current <= sortedReportActions.length) { return; } reportScrollManager.scrollToBottom(); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [previousLastIndex, sortedReportActions[0].reportActionID]); + previousLastIndex.current = lastActionIndex; + }, [lastActionIndex, sortedReportActions.length, reportScrollManager]); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because From 0c45632a043ae8127c9fb0fdeac46e96d1dadb1e Mon Sep 17 00:00:00 2001 From: tienifr Date: Wed, 22 Nov 2023 15:40:34 +0700 Subject: [PATCH 024/526] refactor --- src/pages/home/report/ReportActionsList.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index c02c1c162946..1575f6054661 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -179,10 +179,9 @@ function ReportActionsList({ }, [opacity]); useEffect(() => { - if (previousLastIndex.current === lastActionIndex || reportActionSize.current <= sortedReportActions.length) { - return; + if (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedReportActions.length) { + reportScrollManager.scrollToBottom(); } - reportScrollManager.scrollToBottom(); previousLastIndex.current = lastActionIndex; }, [lastActionIndex, sortedReportActions.length, reportScrollManager]); From ee63ef7d92f619ace709894b84c37e96640d4b19 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20=27fvlvte=27=20Fa=C5=82at?= Date: Wed, 22 Nov 2023 14:41:21 +0100 Subject: [PATCH 025/526] Migrated ConfirmContent to typescript. --- .../{ConfirmContent.js => ConfirmContent.tsx} | 77 ++++++------------- 1 file changed, 25 insertions(+), 52 deletions(-) rename src/components/{ConfirmContent.js => ConfirmContent.tsx} (67%) diff --git a/src/components/ConfirmContent.js b/src/components/ConfirmContent.tsx similarity index 67% rename from src/components/ConfirmContent.js rename to src/components/ConfirmContent.tsx index ff8ee4f861a4..317d48f4954a 100644 --- a/src/components/ConfirmContent.js +++ b/src/components/ConfirmContent.tsx @@ -1,7 +1,5 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import {View} from 'react-native'; -import _ from 'underscore'; +import React, {ReactNode} from 'react'; +import {StyleProp, TextStyle, View, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@styles/useThemeStyles'; @@ -11,82 +9,60 @@ import Header from './Header'; import Icon from './Icon'; import Text from './Text'; -const propTypes = { +type ConfirmContentProps = { /** Title of the modal */ - title: PropTypes.string.isRequired, + title: string, /** A callback to call when the form has been submitted */ - onConfirm: PropTypes.func.isRequired, + onConfirm: (...args: unknown[]) => unknown, /** A callback to call when the form has been closed */ - onCancel: PropTypes.func, + onCancel: (...args: unknown[]) => unknown, /** Confirm button text */ - confirmText: PropTypes.string, + confirmText: string, /** Cancel button text */ - cancelText: PropTypes.string, + cancelText: string, /** Modal content text/element */ - prompt: PropTypes.oneOfType([PropTypes.string, PropTypes.element]), + prompt: string | Element, /** Whether we should use the success button color */ - success: PropTypes.bool, + success: boolean, /** Whether we should use the danger button color. Use if the action is destructive */ - danger: PropTypes.bool, + danger: boolean, /** Whether we should disable the confirm button when offline */ - shouldDisableConfirmButtonWhenOffline: PropTypes.bool, + shouldDisableConfirmButtonWhenOffline: boolean, /** Whether we should show the cancel button */ - shouldShowCancelButton: PropTypes.bool, + shouldShowCancelButton: boolean, /** Icon to display above the title */ - iconSource: PropTypes.oneOfType([PropTypes.string, PropTypes.func]), + iconSource: string | ((props: unknown) => ReactNode), /** Whether to center the icon / text content */ - shouldCenterContent: PropTypes.bool, + shouldCenterContent: boolean, /** Whether to stack the buttons */ - shouldStackButtons: PropTypes.bool, + shouldStackButtons: boolean, /** Styles for title */ - // eslint-disable-next-line react/forbid-prop-types - titleStyles: PropTypes.arrayOf(PropTypes.object), + titleStyles: Array>, /** Styles for prompt */ - // eslint-disable-next-line react/forbid-prop-types - promptStyles: PropTypes.arrayOf(PropTypes.object), + promptStyles: Array>, /** Styles for view */ - // eslint-disable-next-line react/forbid-prop-types - contentStyles: PropTypes.arrayOf(PropTypes.object), + contentStyles: Array>, /** Styles for icon */ - // eslint-disable-next-line react/forbid-prop-types - iconAdditionalStyles: PropTypes.arrayOf(PropTypes.object), + iconAdditionalStyles: Array>, }; -const defaultProps = { - confirmText: '', - cancelText: '', - prompt: '', - success: true, - danger: false, - onCancel: () => {}, - shouldDisableConfirmButtonWhenOffline: false, - shouldShowCancelButton: true, - contentStyles: [], - iconSource: null, - shouldCenterContent: false, - shouldStackButtons: true, - titleStyles: [], - promptStyles: [], - iconAdditionalStyles: [], -}; - -function ConfirmContent(props) { +function ConfirmContent(props: ConfirmContentProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isOffline} = useNetwork(); @@ -96,8 +72,8 @@ function ConfirmContent(props) { return ( - {!_.isEmpty(props.iconSource) || - (_.isFunction(props.iconSource) && ( + {(Object.keys(props.iconSource).length !== 0) || + (typeof props.iconSource === "function") && ( - ))} + )}
- - {_.isString(props.prompt) ? {props.prompt} : props.prompt} + {typeof props.prompt === "string" ? {props.prompt} : props.prompt} {props.shouldStackButtons ? ( @@ -163,7 +138,5 @@ function ConfirmContent(props) { ); } -ConfirmContent.propTypes = propTypes; -ConfirmContent.defaultProps = defaultProps; ConfirmContent.displayName = 'ConfirmContent'; export default ConfirmContent; From 2252f7ca2d08462adca23201c6be65d591de36cd Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 24 Nov 2023 05:51:00 +0700 Subject: [PATCH 026/526] 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 e304bb9f57bfee03c291ee49380cf7b3c484d63f Mon Sep 17 00:00:00 2001 From: tienifr Date: Mon, 27 Nov 2023 10:10:16 +0700 Subject: [PATCH 027/526] remove autoScrollToTopThreshold --- src/components/InvertedFlatList/BaseInvertedFlatList.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/components/InvertedFlatList/BaseInvertedFlatList.js b/src/components/InvertedFlatList/BaseInvertedFlatList.js index 4206d5086a9e..abfad0f04be1 100644 --- a/src/components/InvertedFlatList/BaseInvertedFlatList.js +++ b/src/components/InvertedFlatList/BaseInvertedFlatList.js @@ -2,8 +2,6 @@ import PropTypes from 'prop-types'; import React, {forwardRef} from 'react'; import FlatList from '@components/FlatList'; -const AUTOSCROLL_TO_TOP_THRESHOLD = 128; - const propTypes = { /** Same as FlatList can be any array of anything */ // eslint-disable-next-line react/forbid-prop-types @@ -25,7 +23,6 @@ const BaseInvertedFlatList = forwardRef((props, ref) => ( windowSize={15} maintainVisibleContentPosition={{ minIndexForVisible: 0, - autoscrollToTopThreshold: AUTOSCROLL_TO_TOP_THRESHOLD, }} inverted /> 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 028/526] 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 029/526] 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 030/526] 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 031/526] 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 032/526] 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 033/526] 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 a763f0004c91f5702e6f6e2a4d53c6fea7add3ab Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 29 Nov 2023 10:56:13 +0100 Subject: [PATCH 034/526] migrate PopoverWithoutOverlay.js to TypeScript --- src/components/Modal/types.ts | 2 +- src/components/PopoverWithoutOverlay/index.js | 109 ------------------ .../PopoverWithoutOverlay/index.tsx | 107 +++++++++++++++++ src/components/PopoverWithoutOverlay/types.ts | 28 +++++ 4 files changed, 136 insertions(+), 110 deletions(-) delete mode 100644 src/components/PopoverWithoutOverlay/index.js create mode 100644 src/components/PopoverWithoutOverlay/index.tsx create mode 100644 src/components/PopoverWithoutOverlay/types.ts diff --git a/src/components/Modal/types.ts b/src/components/Modal/types.ts index 3fa60e6ac765..bf1a34d9ca94 100644 --- a/src/components/Modal/types.ts +++ b/src/components/Modal/types.ts @@ -23,7 +23,7 @@ type BaseModalProps = WindowDimensionsProps & shouldSetModalVisibility?: boolean; /** Callback method fired when the user requests to close the modal */ - onClose: () => void; + onClose: (ref?: React.RefObject) => void; /** State that determines whether to display the modal or not */ isVisible: boolean; diff --git a/src/components/PopoverWithoutOverlay/index.js b/src/components/PopoverWithoutOverlay/index.js deleted file mode 100644 index 8b9dd4ac7a61..000000000000 --- a/src/components/PopoverWithoutOverlay/index.js +++ /dev/null @@ -1,109 +0,0 @@ -import React from 'react'; -import {View} from 'react-native'; -import {SafeAreaInsetsContext} from 'react-native-safe-area-context'; -import {defaultProps, propTypes} from '@components/Popover/popoverPropTypes'; -import {PopoverContext} from '@components/PopoverProvider'; -import withWindowDimensions from '@components/withWindowDimensions'; -import getModalStyles from '@styles/getModalStyles'; -import * as StyleUtils from '@styles/StyleUtils'; -import useThemeStyles from '@styles/useThemeStyles'; -import * as Modal from '@userActions/Modal'; - -function Popover(props) { - const styles = useThemeStyles(); - const {onOpen, close} = React.useContext(PopoverContext); - const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = getModalStyles( - 'popover', - { - windowWidth: props.windowWidth, - windowHeight: props.windowHeight, - isSmallScreenWidth: false, - }, - props.anchorPosition, - props.innerContainerStyle, - props.outerStyle, - ); - - React.useEffect(() => { - let removeOnClose; - if (props.isVisible) { - props.onModalShow(); - onOpen({ - ref: props.withoutOverlayRef, - close: props.onClose, - anchorRef: props.anchorRef, - }); - removeOnClose = Modal.setCloseModal(() => props.onClose(props.anchorRef)); - } else { - props.onModalHide(); - close(props.anchorRef); - Modal.onModalDidClose(); - } - Modal.willAlertModalBecomeVisible(props.isVisible); - - return () => { - if (!removeOnClose) { - return; - } - removeOnClose(); - }; - // We want this effect to run strictly ONLY when isVisible prop changes - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [props.isVisible]); - - if (!props.isVisible) { - return null; - } - - return ( - - - {(insets) => { - const { - paddingTop: safeAreaPaddingTop, - paddingBottom: safeAreaPaddingBottom, - paddingLeft: safeAreaPaddingLeft, - paddingRight: safeAreaPaddingRight, - } = StyleUtils.getSafeAreaPadding(insets); - - const modalPaddingStyles = StyleUtils.getModalPaddingStyles({ - safeAreaPaddingTop, - safeAreaPaddingBottom, - safeAreaPaddingLeft, - safeAreaPaddingRight, - shouldAddBottomSafeAreaMargin, - shouldAddTopSafeAreaMargin, - shouldAddBottomSafeAreaPadding, - shouldAddTopSafeAreaPadding, - modalContainerStyleMarginTop: modalContainerStyle.marginTop, - modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, - modalContainerStylePaddingTop: modalContainerStyle.paddingTop, - modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, - insets, - }); - return ( - - {props.children} - - ); - }} - - - ); -} - -Popover.propTypes = propTypes; -Popover.defaultProps = defaultProps; -Popover.displayName = 'Popover'; - -export default withWindowDimensions(Popover); diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx new file mode 100644 index 000000000000..5bcd4c64cb0c --- /dev/null +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -0,0 +1,107 @@ +import React, {ForwardedRef} from 'react'; +import {View} from 'react-native'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; +import {PopoverContext} from '@components/PopoverProvider'; +import useWindowDimensions from '@hooks/useWindowDimensions'; +import getModalStyles from '@styles/getModalStyles'; +import * as StyleUtils from '@styles/StyleUtils'; +import useThemeStyles from '@styles/useThemeStyles'; +import * as Modal from '@userActions/Modal'; +import PopoverProps from './types'; + +function Popover( + {anchorPosition = {}, anchorRef, withoutOverlayRef, innerContainerStyle = {}, outerStyle, onModalShow = () => {}, isVisible, onClose, onModalHide = () => {}, children}: PopoverProps, + ref: ForwardedRef, +) { + const styles = useThemeStyles(); + const {onOpen, close} = React.useContext(PopoverContext); + const {windowWidth, windowHeight} = useWindowDimensions(); + const insets = useSafeAreaInsets(); + const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = getModalStyles( + 'popover', + { + windowWidth, + windowHeight, + isSmallScreenWidth: false, + }, + anchorPosition, + innerContainerStyle, + outerStyle, + ); + + React.useEffect(() => { + let removeOnClose: () => void; + if (isVisible && anchorRef && onOpen) { + onModalShow(); + onOpen({ + ref: withoutOverlayRef, + close: onClose, + anchorRef, + }); + removeOnClose = Modal.setCloseModal(() => onClose(anchorRef)); + } else { + onModalHide(); + close(anchorRef); + Modal.onModalDidClose(); + } + Modal.willAlertModalBecomeVisible(isVisible); + + return () => { + if (!removeOnClose) { + return; + } + removeOnClose(); + }; + // We want this effect to run strictly ONLY when isVisible prop changes + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [isVisible]); + + if (!isVisible) { + return null; + } + + const { + paddingTop: safeAreaPaddingTop, + paddingBottom: safeAreaPaddingBottom, + paddingLeft: safeAreaPaddingLeft, + paddingRight: safeAreaPaddingRight, + } = StyleUtils.getSafeAreaPadding(insets); + + const modalPaddingStyles = StyleUtils.getModalPaddingStyles({ + safeAreaPaddingTop, + safeAreaPaddingBottom, + safeAreaPaddingLeft, + safeAreaPaddingRight, + shouldAddBottomSafeAreaMargin, + shouldAddTopSafeAreaMargin, + shouldAddBottomSafeAreaPadding, + shouldAddTopSafeAreaPadding, + modalContainerStyleMarginTop: modalContainerStyle.marginTop, + modalContainerStyleMarginBottom: modalContainerStyle.marginBottom, + modalContainerStylePaddingTop: modalContainerStyle.paddingTop, + modalContainerStylePaddingBottom: modalContainerStyle.paddingBottom, + insets, + }); + + return ( + + + {children} + + + ); +} + +Popover.displayName = 'Popover'; + +export default React.forwardRef(Popover); diff --git a/src/components/PopoverWithoutOverlay/types.ts b/src/components/PopoverWithoutOverlay/types.ts new file mode 100644 index 000000000000..7e73a7ae8408 --- /dev/null +++ b/src/components/PopoverWithoutOverlay/types.ts @@ -0,0 +1,28 @@ +import {View} from 'react-native'; +import BaseModalProps from '@components/Modal/types'; +import ChildrenProps from '@src/types/utils/ChildrenProps'; + +type PopoverProps = ChildrenProps & + Omit & { + /** The anchor position of the popover */ + anchorPosition?: { + top?: number; + right?: number; + bottom?: number; + left?: number; + }; + + /** The anchor ref of the popover */ + anchorRef?: React.RefObject; + + /** A react-native-animatable animation timing for the modal display animation. */ + animationInTiming?: number; + + /** Whether disable the animations */ + disableAnimation?: boolean; + + /** The ref of the popover */ + withoutOverlayRef: React.RefObject; + }; + +export default PopoverProps; From e1afa7c71dbd0a50b5f012a3e47d63448e72a070 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 29 Nov 2023 12:05:23 +0100 Subject: [PATCH 035/526] fix type error in BaseModal --- src/components/Modal/BaseModal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/Modal/BaseModal.tsx b/src/components/Modal/BaseModal.tsx index 95a7f3adc279..c8fe65af086f 100644 --- a/src/components/Modal/BaseModal.tsx +++ b/src/components/Modal/BaseModal.tsx @@ -180,7 +180,7 @@ function BaseModal( onModalHide={hideModal} onModalWillShow={() => ComposerFocusManager.resetReadyToFocus()} onDismiss={handleDismissModal} - onSwipeComplete={onClose} + onSwipeComplete={() => onClose()} swipeDirection={swipeDirection} isVisible={isVisible} backdropColor={theme.overlay} From cd4167d682ecd8598599d588d47044a99da404a9 Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Wed, 29 Nov 2023 13:15:27 +0100 Subject: [PATCH 036/526] import hooks from react --- src/components/PopoverWithoutOverlay/index.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index 5bcd4c64cb0c..ddcdec247fac 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -1,4 +1,4 @@ -import React, {ForwardedRef} from 'react'; +import React, {ForwardedRef, forwardRef, useContext, useEffect} from 'react'; import {View} from 'react-native'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; import {PopoverContext} from '@components/PopoverProvider'; @@ -14,7 +14,7 @@ function Popover( ref: ForwardedRef, ) { const styles = useThemeStyles(); - const {onOpen, close} = React.useContext(PopoverContext); + const {onOpen, close} = useContext(PopoverContext); const {windowWidth, windowHeight} = useWindowDimensions(); const insets = useSafeAreaInsets(); const {modalStyle, modalContainerStyle, shouldAddTopSafeAreaMargin, shouldAddBottomSafeAreaMargin, shouldAddTopSafeAreaPadding, shouldAddBottomSafeAreaPadding} = getModalStyles( @@ -29,7 +29,7 @@ function Popover( outerStyle, ); - React.useEffect(() => { + useEffect(() => { let removeOnClose: () => void; if (isVisible && anchorRef && onOpen) { onModalShow(); @@ -104,4 +104,4 @@ function Popover( Popover.displayName = 'Popover'; -export default React.forwardRef(Popover); +export default forwardRef(Popover); From 2ffcec2417dd634c342460171de57b2bc847f049 Mon Sep 17 00:00:00 2001 From: Agata Kosior Date: Thu, 30 Nov 2023 08:58:31 +0100 Subject: [PATCH 037/526] 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 038/526] 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 c5a641859a4ac66250438383556f33b24e12ee1f Mon Sep 17 00:00:00 2001 From: Julian Kobrynski Date: Thu, 30 Nov 2023 15:07:09 +0100 Subject: [PATCH 039/526] make anchorRef a required prop, conditional onOpen call --- .../PopoverWithoutOverlay/index.tsx | 25 +++++++++++++------ src/components/PopoverWithoutOverlay/types.ts | 6 ++--- 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/src/components/PopoverWithoutOverlay/index.tsx b/src/components/PopoverWithoutOverlay/index.tsx index ddcdec247fac..8786d5593e33 100644 --- a/src/components/PopoverWithoutOverlay/index.tsx +++ b/src/components/PopoverWithoutOverlay/index.tsx @@ -7,10 +7,21 @@ import getModalStyles from '@styles/getModalStyles'; import * as StyleUtils from '@styles/StyleUtils'; import useThemeStyles from '@styles/useThemeStyles'; import * as Modal from '@userActions/Modal'; -import PopoverProps from './types'; +import PopoverWithoutOverlayProps from './types'; -function Popover( - {anchorPosition = {}, anchorRef, withoutOverlayRef, innerContainerStyle = {}, outerStyle, onModalShow = () => {}, isVisible, onClose, onModalHide = () => {}, children}: PopoverProps, +function PopoverWithoutOverlay( + { + anchorPosition = {}, + anchorRef, + withoutOverlayRef, + innerContainerStyle = {}, + outerStyle, + onModalShow = () => {}, + isVisible, + onClose, + onModalHide = () => {}, + children, + }: PopoverWithoutOverlayProps, ref: ForwardedRef, ) { const styles = useThemeStyles(); @@ -31,9 +42,9 @@ function Popover( useEffect(() => { let removeOnClose: () => void; - if (isVisible && anchorRef && onOpen) { + if (isVisible) { onModalShow(); - onOpen({ + onOpen?.({ ref: withoutOverlayRef, close: onClose, anchorRef, @@ -102,6 +113,6 @@ function Popover( ); } -Popover.displayName = 'Popover'; +PopoverWithoutOverlay.displayName = 'PopoverWithoutOverlay'; -export default forwardRef(Popover); +export default forwardRef(PopoverWithoutOverlay); diff --git a/src/components/PopoverWithoutOverlay/types.ts b/src/components/PopoverWithoutOverlay/types.ts index 7e73a7ae8408..61b6727b40ee 100644 --- a/src/components/PopoverWithoutOverlay/types.ts +++ b/src/components/PopoverWithoutOverlay/types.ts @@ -2,7 +2,7 @@ import {View} from 'react-native'; import BaseModalProps from '@components/Modal/types'; import ChildrenProps from '@src/types/utils/ChildrenProps'; -type PopoverProps = ChildrenProps & +type PopoverWithoutOverlayProps = ChildrenProps & Omit & { /** The anchor position of the popover */ anchorPosition?: { @@ -13,7 +13,7 @@ type PopoverProps = ChildrenProps & }; /** The anchor ref of the popover */ - anchorRef?: React.RefObject; + anchorRef: React.RefObject; /** A react-native-animatable animation timing for the modal display animation. */ animationInTiming?: number; @@ -25,4 +25,4 @@ type PopoverProps = ChildrenProps & withoutOverlayRef: React.RefObject; }; -export default PopoverProps; +export default PopoverWithoutOverlayProps; From 87f5f0e847c0147cd0fe688a70e32b91ad409d75 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Thu, 30 Nov 2023 22:46:30 +0700 Subject: [PATCH 040/526] 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 041/526] 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 042/526] 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 043/526] 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 12:48:51 +0100 Subject: [PATCH 044/526] Use PersonalDetailsUtils.getDisplayNameOrDefault to escape merged account prefix --- src/libs/OptionsListUtils.js | 17 +++++++++++------ src/libs/PersonalDetailsUtils.js | 3 ++- src/libs/ReportUtils.ts | 5 +++-- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/libs/OptionsListUtils.js b/src/libs/OptionsListUtils.js index e3b6ec77380e..e80f8e7dd1ba 100644 --- a/src/libs/OptionsListUtils.js +++ b/src/libs/OptionsListUtils.js @@ -200,7 +200,7 @@ function isPersonalDetailsReady(personalDetails) { function getParticipantsOption(participant, personalDetails) { const detail = getPersonalDetailsForAccountIDs([participant.accountID], personalDetails)[participant.accountID]; const login = detail.login || participant.login; - const displayName = detail.displayName || LocalePhoneNumber.formatPhoneNumber(login); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail, 'displayName', LocalePhoneNumber.formatPhoneNumber(login)); return { keyForList: String(detail.accountID), login, @@ -245,7 +245,8 @@ function getParticipantNames(personalDetailList) { participantNames.add(participant.lastName.toLowerCase()); } if (participant.displayName) { - participantNames.add(participant.displayName.toLowerCase()); + const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(participant, 'displayName'); + participantNames.add(displayName.toLowerCase()); } }); return participantNames; @@ -298,7 +299,11 @@ function getSearchText(report, reportName, personalDetailList, isChatRoomOrPolic // The regex below is used to remove dots only from the local part of the user email (local-part@domain) // so that we can match emails that have dots without explicitly writing the dots (e.g: fistlast@domain will match first.last@domain) // More info https://github.com/Expensify/App/issues/8007 - searchTerms = searchTerms.concat([personalDetail.displayName, personalDetail.login, personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '')]); + searchTerms = searchTerms.concat([ + PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), + personalDetail.login, + personalDetail.login.replace(/\.(?=[^\s@]*@)/g, '') + ]); } } } @@ -507,7 +512,7 @@ function createOption(accountIDs, personalDetails, report, reportActions = {}, { const lastMessageTextFromReport = getLastMessageTextForReport(report); const lastActorDetails = personalDetailMap[report.lastActorAccountID] || null; - let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${lastActorDetails.displayName}: ` : ''; + let lastMessageText = hasMultipleParticipants && lastActorDetails && lastActorDetails.accountID !== currentUserAccountID ? `${PersonalDetailsUtils.getDisplayNameOrDefault(lastActorDetails, 'displayName')}: ` : ''; lastMessageText += report ? lastMessageTextFromReport : ''; if (result.isArchivedRoom) { @@ -1429,8 +1434,8 @@ function getSearchOptions(reports, personalDetails, searchValue = '', betas) { function getIOUConfirmationOptionsFromPayeePersonalDetail(personalDetail, amountText) { const formattedLogin = LocalePhoneNumber.formatPhoneNumber(personalDetail.login); return { - text: personalDetail.displayName || formattedLogin, - alternateText: formattedLogin || personalDetail.displayName, + text: PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName', formattedLogin), + alternateText: formattedLogin || PersonalDetailsUtils.getDisplayNameOrDefault(personalDetail, 'displayName'), icons: [ { source: UserUtils.getAvatar(personalDetail.avatar, personalDetail.accountID), diff --git a/src/libs/PersonalDetailsUtils.js b/src/libs/PersonalDetailsUtils.js index 560480dcec9d..30b710e9d15c 100644 --- a/src/libs/PersonalDetailsUtils.js +++ b/src/libs/PersonalDetailsUtils.js @@ -1,6 +1,7 @@ import lodashGet from 'lodash/get'; import Onyx from 'react-native-onyx'; import _ from 'underscore'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import * as LocalePhoneNumber from './LocalePhoneNumber'; import * as Localize from './Localize'; @@ -23,7 +24,7 @@ Onyx.connect({ * @returns {String} */ function getDisplayNameOrDefault(passedPersonalDetails, pathToDisplayName, defaultValue = '') { - const displayName = lodashGet(passedPersonalDetails, pathToDisplayName); + const displayName = lodashGet(passedPersonalDetails, pathToDisplayName, '').replace(CONST.REGEX.MERGED_ACCOUNT_PREFIX, ''); return displayName || defaultValue || Localize.translateLocal('common.hidden'); } diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index f6c3090143f4..b914658fe448 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -31,6 +31,7 @@ import linkingConfig from './Navigation/linkingConfig'; import Navigation from './Navigation/Navigation'; import * as NumberUtils from './NumberUtils'; import Permissions from './Permissions'; +import * as PersonalDetailsUtils from './PersonalDetailsUtils'; import * as PolicyUtils from './PolicyUtils'; import * as ReportActionsUtils from './ReportActionsUtils'; import {LastVisibleMessage} from './ReportActionsUtils'; @@ -1270,7 +1271,7 @@ function getIcons( const parentReportAction = ReportActionsUtils.getParentReportAction(report); const actorAccountID = parentReportAction.actorAccountID; - const actorDisplayName = allPersonalDetails?.[actorAccountID ?? -1]?.displayName ?? ''; + const actorDisplayName = PersonalDetailsUtils.getDisplayNameOrDefault(allPersonalDetails, [actorAccountID ?? -1, 'displayName']); const actorIcon = { id: actorAccountID, source: UserUtils.getAvatar(personalDetails?.[actorAccountID ?? -1]?.avatar ?? '', actorAccountID ?? -1), @@ -1394,7 +1395,7 @@ function getDisplayNameForParticipant(accountID?: number, shouldUseShortForm = f // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return formattedLogin; } - const longName = personalDetails.displayName ? personalDetails.displayName : formattedLogin; + const longName = PersonalDetailsUtils.getDisplayNameOrDefault(personalDetails, 'displayName', formattedLogin); const shortName = personalDetails.firstName ? personalDetails.firstName : longName; From 410e3d44e1e6725cb1c6182eb29cf1ff660225f6 Mon Sep 17 00:00:00 2001 From: DylanDylann Date: Fri, 1 Dec 2023 21:29:48 +0700 Subject: [PATCH 045/526] 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 046/526] 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 047/526] 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 048/526] 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 049/526] 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 050/526] 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 22502c94c3a1e79050b99736778405c8798f53e7 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 5 Dec 2023 17:55:58 +0700 Subject: [PATCH 051/526] fix: bug --- src/pages/home/report/ReportActionsList.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index d9ba4d1dc4d1..43560726327c 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -2,7 +2,7 @@ import {useRoute} from '@react-navigation/native'; import lodashGet from 'lodash/get'; import PropTypes from 'prop-types'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {DeviceEventEmitter} from 'react-native'; +import {DeviceEventEmitter, InteractionManager} from 'react-native'; import Animated, {useAnimatedStyle, useSharedValue, withTiming} from 'react-native-reanimated'; import _ from 'underscore'; import InvertedFlatList from '@components/InvertedFlatList'; @@ -159,7 +159,8 @@ function ReportActionsList({ const reportActionSize = useRef(sortedReportActions.length); const lastReadTimeRef = useRef(report.lastReadTime); - const lastActionIndex = lodashGet(sortedReportActions, [0, 'reportActionID']); + const sortedVisibleReportActions = _.filter(sortedReportActions, (s) => isOffline || s.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || s.errors); + const lastActionIndex = lodashGet(sortedVisibleReportActions, [0, 'reportActionID']); const previousLastIndex = useRef(lastActionIndex); @@ -178,11 +179,11 @@ function ReportActionsList({ }, [opacity]); useEffect(() => { - if (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedReportActions.length) { + if (previousLastIndex.current !== lastActionIndex && reportActionSize.current > sortedVisibleReportActions.length) { reportScrollManager.scrollToBottom(); } previousLastIndex.current = lastActionIndex; - }, [lastActionIndex, sortedReportActions.length, reportScrollManager]); + }, [lastActionIndex, sortedVisibleReportActions.length, reportScrollManager]); useEffect(() => { // If the reportID changes, we reset the userActiveSince to null, we need to do it because @@ -270,7 +271,9 @@ function ReportActionsList({ if (!isFromCurrentUser) { return; } - reportScrollManager.scrollToBottom(); + InteractionManager.runAfterInteractions(()=>{ + reportScrollManager.scrollToBottom(); + }) }); const cleanup = () => { From fee91999b2d48c336afaee00780fd731257e9740 Mon Sep 17 00:00:00 2001 From: tienifr Date: Tue, 5 Dec 2023 18:10:07 +0700 Subject: [PATCH 052/526] lint fix --- src/pages/home/report/ReportActionsList.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/home/report/ReportActionsList.js b/src/pages/home/report/ReportActionsList.js index 43560726327c..6ae4555d02c1 100644 --- a/src/pages/home/report/ReportActionsList.js +++ b/src/pages/home/report/ReportActionsList.js @@ -271,9 +271,9 @@ function ReportActionsList({ if (!isFromCurrentUser) { return; } - InteractionManager.runAfterInteractions(()=>{ + InteractionManager.runAfterInteractions(() => { reportScrollManager.scrollToBottom(); - }) + }); }); const cleanup = () => { From 4bc71cd09ed669d7565b81a58aa33a64059f4d31 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 5 Dec 2023 16:16:55 +0100 Subject: [PATCH 053/526] 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 cd7f52b1a39a6b60613dac842d0c4fd6d3092667 Mon Sep 17 00:00:00 2001 From: Ishpaul Singh Date: Tue, 5 Dec 2023 21:10:45 +0530 Subject: [PATCH 054/526] fix reposition of popover when window resize --- src/CONST.ts | 1 + .../settings/Wallet/PaymentMethodList.js | 5 +++-- .../settings/Wallet/WalletPage/WalletPage.js | 21 ++++++++++++++++--- 3 files changed, 22 insertions(+), 5 deletions(-) diff --git a/src/CONST.ts b/src/CONST.ts index 3d69c83c5c22..b461c702c53f 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -689,6 +689,7 @@ const CONST = { TRIE_INITIALIZATION: 'trie_initialization', COMMENT_LENGTH_DEBOUNCE_TIME: 500, SEARCH_FOR_REPORTS_DEBOUNCE_TIME: 300, + RESIZE_DEBOUNCE_TIME: 100, }, PRIORITY_MODE: { GSD: 'gsd', diff --git a/src/pages/settings/Wallet/PaymentMethodList.js b/src/pages/settings/Wallet/PaymentMethodList.js index 5af4129aefbc..5ac623008fd4 100644 --- a/src/pages/settings/Wallet/PaymentMethodList.js +++ b/src/pages/settings/Wallet/PaymentMethodList.js @@ -289,10 +289,11 @@ function PaymentMethodList({ title={translate('walletPage.addBankAccount')} icon={Expensicons.Plus} wrapperStyle={styles.paymentMethod} + ref={buttonRef} /> ), - [onPress, styles.paymentMethod, translate], + [onPress, styles.paymentMethod, translate, buttonRef], ); /** @@ -346,10 +347,10 @@ function PaymentMethodList({ keyExtractor={keyExtractor} ListEmptyComponent={shouldShowEmptyListMessage ? renderListEmptyComponent : null} ListHeaderComponent={listHeaderComponent} - ListFooterComponent={shouldShowAddBankAccount ? renderListFooterComponent : null} onContentSizeChange={onListContentSizeChange} scrollEnabled={shouldEnableScroll} /> + {shouldShowAddBankAccount && renderListFooterComponent()} {shouldShowAddPaymentMethodButton && ( diff --git a/src/pages/settings/Wallet/WalletPage/WalletPage.js b/src/pages/settings/Wallet/WalletPage/WalletPage.js index 6f452eed3629..9dc6c878a1bf 100644 --- a/src/pages/settings/Wallet/WalletPage/WalletPage.js +++ b/src/pages/settings/Wallet/WalletPage/WalletPage.js @@ -1,6 +1,6 @@ import lodashGet from 'lodash/get'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import {ActivityIndicator, InteractionManager, ScrollView, View} from 'react-native'; +import {ActivityIndicator, Dimensions, InteractionManager, ScrollView, View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import AddPaymentMethodMenu from '@components/AddPaymentMethodMenu'; @@ -283,8 +283,23 @@ function WalletPage({bankAccountList, cardList, fundList, isLoadingPaymentMethod if (!shouldListenForResize) { return; } - setMenuPosition(); - }, [shouldListenForResize, setMenuPosition]); + const popoverPositionListener = Dimensions.addEventListener('change', () => { + if (!shouldShowAddPaymentMenu && !shouldShowDefaultDeleteMenu) { + return; + } + if (shouldShowAddPaymentMenu) { + _.debounce(setMenuPosition, CONST.TIMING.RESIZE_DEBOUNCE_TIME)(); + return; + } + setMenuPosition(); + }); + return () => { + if (!popoverPositionListener) { + return; + } + popoverPositionListener.remove(); + }; + }, [shouldShowAddPaymentMenu, shouldShowDefaultDeleteMenu, setMenuPosition, shouldListenForResize]); useEffect(() => { if (!shouldShowDefaultDeleteMenu) { From 6ed9bcbdca840da6265d2a489cc2450e13fa9c40 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Tue, 5 Dec 2023 17:01:08 +0100 Subject: [PATCH 055/526] Add border radius 3 style --- src/styles/utilities/borders.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/styles/utilities/borders.ts b/src/styles/utilities/borders.ts index 5d5110f858e4..9cd02dcd22ae 100644 --- a/src/styles/utilities/borders.ts +++ b/src/styles/utilities/borders.ts @@ -12,6 +12,10 @@ export default { borderRadius: 8, }, + br3: { + borderRadius: 12, + }, + br4: { borderRadius: 16, }, From 4346c84849c43a6deef3c5a33781ecc284ab0f40 Mon Sep 17 00:00:00 2001 From: Maciej Dobosz Date: Tue, 5 Dec 2023 17:01:40 +0100 Subject: [PATCH 056/526] Create WorkspacesListRow --- src/pages/workspace/WorkspacesListPage.js | 22 ++++++---------------- src/pages/workspace/WorkspacesListRow.tsx | 20 ++++++++++++++++++++ 2 files changed, 26 insertions(+), 16 deletions(-) create mode 100755 src/pages/workspace/WorkspacesListRow.tsx diff --git a/src/pages/workspace/WorkspacesListPage.js b/src/pages/workspace/WorkspacesListPage.js index cf258b462285..c031e061fbe2 100755 --- a/src/pages/workspace/WorkspacesListPage.js +++ b/src/pages/workspace/WorkspacesListPage.js @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; +import {View} from 'react-native'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import Button from '@components/Button'; @@ -28,6 +29,7 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import SCREENS from '@src/SCREENS'; import withPolicyAndFullscreenLoading from './withPolicyAndFullscreenLoading'; +import WorkspacesListRow from './WorkspacesListRow'; const propTypes = { /** The list of this user's policies */ @@ -182,20 +184,7 @@ function WorkspacesListPage({policies, allPolicyMembers, reimbursementAccount, u }, [reimbursementAccount.errors, policies, isOffline, theme.textLight, allPolicyMembers]); return ( - Navigation.goBack(ROUTES.SETTINGS)} - title={translate('common.workspaces')} - footer={ -