From fab9c8e6ed313751ec28fcd8bb15b8aeba0ccc1d Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 17 Jan 2024 15:37:20 +0100 Subject: [PATCH 01/29] fix: wip --- src/libs/actions/EmojiPickerAction.ts | 2 +- src/libs/actions/User.ts | 6 +- ...tionCompose.js => ReportActionCompose.tsx} | 153 ++++++++++-------- 3 files changed, 88 insertions(+), 73 deletions(-) rename src/pages/home/report/ReportActionCompose/{ReportActionCompose.js => ReportActionCompose.tsx} (81%) diff --git a/src/libs/actions/EmojiPickerAction.ts b/src/libs/actions/EmojiPickerAction.ts index 56a5f34c0b8e..3a4da86dcbb9 100644 --- a/src/libs/actions/EmojiPickerAction.ts +++ b/src/libs/actions/EmojiPickerAction.ts @@ -66,7 +66,7 @@ function showEmojiPicker( /** * Hide the Emoji Picker modal. */ -function hideEmojiPicker(isNavigating: boolean) { +function hideEmojiPicker(isNavigating?: boolean) { if (!emojiPickerRef.current) { return; } diff --git a/src/libs/actions/User.ts b/src/libs/actions/User.ts index 8e3bd5f2c017..e0f3003ed9e8 100644 --- a/src/libs/actions/User.ts +++ b/src/libs/actions/User.ts @@ -13,7 +13,7 @@ import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {FrequentlyUsedEmoji} from '@src/types/onyx'; +import type {BlockedFromConcierge, FrequentlyUsedEmoji} from '@src/types/onyx'; import type Login from '@src/types/onyx/Login'; import type {OnyxServerUpdate} from '@src/types/onyx/OnyxUpdatesFromServer'; import type OnyxPersonalDetails from '@src/types/onyx/PersonalDetails'; @@ -27,8 +27,6 @@ import * as PersonalDetails from './PersonalDetails'; import * as Report from './Report'; import * as Session from './Session'; -type BlockedFromConciergeNVP = {expiresAt: number}; - let currentUserAccountID = -1; let currentEmail = ''; Onyx.connect({ @@ -445,7 +443,7 @@ function validateSecondaryLogin(contactMethod: string, validateCode: string) { * and if so whether the expiresAt date of a user's ban is before right now * */ -function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry): boolean { +function isBlockedFromConcierge(blockedFromConciergeNVP: OnyxEntry): boolean { if (isEmptyObject(blockedFromConciergeNVP)) { return false; } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx similarity index 81% rename from src/pages/home/report/ReportActionCompose/ReportActionCompose.js rename to src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index c072666920ae..1f0574f577cb 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.js +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,21 +1,23 @@ import {PortalHost} from '@gorhom/portal'; import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import type {SyntheticEvent} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {View} from 'react-native'; +import {MeasureInWindowOnSuccessCallback, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; -import _ from 'underscore'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; import OfflineIndicator from '@components/OfflineIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; -import {usePersonalDetails, withNetwork} from '@components/OnyxProvider'; -import withCurrentUserPersonalDetails, {withCurrentUserPersonalDetailsDefaultProps, withCurrentUserPersonalDetailsPropTypes} from '@components/withCurrentUserPersonalDetails'; +import {usePersonalDetails} from '@components/OnyxProvider'; +import type {WithCurrentUserPersonalDetailsProps} from '@components/withCurrentUserPersonalDetails'; +import withCurrentUserPersonalDetails from '@components/withCurrentUserPersonalDetails'; import useDebounce from '@hooks/useDebounce'; import useHandleExceedMaxCommentLength from '@hooks/useHandleExceedMaxCommentLength'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; @@ -26,69 +28,88 @@ import getModalState from '@libs/getModalState'; import * as ReportUtils from '@libs/ReportUtils'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; import ParticipantLocalTime from '@pages/home/report/ParticipantLocalTime'; -import reportActionPropTypes from '@pages/home/report/reportActionPropTypes'; import ReportDropUI from '@pages/home/report/ReportDropUI'; import ReportTypingIndicator from '@pages/home/report/ReportTypingIndicator'; -import reportPropTypes from '@pages/reportPropTypes'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; import * as Report from '@userActions/Report'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems'; import ComposerWithSuggestions from './ComposerWithSuggestions'; import SendButton from './SendButton'; -const propTypes = { +type ComposerRef = { + blur: () => void; + focus: (shouldDelay?: boolean) => void; + replaceSelectionWithText: (text: string, shouldAddTrailSpace: boolean) => void; + prepareCommentAndResetComposer: () => string; + isFocused: () => boolean; +}; + +type SuggestionsRef = { + resetSuggestions: () => void; + onSelectionChange: (event: any) => void; + triggerHotkeyActions: (event: any) => void; + updateShouldShowSuggestionMenuToFalse: () => void; + setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; + getSuggestions: () => string[]; +}; + +type ReportActionComposeOnyxProps = { + /** The NVP describing a user's block status */ + blockedFromConcierge: OnyxEntry; + + /** Whether the composer input should be shown */ + shouldShowComposeInput: OnyxEntry; +}; + +type ReportActionComposeProps = { /** A method to call when the form is submitted */ - onSubmit: PropTypes.func.isRequired, + onSubmit: (newComment: string | undefined) => void; /** The ID of the report actions will be created for */ - reportID: PropTypes.string.isRequired, + reportID: string; /** Array of report actions for this report */ - reportActions: PropTypes.arrayOf(PropTypes.shape(reportActionPropTypes)), + reportActions?: OnyxTypes.ReportAction[]; /** The report currently being looked at */ - report: reportPropTypes, + report: OnyxEntry; /** Is composer full size */ - isComposerFullSize: PropTypes.bool, + isComposerFullSize?: boolean; /** Whether user interactions should be disabled */ - disabled: PropTypes.bool, + disabled?: boolean; /** Height of the list which the composer is part of */ - listHeight: PropTypes.number, - - // The NVP describing a user's block status - blockedFromConcierge: PropTypes.shape({ - // The date that the user will be unblocked - expiresAt: PropTypes.string, - }), + listHeight?: number; /** Whether the composer input should be shown */ - shouldShowComposeInput: PropTypes.bool, + shouldShowComposeInput?: boolean; /** The type of action that's pending */ - pendingAction: PropTypes.oneOf(['add', 'update', 'delete']), + pendingAction?: OnyxCommon.PendingAction; /** /** Whetjer the report is ready for display */ - isReportReadyForDisplay: PropTypes.bool, - ...withCurrentUserPersonalDetailsPropTypes, -}; - -const defaultProps = { - report: {}, - blockedFromConcierge: {}, - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - isComposerFullSize: false, - pendingAction: null, - shouldShowComposeInput: true, - listHeight: 0, - isReportReadyForDisplay: true, - ...withCurrentUserPersonalDetailsDefaultProps, -}; + isReportReadyForDisplay?: boolean; +} & ReportActionComposeOnyxProps & + WithCurrentUserPersonalDetailsProps; + +// const defaultProps = { +// report: {}, +// blockedFromConcierge: {}, +// preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, +// isComposerFullSize: false, +// pendingAction: null, +// shouldShowComposeInput: true, +// listHeight: 0, +// isReportReadyForDisplay: true, +// ...withCurrentUserPersonalDetailsDefaultProps, +// }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus on existing chat for mobile device @@ -101,7 +122,6 @@ function ReportActionCompose({ currentUserPersonalDetails, disabled, isComposerFullSize, - network, onSubmit, pendingAction, report, @@ -110,10 +130,11 @@ function ReportActionCompose({ listHeight, shouldShowComposeInput, isReportReadyForDisplay, -}) { +}: ReportActionComposeProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); + const {isOffline} = useNetwork(); const animatedRef = useAnimatedRef(); const actionButtonRef = useRef(null); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; @@ -122,7 +143,7 @@ function ReportActionCompose({ */ const [isFocused, setIsFocused] = useState(() => { const initialModalState = getModalState(); - return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState.isVisible && !initialModalState.willAlertModalBecomeVisible; + return shouldFocusInputOnScreenFocus && shouldShowComposeInput && !initialModalState?.isVisible && !initialModalState?.willAlertModalBecomeVisible; }); const [isFullComposerAvailable, setIsFullComposerAvailable] = useState(isComposerFullSize); @@ -167,11 +188,11 @@ function ReportActionCompose({ */ const {hasExceededMaxCommentLength, validateCommentMaxLength} = useHandleExceedMaxCommentLength(); - const suggestionsRef = useRef(null); - const composerRef = useRef(null); + const suggestionsRef = useRef(null); + const composerRef = useRef(null); const reportParticipantIDs = useMemo( - () => _.without(lodashGet(report, 'participantAccountIDs', []), currentUserPersonalDetails.accountID), + () => report?.participantAccountIDs?.filter((accountID) => accountID !== currentUserPersonalDetails.accountID), [currentUserPersonalDetails.accountID, report], ); @@ -184,7 +205,7 @@ function ReportActionCompose({ // If we are on a small width device then don't show last 3 items from conciergePlaceholderOptions const conciergePlaceholderRandomIndex = useMemo( - () => _.random(translate('reportActionCompose.conciergePlaceholderOptions').length - (isSmallScreenWidth ? 4 : 1)), + () => Math.floor(Math.random() * (translate('reportActionCompose.conciergePlaceholderOptions').length - (isSmallScreenWidth ? 4 : 1) + 1)), // eslint-disable-next-line react-hooks/exhaustive-deps [], ); @@ -203,7 +224,7 @@ function ReportActionCompose({ }, [report, blockedFromConcierge, translate, conciergePlaceholderRandomIndex]); const focus = () => { - if (composerRef === null || composerRef.current === null) { + if (composerRef?.current === null) { return; } composerRef.current.focus(true); @@ -219,9 +240,9 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = false; }, []); - const containerRef = useRef(null); + const containerRef = useRef(null); const measureContainer = useCallback( - (callback) => { + (callback: MeasureInWindowOnSuccessCallback) => { if (!containerRef.current) { return; } @@ -234,9 +255,9 @@ function ReportActionCompose({ const onAddActionPressed = useCallback(() => { if (!willBlurTextInputOnTapOutside) { - isKeyboardVisibleWhenShowingModalRef.current = composerRef.current.isFocused(); + isKeyboardVisibleWhenShowingModalRef.current = !!composerRef.current?.isFocused(); } - composerRef.current.blur(); + composerRef.current?.blur(); }, []); const onItemSelected = useCallback(() => { @@ -250,12 +271,9 @@ function ReportActionCompose({ suggestionsRef.current.updateShouldShowSuggestionMenuToFalse(false); }, []); - /** - * @param {Object} file - */ const addAttachment = useCallback( - (file) => { - const newComment = composerRef.current.prepareCommentAndResetComposer(); + (file: File) => { + const newComment = composerRef.current?.prepareCommentAndResetComposer(); Report.addAttachment(reportID, file, newComment); setTextInputShouldClear(false); }, @@ -273,16 +291,14 @@ function ReportActionCompose({ /** * Add a new comment to this chat - * - * @param {SyntheticEvent} [e] */ const submitForm = useCallback( - (e) => { + (e?: SyntheticEvent) => { if (e) { e.preventDefault(); } - const newComment = composerRef.current.prepareCommentAndResetComposer(); + const newComment = composerRef.current?.prepareCommentAndResetComposer(); if (!newComment) { return; } @@ -323,7 +339,7 @@ function ReportActionCompose({ // We are returning a callback here as we want to incoke the method on unmount only useEffect( () => () => { - if (!EmojiPickerActions.isActive(report.reportID)) { + if (!EmojiPickerActions.isActive(report?.reportID ?? '')) { return; } EmojiPickerActions.hideEmojiPicker(); @@ -338,7 +354,7 @@ function ReportActionCompose({ const hasReportRecipient = _.isObject(reportRecipient) && !_.isEmpty(reportRecipient); - const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || disabled || hasExceededMaxCommentLength; + const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength; const handleSendMessage = useCallback(() => { 'worklet'; @@ -355,7 +371,7 @@ function ReportActionCompose({ }, [isSendDisabled, resetFullComposerSize, submitForm, animatedRef, isReportReadyForDisplay]); return ( - + {shouldShowReportRecipientLocalTime && hasReportRecipient && } @@ -376,12 +392,14 @@ function ReportActionCompose({ hasExceededMaxCommentLength && styles.borderColorDanger, ]} > + {/* @ts-expect-error TODO: Remove this once AttachmentModal (https://github.com/Expensify/App/issues/25130) is migrated to TypeScript. */} setIsAttachmentPreviewActive(true)} onModalHide={onAttachmentPreviewClose} > + {/* @ts-expect-error TODO: Remove this once AttachmentModal (https://github.com/Expensify/App/issues/25130) is migrated to TypeScript. */} {({displayFileInModal}) => ( <> composerRef.current.replaceSelectionWithText(...args)} - emojiPickerID={report.reportID} + // @ts-expect-error TODO: Remove this once EmojiPickerButton is migrated to TypeScript. + onEmojiSelected={(...args) => composerRef.current?.replaceSelectionWithText(...args)} + emojiPickerID={report?.reportID} /> )} {!isSmallScreenWidth && } @@ -478,12 +498,9 @@ function ReportActionCompose({ ); } -ReportActionCompose.propTypes = propTypes; -ReportActionCompose.defaultProps = defaultProps; ReportActionCompose.displayName = 'ReportActionCompose'; export default compose( - withNetwork(), withCurrentUserPersonalDetails, withOnyx({ blockedFromConcierge: { From 25f6646d0486eb0ce45714956e7118c65281ef31 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 18 Jan 2024 10:46:39 +0100 Subject: [PATCH 02/29] fix: finish working on migration --- .../ReportActionCompose.tsx | 52 +++++++------------ 1 file changed, 20 insertions(+), 32 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 1f0574f577cb..2cdb4e4d3602 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,8 +1,8 @@ import {PortalHost} from '@gorhom/portal'; -import lodashGet from 'lodash/get'; import type {SyntheticEvent} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import {MeasureInWindowOnSuccessCallback, View} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; @@ -21,7 +21,6 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import compose from '@libs/compose'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import getModalState from '@libs/getModalState'; @@ -37,6 +36,7 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems'; import ComposerWithSuggestions from './ComposerWithSuggestions'; import SendButton from './SendButton'; @@ -51,9 +51,9 @@ type ComposerRef = { type SuggestionsRef = { resetSuggestions: () => void; - onSelectionChange: (event: any) => void; - triggerHotkeyActions: (event: any) => void; - updateShouldShowSuggestionMenuToFalse: () => void; + onSelectionChange: (event: NativeSyntheticEvent) => void; + triggerHotkeyActions: (event: KeyboardEvent) => void; + updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; getSuggestions: () => string[]; }; @@ -99,18 +99,6 @@ type ReportActionComposeProps = { } & ReportActionComposeOnyxProps & WithCurrentUserPersonalDetailsProps; -// const defaultProps = { -// report: {}, -// blockedFromConcierge: {}, -// preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, -// isComposerFullSize: false, -// pendingAction: null, -// shouldShowComposeInput: true, -// listHeight: 0, -// isReportReadyForDisplay: true, -// ...withCurrentUserPersonalDetailsDefaultProps, -// }; - // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus on existing chat for mobile device const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); @@ -119,17 +107,17 @@ const willBlurTextInputOnTapOutside = willBlurTextInputOnTapOutsideFunc(); function ReportActionCompose({ blockedFromConcierge, - currentUserPersonalDetails, + currentUserPersonalDetails = {}, disabled, - isComposerFullSize, + isComposerFullSize = false, onSubmit, pendingAction, report, reportID, reportActions, - listHeight, - shouldShowComposeInput, - isReportReadyForDisplay, + listHeight = 0, + shouldShowComposeInput = true, + isReportReadyForDisplay = true, }: ReportActionComposeProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -313,7 +301,7 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = true; }, []); - const onBlur = useCallback((e) => { + const onBlur = useCallback((e: FocusEvent) => { setIsFocused(false); if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); @@ -352,7 +340,7 @@ function ReportActionCompose({ const reportRecipient = personalDetails[reportRecipientAcountIDs[0]]; const shouldUseFocusedColor = !isBlockedFromConcierge && !disabled && isFocused; - const hasReportRecipient = _.isObject(reportRecipient) && !_.isEmpty(reportRecipient); + const hasReportRecipient = !isEmptyObject(reportRecipient); const isSendDisabled = isCommentEmpty || isBlockedFromConcierge || !!disabled || hasExceededMaxCommentLength; @@ -403,6 +391,7 @@ function ReportActionCompose({ {({displayFileInModal}) => ( <> { + onDrop={(e: DragEvent) => { if (isAttachmentPreviewActive) { return; } - const data = lodashGet(e, ['dataTransfer', 'items', 0]); + const data = e.dataTransfer?.items[0]; displayFileInModal(data); }} /> @@ -500,14 +489,13 @@ function ReportActionCompose({ ReportActionCompose.displayName = 'ReportActionCompose'; -export default compose( - withCurrentUserPersonalDetails, - withOnyx({ +export default withCurrentUserPersonalDetails( + withOnyx({ blockedFromConcierge: { key: ONYXKEYS.NVP_BLOCKED_FROM_CONCIERGE, }, shouldShowComposeInput: { key: ONYXKEYS.SHOULD_SHOW_COMPOSE_INPUT, }, - }), -)(ReportActionCompose); + })(ReportActionCompose), +); From 5e2f31c82fda91671e4c4c1e601a7755335fb67f Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 19 Jan 2024 11:33:28 +0100 Subject: [PATCH 03/29] fix: resolve comments --- .../ReportActionCompose.tsx | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 2cdb4e4d3602..73b025c50df2 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -88,13 +88,10 @@ type ReportActionComposeProps = { /** Height of the list which the composer is part of */ listHeight?: number; - /** Whether the composer input should be shown */ - shouldShowComposeInput?: boolean; - /** The type of action that's pending */ pendingAction?: OnyxCommon.PendingAction; - /** /** Whetjer the report is ready for display */ + /** Whether the report is ready for display */ isReportReadyForDisplay?: boolean; } & ReportActionComposeOnyxProps & WithCurrentUserPersonalDetailsProps; @@ -212,7 +209,7 @@ function ReportActionCompose({ }, [report, blockedFromConcierge, translate, conciergePlaceholderRandomIndex]); const focus = () => { - if (composerRef?.current === null) { + if (composerRef.current === null) { return; } composerRef.current.focus(true); @@ -281,9 +278,9 @@ function ReportActionCompose({ * Add a new comment to this chat */ const submitForm = useCallback( - (e?: SyntheticEvent) => { - if (e) { - e.preventDefault(); + (event?: SyntheticEvent) => { + if (event) { + event.preventDefault(); } const newComment = composerRef.current?.prepareCommentAndResetComposer(); @@ -301,12 +298,12 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = true; }, []); - const onBlur = useCallback((e: FocusEvent) => { + const onBlur = useCallback((event: FocusEvent) => { setIsFocused(false); if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); } - if (e.relatedTarget && e.relatedTarget === actionButtonRef.current) { + if (event.relatedTarget && event.relatedTarget === actionButtonRef.current) { isKeyboardVisibleWhenShowingModalRef.current = true; } }, []); @@ -444,11 +441,11 @@ function ReportActionCompose({ onValueChange={validateCommentMaxLength} /> { + onDrop={(event: DragEvent) => { if (isAttachmentPreviewActive) { return; } - const data = e.dataTransfer?.items[0]; + const data = event.dataTransfer?.items[0]; displayFileInModal(data); }} /> @@ -459,7 +456,7 @@ function ReportActionCompose({ composerRef.current?.replaceSelectionWithText(...args)} emojiPickerID={report?.reportID} /> From 2a0e3e21c0b61a5f9409137e29986e4d14c93bac Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 19 Jan 2024 15:14:18 +0100 Subject: [PATCH 04/29] ref: move SuggestionMention, Suggestions to TS, fixes to MentionSuggestions and ReportActionCompose --- src/components/MentionSuggestions.tsx | 6 +- .../ReportActionCompose.tsx | 9 +- ...SuggestionEmoji.js => SuggestionEmoji.tsx} | 84 ++++------ ...estionMention.js => SuggestionMention.tsx} | 145 ++++++++---------- .../{Suggestions.js => Suggestions.tsx} | 119 +++++++------- src/types/onyx/OnyxCommon.ts | 2 +- 6 files changed, 159 insertions(+), 206 deletions(-) rename src/pages/home/report/ReportActionCompose/{SuggestionEmoji.js => SuggestionEmoji.tsx} (86%) rename src/pages/home/report/ReportActionCompose/{SuggestionMention.js => SuggestionMention.tsx} (73%) rename src/pages/home/report/ReportActionCompose/{Suggestions.js => Suggestions.tsx} (54%) diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 459131ecc434..99930f995a3a 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -18,7 +18,7 @@ type Mention = { alternateText: string; /** Email/phone number of the user */ - login: string; + login?: string; /** Array of icons of the user. We use the first element of this array */ icons: Icon[]; @@ -32,7 +32,7 @@ type MentionSuggestionsProps = { mentions: Mention[]; /** Fired when the user selects a mention */ - onSelect: () => void; + onSelect: (highlightedMentionIndex: number) => void; /** Mention prefix that follows the @ sign */ prefix: string; @@ -142,3 +142,5 @@ function MentionSuggestions({prefix, mentions, highlightedMentionIndex = 0, onSe MentionSuggestions.displayName = 'MentionSuggestions'; export default MentionSuggestions; + +export type {Mention}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 73b025c50df2..025b7142df0d 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -9,6 +9,7 @@ import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; +import type {Mention} from '@components/MentionSuggestions'; import OfflineIndicator from '@components/OfflineIndicator'; import OfflineWithFeedback from '@components/OfflineWithFeedback'; import {usePersonalDetails} from '@components/OnyxProvider'; @@ -51,11 +52,11 @@ type ComposerRef = { type SuggestionsRef = { resetSuggestions: () => void; - onSelectionChange: (event: NativeSyntheticEvent) => void; + onSelectionChange?: (event: NativeSyntheticEvent) => void; triggerHotkeyActions: (event: KeyboardEvent) => void; - updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu: boolean) => void; + updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu?: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; - getSuggestions: () => string[]; + getSuggestions: () => Mention[]; }; type ReportActionComposeOnyxProps = { @@ -496,3 +497,5 @@ export default withCurrentUserPersonalDetails( }, })(ReportActionCompose), ); + +export type {SuggestionsRef}; diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx similarity index 86% rename from src/pages/home/report/ReportActionCompose/SuggestionEmoji.js rename to src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index e35d1e90bd5a..06089b748554 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -1,5 +1,6 @@ import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import React, {ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import EmojiSuggestions from '@components/EmojiSuggestions'; @@ -9,17 +10,15 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import * as SuggestionProps from './suggestionProps'; +// eslint-disable-next-line import/no-cycle +import {SuggestionProps} from './Suggestions'; /** * Check if this piece of string looks like an emoji - * @param {String} str - * @param {Number} pos - * @returns {Boolean} */ -const isEmojiCode = (str, pos) => { +const isEmojiCode = (str: string, pos: number): boolean => { const leftWords = str.slice(0, pos).split(CONST.REGEX.SPECIAL_CHAR_OR_EMOJI); - const leftWord = _.last(leftWords); + const leftWord = leftWords.at(-1) ?? ''; return CONST.REGEX.HAS_COLON_ONLY_AT_THE_BEGINNING.test(leftWord) && leftWord.length > 2; }; @@ -29,38 +28,33 @@ const defaultSuggestionsValues = { shouldShowSuggestionMenu: false, }; -const propTypes = { +type SuggestionEmojiOnyxProps = { /** Preferred skin tone */ - preferredSkinTone: PropTypes.number, - - /** A ref to this component */ - forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), - - /** Function to clear the input */ - resetKeyboardInput: PropTypes.func.isRequired, - - ...SuggestionProps.baseProps, + preferredSkinTone: OnyxEntry; }; -const defaultProps = { - preferredSkinTone: CONST.EMOJI_DEFAULT_SKIN_TONE, - forwardedRef: null, -}; - -function SuggestionEmoji({ - preferredSkinTone, - value, - setValue, - selection, - setSelection, - updateComment, - isComposerFullSize, - isAutoSuggestionPickerLarge, - forwardedRef, - resetKeyboardInput, - measureParentContainer, - isComposerFocused, -}) { +type SuggestionEmojiProps = { + /** Function to clear the input */ + resetKeyboardInput: () => void; +} & SuggestionEmojiOnyxProps & + SuggestionProps; + +function SuggestionEmoji( + { + preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, + value, + setValue, + selection, + setSelection, + updateComment, + isComposerFullSize, + isAutoSuggestionPickerLarge, + resetKeyboardInput, + measureParentContainer, + isComposerFocused, + }: SuggestionEmojiProps, + ref: ForwardedRef, +) { const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); const isEmojiSuggestionsMenuVisible = !_.isEmpty(suggestionValues.suggestedEmojis) && suggestionValues.shouldShowSuggestionMenu; @@ -214,7 +208,7 @@ function SuggestionEmoji({ }, []); useImperativeHandle( - forwardedRef, + ref, () => ({ resetSuggestions, onSelectionChange, @@ -248,23 +242,11 @@ function SuggestionEmoji({ ); } -SuggestionEmoji.propTypes = propTypes; -SuggestionEmoji.defaultProps = defaultProps; SuggestionEmoji.displayName = 'SuggestionEmoji'; -const SuggestionEmojiWithRef = React.forwardRef((props, ref) => ( - -)); - -SuggestionEmojiWithRef.displayName = 'SuggestionEmojiWithRef'; - -export default withOnyx({ +export default withOnyx({ preferredSkinTone: { key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, selector: EmojiUtils.getPreferredSkinToneIndex, }, -})(SuggestionEmojiWithRef); +})(forwardRef(SuggestionEmoji)); diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.js b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx similarity index 73% rename from src/pages/home/report/ReportActionCompose/SuggestionMention.js rename to src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index af3074eec06d..5ce33d916a08 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.js +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -1,7 +1,7 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import _ from 'underscore'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; +import type {Mention} from '@components/MentionSuggestions'; import MentionSuggestions from '@components/MentionSuggestions'; import {usePersonalDetails} from '@components/OnyxProvider'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; @@ -11,52 +11,41 @@ import * as PersonalDetailsUtils from '@libs/PersonalDetailsUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import * as UserUtils from '@libs/UserUtils'; import CONST from '@src/CONST'; -import * as SuggestionProps from './suggestionProps'; +import type {PersonalDetailsList} from '@src/types/onyx'; +import type {SuggestionsRef} from './ReportActionCompose'; +import type {SuggestionProps} from './Suggestions'; + +type SuggestionMentionProps = {isAutoSuggestionPickerLarge: boolean} & SuggestionProps; + +type SuggestionValues = { + suggestedMentions: Mention[]; + atSignIndex: number; + shouldShowSuggestionMenu: boolean; + mentionPrefix: string; +}; /** * Check if this piece of string looks like a mention - * @param {String} str - * @returns {Boolean} */ -const isMentionCode = (str) => CONST.REGEX.HAS_AT_MOST_TWO_AT_SIGNS.test(str); +const isMentionCode = (str: string): boolean => CONST.REGEX.HAS_AT_MOST_TWO_AT_SIGNS.test(str); -const defaultSuggestionsValues = { +const defaultSuggestionsValues: SuggestionValues = { suggestedMentions: [], atSignIndex: -1, shouldShowSuggestionMenu: false, mentionPrefix: '', }; -const propTypes = { - /** A ref to this component */ - forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), - - ...SuggestionProps.implementationBaseProps, -}; - -const defaultProps = { - forwardedRef: null, -}; - -function SuggestionMention({ - value, - setValue, - selection, - setSelection, - isComposerFullSize, - updateComment, - composerHeight, - forwardedRef, - isAutoSuggestionPickerLarge, - measureParentContainer, - isComposerFocused, -}) { - const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; +function SuggestionMention( + {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused}: SuggestionMentionProps, + ref: ForwardedRef, +) { + const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; const {translate, formatPhoneNumber} = useLocalize(); const previousValue = usePrevious(value); const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); - const isMentionSuggestionsMenuVisible = !_.isEmpty(suggestionValues.suggestedMentions) && suggestionValues.shouldShowSuggestionMenu; + const isMentionSuggestionsMenuVisible = !!suggestionValues.suggestedMentions.length && suggestionValues.shouldShowSuggestionMenu; const [highlightedMentionIndex, setHighlightedMentionIndex] = useArrowKeyFocusManager({ isActive: isMentionSuggestionsMenuVisible, @@ -69,10 +58,9 @@ function SuggestionMention({ /** * Replace the code of mention and update selection - * @param {Number} highlightedMentionIndex */ const insertSelectedMention = useCallback( - (highlightedMentionIndexInner) => { + (highlightedMentionIndexInner: number) => { const commentBeforeAtSign = value.slice(0, suggestionValues.atSignIndex); const mentionObject = suggestionValues.suggestedMentions[highlightedMentionIndexInner]; const mentionCode = mentionObject.text === CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT ? CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT : `@${mentionObject.login}`; @@ -100,23 +88,21 @@ function SuggestionMention({ /** * Listens for keyboard shortcuts and applies the action - * - * @param {Object} e */ const triggerHotkeyActions = useCallback( - (e) => { + (event: KeyboardEvent) => { const suggestionsExist = suggestionValues.suggestedMentions.length > 0; - if (((!e.shiftKey && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) || e.key === CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) && suggestionsExist) { - e.preventDefault(); + if (((!event.shiftKey && event.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) || event.key === CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) && suggestionsExist) { + event.preventDefault(); if (suggestionValues.suggestedMentions.length > 0) { insertSelectedMention(highlightedMentionIndex); return true; } } - if (e.key === CONST.KEYBOARD_SHORTCUTS.ESCAPE.shortcutKey) { - e.preventDefault(); + if (event.key === CONST.KEYBOARD_SHORTCUTS.ESCAPE.shortcutKey) { + event.preventDefault(); if (suggestionsExist) { resetSuggestions(); @@ -129,7 +115,7 @@ function SuggestionMention({ ); const getMentionOptions = useCallback( - (personalDetailsParam, searchValue = '') => { + (personalDetailsParam: PersonalDetailsList, searchValue = ''): Mention[] => { const suggestions = []; if (CONST.AUTO_COMPLETE_SUGGESTER.HERE_TEXT.includes(searchValue.toLowerCase())) { @@ -139,15 +125,15 @@ function SuggestionMention({ icons: [ { source: Expensicons.Megaphone, - type: 'avatar', + type: CONST.ICON_TYPE_AVATAR, }, ], }); } - const filteredPersonalDetails = _.filter(_.values(personalDetailsParam), (detail) => { + const filteredPersonalDetails = Object.values(personalDetailsParam ?? {}).filter((detail) => { // If we don't have user's primary login, that member is not known to the current user and hence we do not allow them to be mentioned - if (!detail.login || detail.isOptimisticPersonalDetail) { + if (!detail?.login || detail.isOptimisticPersonalDetail) { return false; } const displayName = PersonalDetailsUtils.getDisplayNameOrDefault(detail); @@ -158,18 +144,29 @@ function SuggestionMention({ return true; }); - const sortedPersonalDetails = _.sortBy(filteredPersonalDetails, (detail) => detail.displayName || detail.login); - _.each(_.first(sortedPersonalDetails, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length), (detail) => { + const sortedPersonalDetails = filteredPersonalDetails.sort((a, b) => { + const nameA = a?.displayName ?? a?.login ?? ''; + const nameB = b?.displayName ?? b?.login ?? ''; + + if (nameA < nameB) { + return -1; + } + if (nameA > nameB) { + return 1; + } + return 0; + }); + sortedPersonalDetails.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length).forEach((detail) => { suggestions.push({ text: PersonalDetailsUtils.getDisplayNameOrDefault(detail), - alternateText: formatPhoneNumber(detail.login), - login: detail.login, + alternateText: formatPhoneNumber(detail?.login ?? ''), + login: detail?.login, icons: [ { - name: detail.login, - source: UserUtils.getAvatar(detail.avatar, detail.accountID), - type: 'avatar', - fallbackIcon: detail.fallbackIcon, + name: detail?.login, + source: UserUtils.getAvatar(detail?.avatar, detail?.accountID), + type: CONST.ICON_TYPE_AVATAR, + fallbackIcon: detail?.fallbackIcon, }, ], }); @@ -181,7 +178,7 @@ function SuggestionMention({ ); const calculateMentionSuggestion = useCallback( - (selectionEnd) => { + (selectionEnd: number) => { if (shouldBlockCalc.current || selectionEnd < 1 || !isComposerFocused) { shouldBlockCalc.current = false; resetSuggestions(); @@ -201,11 +198,11 @@ function SuggestionMention({ const leftString = value.substring(0, suggestionEndIndex); const words = leftString.split(CONST.REGEX.SPACE_OR_EMOJI); - const lastWord = _.last(words); + const lastWord = words.at(-1) ?? ''; const secondToLastWord = words[words.length - 3]; let atSignIndex; - let suggestionWord; + let suggestionWord = ''; let prefix; // Detect if the last two words contain a mention (two words are needed to detect a mention with a space in it) @@ -223,7 +220,7 @@ function SuggestionMention({ prefix = lastWord.substring(1); } - const nextState = { + const nextState: Partial = { suggestedMentions: [], atSignIndex, mentionPrefix: prefix, @@ -235,7 +232,7 @@ function SuggestionMention({ const suggestions = getMentionOptions(personalDetails, prefix); nextState.suggestedMentions = suggestions; - nextState.shouldShowSuggestionMenu = !_.isEmpty(suggestions); + nextState.shouldShowSuggestionMenu = !!suggestions.length; } setSuggestionValues((prevState) => ({ @@ -268,20 +265,16 @@ function SuggestionMention({ }, []); const setShouldBlockSuggestionCalc = useCallback( - (shouldBlockSuggestionCalc) => { + (shouldBlockSuggestionCalc: boolean) => { shouldBlockCalc.current = shouldBlockSuggestionCalc; }, [shouldBlockCalc], ); - const onClose = useCallback(() => { - setSuggestionValues((prevState) => ({...prevState, suggestedMentions: []})); - }, []); - const getSuggestions = useCallback(() => suggestionValues.suggestedMentions, [suggestionValues]); useImperativeHandle( - forwardedRef, + ref, () => ({ resetSuggestions, triggerHotkeyActions, @@ -298,34 +291,16 @@ function SuggestionMention({ return ( ); } -SuggestionMention.propTypes = propTypes; -SuggestionMention.defaultProps = defaultProps; SuggestionMention.displayName = 'SuggestionMention'; -const SuggestionMentionWithRef = React.forwardRef((props, ref) => ( - -)); - -SuggestionMentionWithRef.displayName = 'SuggestionMentionWithRef'; - -export default SuggestionMentionWithRef; +export default forwardRef(SuggestionMention); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.js b/src/pages/home/report/ReportActionCompose/Suggestions.tsx similarity index 54% rename from src/pages/home/report/ReportActionCompose/Suggestions.js rename to src/pages/home/report/ReportActionCompose/Suggestions.tsx index 5dc71fec6419..f997637a8c4c 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.js +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -1,64 +1,67 @@ -import PropTypes from 'prop-types'; -import React, {useCallback, useContext, useEffect, useImperativeHandle, useRef} from 'react'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef} from 'react'; +import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; import {DragAndDropContext} from '@components/DragAndDrop/Provider'; import usePrevious from '@hooks/usePrevious'; +import type {SuggestionsRef} from './ReportActionCompose'; import SuggestionEmoji from './SuggestionEmoji'; import SuggestionMention from './SuggestionMention'; -import * as SuggestionProps from './suggestionProps'; -const propTypes = { - /** A ref to this component */ - forwardedRef: PropTypes.shape({current: PropTypes.shape({})}), - - /** Function to clear the input */ - resetKeyboardInput: PropTypes.func.isRequired, - - /** Is auto suggestion picker large */ - isAutoSuggestionPickerLarge: PropTypes.bool, - - ...SuggestionProps.baseProps, +type Selection = { + start: number; + end: number; }; -const defaultProps = { - forwardedRef: null, - isAutoSuggestionPickerLarge: true, +type SuggestionProps = { + value: string; + setValue: (newValue: string) => void; + selection: Selection; + setSelection: (newSelection: Selection) => void; + updateComment: (newComment: string, shouldDebounceSaveComment?: boolean) => void; + measureParentContainer: () => void; + isComposerFullSize: boolean; + isComposerFocused?: boolean; + resetKeyboardInput: () => void; + isAutoSuggestionPickerLarge?: boolean; + composerHeight?: number; }; /** * This component contains the individual suggestion components. * If you want to add a new suggestion type, add it here. * - * @returns {React.Component} */ -function Suggestions({ - isComposerFullSize, - value, - setValue, - selection, - setSelection, - updateComment, - composerHeight, - forwardedRef, - resetKeyboardInput, - measureParentContainer, - isAutoSuggestionPickerLarge, - isComposerFocused, -}) { - const suggestionEmojiRef = useRef(null); - const suggestionMentionRef = useRef(null); +function Suggestions( + { + isComposerFullSize, + value, + setValue, + selection, + setSelection, + updateComment, + composerHeight, + resetKeyboardInput, + measureParentContainer, + isAutoSuggestionPickerLarge = true, + isComposerFocused, + }: SuggestionProps, + ref: ForwardedRef, +) { + const suggestionEmojiRef = useRef(null); + const suggestionMentionRef = useRef(null); const {isDraggingOver} = useContext(DragAndDropContext); const prevIsDraggingOver = usePrevious(isDraggingOver); const getSuggestions = useCallback(() => { - if (suggestionEmojiRef.current && suggestionEmojiRef.current.getSuggestions) { + if (suggestionEmojiRef.current?.getSuggestions) { const emojiSuggestions = suggestionEmojiRef.current.getSuggestions(); if (emojiSuggestions.length > 0) { return emojiSuggestions; } } - if (suggestionMentionRef.current && suggestionMentionRef.current.getSuggestions) { + if (suggestionMentionRef.current?.getSuggestions) { const mentionSuggestions = suggestionMentionRef.current.getSuggestions(); if (mentionSuggestions.length > 0) { return mentionSuggestions; @@ -72,38 +75,36 @@ function Suggestions({ * Clean data related to EmojiSuggestions */ const resetSuggestions = useCallback(() => { - suggestionEmojiRef.current.resetSuggestions(); - suggestionMentionRef.current.resetSuggestions(); + suggestionEmojiRef.current?.resetSuggestions(); + suggestionMentionRef.current?.resetSuggestions(); }, []); /** * Listens for keyboard shortcuts and applies the action - * - * @param {Object} e */ - const triggerHotkeyActions = useCallback((e) => { - const emojiHandler = suggestionEmojiRef.current.triggerHotkeyActions(e); - const mentionHandler = suggestionMentionRef.current.triggerHotkeyActions(e); - return emojiHandler || mentionHandler; + const triggerHotkeyActions = useCallback((e: KeyboardEvent) => { + const emojiHandler = suggestionEmojiRef.current?.triggerHotkeyActions(e); + const mentionHandler = suggestionMentionRef.current?.triggerHotkeyActions(e); + return emojiHandler ?? mentionHandler; }, []); - const onSelectionChange = useCallback((e) => { - const emojiHandler = suggestionEmojiRef.current.onSelectionChange(e); + const onSelectionChange = useCallback((e: NativeSyntheticEvent) => { + const emojiHandler = suggestionEmojiRef.current?.onSelectionChange(e); return emojiHandler; }, []); const updateShouldShowSuggestionMenuToFalse = useCallback(() => { - suggestionEmojiRef.current.updateShouldShowSuggestionMenuToFalse(); - suggestionMentionRef.current.updateShouldShowSuggestionMenuToFalse(); + suggestionEmojiRef.current?.updateShouldShowSuggestionMenuToFalse(); + suggestionMentionRef.current?.updateShouldShowSuggestionMenuToFalse(); }, []); - const setShouldBlockSuggestionCalc = useCallback((shouldBlock) => { - suggestionEmojiRef.current.setShouldBlockSuggestionCalc(shouldBlock); - suggestionMentionRef.current.setShouldBlockSuggestionCalc(shouldBlock); + const setShouldBlockSuggestionCalc = useCallback((shouldBlock: boolean) => { + suggestionEmojiRef.current?.setShouldBlockSuggestionCalc(shouldBlock); + suggestionMentionRef.current?.setShouldBlockSuggestionCalc(shouldBlock); }, []); useImperativeHandle( - forwardedRef, + ref, () => ({ resetSuggestions, onSelectionChange, @@ -152,18 +153,8 @@ function Suggestions({ ); } -Suggestions.propTypes = propTypes; -Suggestions.defaultProps = defaultProps; Suggestions.displayName = 'Suggestions'; -const SuggestionsWithRef = React.forwardRef((props, ref) => ( - -)); - -SuggestionsWithRef.displayName = 'SuggestionsWithRef'; +export default forwardRef(Suggestions); -export default SuggestionsWithRef; +export type {SuggestionProps}; diff --git a/src/types/onyx/OnyxCommon.ts b/src/types/onyx/OnyxCommon.ts index b26dc167ed44..a4d29967be1e 100644 --- a/src/types/onyx/OnyxCommon.ts +++ b/src/types/onyx/OnyxCommon.ts @@ -20,7 +20,7 @@ type Icon = { type: AvatarType; /** Owner of the avatar. If user, displayName. If workspace, policy name */ - name: string; + name?: string; /** Avatar id */ id?: number | string; From 717a5e8d5f3229cf1611c14382adfb0ae5cd1268 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Sun, 21 Jan 2024 14:38:40 +0100 Subject: [PATCH 05/29] fix: started migrating SuggestionEmoji component --- .../ReportActionCompose/SuggestionEmoji.tsx | 44 ++++++++++--------- 1 file changed, 24 insertions(+), 20 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index 06089b748554..c5eb13176d44 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -1,8 +1,7 @@ -import PropTypes from 'prop-types'; -import React, {ForwardedRef, forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; +import type {ForwardedRef} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import EmojiSuggestions from '@components/EmojiSuggestions'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; @@ -10,22 +9,12 @@ import * as EmojiUtils from '@libs/EmojiUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -// eslint-disable-next-line import/no-cycle -import {SuggestionProps} from './Suggestions'; +import type {SuggestionProps} from './Suggestions'; -/** - * Check if this piece of string looks like an emoji - */ -const isEmojiCode = (str: string, pos: number): boolean => { - const leftWords = str.slice(0, pos).split(CONST.REGEX.SPECIAL_CHAR_OR_EMOJI); - const leftWord = leftWords.at(-1) ?? ''; - return CONST.REGEX.HAS_COLON_ONLY_AT_THE_BEGINNING.test(leftWord) && leftWord.length > 2; -}; - -const defaultSuggestionsValues = { - suggestedEmojis: [], - colonSignIndex: -1, - shouldShowSuggestionMenu: false, +type SuggestionsValue = { + suggestedEmojis: any[]; + colonIndex: number; + shouldShowSuggestionMenu: boolean; }; type SuggestionEmojiOnyxProps = { @@ -39,6 +28,21 @@ type SuggestionEmojiProps = { } & SuggestionEmojiOnyxProps & SuggestionProps; +/** + * Check if this piece of string looks like an emoji + */ +const isEmojiCode = (str: string, pos: number): boolean => { + const leftWords = str.slice(0, pos).split(CONST.REGEX.SPECIAL_CHAR_OR_EMOJI); + const leftWord = leftWords.at(-1) ?? ''; + return CONST.REGEX.HAS_COLON_ONLY_AT_THE_BEGINNING.test(leftWord) && leftWord.length > 2; +}; + +const defaultSuggestionsValues: SuggestionsValue = { + suggestedEmojis: [], + colonIndex: -1, + shouldShowSuggestionMenu: false, +}; + function SuggestionEmoji( { preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, @@ -61,7 +65,7 @@ function SuggestionEmoji( const [highlightedEmojiIndex, setHighlightedEmojiIndex] = useArrowKeyFocusManager({ isActive: isEmojiSuggestionsMenuVisible, - maxIndex: SuggestionsUtils.getMaxArrowIndex(suggestionValues.suggestedEmojis.length, isAutoSuggestionPickerLarge), + maxIndex: SuggestionsUtils.getMaxArrowIndex(suggestionValues.suggestedEmojis.length, !!isAutoSuggestionPickerLarge), shouldExcludeTextAreaNodes: false, }); @@ -75,7 +79,7 @@ function SuggestionEmoji( * @param {Number} selectedEmoji */ const insertSelectedEmoji = useCallback( - (highlightedEmojiIndexInner) => { + (highlightedEmojiIndexInner: number) => { const commentBeforeColon = value.slice(0, suggestionValues.colonIndex); const emojiObject = suggestionValues.suggestedEmojis[highlightedEmojiIndexInner]; const emojiCode = emojiObject.types && emojiObject.types[preferredSkinTone] ? emojiObject.types[preferredSkinTone] : emojiObject.code; From 7c7132531962d803f64392171d7c3c9d95fc9b6c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Jan 2024 13:55:47 +0100 Subject: [PATCH 06/29] fix: migrate SuggestionEmoji --- src/libs/EmojiUtils.ts | 2 +- .../ReportActionCompose.tsx | 2 +- .../ReportActionCompose/SuggestionEmoji.tsx | 51 ++++++++----------- .../ReportActionCompose/Suggestions.tsx | 2 +- 4 files changed, 25 insertions(+), 32 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index e34fa0b90fc6..7971e6147c19 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -384,7 +384,7 @@ function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST * Suggest emojis when typing emojis prefix after colon * @param [limit] - matching emojis limit */ -function suggestEmojis(text: string, lang: keyof SupportedLanguage, limit = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { +function suggestEmojis(text: string, lang: SupportedLanguage, limit: number = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 025b7142df0d..9754d45be834 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -56,7 +56,7 @@ type SuggestionsRef = { triggerHotkeyActions: (event: KeyboardEvent) => void; updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu?: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; - getSuggestions: () => Mention[]; + getSuggestions: () => Mention[] | Emoji[]; }; type ReportActionComposeOnyxProps = { diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index c5eb13176d44..dac59065371d 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -1,25 +1,29 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import {withOnyx} from 'react-native-onyx'; +import type {Emoji} from '@assets/emojis/types'; import EmojiSuggestions from '@components/EmojiSuggestions'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; +import type {SupportedLanguage} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import type {SuggestionsRef} from './ReportActionCompose'; import type {SuggestionProps} from './Suggestions'; type SuggestionsValue = { - suggestedEmojis: any[]; + suggestedEmojis: Emoji[]; colonIndex: number; shouldShowSuggestionMenu: boolean; }; type SuggestionEmojiOnyxProps = { /** Preferred skin tone */ - preferredSkinTone: OnyxEntry; + preferredSkinTone: number; }; type SuggestionEmojiProps = { @@ -47,21 +51,19 @@ function SuggestionEmoji( { preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, value, - setValue, selection, setSelection, updateComment, - isComposerFullSize, isAutoSuggestionPickerLarge, resetKeyboardInput, measureParentContainer, isComposerFocused, }: SuggestionEmojiProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const [suggestionValues, setSuggestionValues] = useState(defaultSuggestionsValues); - const isEmojiSuggestionsMenuVisible = !_.isEmpty(suggestionValues.suggestedEmojis) && suggestionValues.shouldShowSuggestionMenu; + const isEmojiSuggestionsMenuVisible = suggestionValues.suggestedEmojis.length > 0 && suggestionValues.shouldShowSuggestionMenu; const [highlightedEmojiIndex, setHighlightedEmojiIndex] = useArrowKeyFocusManager({ isActive: isEmojiSuggestionsMenuVisible, @@ -82,7 +84,7 @@ function SuggestionEmoji( (highlightedEmojiIndexInner: number) => { const commentBeforeColon = value.slice(0, suggestionValues.colonIndex); const emojiObject = suggestionValues.suggestedEmojis[highlightedEmojiIndexInner]; - const emojiCode = emojiObject.types && emojiObject.types[preferredSkinTone] ? emojiObject.types[preferredSkinTone] : emojiObject.code; + const emojiCode = emojiObject.types?.[preferredSkinTone] ? emojiObject.types[preferredSkinTone] : emojiObject.code; const commentAfterColonWithEmojiNameRemoved = value.slice(selection.end); updateComment(`${commentBeforeColon}${emojiCode} ${SuggestionsUtils.trimLeadingSpace(commentAfterColonWithEmojiNameRemoved)}`, true); @@ -119,11 +121,9 @@ function SuggestionEmoji( /** * Listens for keyboard shortcuts and applies the action - * - * @param {Object} e */ const triggerHotkeyActions = useCallback( - (e) => { + (e: KeyboardEvent) => { const suggestionsExist = suggestionValues.suggestedEmojis.length > 0; if (((!e.shiftKey && e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey) || e.key === CONST.KEYBOARD_SHORTCUTS.TAB.shortcutKey) && suggestionsExist) { @@ -151,7 +151,7 @@ function SuggestionEmoji( * Calculates and cares about the content of an Emoji Suggester */ const calculateEmojiSuggestion = useCallback( - (selectionEnd) => { + (selectionEnd: number) => { if (shouldBlockCalc.current || !value) { shouldBlockCalc.current = false; resetSuggestions(); @@ -161,16 +161,16 @@ function SuggestionEmoji( const colonIndex = leftString.lastIndexOf(':'); const isCurrentlyShowingEmojiSuggestion = isEmojiCode(value, selectionEnd); - const nextState = { + const nextState: SuggestionsValue = { suggestedEmojis: [], colonIndex, shouldShowSuggestionMenu: false, }; - const newSuggestedEmojis = EmojiUtils.suggestEmojis(leftString, preferredLocale); + const newSuggestedEmojis = EmojiUtils.suggestEmojis(leftString, preferredLocale as SupportedLanguage); - if (newSuggestedEmojis.length && isCurrentlyShowingEmojiSuggestion) { + if (newSuggestedEmojis?.length && isCurrentlyShowingEmojiSuggestion) { nextState.suggestedEmojis = newSuggestedEmojis; - nextState.shouldShowSuggestionMenu = !_.isEmpty(newSuggestedEmojis); + nextState.shouldShowSuggestionMenu = !isEmptyObject(newSuggestedEmojis); } setSuggestionValues((prevState) => ({...prevState, ...nextState})); @@ -187,7 +187,7 @@ function SuggestionEmoji( }, [selection, calculateEmojiSuggestion, isComposerFocused]); const onSelectionChange = useCallback( - (e) => { + (e: NativeSyntheticEvent) => { /** * we pass here e.nativeEvent.selection.end directly to calculateEmojiSuggestion * because in other case calculateEmojiSuggestion will have an old calculation value @@ -199,7 +199,7 @@ function SuggestionEmoji( ); const setShouldBlockSuggestionCalc = useCallback( - (shouldBlockSuggestionCalc) => { + (shouldBlockSuggestionCalc: boolean) => { shouldBlockCalc.current = shouldBlockSuggestionCalc; }, [shouldBlockCalc], @@ -207,10 +207,6 @@ function SuggestionEmoji( const getSuggestions = useCallback(() => suggestionValues.suggestedEmojis, [suggestionValues]); - const resetEmojiSuggestions = useCallback(() => { - setSuggestionValues((prevState) => ({...prevState, suggestedEmojis: []})); - }, []); - useImperativeHandle( ref, () => ({ @@ -230,17 +226,12 @@ function SuggestionEmoji( return ( ); @@ -248,9 +239,11 @@ function SuggestionEmoji( SuggestionEmoji.displayName = 'SuggestionEmoji'; +const SuggestionEmojiForwardedRef = forwardRef(SuggestionEmoji); + export default withOnyx({ preferredSkinTone: { key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, selector: EmojiUtils.getPreferredSkinToneIndex, }, -})(forwardRef(SuggestionEmoji)); +})(SuggestionEmojiForwardedRef); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index f997637a8c4c..3a009728a915 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -89,7 +89,7 @@ function Suggestions( }, []); const onSelectionChange = useCallback((e: NativeSyntheticEvent) => { - const emojiHandler = suggestionEmojiRef.current?.onSelectionChange(e); + const emojiHandler = suggestionEmojiRef.current?.onSelectionChange?.(e); return emojiHandler; }, []); From be57be30f750da1ca8a1374213907c3c8e8b8eb1 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Jan 2024 14:11:08 +0100 Subject: [PATCH 07/29] fix: SuggestionEmoji and Suggestions types, migrate SendButton --- .../{SendButton.js => SendButton.tsx} | 14 +++++++------- .../report/ReportActionCompose/SuggestionEmoji.tsx | 12 +++++------- .../report/ReportActionCompose/Suggestions.tsx | 2 +- 3 files changed, 13 insertions(+), 15 deletions(-) rename src/pages/home/report/ReportActionCompose/{SendButton.js => SendButton.tsx} (90%) diff --git a/src/pages/home/report/ReportActionCompose/SendButton.js b/src/pages/home/report/ReportActionCompose/SendButton.tsx similarity index 90% rename from src/pages/home/report/ReportActionCompose/SendButton.js rename to src/pages/home/report/ReportActionCompose/SendButton.tsx index d0b0453ace2f..b6c6200fc7c0 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.js +++ b/src/pages/home/report/ReportActionCompose/SendButton.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import {Gesture, GestureDetector} from 'react-native-gesture-handler'; @@ -11,21 +10,21 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; -const propTypes = { +type SendButtonProps = { /** Whether the button is disabled */ - isDisabled: PropTypes.bool.isRequired, + isDisabled: boolean; /** Handle clicking on send button */ - handleSendMessage: PropTypes.func.isRequired, + handleSendMessage: () => void; }; -function SendButton({isDisabled: isDisabledProp, handleSendMessage}) { +function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonProps) { const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); const Tap = Gesture.Tap() - .enabled() + .enabled(!isDisabledProp) .onEnd(() => { handleSendMessage(); }); @@ -46,6 +45,8 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}) { ]} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.send')} + accessible + onPress={() => {}} > {({pressed}) => ( void; + resetKeyboardInput: (() => void) | undefined; } & SuggestionEmojiOnyxProps & SuggestionProps; @@ -92,7 +92,7 @@ function SuggestionEmoji( // In some Android phones keyboard, the text to search for the emoji is not cleared // will be added after the user starts typing again on the keyboard. This package is // a workaround to reset the keyboard natively. - resetKeyboardInput(); + resetKeyboardInput?.(); setSelection({ start: suggestionValues.colonIndex + emojiCode.length + CONST.SPACE_LENGTH, @@ -239,11 +239,9 @@ function SuggestionEmoji( SuggestionEmoji.displayName = 'SuggestionEmoji'; -const SuggestionEmojiForwardedRef = forwardRef(SuggestionEmoji); - -export default withOnyx({ +export default withOnyx, SuggestionEmojiOnyxProps>({ preferredSkinTone: { key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, selector: EmojiUtils.getPreferredSkinToneIndex, }, -})(SuggestionEmojiForwardedRef); +})(forwardRef(SuggestionEmoji)); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index 3a009728a915..2500af40c7c6 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -22,7 +22,7 @@ type SuggestionProps = { measureParentContainer: () => void; isComposerFullSize: boolean; isComposerFocused?: boolean; - resetKeyboardInput: () => void; + resetKeyboardInput?: () => void; isAutoSuggestionPickerLarge?: boolean; composerHeight?: number; }; From 929a51a9a8be77696c674b881b7835d85147e481 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Jan 2024 14:38:55 +0100 Subject: [PATCH 08/29] fix: migrate AttachmentPickerWithMenuItems --- src/components/PopoverMenu.tsx | 2 +- ...s.js => AttachmentPickerWithMenuItems.tsx} | 119 ++++++++---------- 2 files changed, 52 insertions(+), 69 deletions(-) rename src/pages/home/report/ReportActionCompose/{AttachmentPickerWithMenuItems.js => AttachmentPickerWithMenuItems.tsx} (82%) diff --git a/src/components/PopoverMenu.tsx b/src/components/PopoverMenu.tsx index 17b1a119671a..b411de1103f1 100644 --- a/src/components/PopoverMenu.tsx +++ b/src/components/PopoverMenu.tsx @@ -69,7 +69,7 @@ type PopoverMenuProps = Partial & { anchorPosition: AnchorPosition; /** Ref of the anchor */ - anchorRef: RefObject; + anchorRef: RefObject; /** Where the popover should be positioned relative to the anchor points. */ anchorAlignment?: AnchorAlignment; diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx similarity index 82% rename from src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js rename to src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 444dd939142b..547283cc4eb0 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.js +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -1,23 +1,23 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; +import {useIsFocused} from '@react-navigation/native'; +import type {FC} from 'react'; import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {SvgProps} from 'react-native-svg'; +import type {ValueOf} from 'type-fest'; import AttachmentPicker from '@components/AttachmentPicker'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; -import withNavigationFocus from '@components/withNavigationFocus'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; -import compose from '@libs/compose'; import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; @@ -26,88 +26,77 @@ import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; +import type * as OnyxTypes from '@src/types/onyx'; -const propTypes = { - /** The report currently being looked at */ - report: PropTypes.shape({ - /** ID of the report */ - reportID: PropTypes.string, +type MoneyRequestOption = { + icon: FC; + text: string; + onSelected: () => void; +}; - /** Whether or not the report is in the process of being created */ - loading: PropTypes.bool, - }).isRequired, +type MoneyRequestOptions = Record, MoneyRequestOption>; +type AttachmentPickerWithMenuItemsOnyxProps = { /** The policy tied to the report */ - policy: PropTypes.shape({ - /** Type of the policy */ - type: PropTypes.string, - }), + policy: OnyxEntry; +}; - /** The personal details of everyone in the report */ - reportParticipantIDs: PropTypes.arrayOf(PropTypes.number), +type AttachmentPickerWithMenuItemsProps = { + /** The report currently being looked at */ + report: OnyxTypes.Report; /** Callback to open the file in the modal */ - displayFileInModal: PropTypes.func.isRequired, + displayFileInModal: (url: string) => void; /** Whether or not the full size composer is available */ - isFullComposerAvailable: PropTypes.bool.isRequired, + isFullComposerAvailable: boolean; /** Whether or not the composer is full size */ - isComposerFullSize: PropTypes.bool.isRequired, + isComposerFullSize: boolean; /** Whether or not the user is blocked from concierge */ - isBlockedFromConcierge: PropTypes.bool.isRequired, + isBlockedFromConcierge: boolean; /** Whether or not the attachment picker is disabled */ - disabled: PropTypes.bool.isRequired, + disabled: boolean; /** Sets the menu visibility */ - setMenuVisibility: PropTypes.func.isRequired, + setMenuVisibility: (isVisible: boolean) => void; /** Whether or not the menu is visible */ - isMenuVisible: PropTypes.bool.isRequired, + isMenuVisible: boolean; /** Report ID */ - reportID: PropTypes.string.isRequired, + reportID: string; /** Called when opening the attachment picker */ - onTriggerAttachmentPicker: PropTypes.func.isRequired, + onTriggerAttachmentPicker: () => void; /** Called when cancelling the attachment picker */ - onCanceledAttachmentPicker: PropTypes.func.isRequired, + onCanceledAttachmentPicker: () => void; /** Called when the menu with the items is closed after it was open */ - onMenuClosed: PropTypes.func.isRequired, + onMenuClosed: () => void; /** Called when the add action button is pressed */ - onAddActionPressed: PropTypes.func.isRequired, + onAddActionPressed: () => void; /** Called when the menu item is selected */ - onItemSelected: PropTypes.func.isRequired, + onItemSelected: () => void; /** A ref for the add action button */ - actionButtonRef: PropTypes.shape({ - // eslint-disable-next-line react/forbid-prop-types - current: PropTypes.object, - }).isRequired, - - /** Whether or not the screen is focused */ - isFocused: PropTypes.bool.isRequired, + actionButtonRef: React.RefObject; /** A function that toggles isScrollLikelyLayoutTriggered flag for a certain period of time */ - raiseIsScrollLikelyLayoutTriggered: PropTypes.func.isRequired, -}; + raiseIsScrollLikelyLayoutTriggered: () => void; -const defaultProps = { - reportParticipantIDs: [], - policy: {}, -}; + /** The personal details of everyone in the report */ + reportParticipantIDs?: number[]; +} & AttachmentPickerWithMenuItemsOnyxProps; /** * This includes the popover of options you see when pressing the + button in the composer. * It also contains the attachment picker, as the menu items need to be able to open it. - * - * @returns {React.Component} */ function AttachmentPickerWithMenuItems({ report, @@ -127,9 +116,9 @@ function AttachmentPickerWithMenuItems({ onAddActionPressed, onItemSelected, actionButtonRef, - isFocused, raiseIsScrollLikelyLayoutTriggered, -}) { +}: AttachmentPickerWithMenuItemsProps) { + const isFocused = useIsFocused(); const theme = useTheme(); const styles = useThemeStyles(); const {translate} = useLocalize(); @@ -137,10 +126,9 @@ function AttachmentPickerWithMenuItems({ /** * Returns the list of IOU Options - * @returns {Array} */ const moneyRequestOptions = useMemo(() => { - const options = { + const options: MoneyRequestOptions = { [CONST.IOU.TYPE.SPLIT]: { icon: Expensicons.Receipt, text: translate('iou.splitBill'), @@ -158,16 +146,15 @@ function AttachmentPickerWithMenuItems({ }, }; - return _.map(ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs), (option) => ({ + return ReportUtils.getMoneyRequestOptions(report, policy, reportParticipantIDs ?? []).map((option) => ({ ...options[option], })); }, [report, policy, reportParticipantIDs, translate]); /** * Determines if we can show the task option - * @returns {Boolean} */ - const taskOption = useMemo(() => { + const taskOption: MoneyRequestOption[] = useMemo(() => { if (!ReportUtils.canCreateTaskInReport(report)) { return []; } @@ -206,6 +193,7 @@ function AttachmentPickerWithMenuItems({ return ( + {/* @ts-expect-error TODO: Remove this once SettlementButton (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */} {({openPicker}) => { const triggerAttachmentPicker = () => { onTriggerAttachmentPicker(); @@ -235,7 +223,7 @@ function AttachmentPickerWithMenuItems({ { - e.preventDefault(); + e?.preventDefault(); raiseIsScrollLikelyLayoutTriggered(); Report.setIsComposerFullSize(reportID, false); }} @@ -257,7 +245,7 @@ function AttachmentPickerWithMenuItems({ { - e.preventDefault(); + e?.preventDefault(); raiseIsScrollLikelyLayoutTriggered(); Report.setIsComposerFullSize(reportID, true); }} @@ -279,14 +267,14 @@ function AttachmentPickerWithMenuItems({ { - e.preventDefault(); + e?.preventDefault(); if (!isFocused) { return; } onAddActionPressed(); // Drop focus to avoid blue focus ring. - actionButtonRef.current.blur(); + actionButtonRef.current?.blur(); setMenuVisibility(!isMenuVisible); }} style={styles.composerSizeButton} @@ -329,15 +317,10 @@ function AttachmentPickerWithMenuItems({ ); } -AttachmentPickerWithMenuItems.propTypes = propTypes; -AttachmentPickerWithMenuItems.defaultProps = defaultProps; AttachmentPickerWithMenuItems.displayName = 'AttachmentPickerWithMenuItems'; -export default compose( - withNavigationFocus, - withOnyx({ - policy: { - key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${lodashGet(report, 'policyID')}`, - }, - }), -)(AttachmentPickerWithMenuItems); +export default withOnyx({ + policy: { + key: ({report}) => `${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`, + }, +})(AttachmentPickerWithMenuItems); From 0cc6502443d17483711b9a26bd010e878e944626 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 22 Jan 2024 15:09:18 +0100 Subject: [PATCH 09/29] fix: Migrate SilentCommentUpdater --- ...estions.js => ComposerWithSuggestions.tsx} | 0 .../{index.android.js => index.android.tsx} | 22 ++-------- .../{index.js => index.tsx} | 42 +++---------------- .../SilentCommentUpdater/types.ts | 29 +++++++++++++ 4 files changed, 38 insertions(+), 55 deletions(-) rename src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/{ComposerWithSuggestions.js => ComposerWithSuggestions.tsx} (100%) rename src/pages/home/report/ReportActionCompose/SilentCommentUpdater/{index.android.js => index.android.tsx} (69%) rename src/pages/home/report/ReportActionCompose/SilentCommentUpdater/{index.js => index.tsx} (60%) create mode 100644 src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx similarity index 100% rename from src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.js rename to src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.js b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx similarity index 69% rename from src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.js rename to src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx index f924a7b59194..cee9cbf794bf 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.js +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.android.tsx @@ -1,19 +1,7 @@ -import PropTypes from 'prop-types'; import {useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /** The comment of the report */ - comment: PropTypes.string, - - /** Updates the comment */ - updateComment: PropTypes.func.isRequired, -}; - -const defaultProps = { - comment: '', -}; +import type {SilentCommentUpdaterOnyxProps, SilentCommentUpdaterProps} from './types'; /** * Adding .android component to disable updating comment when prev comment is different @@ -24,9 +12,8 @@ const defaultProps = { * This component doesn't render anything. It runs a side effect to update the comment of a report under certain conditions. * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. - * @returns {null} */ -function SilentCommentUpdater({comment, updateComment}) { +function SilentCommentUpdater({comment = '', updateComment}: SilentCommentUpdaterProps) { useEffect(() => { updateComment(comment); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount @@ -35,13 +22,10 @@ function SilentCommentUpdater({comment, updateComment}) { return null; } -SilentCommentUpdater.propTypes = propTypes; -SilentCommentUpdater.defaultProps = defaultProps; SilentCommentUpdater.displayName = 'SilentCommentUpdater'; -export default withOnyx({ +export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, - initialValue: '', }, })(SilentCommentUpdater); diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx similarity index 60% rename from src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js rename to src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index 9aa997a892f4..afee06dbc8ff 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.js +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -1,46 +1,18 @@ -import PropTypes from 'prop-types'; import {useEffect} from 'react'; import {withOnyx} from 'react-native-onyx'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import ONYXKEYS from '@src/ONYXKEYS'; - -const propTypes = { - /** The comment of the report */ - comment: PropTypes.string, - - /** The report associated with the comment */ - report: PropTypes.shape({ - /** The ID of the report */ - reportID: PropTypes.string, - }).isRequired, - - /** The value of the comment */ - value: PropTypes.string.isRequired, - - /** The ref of the comment */ - commentRef: PropTypes.shape({ - /** The current value of the comment */ - current: PropTypes.string, - }).isRequired, - - /** Updates the comment */ - updateComment: PropTypes.func.isRequired, -}; - -const defaultProps = { - comment: '', -}; +import type {SilentCommentUpdaterOnyxProps, SilentCommentUpdaterProps} from './types'; /** * This component doesn't render anything. It runs a side effect to update the comment of a report under certain conditions. * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. - * @returns {null} */ -function SilentCommentUpdater({comment, commentRef, report, value, updateComment}) { +function SilentCommentUpdater({comment = '', commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { const prevCommentProp = usePrevious(comment); - const prevReportId = usePrevious(report.reportID); + const prevReportId = usePrevious(report?.reportID); const {preferredLocale} = useLocalize(); const prevPreferredLocale = usePrevious(preferredLocale); @@ -56,21 +28,19 @@ function SilentCommentUpdater({comment, commentRef, report, value, updateComment // As the report IDs change, make sure to update the composer comment as we need to make sure // we do not show incorrect data in there (ie. draft of message from other report). - if (preferredLocale === prevPreferredLocale && report.reportID === prevReportId && !shouldSyncComment) { + if (preferredLocale === prevPreferredLocale && report?.reportID === prevReportId && !shouldSyncComment) { return; } updateComment(comment); - }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, report.reportID, updateComment, value, commentRef]); + }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, report?.reportID, updateComment, value, commentRef]); return null; } -SilentCommentUpdater.propTypes = propTypes; -SilentCommentUpdater.defaultProps = defaultProps; SilentCommentUpdater.displayName = 'SilentCommentUpdater'; -export default withOnyx({ +export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, initialValue: '', diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts new file mode 100644 index 000000000000..60924f24099b --- /dev/null +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts @@ -0,0 +1,29 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import type {Report} from '@src/types/onyx'; + +type SilentCommentUpdaterOnyxProps = { + /** The comment of the report */ + comment: OnyxEntry; +}; + +type SilentCommentUpdaterProps = { + /** Updates the comment */ + updateComment: (comment: OnyxEntry) => void; + + /** The ID of the report associated with the comment */ + reportID: string; + + /** The report associated with the comment */ + report: OnyxEntry; + + /** The value of the comment */ + value: string; + + /** The ref of the comment */ + commentRef: React.RefObject; + + /** The comment of the report */ + commnet: string; +} & SilentCommentUpdaterOnyxProps; + +export type {SilentCommentUpdaterProps, SilentCommentUpdaterOnyxProps}; From 8ed208cb62610d729bd6a7d1c9da1ec477b96c68 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 23 Jan 2024 10:49:06 +0100 Subject: [PATCH 10/29] fix: ComposerWithSuggestion migration --- .../AttachmentPickerWithMenuItems.tsx | 8 +- .../ComposerWithSuggestions.tsx | 239 ++++++++++-------- .../ReportActionCompose.tsx | 9 +- 3 files changed, 146 insertions(+), 110 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 547283cc4eb0..0dbfbba2759d 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -43,7 +43,7 @@ type AttachmentPickerWithMenuItemsOnyxProps = { type AttachmentPickerWithMenuItemsProps = { /** The report currently being looked at */ - report: OnyxTypes.Report; + report: OnyxEntry; /** Callback to open the file in the modal */ displayFileInModal: (url: string) => void; @@ -132,17 +132,17 @@ function AttachmentPickerWithMenuItems({ [CONST.IOU.TYPE.SPLIT]: { icon: Expensicons.Receipt, text: translate('iou.splitBill'), - onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.SPLIT, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report.reportID)), + onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.SPLIT, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report?.reportID ?? '')), }, [CONST.IOU.TYPE.REQUEST]: { icon: Expensicons.MoneyCircle, text: translate('iou.requestMoney'), - onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.REQUEST, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report.reportID)), + onSelected: () => Navigation.navigate(ROUTES.MONEY_REQUEST_CREATE.getRoute(CONST.IOU.TYPE.REQUEST, CONST.IOU.OPTIMISTIC_TRANSACTION_ID, report?.reportID ?? '')), }, [CONST.IOU.TYPE.SEND]: { icon: Expensicons.Send, text: translate('iou.sendMoney'), - onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report.reportID), + onSelected: () => IOU.startMoneyRequest(CONST.IOU.TYPE.SEND, report?.reportID), }, }; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 413807b1f992..bce7d80121b4 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -1,11 +1,14 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; -import lodashGet from 'lodash/get'; -import React, {useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import lodashDebounce from 'lodash/debounce'; +import type {ForwardedRef, RefAttributes, RefObject} from 'react'; +import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; +import type {MeasureInWindowOnSuccessCallback} from 'react-native'; import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; +import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; -import withKeyboardState from '@components/withKeyboardState'; +import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; import usePrevious from '@hooks/usePrevious'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -14,7 +17,6 @@ import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; import canFocusInputOnScreenFocus from '@libs/canFocusInputOnScreenFocus'; -import compose from '@libs/compose'; import * as ComposerUtils from '@libs/ComposerUtils'; import getDraftComment from '@libs/ComposerUtils/getDraftComment'; import convertToLTRForComposer from '@libs/convertToLTRForComposer'; @@ -28,6 +30,7 @@ import * as ReportUtils from '@libs/ReportUtils'; import * as SuggestionUtils from '@libs/SuggestionUtils'; import updateMultilineInputRange from '@libs/updateMultilineInputRange'; import willBlurTextInputOnTapOutsideFunc from '@libs/willBlurTextInputOnTapOutside'; +import type {ComposerRef, SuggestionsRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; import SilentCommentUpdater from '@pages/home/report/ReportActionCompose/SilentCommentUpdater'; import Suggestions from '@pages/home/report/ReportActionCompose/Suggestions'; import * as EmojiPickerActions from '@userActions/EmojiPickerAction'; @@ -36,17 +39,65 @@ import * as Report from '@userActions/Report'; import * as User from '@userActions/User'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultProps, propTypes} from './composerWithSuggestionsProps'; +type ComposerWithSuggestionsOnyxProps = { + /** The number of lines the comment should take up */ + numberOfLines: number; + + /** The parent report actions for the report */ + parentReportActions: OnyxEntry; + + /** The modal state */ + modal: OnyxEntry; + + /** The preferred skin tone of the user */ + preferredSkinTone: number; + + /** Whether the input is focused */ + editFocused: boolean; +}; +type ComposerWithSuggestionsProps = { + reportID: string; + report: OnyxEntry; + reportActions: OnyxTypes.ReportAction[] | undefined; + onFocus: () => void; + onBlur: (event: FocusEvent) => void; + onValueChange: (value: string) => void; + isComposerFullSize: boolean; + isMenuVisible: boolean; + inputPlaceholder: string; + displayFileInModal: (fileURL: string) => void; + textInputShouldClear: boolean; + setTextInputShouldClear: (shouldClear: boolean) => void; + isBlockedFromConcierge: boolean; + disabled: boolean; + isFullComposerAvailable: boolean; + setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; + setIsCommentEmpty: (isCommentEmpty: boolean) => void; + handleSendMessage: () => void; + shouldShowComposeInput: OnyxEntry; + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + listHeight: number; + isScrollLikelyLayoutTriggered: RefObject; + raiseIsScrollLikelyLayoutTriggered: () => void; + suggestionsRef: React.RefObject; + animatedRef: RefObject; + isNextModalWillOpenRef: RefObject; + editFocused: boolean; +} & ComposerWithSuggestionsOnyxProps & + Partial; + const {RNTextInputReset} = NativeModules; const isIOSNative = getPlatform() === CONST.PLATFORM.IOS; /** * Broadcast that the user is typing. Debounced to limit how often we publish client events. - * @param {String} reportID */ -const debouncedBroadcastUserIsTyping = _.debounce((reportID) => { +const debouncedBroadcastUserIsTyping = lodashDebounce((reportID: string) => { Report.broadcastUserIsTyping(reportID); }, 100); @@ -61,61 +112,61 @@ const shouldFocusInputOnScreenFocus = canFocusInputOnScreenFocus(); * If a component really needs access to these state values it should be put here. * However, double check if the component really needs access, as it will re-render * on every key press. - * @param {Object} props - * @returns {React.Component} */ -function ComposerWithSuggestions({ - // Onyx - modal, - preferredSkinTone, - parentReportActions, - numberOfLines, - // HOCs - isKeyboardShown, - // Props: Report - reportID, - report, - reportActions, - // Focus - onFocus, - onBlur, - onValueChange, - // Composer - isComposerFullSize, - isMenuVisible, - inputPlaceholder, - displayFileInModal, - textInputShouldClear, - setTextInputShouldClear, - isBlockedFromConcierge, - disabled, - isFullComposerAvailable, - setIsFullComposerAvailable, - setIsCommentEmpty, - handleSendMessage, - shouldShowComposeInput, - measureParentContainer, - listHeight, - isScrollLikelyLayoutTriggered, - raiseIsScrollLikelyLayoutTriggered, - // Refs - suggestionsRef, - animatedRef, - forwardedRef, - isNextModalWillOpenRef, - editFocused, - // For testing - children, -}) { +function ComposerWithSuggestions( + { + // Onyx + modal, + preferredSkinTone, + parentReportActions, + numberOfLines, + + // Props: Report + reportID, + report, + reportActions, + // Focus + onFocus, + onBlur, + onValueChange, + // Composer + isComposerFullSize, + isMenuVisible, + inputPlaceholder, + displayFileInModal, + textInputShouldClear, + setTextInputShouldClear, + isBlockedFromConcierge, + disabled, + isFullComposerAvailable, + setIsFullComposerAvailable, + setIsCommentEmpty, + handleSendMessage, + shouldShowComposeInput, + measureParentContainer, + listHeight, + isScrollLikelyLayoutTriggered, + raiseIsScrollLikelyLayoutTriggered, + // Refs + suggestionsRef, + animatedRef, + isNextModalWillOpenRef, + editFocused, + // For testing + children, + }: ComposerWithSuggestionsProps, + ref: ForwardedRef, +) { + const {isKeyboardShown} = useKeyboardState(); const theme = useTheme(); const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {preferredLocale} = useLocalize(); const isFocused = useIsFocused(); const navigation = useNavigation(); - const emojisPresentBefore = useRef([]); + const emojisPresentBefore = useRef([]); const [value, setValue] = useState(() => { - const draft = getDraftComment(reportID) || ''; + const draft = getDraftComment(reportID) ?? ''; if (draft) { emojisPresentBefore.current = EmojiUtils.extractEmojis(draft); } @@ -126,9 +177,9 @@ function ComposerWithSuggestions({ const {isSmallScreenWidth} = useWindowDimensions(); const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; - const isEmptyChat = useMemo(() => _.size(reportActions) === 1, [reportActions]); - const parentReportAction = lodashGet(parentReportActions, [report.parentReportActionID]); - const shouldAutoFocus = !modal.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && shouldShowComposeInput; + const isEmptyChat = useMemo(() => reportActions?.length === 1, [reportActions]); + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '']; + const shouldAutoFocus = !modal?.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && shouldShowComposeInput; const valueRef = useRef(value); valueRef.current = value; @@ -138,11 +189,11 @@ function ComposerWithSuggestions({ const [composerHeight, setComposerHeight] = useState(0); const textInputRef = useRef(null); - const insertedEmojisRef = useRef([]); + const insertedEmojisRef = useRef([]); const syncSelectionWithOnChangeTextRef = useRef(null); - const suggestions = lodashGet(suggestionsRef, 'current.getSuggestions', () => [])(); + const suggestions = suggestionsRef.current?.getSuggestions(); const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions.length); @@ -159,15 +210,12 @@ function ComposerWithSuggestions({ /** * Set the TextInput Ref - * - * @param {Element} el - * @memberof ReportActionCompose */ const setTextInputRef = useCallback( (el) => { ReportActionComposeFocusManager.composerRef.current = el; textInputRef.current = el; - if (_.isFunction(animatedRef)) { + if (typeof animatedRef === 'function') { animatedRef(el); } }, @@ -196,7 +244,7 @@ function ComposerWithSuggestions({ * @param {Boolean} shouldDebounceSaveComment */ const updateComment = useCallback( - (commentValue, shouldDebounceSaveComment) => { + (commentValue: string, shouldDebounceSaveComment: boolean) => { raiseIsScrollLikelyLayoutTriggered(); const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); if (!_.isEmpty(emojis)) { @@ -514,7 +562,7 @@ function ComposerWithSuggestions({ }, []); useImperativeHandle( - forwardedRef, + ref, () => ({ blur, focus, @@ -608,39 +656,28 @@ ComposerWithSuggestions.propTypes = propTypes; ComposerWithSuggestions.defaultProps = defaultProps; ComposerWithSuggestions.displayName = 'ComposerWithSuggestions'; -const ComposerWithSuggestionsWithRef = React.forwardRef((props, ref) => ( - -)); - -ComposerWithSuggestionsWithRef.displayName = 'ComposerWithSuggestionsWithRef'; - -export default compose( - withKeyboardState, - withOnyx({ - numberOfLines: { - key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, - // We might not have number of lines in onyx yet, for which the composer would be rendered as null - // during the first render, which we want to avoid: - initWithStoredValues: false, - }, - modal: { - key: ONYXKEYS.MODAL, - }, - preferredSkinTone: { - key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, - selector: EmojiUtils.getPreferredSkinToneIndex, - }, - editFocused: { - key: ONYXKEYS.INPUT_FOCUSED, - }, - parentReportActions: { - key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, - canEvict: false, - initWithStoredValues: false, - }, - }), -)(ComposerWithSuggestionsWithRef); +const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions); + +export default withOnyx, ComposerWithSuggestionsOnyxProps>({ + numberOfLines: { + key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT_NUMBER_OF_LINES}${reportID}`, + // We might not have number of lines in onyx yet, for which the composer would be rendered as null + // during the first render, which we want to avoid: + initWithStoredValues: false, + }, + modal: { + key: ONYXKEYS.MODAL, + }, + preferredSkinTone: { + key: ONYXKEYS.PREFERRED_EMOJI_SKIN_TONE, + selector: EmojiUtils.getPreferredSkinToneIndex, + }, + editFocused: { + key: ONYXKEYS.INPUT_FOCUSED, + }, + parentReportActions: { + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + canEvict: false, + initWithStoredValues: false, + }, +})(ComposerWithSuggestionsWithRef); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 9754d45be834..b46eb145173e 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -6,6 +6,7 @@ import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import {runOnJS, setNativeProps, useAnimatedRef} from 'react-native-reanimated'; +import type {Emoji} from '@assets/emojis/types'; import AttachmentModal from '@components/AttachmentModal'; import EmojiPickerButton from '@components/EmojiPicker/EmojiPickerButton'; import ExceededCommentLength from '@components/ExceededCommentLength'; @@ -389,7 +390,6 @@ function ReportActionCompose({ {({displayFileInModal}) => ( <> Date: Wed, 24 Jan 2024 09:45:39 +0100 Subject: [PATCH 11/29] fix: keep working on ComposerWithSuggestions --- src/components/Composer/index.tsx | 5 +- src/libs/updateMultilineInputRange/types.ts | 2 +- .../ComposerWithSuggestions.tsx | 110 +++++++++--------- .../{index.e2e.js => index.e2e.tsx} | 0 .../{index.js => index.tsx} | 0 .../ReportActionCompose.tsx | 2 +- .../SilentCommentUpdater/index.tsx | 2 +- .../ReportActionCompose/Suggestions.tsx | 4 +- 8 files changed, 65 insertions(+), 60 deletions(-) rename src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/{index.e2e.js => index.e2e.tsx} (100%) rename src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/{index.js => index.tsx} (100%) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index 3c2caf020ef7..e78e0c2a4039 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -4,9 +4,8 @@ import type {BaseSyntheticEvent, ForwardedRef} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; import {flushSync} from 'react-dom'; // eslint-disable-next-line no-restricted-imports -import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputProps, TextInputSelectionChangeEventData} from 'react-native'; +import type {DimensionValue, NativeSyntheticEvent, Text as RNText, TextInput, TextInputKeyPressEventData, TextInputSelectionChangeEventData} from 'react-native'; import {StyleSheet, View} from 'react-native'; -import type {AnimatedProps} from 'react-native-reanimated'; import RNTextInput from '@components/RNTextInput'; import Text from '@components/Text'; import useIsScrollBarVisible from '@hooks/useIsScrollBarVisible'; @@ -75,7 +74,7 @@ function Composer( shouldContainScroll = false, ...props }: ComposerProps, - ref: ForwardedRef>>, + ref: ForwardedRef, ) { const theme = useTheme(); const styles = useThemeStyles(); diff --git a/src/libs/updateMultilineInputRange/types.ts b/src/libs/updateMultilineInputRange/types.ts index d1b134b09a99..ce8f553c51f8 100644 --- a/src/libs/updateMultilineInputRange/types.ts +++ b/src/libs/updateMultilineInputRange/types.ts @@ -1,5 +1,5 @@ import type {TextInput} from 'react-native'; -type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput, shouldAutoFocus?: boolean) => void; +type UpdateMultilineInputRange = (input: HTMLInputElement | TextInput | null, shouldAutoFocus?: boolean) => void; export default UpdateMultilineInputRange; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index bce7d80121b4..cb5f2b52ffbc 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -2,10 +2,11 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, RefAttributes, RefObject} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {MeasureInWindowOnSuccessCallback} from 'react-native'; +import type {LayoutChangeEvent, MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputSelectionChangeEventData} from 'react-native'; import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; +import type {useAnimatedRef} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; import Composer from '@components/Composer'; import useKeyboardState from '@hooks/useKeyboardState'; @@ -43,9 +44,16 @@ import type * as OnyxTypes from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; import {defaultProps, propTypes} from './composerWithSuggestionsProps'; +type SyncSelection = { + position: number; + value: string; +}; + +type AnimatedRef = ReturnType; + type ComposerWithSuggestionsOnyxProps = { /** The number of lines the comment should take up */ - numberOfLines: number; + numberOfLines: OnyxEntry; /** The parent report actions for the report */ parentReportActions: OnyxEntry; @@ -57,8 +65,9 @@ type ComposerWithSuggestionsOnyxProps = { preferredSkinTone: number; /** Whether the input is focused */ - editFocused: boolean; + editFocused: OnyxEntry; }; + type ComposerWithSuggestionsProps = { reportID: string; report: OnyxEntry; @@ -69,7 +78,7 @@ type ComposerWithSuggestionsProps = { isComposerFullSize: boolean; isMenuVisible: boolean; inputPlaceholder: string; - displayFileInModal: (fileURL: string) => void; + displayFileInModal: (file: File | undefined) => void; textInputShouldClear: boolean; setTextInputShouldClear: (shouldClear: boolean) => void; isBlockedFromConcierge: boolean; @@ -84,8 +93,8 @@ type ComposerWithSuggestionsProps = { isScrollLikelyLayoutTriggered: RefObject; raiseIsScrollLikelyLayoutTriggered: () => void; suggestionsRef: React.RefObject; - animatedRef: RefObject; - isNextModalWillOpenRef: RefObject; + animatedRef: AnimatedRef; + isNextModalWillOpenRef: RefObject; editFocused: boolean; } & ComposerWithSuggestionsOnyxProps & Partial; @@ -178,7 +187,7 @@ function ComposerWithSuggestions( const maxComposerLines = isSmallScreenWidth ? CONST.COMPOSER.MAX_LINES_SMALL_SCREEN : CONST.COMPOSER.MAX_LINES; const isEmptyChat = useMemo(() => reportActions?.length === 1, [reportActions]); - const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? '']; + const parentReportAction = parentReportActions?.[report?.parentReportActionID ?? ''] ?? null; const shouldAutoFocus = !modal?.isVisible && (shouldFocusInputOnScreenFocus || (isEmptyChat && !ReportActionsUtils.isTransactionThread(parentReportAction))) && shouldShowComposeInput; const valueRef = useRef(value); @@ -188,14 +197,14 @@ function ComposerWithSuggestions( const [composerHeight, setComposerHeight] = useState(0); - const textInputRef = useRef(null); + const textInputRef = useRef(null); const insertedEmojisRef = useRef([]); - const syncSelectionWithOnChangeTextRef = useRef(null); + const syncSelectionWithOnChangeTextRef = useRef(null); const suggestions = suggestionsRef.current?.getSuggestions(); - const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions.length); + const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions?.length ?? 0); const isAutoSuggestionPickerLarge = !isSmallScreenWidth || (isSmallScreenWidth && hasEnoughSpaceForLargeSuggestion); @@ -212,7 +221,7 @@ function ComposerWithSuggestions( * Set the TextInput Ref */ const setTextInputRef = useCallback( - (el) => { + (el: TextInput) => { ReportActionComposeFocusManager.composerRef.current = el; textInputRef.current = el; if (typeof animatedRef === 'function') { @@ -231,7 +240,7 @@ function ComposerWithSuggestions( const debouncedSaveReportComment = useMemo( () => - _.debounce((selectedReportID, newComment) => { + lodashDebounce((selectedReportID, newComment) => { Report.saveReportComment(selectedReportID, newComment || ''); }, 1000), [], @@ -239,17 +248,14 @@ function ComposerWithSuggestions( /** * Update the value of the comment in Onyx - * - * @param {String} comment - * @param {Boolean} shouldDebounceSaveComment */ const updateComment = useCallback( - (commentValue: string, shouldDebounceSaveComment: boolean) => { + (commentValue: string | null, shouldDebounceSaveComment?: boolean) => { raiseIsScrollLikelyLayoutTriggered(); - const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(commentValue, preferredSkinTone, preferredLocale); - if (!_.isEmpty(emojis)) { + const {text: newComment, emojis, cursorPosition} = EmojiUtils.replaceAndExtractEmojis(commentValue ?? '', preferredSkinTone, preferredLocale); + if (emojis.length) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); - if (!_.isEmpty(newEmojis)) { + if (newEmojis.length) { // Ensure emoji suggestions are hidden after inserting emoji even when the selection is not changed if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); @@ -269,7 +275,7 @@ function ComposerWithSuggestions( emojisPresentBefore.current = emojis; setValue(newCommentConverted); if (commentValue !== newComment) { - const position = Math.max(selection.end + (newComment.length - commentRef.current.length), cursorPosition || 0); + const position = Math.max(selection.end + (newComment.length - commentRef.current.length), cursorPosition ?? 0); if (isIOSNative) { syncSelectionWithOnChangeTextRef.current = {position, value: newComment}; @@ -316,10 +322,9 @@ function ComposerWithSuggestions( /** * Update the number of lines for a comment in Onyx - * @param {Number} numberOfLines */ const updateNumberOfLines = useCallback( - (newNumberOfLines) => { + (newNumberOfLines: number) => { if (newNumberOfLines === numberOfLines) { return; } @@ -328,9 +333,6 @@ function ComposerWithSuggestions( [reportID, numberOfLines], ); - /** - * @returns {String} - */ const prepareCommentAndResetComposer = useCallback(() => { const trimmedComment = commentRef.current.trim(); const commentLength = ReportUtils.getCommentLength(trimmedComment); @@ -356,11 +358,9 @@ function ComposerWithSuggestions( /** * Callback to add whatever text is chosen into the main input (used f.e as callback for the emoji picker) - * @param {String} text - * @param {Boolean} shouldAddTrailSpace */ const replaceSelectionWithText = useCallback( - (text, shouldAddTrailSpace = true) => { + (text: string, shouldAddTrailSpace = true) => { const updatedText = shouldAddTrailSpace ? `${text} ` : text; const selectionSpaceLength = shouldAddTrailSpace ? CONST.SPACE_LENGTH : 0; updateComment(ComposerUtils.insertText(commentRef.current, selection, updatedText)); @@ -373,12 +373,12 @@ function ComposerWithSuggestions( ); const triggerHotkeyActions = useCallback( - (e) => { + (e: KeyboardEvent) => { if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { return; } - if (suggestionsRef.current.triggerHotkeyActions(e)) { + if (suggestionsRef.current?.triggerHotkeyActions(e)) { return; } @@ -390,14 +390,20 @@ function ComposerWithSuggestions( // Trigger the edit box for last sent message if ArrowUp is pressed and the comment is empty and Chronos is not in the participants const valueLength = valueRef.current.length; - if (e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current.selectionStart === 0 && valueLength === 0 && !ReportUtils.chatIncludesChronos(report)) { + if ( + e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && + textInputRef.current && + 'selectionStart' in textInputRef.current && + textInputRef.current?.selectionStart === 0 && + valueLength === 0 && + !ReportUtils.chatIncludesChronos(report) + ) { e.preventDefault(); - const lastReportAction = _.find( - [...reportActions, parentReportAction], + const lastReportAction = [...(reportActions ?? []), parentReportAction].find( (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action), ); if (lastReportAction) { - Report.saveReportActionDraft(reportID, lastReportAction, _.last(lastReportAction.message).html); + Report.saveReportActionDraft(reportID, lastReportAction, lastReportAction.message?.at(-1)?.html ?? ''); } } }, @@ -405,7 +411,7 @@ function ComposerWithSuggestions( ); const onChangeText = useCallback( - (commentValue) => { + (commentValue: string) => { updateComment(commentValue, true); if (isIOSNative && syncSelectionWithOnChangeTextRef.current) { @@ -416,7 +422,7 @@ function ComposerWithSuggestions( InteractionManager.runAfterInteractions(() => { // note: this implementation is only available on non-web RN, thus the wrapping // 'if' block contains a redundant (since the ref is only used on iOS) platform check - textInputRef.current.setSelection(positionSnapshot, positionSnapshot); + textInputRef.current?.setSelection(positionSnapshot, positionSnapshot); }); } }, @@ -424,8 +430,8 @@ function ComposerWithSuggestions( ); const onSelectionChange = useCallback( - (e) => { - if (textInputRef.current && textInputRef.current.isFocused() && suggestionsRef.current.onSelectionChange(e)) { + (e: NativeSyntheticEvent) => { + if (textInputRef.current?.isFocused() && suggestionsRef.current?.onSelectionChange?.(e)) { return; } @@ -451,7 +457,7 @@ function ComposerWithSuggestions( /** * Focus the composer text input - * @param {Boolean} [shouldDelay=false] Impose delay before focusing the composer + * @param [shouldDelay=false] Impose delay before focusing the composer * @memberof ReportActionCompose */ const focus = useCallback((shouldDelay = false) => { @@ -475,12 +481,12 @@ function ComposerWithSuggestions( */ const checkComposerVisibility = useCallback(() => { // Checking whether the screen is focused or not, helps avoid `modal.isVisible` false when popups are closed, even if the modal is opened. - const isComposerCoveredUp = !isFocused || EmojiPickerActions.isEmojiPickerVisible() || isMenuVisible || modal.isVisible || modal.willAlertModalBecomeVisible; + const isComposerCoveredUp = !isFocused || EmojiPickerActions.isEmojiPickerVisible() || isMenuVisible || !!modal?.isVisible || modal?.willAlertModalBecomeVisible; return !isComposerCoveredUp; }, [isMenuVisible, modal, isFocused]); const focusComposerOnKeyPress = useCallback( - (e) => { + (e: KeyboardEvent) => { const isComposerVisible = checkComposerVisibility(); if (!isComposerVisible) { return; @@ -491,7 +497,7 @@ function ComposerWithSuggestions( } // if we're typing on another input/text area, do not focus - if (['INPUT', 'TEXTAREA'].includes(e.target.nodeName)) { + if (['INPUT', 'TEXTAREA'].includes((e.target as Element)?.nodeName)) { return; } @@ -527,17 +533,17 @@ function ComposerWithSuggestions( }; }, [focusComposerOnKeyPress, navigation, setUpComposeFocusManager]); - const prevIsModalVisible = usePrevious(modal.isVisible); + const prevIsModalVisible = usePrevious(modal?.isVisible); const prevIsFocused = usePrevious(isFocused); useEffect(() => { - if (modal.isVisible && !prevIsModalVisible) { + if (modal?.isVisible && !prevIsModalVisible) { // eslint-disable-next-line no-param-reassign isNextModalWillOpenRef.current = false; } // We want to focus or refocus the input when a modal has been closed or the underlying screen is refocused. // We avoid doing this on native platforms since the software keyboard popping // open creates a jarring and broken UX. - if (!((willBlurTextInputOnTapOutside || shouldAutoFocus) && !isNextModalWillOpenRef.current && !modal.isVisible && isFocused && (prevIsModalVisible || !prevIsFocused))) { + if (!((willBlurTextInputOnTapOutside || shouldAutoFocus) && !isNextModalWillOpenRef.current && !modal?.isVisible && isFocused && (!!prevIsModalVisible || !prevIsFocused))) { return; } @@ -546,11 +552,11 @@ function ComposerWithSuggestions( return; } focus(true); - }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal.isVisible, isNextModalWillOpenRef, shouldAutoFocus]); + }, [focus, prevIsFocused, editFocused, prevIsModalVisible, isFocused, modal?.isVisible, isNextModalWillOpenRef, shouldAutoFocus]); useEffect(() => { // Scrolls the composer to the bottom and sets the selection to the end, so that longer drafts are easier to edit - updateMultilineInputRange(textInputRef.current, shouldAutoFocus); + updateMultilineInputRange(textInputRef.current, !!shouldAutoFocus); if (value.length === 0) { return; @@ -568,7 +574,7 @@ function ComposerWithSuggestions( focus, replaceSelectionWithText, prepareCommentAndResetComposer, - isFocused: () => textInputRef.current.isFocused(), + isFocused: () => !!textInputRef.current?.isFocused(), }), [blur, focus, prepareCommentAndResetComposer, replaceSelectionWithText], ); @@ -582,7 +588,7 @@ function ComposerWithSuggestions( { + onLayout={(e: LayoutChangeEvent) => { const composerLayoutHeight = e.nativeEvent.layout.height; if (composerHeight === composerLayoutHeight) { return; @@ -625,7 +631,7 @@ function ComposerWithSuggestions( void; onSelectionChange?: (event: NativeSyntheticEvent) => void; - triggerHotkeyActions: (event: KeyboardEvent) => void; + triggerHotkeyActions: (event: KeyboardEvent) => boolean | undefined; updateShouldShowSuggestionMenuToFalse: (shouldShowSuggestionMenu?: boolean) => void; setShouldBlockSuggestionCalc: (shouldBlock: boolean) => void; getSuggestions: () => Mention[] | Emoji[]; diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index afee06dbc8ff..3f883d46a995 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -10,7 +10,7 @@ import type {SilentCommentUpdaterOnyxProps, SilentCommentUpdaterProps} from './t * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. */ -function SilentCommentUpdater({comment = '', commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { +function SilentCommentUpdater({comment, commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { const prevCommentProp = usePrevious(comment); const prevReportId = usePrevious(report?.reportID); const {preferredLocale} = useLocalize(); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index 2500af40c7c6..0968a61c3abb 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -1,6 +1,6 @@ import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useRef} from 'react'; -import type {NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; import {DragAndDropContext} from '@components/DragAndDrop/Provider'; import usePrevious from '@hooks/usePrevious'; @@ -19,7 +19,7 @@ type SuggestionProps = { selection: Selection; setSelection: (newSelection: Selection) => void; updateComment: (newComment: string, shouldDebounceSaveComment?: boolean) => void; - measureParentContainer: () => void; + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; isComposerFullSize: boolean; isComposerFocused?: boolean; resetKeyboardInput?: () => void; From 223fdd804f52f0adf9eb5891f8cd00da0dd91726 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 12:51:51 +0100 Subject: [PATCH 12/29] fix: wip --- src/components/Composer/index.tsx | 2 +- src/components/Composer/types.ts | 2 +- .../ComposerWithSuggestions.tsx | 10 ++++++---- .../ReportActionCompose/SilentCommentUpdater/index.tsx | 7 +++---- .../ReportActionCompose/SilentCommentUpdater/types.ts | 3 --- 5 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index e78e0c2a4039..a23a176c5bcd 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -300,7 +300,7 @@ function Composer( }, []); const handleKeyPress = useCallback( - (e: NativeSyntheticEvent) => { + (e: NativeSyntheticEvent & KeyboardEvent) => { // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed if (!onKeyPress || isEnterWhileComposition(e as unknown as KeyboardEvent)) { return; diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index d8d88970ea78..6e486771c9aa 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -74,7 +74,7 @@ type ComposerProps = { /** Whether the sull composer is open */ isComposerFullSize?: boolean; - onKeyPress?: (event: NativeSyntheticEvent) => void; + onKeyPress?: (event: KeyboardEvent & NativeSyntheticEvent) => void; onFocus?: (event: NativeSyntheticEvent) => void; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index cb5f2b52ffbc..034affefd747 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -2,7 +2,7 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, RefAttributes, RefObject} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {LayoutChangeEvent, MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputSelectionChangeEventData} from 'react-native'; +import type {LayoutChangeEvent, MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -73,7 +73,7 @@ type ComposerWithSuggestionsProps = { report: OnyxEntry; reportActions: OnyxTypes.ReportAction[] | undefined; onFocus: () => void; - onBlur: (event: FocusEvent) => void; + onBlur: (event: NativeSyntheticEvent) => void; onValueChange: (value: string) => void; isComposerFullSize: boolean; isMenuVisible: boolean; @@ -222,6 +222,7 @@ function ComposerWithSuggestions( */ const setTextInputRef = useCallback( (el: TextInput) => { + // @ts-expect-error need to reassign this ref ReportActionComposeFocusManager.composerRef.current = el; textInputRef.current = el; if (typeof animatedRef === 'function') { @@ -373,7 +374,7 @@ function ComposerWithSuggestions( ); const triggerHotkeyActions = useCallback( - (e: KeyboardEvent) => { + (e: KeyboardEvent & NativeSyntheticEvent) => { if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { return; } @@ -537,6 +538,7 @@ function ComposerWithSuggestions( const prevIsFocused = usePrevious(isFocused); useEffect(() => { if (modal?.isVisible && !prevIsModalVisible) { + // @ts-expect-error need to reassign this ref // eslint-disable-next-line no-param-reassign isNextModalWillOpenRef.current = false; } @@ -682,7 +684,7 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.parentReportID}`, + key: ({report}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, canEvict: false, initWithStoredValues: false, }, diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index 3f883d46a995..626938c235da 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -10,14 +10,14 @@ import type {SilentCommentUpdaterOnyxProps, SilentCommentUpdaterProps} from './t * It is connected to the actual draft comment in onyx. The comment in onyx might updates multiple times, and we want to avoid * re-rendering a UI component for that. That's why the side effect was moved down to a separate component. */ -function SilentCommentUpdater({comment, commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { +function SilentCommentUpdater({comment = '', commentRef, report, value, updateComment}: SilentCommentUpdaterProps) { const prevCommentProp = usePrevious(comment); const prevReportId = usePrevious(report?.reportID); const {preferredLocale} = useLocalize(); const prevPreferredLocale = usePrevious(preferredLocale); useEffect(() => { - updateComment(comment); + updateComment(comment ?? null); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount }, []); @@ -32,7 +32,7 @@ function SilentCommentUpdater({comment, commentRef, report, value, updateComment return; } - updateComment(comment); + updateComment(comment ?? null); }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, report?.reportID, updateComment, value, commentRef]); return null; @@ -43,6 +43,5 @@ SilentCommentUpdater.displayName = 'SilentCommentUpdater'; export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, - initialValue: '', }, })(SilentCommentUpdater); diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts index 60924f24099b..591ee43ce6cd 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts @@ -21,9 +21,6 @@ type SilentCommentUpdaterProps = { /** The ref of the comment */ commentRef: React.RefObject; - - /** The comment of the report */ - commnet: string; } & SilentCommentUpdaterOnyxProps; export type {SilentCommentUpdaterProps, SilentCommentUpdaterOnyxProps}; From 05bb0a591c9401ae62f15833afd801237e6af3ee Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 16:20:44 +0100 Subject: [PATCH 13/29] fix: finished migrating ComposerWithSuggestions, removed unused files, start migrating ReportTypeingIndicator --- src/components/Composer/index.tsx | 3 +- src/components/Composer/types.ts | 10 +- src/components/MentionSuggestions.tsx | 3 +- src/libs/E2E/client.ts | 3 + .../ComposerWithSuggestions.tsx | 37 ++--- .../composerWithSuggestionsProps.js | 126 ------------------ .../ComposerWithSuggestions/index.e2e.tsx | 18 +-- .../ReportActionCompose.tsx | 9 +- .../ReportActionCompose/suggestionProps.js | 39 ------ ...Indicator.js => ReportTypingIndicator.tsx} | 0 src/types/modules/react-native.d.ts | 5 - 11 files changed, 46 insertions(+), 207 deletions(-) delete mode 100644 src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/composerWithSuggestionsProps.js delete mode 100644 src/pages/home/report/ReportActionCompose/suggestionProps.js rename src/pages/home/report/{ReportTypingIndicator.js => ReportTypingIndicator.tsx} (100%) diff --git a/src/components/Composer/index.tsx b/src/components/Composer/index.tsx index a23a176c5bcd..493b37ff3a31 100755 --- a/src/components/Composer/index.tsx +++ b/src/components/Composer/index.tsx @@ -300,11 +300,12 @@ function Composer( }, []); const handleKeyPress = useCallback( - (e: NativeSyntheticEvent & KeyboardEvent) => { + (e: NativeSyntheticEvent) => { // Prevent onKeyPress from being triggered if the Enter key is pressed while text is being composed if (!onKeyPress || isEnterWhileComposition(e as unknown as KeyboardEvent)) { return; } + onKeyPress(e); }, [onKeyPress], diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 6e486771c9aa..9565eaf6208f 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -1,4 +1,4 @@ -import type {NativeSyntheticEvent, StyleProp, TextInputFocusEventData, TextInputKeyPressEventData, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; +import type {NativeSyntheticEvent, StyleProp, TextInputProps, TextInputSelectionChangeEventData, TextStyle} from 'react-native'; type TextSelection = { start: number; @@ -74,14 +74,8 @@ type ComposerProps = { /** Whether the sull composer is open */ isComposerFullSize?: boolean; - onKeyPress?: (event: KeyboardEvent & NativeSyntheticEvent) => void; - - onFocus?: (event: NativeSyntheticEvent) => void; - - onBlur?: (event: NativeSyntheticEvent) => void; - /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; -}; +} & TextInputProps; export type {TextSelection, ComposerProps}; diff --git a/src/components/MentionSuggestions.tsx b/src/components/MentionSuggestions.tsx index 99930f995a3a..23040a242807 100644 --- a/src/components/MentionSuggestions.tsx +++ b/src/components/MentionSuggestions.tsx @@ -1,4 +1,5 @@ import React, {useCallback} from 'react'; +import type {MeasureInWindowOnSuccessCallback} from 'react-native'; import {View} from 'react-native'; import useStyleUtils from '@hooks/useStyleUtils'; import useTheme from '@hooks/useTheme'; @@ -43,7 +44,7 @@ type MentionSuggestionsProps = { isMentionPickerLarge: boolean; /** Measures the parent container's position and dimensions. */ - measureParentContainer: () => void; + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; }; /** diff --git a/src/libs/E2E/client.ts b/src/libs/E2E/client.ts index 472567cc6c1d..7dc3ad4073b9 100644 --- a/src/libs/E2E/client.ts +++ b/src/libs/E2E/client.ts @@ -11,6 +11,9 @@ type TestResult = { type TestConfig = { name: string; + reportScreen?: { + autoFocus?: boolean; + }; }; type NativeCommandPayload = { diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 034affefd747..c02bda08cac7 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -2,7 +2,15 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; import type {ForwardedRef, RefAttributes, RefObject} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; -import type {LayoutChangeEvent, MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInput, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; +import type { + LayoutChangeEvent, + MeasureInWindowOnSuccessCallback, + NativeSyntheticEvent, + TextInput, + TextInputFocusEventData, + TextInputKeyPressEventData, + TextInputSelectionChangeEventData, +} from 'react-native'; import {findNodeHandle, InteractionManager, NativeModules, View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -42,7 +50,6 @@ import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; -import {defaultProps, propTypes} from './composerWithSuggestionsProps'; type SyncSelection = { position: number; @@ -126,7 +133,7 @@ function ComposerWithSuggestions( { // Onyx modal, - preferredSkinTone, + preferredSkinTone = CONST.EMOJI_DEFAULT_SKIN_TONE, parentReportActions, numberOfLines, @@ -152,7 +159,7 @@ function ComposerWithSuggestions( setIsCommentEmpty, handleSendMessage, shouldShowComposeInput, - measureParentContainer, + measureParentContainer = () => {}, listHeight, isScrollLikelyLayoutTriggered, raiseIsScrollLikelyLayoutTriggered, @@ -164,7 +171,7 @@ function ComposerWithSuggestions( // For testing children, }: ComposerWithSuggestionsProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const {isKeyboardShown} = useKeyboardState(); const theme = useTheme(); @@ -374,32 +381,33 @@ function ComposerWithSuggestions( ); const triggerHotkeyActions = useCallback( - (e: KeyboardEvent & NativeSyntheticEvent) => { - if (!e || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { + (event: NativeSyntheticEvent) => { + const webEvent = event as unknown as KeyboardEvent; + if (!webEvent || ComposerUtils.canSkipTriggerHotkeys(isSmallScreenWidth, isKeyboardShown)) { return; } - if (suggestionsRef.current?.triggerHotkeyActions(e)) { + if (suggestionsRef.current?.triggerHotkeyActions(webEvent)) { return; } // Submit the form when Enter is pressed - if (e.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !e.shiftKey) { - e.preventDefault(); + if (webEvent.key === CONST.KEYBOARD_SHORTCUTS.ENTER.shortcutKey && !webEvent.shiftKey) { + webEvent.preventDefault(); handleSendMessage(); } // Trigger the edit box for last sent message if ArrowUp is pressed and the comment is empty and Chronos is not in the participants const valueLength = valueRef.current.length; if ( - e.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && + webEvent.key === CONST.KEYBOARD_SHORTCUTS.ARROW_UP.shortcutKey && textInputRef.current && 'selectionStart' in textInputRef.current && textInputRef.current?.selectionStart === 0 && valueLength === 0 && !ReportUtils.chatIncludesChronos(report) ) { - e.preventDefault(); + webEvent.preventDefault(); const lastReportAction = [...(reportActions ?? []), parentReportAction].find( (action) => ReportUtils.canEditReportAction(action) && !ReportActionsUtils.isMoneyRequestAction(action), ); @@ -568,7 +576,6 @@ function ComposerWithSuggestions( // eslint-disable-next-line react-hooks/exhaustive-deps }, []); - useImperativeHandle( ref, () => ({ @@ -660,8 +667,6 @@ function ComposerWithSuggestions( ); } -ComposerWithSuggestions.propTypes = propTypes; -ComposerWithSuggestions.defaultProps = defaultProps; ComposerWithSuggestions.displayName = 'ComposerWithSuggestions'; const ComposerWithSuggestionsWithRef = forwardRef(ComposerWithSuggestions); @@ -689,3 +694,5 @@ export default withOnyx {}, -}; - -export {propTypes, defaultProps}; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx index cbbd1758c9cb..87352981ba9a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx @@ -1,6 +1,8 @@ -import _ from 'lodash'; -import React, {useEffect} from 'react'; +import type {ForwardedRef, RefObject} from 'react'; +import React, {forwardRef, useEffect} from 'react'; import E2EClient from '@libs/E2E/client'; +import type {ComposerRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; +import type {ComposerWithSuggestionsProps} from './ComposerWithSuggestions'; import ComposerWithSuggestions from './ComposerWithSuggestions'; let rerenderCount = 0; @@ -14,20 +16,20 @@ function IncrementRenderCount() { return null; } -const ComposerWithSuggestionsE2e = React.forwardRef((props, ref) => { +function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: ForwardedRef) { // Eventually Auto focus on e2e tests useEffect(() => { - if (_.get(E2EClient.getCurrentActiveTestConfig(), 'reportScreen.autoFocus', false) === false) { + if ((E2EClient.getCurrentActiveTestConfig()?.reportScreen?.autoFocus ?? false) === false) { return; } // We need to wait for the component to be mounted before focusing setTimeout(() => { - if (!ref || !ref.current) { + if (!(ref as RefObject)?.current) { return; } - ref.current.focus(true); + (ref as RefObject).current?.focus(true); }, 1); }, [ref]); @@ -44,9 +46,9 @@ const ComposerWithSuggestionsE2e = React.forwardRef((props, ref) => { ); -}); +} ComposerWithSuggestionsE2e.displayName = 'ComposerWithSuggestionsE2e'; -export default ComposerWithSuggestionsE2e; +export default forwardRef(ComposerWithSuggestionsE2e); export {getRerenderCount, resetRerenderCount}; diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index d7e02fe29fcd..70c5f61d7725 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -1,7 +1,7 @@ import {PortalHost} from '@gorhom/portal'; import type {SyntheticEvent} from 'react'; import React, {useCallback, useEffect, useMemo, useRef, useState} from 'react'; -import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputSelectionChangeEventData} from 'react-native'; +import type {MeasureInWindowOnSuccessCallback, NativeSyntheticEvent, TextInputFocusEventData, TextInputSelectionChangeEventData} from 'react-native'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; @@ -123,7 +123,7 @@ function ReportActionCompose({ const {isMediumScreenWidth, isSmallScreenWidth} = useWindowDimensions(); const {isOffline} = useNetwork(); const animatedRef = useAnimatedRef(); - const actionButtonRef = useRef(null); + const actionButtonRef = useRef(null); const personalDetails = usePersonalDetails() || CONST.EMPTY_OBJECT; /** * Updates the Highlight state of the composer @@ -300,12 +300,13 @@ function ReportActionCompose({ isKeyboardVisibleWhenShowingModalRef.current = true; }, []); - const onBlur = useCallback((event: FocusEvent) => { + const onBlur = useCallback((event: NativeSyntheticEvent) => { + const webEvent = event as unknown as FocusEvent; setIsFocused(false); if (suggestionsRef.current) { suggestionsRef.current.resetSuggestions(); } - if (event.relatedTarget && event.relatedTarget === actionButtonRef.current) { + if (webEvent.relatedTarget && webEvent.relatedTarget === actionButtonRef.current) { isKeyboardVisibleWhenShowingModalRef.current = true; } }, []); diff --git a/src/pages/home/report/ReportActionCompose/suggestionProps.js b/src/pages/home/report/ReportActionCompose/suggestionProps.js deleted file mode 100644 index 62c29f3d418e..000000000000 --- a/src/pages/home/report/ReportActionCompose/suggestionProps.js +++ /dev/null @@ -1,39 +0,0 @@ -import PropTypes from 'prop-types'; - -const baseProps = { - /** The current input value */ - value: PropTypes.string.isRequired, - - /** Callback to update the current input value */ - setValue: PropTypes.func.isRequired, - - /** The current selection value */ - selection: PropTypes.shape({ - start: PropTypes.number.isRequired, - end: PropTypes.number.isRequired, - }).isRequired, - - /** Callback to update the current selection */ - setSelection: PropTypes.func.isRequired, - - /** Whether the composer is expanded */ - isComposerFullSize: PropTypes.bool.isRequired, - - /** Callback to update the comment draft */ - updateComment: PropTypes.func.isRequired, - - /** Meaures the parent container's position and dimensions. */ - measureParentContainer: PropTypes.func.isRequired, - - /** Report composer focus state */ - isComposerFocused: PropTypes.bool, -}; - -const implementationBaseProps = { - /** Whether to use the small or the big suggestion picker */ - isAutoSuggestionPickerLarge: PropTypes.bool.isRequired, - - ...baseProps, -}; - -export {baseProps, implementationBaseProps}; diff --git a/src/pages/home/report/ReportTypingIndicator.js b/src/pages/home/report/ReportTypingIndicator.tsx similarity index 100% rename from src/pages/home/report/ReportTypingIndicator.js rename to src/pages/home/report/ReportTypingIndicator.tsx diff --git a/src/types/modules/react-native.d.ts b/src/types/modules/react-native.d.ts index aaa7058737ae..7313a28984fc 100644 --- a/src/types/modules/react-native.d.ts +++ b/src/types/modules/react-native.d.ts @@ -279,14 +279,9 @@ declare module 'react-native' { * Extracted from react-native-web, packages/react-native-web/src/exports/TextInput/types.js */ interface WebTextInputProps extends WebSharedProps { - dir?: 'auto' | 'ltr' | 'rtl'; disabled?: boolean; - enterKeyHint?: 'enter' | 'done' | 'go' | 'next' | 'previous' | 'search' | 'send'; - readOnly?: boolean; } interface TextInputProps extends WebTextInputProps { - // TODO: remove once the app is updated to RN 0.73 - smartInsertDelete?: boolean; isFullComposerAvailable?: boolean; } From cf84b3b817a85d6baa5c28e2913068e2d487b260 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 17:47:16 +0100 Subject: [PATCH 14/29] fix: move ReportTypingIndicator to TS --- .../home/report/ReportTypingIndicator.tsx | 27 +++++++++---------- 1 file changed, 13 insertions(+), 14 deletions(-) diff --git a/src/pages/home/report/ReportTypingIndicator.tsx b/src/pages/home/report/ReportTypingIndicator.tsx index 785f1e3f6a1e..f7f135e7374e 100755 --- a/src/pages/home/report/ReportTypingIndicator.tsx +++ b/src/pages/home/report/ReportTypingIndicator.tsx @@ -1,7 +1,6 @@ -import PropTypes from 'prop-types'; import React, {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import Text from '@components/Text'; import TextWithEllipsis from '@components/TextWithEllipsis'; import useLocalize from '@hooks/useLocalize'; @@ -9,28 +8,30 @@ import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import * as ReportUtils from '@libs/ReportUtils'; import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportUserIsTyping} from '@src/types/onyx'; -const propTypes = { +type ReportTypingIndicatorOnyxProps = { /** Key-value pairs of user accountIDs/logins and whether or not they are typing. Keys are accountIDs or logins. */ - userTypingStatuses: PropTypes.objectOf(PropTypes.bool), + userTypingStatuses: OnyxEntry; }; -const defaultProps = { - userTypingStatuses: {}, -}; +type ReportTypingIndicatorProps = { + // eslint-disable-next-line react/no-unused-prop-types + reportID: string; +} & ReportTypingIndicatorOnyxProps; -function ReportTypingIndicator({userTypingStatuses}) { +function ReportTypingIndicator({userTypingStatuses}: ReportTypingIndicatorProps) { const {translate} = useLocalize(); const {isOffline} = useNetwork(); const styles = useThemeStyles(); - const usersTyping = useMemo(() => _.filter(_.keys(userTypingStatuses), (loginOrAccountID) => userTypingStatuses[loginOrAccountID]), [userTypingStatuses]); + const usersTyping = useMemo(() => Object.keys(userTypingStatuses ?? {}).filter((loginOrAccountID) => userTypingStatuses?.[loginOrAccountID]), [userTypingStatuses]); const firstUserTyping = usersTyping[0]; const isUserTypingADisplayName = Number.isNaN(Number(firstUserTyping)); // If we are offline, the user typing statuses are not up-to-date so do not show them - if (isOffline || !firstUserTyping) { + if (!!isOffline || !firstUserTyping) { return null; } @@ -40,6 +41,7 @@ function ReportTypingIndicator({userTypingStatuses}) { if (usersTyping.length === 1) { return ( ({ userTypingStatuses: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_USER_IS_TYPING}${reportID}`, - initialValue: {}, }, })(ReportTypingIndicator); From 87f22f7e925b169a76d74e9a3eed6643283306c9 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 24 Jan 2024 18:26:51 +0100 Subject: [PATCH 15/29] fix: migrate ParticipanLocalTime and ReportDropUI --- ...tLocalTime.js => ParticipantLocalTime.tsx} | 35 ++++++++++--------- .../{ReportDropUI.js => ReportDropUI.tsx} | 9 ++--- .../home/report/ReportTypingIndicator.tsx | 2 +- src/types/onyx/IOU.ts | 2 ++ 4 files changed, 25 insertions(+), 23 deletions(-) rename src/pages/home/report/{ParticipantLocalTime.js => ParticipantLocalTime.tsx} (66%) rename src/pages/home/report/{ReportDropUI.js => ReportDropUI.tsx} (87%) diff --git a/src/pages/home/report/ParticipantLocalTime.js b/src/pages/home/report/ParticipantLocalTime.tsx similarity index 66% rename from src/pages/home/report/ParticipantLocalTime.js rename to src/pages/home/report/ParticipantLocalTime.tsx index 1992953c959e..c8bbaf031e32 100644 --- a/src/pages/home/report/ParticipantLocalTime.js +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -1,36 +1,39 @@ -import lodashGet from 'lodash/get'; import React, {useEffect, useState} from 'react'; import {View} from 'react-native'; -import participantPropTypes from '@components/participantPropTypes'; import Text from '@components/Text'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import Timers from '@libs/Timers'; import CONST from '@src/CONST'; +import type {PersonalDetails} from '@src/types/onyx'; +import type DeepValueOf from '@src/types/utils/DeepValueOf'; -const propTypes = { +type Locales = DeepValueOf; + +type ParticipantLocalTimeProps = { /** Personal details of the participant */ - participant: participantPropTypes.isRequired, + participant: PersonalDetails; - ...withLocalizePropTypes, + preferredLocale: Locales; }; -function getParticipantLocalTime(participant, preferredLocale) { - const reportRecipientTimezone = lodashGet(participant, 'timezone', CONST.DEFAULT_TIME_ZONE); - const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale, null, reportRecipientTimezone.selected); +function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locales) { + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; + const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale, undefined, reportRecipientTimezone.selected); const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale); - const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone); - const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone); + const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone.toDateString()); + const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone.toDateString()); if (reportRecipientDay !== currentUserDay) { return `${DateUtils.formatToLocalTime(reportTimezone)} ${reportRecipientDay}`; } return `${DateUtils.formatToLocalTime(reportTimezone)}`; } -function ParticipantLocalTime(props) { +function ParticipantLocalTime({participant, preferredLocale}: ParticipantLocalTimeProps) { + const {translate} = useLocalize(); const styles = useThemeStyles(); - const {participant, preferredLocale, translate} = props; const [localTime, setLocalTime] = useState(() => getParticipantLocalTime(participant, preferredLocale)); useEffect(() => { @@ -44,7 +47,8 @@ function ParticipantLocalTime(props) { }; }, [participant, preferredLocale]); - const reportRecipientDisplayName = lodashGet(props, 'participant.firstName') || lodashGet(props, 'participant.displayName'); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + const reportRecipientDisplayName = participant.firstName || participant.displayName; if (!reportRecipientDisplayName) { return null; @@ -65,7 +69,6 @@ function ParticipantLocalTime(props) { ); } -ParticipantLocalTime.propTypes = propTypes; ParticipantLocalTime.displayName = 'ParticipantLocalTime'; -export default withLocalize(ParticipantLocalTime); +export default ParticipantLocalTime; diff --git a/src/pages/home/report/ReportDropUI.js b/src/pages/home/report/ReportDropUI.tsx similarity index 87% rename from src/pages/home/report/ReportDropUI.js rename to src/pages/home/report/ReportDropUI.tsx index c1c3b8e506ab..d147d0c0c03d 100644 --- a/src/pages/home/report/ReportDropUI.js +++ b/src/pages/home/report/ReportDropUI.tsx @@ -1,4 +1,3 @@ -import PropTypes from 'prop-types'; import React from 'react'; import {View} from 'react-native'; import DragAndDropConsumer from '@components/DragAndDrop/Consumer'; @@ -8,12 +7,11 @@ import Text from '@components/Text'; import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; -const propTypes = { +type ReportDropUIProps = { /** Callback to execute when a file is dropped. */ - onDrop: PropTypes.func.isRequired, + onDrop: () => void; }; - -function ReportDropUI({onDrop}) { +function ReportDropUI({onDrop}: ReportDropUIProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); return ( @@ -33,6 +31,5 @@ function ReportDropUI({onDrop}) { } ReportDropUI.displayName = 'ReportDropUI'; -ReportDropUI.propTypes = propTypes; export default ReportDropUI; diff --git a/src/pages/home/report/ReportTypingIndicator.tsx b/src/pages/home/report/ReportTypingIndicator.tsx index f7f135e7374e..f62db8bf0337 100755 --- a/src/pages/home/report/ReportTypingIndicator.tsx +++ b/src/pages/home/report/ReportTypingIndicator.tsx @@ -16,7 +16,7 @@ type ReportTypingIndicatorOnyxProps = { }; type ReportTypingIndicatorProps = { - // eslint-disable-next-line react/no-unused-prop-types + // eslint-disable-next-line react/no-unused-prop-types -- This is used by withOnyx reportID: string; } & ReportTypingIndicatorOnyxProps; diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index a89b0d4530ef..220af7005c45 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -23,3 +23,5 @@ type IOU = { }; export default IOU; + +export type {Participant}; From 63a63ad6df7a87ceeb99de66e8316200d30721cf Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 09:36:42 +0100 Subject: [PATCH 16/29] fix: typecheck --- src/components/LocaleContextProvider.tsx | 2 +- src/pages/home/report/ParticipantLocalTime.tsx | 12 +++++------- src/pages/home/report/ReportDropUI.tsx | 2 +- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 7313bb4aa7bb..25b468181b87 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -132,4 +132,4 @@ Provider.displayName = 'withOnyx(LocaleContextProvider)'; export {Provider as LocaleContextProvider, LocaleContext}; -export type {LocaleContextProps}; +export type {LocaleContextProps, Locale}; diff --git a/src/pages/home/report/ParticipantLocalTime.tsx b/src/pages/home/report/ParticipantLocalTime.tsx index c8bbaf031e32..5d90659fb96a 100644 --- a/src/pages/home/report/ParticipantLocalTime.tsx +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -5,24 +5,22 @@ import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@hooks/useThemeStyles'; import DateUtils from '@libs/DateUtils'; import Timers from '@libs/Timers'; +import type {Locale} from '@src/components/LocaleContextProvider'; import CONST from '@src/CONST'; import type {PersonalDetails} from '@src/types/onyx'; -import type DeepValueOf from '@src/types/utils/DeepValueOf'; - -type Locales = DeepValueOf; type ParticipantLocalTimeProps = { /** Personal details of the participant */ participant: PersonalDetails; - preferredLocale: Locales; + preferredLocale?: Locale; }; -function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locales) { +function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locale | undefined) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; - const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale, undefined, reportRecipientTimezone.selected); - const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale); + const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT, undefined, reportRecipientTimezone.selected); + const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT); const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone.toDateString()); const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone.toDateString()); if (reportRecipientDay !== currentUserDay) { diff --git a/src/pages/home/report/ReportDropUI.tsx b/src/pages/home/report/ReportDropUI.tsx index d147d0c0c03d..fad58d60bbfa 100644 --- a/src/pages/home/report/ReportDropUI.tsx +++ b/src/pages/home/report/ReportDropUI.tsx @@ -9,7 +9,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; type ReportDropUIProps = { /** Callback to execute when a file is dropped. */ - onDrop: () => void; + onDrop: (event: DragEvent) => void; }; function ReportDropUI({onDrop}: ReportDropUIProps) { const styles = useThemeStyles(); From 1225d237f1123cb89b8e29cdc36a022042e1ad1e Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 13:47:27 +0100 Subject: [PATCH 17/29] fix: minor fixes --- src/pages/home/report/ParticipantLocalTime.tsx | 5 +++-- .../ComposerWithSuggestions/ComposerWithSuggestions.tsx | 2 +- src/pages/home/report/ReportActionCompose/SendButton.tsx | 3 ++- .../home/report/ReportActionCompose/SuggestionMention.tsx | 6 ++++-- 4 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pages/home/report/ParticipantLocalTime.tsx b/src/pages/home/report/ParticipantLocalTime.tsx index 5d90659fb96a..e97fad8f20f0 100644 --- a/src/pages/home/report/ParticipantLocalTime.tsx +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -13,11 +13,12 @@ type ParticipantLocalTimeProps = { /** Personal details of the participant */ participant: PersonalDetails; + /** The user's preferred locale e.g. 'en', 'es-ES' */ preferredLocale?: Locale; }; function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locale | undefined) { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT, undefined, reportRecipientTimezone.selected); const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT); @@ -45,7 +46,7 @@ function ParticipantLocalTime({participant, preferredLocale}: ParticipantLocalTi }; }, [participant, preferredLocale]); - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null const reportRecipientDisplayName = participant.firstName || participant.displayName; if (!reportRecipientDisplayName) { diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 8c09f53d3b27..96d2e1a26f6b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -209,7 +209,7 @@ function ComposerWithSuggestions( const syncSelectionWithOnChangeTextRef = useRef(null); - const suggestions = suggestionsRef.current?.getSuggestions(); + const suggestions = suggestionsRef.current?.getSuggestions() ?? (() => []); const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions?.length ?? 0); diff --git a/src/pages/home/report/ReportActionCompose/SendButton.tsx b/src/pages/home/report/ReportActionCompose/SendButton.tsx index b6c6200fc7c0..453ee9310f46 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.tsx +++ b/src/pages/home/report/ReportActionCompose/SendButton.tsx @@ -24,7 +24,8 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP const {translate} = useLocalize(); const Tap = Gesture.Tap() - .enabled(!isDisabledProp) + // @ts-expect-error Enabled require argument but when passing something button is not working + .enabled() .onEnd(() => { handleSendMessage(); }); diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index d5a3a8983467..02bcd27093e7 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -145,8 +145,10 @@ function SuggestionMention( }); const sortedPersonalDetails = filteredPersonalDetails.sort((a, b) => { - const nameA = a?.displayName ?? a?.login ?? ''; - const nameB = b?.displayName ?? b?.login ?? ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null + const nameA = a?.displayName || a?.login || ''; + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null + const nameB = b?.displayName || b?.login || ''; if (nameA < nameB) { return -1; From 4f907d25135d3bdc12e7a5b7971e1d71c69b558a Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 25 Jan 2024 13:55:20 +0100 Subject: [PATCH 18/29] fix: removed unused export --- src/types/onyx/IOU.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/types/onyx/IOU.ts b/src/types/onyx/IOU.ts index 220af7005c45..a89b0d4530ef 100644 --- a/src/types/onyx/IOU.ts +++ b/src/types/onyx/IOU.ts @@ -23,5 +23,3 @@ type IOU = { }; export default IOU; - -export type {Participant}; From efa41789b32f559b5618d316be79d8b1398e53e4 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 11:16:15 +0100 Subject: [PATCH 19/29] fix: typecheck --- src/libs/ComposerUtils/index.ts | 4 ++-- src/libs/EmojiUtils.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 94bba5d0d00c..de937c0ba915 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -18,8 +18,8 @@ function insertText(text: string, selection: Selection, textToInsert: string): s * Insert a white space at given index of text * @param text - text that needs whitespace to be appended to */ -function insertWhiteSpaceAtIndex(text: string, index: number) { - return `${text.slice(0, index)} ${text.slice(index)}`; +function insertWhiteSpaceAtIndex(text: string | null, index: number) { + return `${text?.slice(0, index)} ${text?.slice(index)}`; } /** diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 7971e6147c19..ecbed9c87eff 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -370,7 +370,7 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF /** * Find all emojis in a text and replace them with their code. */ -function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { +function replaceAndExtractEmojis(text: string | null, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang); return { From 99a4110c3e4dde03061ee0872b51949b4caec0f8 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Tue, 30 Jan 2024 11:58:11 +0100 Subject: [PATCH 20/29] fix: typecheck --- src/libs/E2E/tests/chatOpeningTest.e2e.ts | 3 ++- src/libs/E2E/tests/reportTypingTest.e2e.ts | 3 ++- src/libs/E2E/types.ts | 2 +- src/libs/EmojiUtils.ts | 4 ++-- .../ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx | 3 ++- 5 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts index ef380f847c3f..2e2ce76b348d 100644 --- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts +++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts @@ -1,3 +1,4 @@ +import type {NativeConfig} from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; @@ -12,7 +13,7 @@ const test = (config: TestConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for chat opening'); - const reportID = getConfigValueOrThrow('reportID', config); + const reportID = getConfigValueOrThrow('reportID', config as NativeConfig); E2ELogin().then((neededLogin) => { if (neededLogin) { diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts index 4e0678aeb020..17464f7eb8d6 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts @@ -1,3 +1,4 @@ +import type {NativeConfig} from 'react-native-config'; import Config from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; @@ -16,7 +17,7 @@ const test = (config: TestConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for typing'); - const reportID = getConfigValueOrThrow('reportID', config); + const reportID = getConfigValueOrThrow('reportID', config as NativeConfig); E2ELogin().then((neededLogin) => { if (neededLogin) { diff --git a/src/libs/E2E/types.ts b/src/libs/E2E/types.ts index 2d48813fa115..93640fbb4ce8 100644 --- a/src/libs/E2E/types.ts +++ b/src/libs/E2E/types.ts @@ -20,7 +20,7 @@ type NetworkCacheMap = Record< type TestConfig = { name: string; - [key: string]: string; + [key: string]: string | {autoFocus: boolean}; }; export type {SigninParams, IsE2ETestSession, NetworkCacheMap, NetworkCacheEntry, TestConfig}; diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index ecbed9c87eff..12319e342fb8 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -371,11 +371,11 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF * Find all emojis in a text and replace them with their code. */ function replaceAndExtractEmojis(text: string | null, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { - const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang); + const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text ?? '', preferredSkinTone, lang); return { text: convertedText, - emojis: emojis.concat(extractEmojis(text)), + emojis: emojis.concat(extractEmojis(text ?? '')), cursorPosition, }; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx index 87352981ba9a..94e65a48e46b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx @@ -19,7 +19,8 @@ function IncrementRenderCount() { function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: ForwardedRef) { // Eventually Auto focus on e2e tests useEffect(() => { - if ((E2EClient.getCurrentActiveTestConfig()?.reportScreen?.autoFocus ?? false) === false) { + const testConfig = E2EClient.getCurrentActiveTestConfig(); + if (testConfig?.reportScreen && typeof testConfig.reportScreen !== 'string' && (testConfig?.reportScreen.autoFocus ?? false) === false) { return; } From 45c6530265b83384f1b06b8f58725e266aa7d315 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 2 Feb 2024 16:57:35 +0100 Subject: [PATCH 21/29] fix: typecheck --- src/components/AttachmentModal.tsx | 8 ++++---- .../API/parameters/AddCommentOrAttachementParams.ts | 4 +++- src/libs/ReportUtils.ts | 5 +++-- src/libs/actions/Report.ts | 5 +++-- .../AttachmentPickerWithMenuItems.tsx | 3 ++- .../ComposerWithSuggestions.tsx | 7 +++++-- .../ReportActionCompose/ReportActionCompose.tsx | 12 +++++------- .../SilentCommentUpdater/types.ts | 4 ---- src/types/onyx/ReportAction.ts | 3 ++- 9 files changed, 27 insertions(+), 24 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 90954c63b751..f4b016b790de 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -80,7 +80,7 @@ type ImagePickerResponse = { type FileObject = File | ImagePickerResponse; type ChildrenProps = { - displayFileInModal: (data: FileObject) => void; + displayFileInModal: (data: FileObject | undefined) => void; show: () => void; }; @@ -317,8 +317,8 @@ function AttachmentModal({ }, []); const validateAndDisplayFileToUpload = useCallback( - (data: FileObject) => { - if (!isDirectoryCheck(data)) { + (data: FileObject | undefined) => { + if (!data || !isDirectoryCheck(data)) { return; } let fileObject = data; @@ -617,4 +617,4 @@ export default withOnyx({ }, })(memo(AttachmentModal)); -export type {Attachment}; +export type {Attachment, FileObject}; diff --git a/src/libs/API/parameters/AddCommentOrAttachementParams.ts b/src/libs/API/parameters/AddCommentOrAttachementParams.ts index 58faf9fdfc9c..4eab35be7dd2 100644 --- a/src/libs/API/parameters/AddCommentOrAttachementParams.ts +++ b/src/libs/API/parameters/AddCommentOrAttachementParams.ts @@ -1,9 +1,11 @@ +import type {FileObject} from '@components/AttachmentModal'; + type AddCommentOrAttachementParams = { reportID: string; reportActionID?: string; commentReportActionID?: string | null; reportComment?: string; - file?: File; + file?: Partial; timezone?: string; shouldAllowActionableMentionWhispers?: boolean; clientCreatedTime?: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 5f3efcbcdbb0..651cd1f6513d 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -8,6 +8,7 @@ import lodashIsEqual from 'lodash/isEqual'; import type {OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-native-onyx'; import Onyx from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@components/AttachmentModal'; import * as Expensicons from '@components/Icon/Expensicons'; import * as defaultWorkspaceAvatars from '@components/Icon/WorkspaceDefaultAvatars'; import CONST from '@src/CONST'; @@ -1290,7 +1291,7 @@ function getRoomWelcomeMessage(report: OnyxEntry, isUserPolicyAdmin: boo /** * Returns true if Concierge is one of the chat participants (1:1 as well as group chats) */ -function chatIncludesConcierge(report: OnyxEntry): boolean { +function chatIncludesConcierge(report: Partial>): boolean { return Boolean(report?.participantAccountIDs?.length && report?.participantAccountIDs?.includes(CONST.ACCOUNT_ID.CONCIERGE)); } @@ -2591,7 +2592,7 @@ function getParsedComment(text: string): string { return text.length <= CONST.MAX_MARKUP_LENGTH ? parser.replace(text) : lodashEscape(text); } -function buildOptimisticAddCommentReportAction(text?: string, file?: File, actorAccountID?: number): OptimisticReportAction { +function buildOptimisticAddCommentReportAction(text?: string, file?: Partial, actorAccountID?: number): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? ''); const isAttachment = !text && file !== undefined; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index 782cf2b174c2..a508325a3206 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -7,6 +7,7 @@ import type {NullishDeep, OnyxCollection, OnyxEntry, OnyxUpdate} from 'react-nat import Onyx from 'react-native-onyx'; import type {PartialDeep, ValueOf} from 'type-fest'; import type {Emoji} from '@assets/emojis/types'; +import type {FileObject} from '@components/AttachmentModal'; import * as ActiveClientManager from '@libs/ActiveClientManager'; import * as API from '@libs/API'; import type { @@ -352,7 +353,7 @@ function notifyNewAction(reportID: string, accountID?: number, reportActionID?: * - Adding one attachment * - Add both a comment and attachment simultaneously */ -function addActions(reportID: string, text = '', file?: File) { +function addActions(reportID: string, text = '', file?: Partial) { let reportCommentText = ''; let reportCommentAction: OptimisticAddCommentReportAction | undefined; let attachmentAction: OptimisticAddCommentReportAction | undefined; @@ -511,7 +512,7 @@ function addActions(reportID: string, text = '', file?: File) { } /** Add an attachment and optional comment. */ -function addAttachment(reportID: string, file: File, text = '') { +function addAttachment(reportID: string, file: Partial, text = '') { addActions(reportID, text, file); } diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 0dbfbba2759d..9511498ea699 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -6,6 +6,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {SvgProps} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -46,7 +47,7 @@ type AttachmentPickerWithMenuItemsProps = { report: OnyxEntry; /** Callback to open the file in the modal */ - displayFileInModal: (url: string) => void; + displayFileInModal: (url: FileObject | undefined) => void; /** Whether or not the full size composer is available */ isFullComposerAvailable: boolean; diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 074383897dac..76388372dd27 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -16,6 +16,7 @@ import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; import type {useAnimatedRef} from 'react-native-reanimated'; import type {Emoji} from '@assets/emojis/types'; +import type {FileObject} from '@components/AttachmentModal'; import Composer from '@components/Composer'; import useKeyboardState from '@hooks/useKeyboardState'; import useLocalize from '@hooks/useLocalize'; @@ -85,7 +86,7 @@ type ComposerWithSuggestionsProps = { isComposerFullSize: boolean; isMenuVisible: boolean; inputPlaceholder: string; - displayFileInModal: (file: File | undefined) => void; + displayFileInModal: (file: FileObject | undefined) => void; textInputShouldClear: boolean; setTextInputShouldClear: (shouldClear: boolean) => void; isBlockedFromConcierge: boolean; @@ -107,6 +108,8 @@ type ComposerWithSuggestionsProps = { lastReportAction?: OnyxTypes.ReportAction; includeChronos?: boolean; parentReportActionID?: string; + // eslint-disable-next-line react/no-unused-prop-types + parentReportID: string | undefined; } & ComposerWithSuggestionsOnyxProps & Partial; @@ -748,7 +751,7 @@ export default withOnyx `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.parentReportID}`, + key: ({parentReportID}) => `${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${parentReportID}`, canEvict: false, initWithStoredValues: false, }, diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index a5646f9c99a3..02d4105c3500 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -76,9 +76,6 @@ type ReportActionComposeProps = { /** The ID of the report actions will be created for */ reportID: string; - /** Array of report actions for this report */ - reportActions?: OnyxTypes.ReportAction[]; - /** The report currently being looked at */ report: OnyxEntry; @@ -97,9 +94,11 @@ type ReportActionComposeProps = { /** Whether the report is ready for display */ isReportReadyForDisplay?: boolean; + /** Whether the chat is empty */ isEmptyChat?: boolean; - lastReportAction?: any; + /** The last report action */ + lastReportAction?: OnyxTypes.ReportAction; } & ReportActionComposeOnyxProps & WithCurrentUserPersonalDetailsProps; @@ -118,7 +117,6 @@ function ReportActionCompose({ pendingAction, report, reportID, - reportActions, listHeight = 0, shouldShowComposeInput = true, isReportReadyForDisplay = true, @@ -269,7 +267,7 @@ function ReportActionCompose({ }, []); const addAttachment = useCallback( - (file: FileObject) => { + (file: Partial) => { const newComment = composerRef.current?.prepareCommentAndResetComposer(); Report.addAttachment(reportID, file, newComment); setTextInputShouldClear(false); @@ -437,7 +435,7 @@ function ReportActionCompose({ reportID={reportID} parentReportID={report?.parentReportID} parentReportActionID={report?.parentReportActionID} - includesChronos={ReportUtils.chatIncludesChronos(report)} + includeChronos={ReportUtils.chatIncludesChronos(report)} isEmptyChat={isEmptyChat} lastReportAction={lastReportAction} isMenuVisible={isMenuVisible} diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts index 591ee43ce6cd..1ba1689e7d6d 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts @@ -1,5 +1,4 @@ import type {OnyxEntry} from 'react-native-onyx'; -import type {Report} from '@src/types/onyx'; type SilentCommentUpdaterOnyxProps = { /** The comment of the report */ @@ -13,9 +12,6 @@ type SilentCommentUpdaterProps = { /** The ID of the report associated with the comment */ reportID: string; - /** The report associated with the comment */ - report: OnyxEntry; - /** The value of the comment */ value: string; diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index d2f0afad5b7a..59fb785234eb 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -1,4 +1,5 @@ import type {ValueOf} from 'type-fest'; +import type {FileObject} from '@components/AttachmentModal'; import type {AvatarSource} from '@libs/UserUtils'; import type CONST from '@src/CONST'; import type {EmptyObject} from '@src/types/utils/EmptyObject'; @@ -165,7 +166,7 @@ type ReportActionBase = { isFirstItem?: boolean; /** Informations about attachments of report action */ - attachmentInfo?: File | EmptyObject; + attachmentInfo?: Partial | EmptyObject; /** Receipt tied to report action */ receipt?: Receipt; From 61ad4769e0215b48706a74615e843fb765eeab58 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 2 Feb 2024 18:51:38 +0100 Subject: [PATCH 22/29] fix: resolve comments --- src/components/Composer/types.ts | 4 +- src/components/LocaleContextProvider.tsx | 4 +- src/libs/ComposerUtils/index.ts | 2 +- src/libs/DateUtils.ts | 2 +- src/libs/E2E/tests/chatOpeningTest.e2e.ts | 5 +- src/libs/E2E/tests/reportTypingTest.e2e.ts | 5 +- src/libs/EmojiUtils.ts | 7 +- .../home/report/ParticipantLocalTime.tsx | 4 +- .../AttachmentPickerWithMenuItems.tsx | 2 +- .../ComposerWithSuggestions.tsx | 73 +++++++++++++++++-- .../ComposerWithSuggestions/index.e2e.tsx | 8 +- .../ReportActionCompose.tsx | 46 ++++++------ .../report/ReportActionCompose/SendButton.tsx | 2 - .../SilentCommentUpdater/index.android.tsx | 2 +- .../SilentCommentUpdater/index.tsx | 4 +- .../SilentCommentUpdater/types.ts | 6 +- .../ReportActionCompose/SuggestionEmoji.tsx | 13 ++-- .../ReportActionCompose/SuggestionMention.tsx | 10 +-- .../ReportActionCompose/Suggestions.tsx | 21 ++++++ .../home/report/ReportTypingIndicator.tsx | 4 +- 20 files changed, 147 insertions(+), 77 deletions(-) diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index 9565eaf6208f..f19feb94dd0a 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -5,7 +5,7 @@ type TextSelection = { end?: number; }; -type ComposerProps = { +type ComposerProps = TextInputProps & { /** identify id in the text input */ id?: string; @@ -76,6 +76,6 @@ type ComposerProps = { /** Should make the input only scroll inside the element avoid scroll out to parent */ shouldContainScroll?: boolean; -} & TextInputProps; +}; export type {TextSelection, ComposerProps}; diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 25b468181b87..6d819f4d6eaa 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -45,7 +45,7 @@ type LocaleContextProps = { /** Returns a locally converted phone number for numbers from the same region * and an internationally converted phone number with the country code for numbers from other regions */ - formatPhoneNumber: (phoneNumber: string) => string; + formatPhoneNumber: (phoneNumber: string | undefined) => string; /** Gets the locale digit corresponding to a standard digit */ toLocaleDigit: (digit: string) => string; @@ -94,7 +94,7 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {} const updateLocale = useMemo(() => () => DateUtils.setLocale(locale), [locale]); - const formatPhoneNumber = useMemo(() => (phoneNumber) => LocalePhoneNumber.formatPhoneNumber(phoneNumber), []); + const formatPhoneNumber = useMemo(() => (phoneNumber) => LocalePhoneNumber.formatPhoneNumber(phoneNumber ?? ''), []); const toLocaleDigit = useMemo(() => (digit) => LocaleDigitUtils.toLocaleDigit(locale, digit), [locale]); diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index de937c0ba915..6018cad86e47 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -18,7 +18,7 @@ function insertText(text: string, selection: Selection, textToInsert: string): s * Insert a white space at given index of text * @param text - text that needs whitespace to be appended to */ -function insertWhiteSpaceAtIndex(text: string | null, index: number) { +function insertWhiteSpaceAtIndex(text: string, index: number) { return `${text?.slice(0, index)} ${text?.slice(index)}`; } diff --git a/src/libs/DateUtils.ts b/src/libs/DateUtils.ts index 9cb08556f082..c10d6b90128b 100644 --- a/src/libs/DateUtils.ts +++ b/src/libs/DateUtils.ts @@ -265,7 +265,7 @@ function formatToLongDateWithWeekday(datetime: string | Date): string { * * @returns Sunday */ -function formatToDayOfWeek(datetime: string): string { +function formatToDayOfWeek(datetime: Date): string { return format(new Date(datetime), CONST.DATE.WEEKDAY_TIME_FORMAT); } diff --git a/src/libs/E2E/tests/chatOpeningTest.e2e.ts b/src/libs/E2E/tests/chatOpeningTest.e2e.ts index 2e2ce76b348d..17d9dfa1cb4d 100644 --- a/src/libs/E2E/tests/chatOpeningTest.e2e.ts +++ b/src/libs/E2E/tests/chatOpeningTest.e2e.ts @@ -2,18 +2,17 @@ import type {NativeConfig} from 'react-native-config'; import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; -import type {TestConfig} from '@libs/E2E/types'; import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; -const test = (config: TestConfig) => { +const test = (config: NativeConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for chat opening'); - const reportID = getConfigValueOrThrow('reportID', config as NativeConfig); + const reportID = getConfigValueOrThrow('reportID', config); E2ELogin().then((neededLogin) => { if (neededLogin) { diff --git a/src/libs/E2E/tests/reportTypingTest.e2e.ts b/src/libs/E2E/tests/reportTypingTest.e2e.ts index 17464f7eb8d6..817bda941611 100644 --- a/src/libs/E2E/tests/reportTypingTest.e2e.ts +++ b/src/libs/E2E/tests/reportTypingTest.e2e.ts @@ -4,7 +4,6 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import waitForKeyboard from '@libs/E2E/actions/waitForKeyboard'; import E2EClient from '@libs/E2E/client'; -import type {TestConfig} from '@libs/E2E/types'; import getConfigValueOrThrow from '@libs/E2E/utils/getConfigValueOrThrow'; import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; @@ -13,11 +12,11 @@ import CONST from '@src/CONST'; import ROUTES from '@src/ROUTES'; import * as NativeCommands from '../../../../tests/e2e/nativeCommands/NativeCommandsAction'; -const test = (config: TestConfig) => { +const test = (config: NativeConfig) => { // check for login (if already logged in the action will simply resolve) console.debug('[E2E] Logging in for typing'); - const reportID = getConfigValueOrThrow('reportID', config as NativeConfig); + const reportID = getConfigValueOrThrow('reportID', config); E2ELogin().then((neededLogin) => { if (neededLogin) { diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index a7063e204efe..819aecf09d43 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -10,7 +10,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {FrequentlyUsedEmoji, Locale} from '@src/types/onyx'; import type {ReportActionReaction, UsersReactions} from '@src/types/onyx/ReportActionReactions'; import type IconAsset from '@src/types/utils/IconAsset'; -import type {SupportedLanguage} from './EmojiTrie'; type HeaderIndice = {code: string; index: number; icon: IconAsset}; type EmojiSpacer = {code: string; spacer: boolean}; @@ -370,12 +369,12 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF /** * Find all emojis in a text and replace them with their code. */ -function replaceAndExtractEmojis(text: string | null, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { +function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text ?? '', preferredSkinTone, lang); return { text: convertedText, - emojis: emojis.concat(extractEmojis(text ?? '')), + emojis: emojis.concat(extractEmojis(text)), cursorPosition, }; } @@ -384,7 +383,7 @@ function replaceAndExtractEmojis(text: string | null, preferredSkinTone: number * Suggest emojis when typing emojis prefix after colon * @param [limit] - matching emojis limit */ -function suggestEmojis(text: string, lang: SupportedLanguage, limit: number = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { +function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default; diff --git a/src/pages/home/report/ParticipantLocalTime.tsx b/src/pages/home/report/ParticipantLocalTime.tsx index e97fad8f20f0..58d85c0662b2 100644 --- a/src/pages/home/report/ParticipantLocalTime.tsx +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -22,8 +22,8 @@ function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT, undefined, reportRecipientTimezone.selected); const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT); - const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone.toDateString()); - const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone.toDateString()); + const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone); + const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone); if (reportRecipientDay !== currentUserDay) { return `${DateUtils.formatToLocalTime(reportTimezone)} ${reportRecipientDay}`; } diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 9511498ea699..e6a75e6f157b 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -194,7 +194,7 @@ function AttachmentPickerWithMenuItems({ return ( - {/* @ts-expect-error TODO: Remove this once SettlementButton (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */} + {/* @ts-expect-error TODO: Remove this once AttachmentPicker (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */} {({openPicker}) => { const triggerAttachmentPicker = () => { onTriggerAttachmentPicker(); diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index 76388372dd27..96989b2a647b 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -1,6 +1,6 @@ import {useIsFocused, useNavigation} from '@react-navigation/native'; import lodashDebounce from 'lodash/debounce'; -import type {ForwardedRef, RefAttributes, RefObject} from 'react'; +import type {ForwardedRef, MutableRefObject, RefAttributes, RefObject} from 'react'; import React, {forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState} from 'react'; import type { LayoutChangeEvent, @@ -79,35 +79,94 @@ type ComposerWithSuggestionsOnyxProps = { }; type ComposerWithSuggestionsProps = { + /** Report ID */ reportID: string; + + /** Callback to focus composer */ onFocus: () => void; + + /** Callback to blur composer */ onBlur: (event: NativeSyntheticEvent) => void; + + /** Callback to update the value of the composer */ onValueChange: (value: string) => void; + + /** Whether the composer is full size */ isComposerFullSize: boolean; + + /** Whether the menu is visible */ isMenuVisible: boolean; + + /** The placeholder for the input */ inputPlaceholder: string; + + /** Function to display a file in a modal */ displayFileInModal: (file: FileObject | undefined) => void; + + /** Whether the text input should clear */ textInputShouldClear: boolean; + + /** Function to set the text input should clear */ setTextInputShouldClear: (shouldClear: boolean) => void; + + /** Whether the user is blocked from concierge */ isBlockedFromConcierge: boolean; + + /** Whether the input is disabled */ disabled: boolean; + + /** Whether the full composer is available */ isFullComposerAvailable: boolean; + + /** Function to set whether the full composer is available */ setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; + + /** Function to set whether the comment is empty */ setIsCommentEmpty: (isCommentEmpty: boolean) => void; + + /** Function to handle sending a message */ handleSendMessage: () => void; + + /** Whether the compose input should show */ shouldShowComposeInput: OnyxEntry; + + /** Function to measure the parent container */ measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + + /** The height of the list */ listHeight: number; + + /** Whether the scroll is likely to trigger a layout */ isScrollLikelyLayoutTriggered: RefObject; + + /** Function to raise the scroll is likely layout triggered */ raiseIsScrollLikelyLayoutTriggered: () => void; + + /** The ref to the suggestions */ suggestionsRef: React.RefObject; + + /** The ref to the animated input */ animatedRef: AnimatedRef; - isNextModalWillOpenRef: RefObject; + + /** The ref to the next modal will open */ + isNextModalWillOpenRef: MutableRefObject; + + /** Whether the edit is focused */ editFocused: boolean; + + /** Wheater chat is empty */ isEmptyChat?: boolean; + + /** The last report action */ lastReportAction?: OnyxTypes.ReportAction; + + /** Whether to include chronos */ includeChronos?: boolean; + + /** The parent report action ID */ parentReportActionID?: string; + + /** The parent report ID */ // eslint-disable-next-line react/no-unused-prop-types parentReportID: string | undefined; } & ComposerWithSuggestionsOnyxProps & @@ -275,13 +334,13 @@ function ComposerWithSuggestions( * @property diff - The newly added characters. */ const findNewlyAddedChars = useCallback( - (prevText: string, newText: string | null): NewlyAddedChars => { + (prevText: string, newText: string): NewlyAddedChars => { let startIndex = -1; let endIndex = -1; let currentIndex = 0; // Find the first character mismatch with newText - while (currentIndex < (newText?.length ?? 0) && prevText.charAt(currentIndex) === newText?.charAt(currentIndex) && selection.start > currentIndex) { + while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText?.charAt(currentIndex) && selection.start > currentIndex) { currentIndex++; } @@ -295,7 +354,6 @@ function ComposerWithSuggestions( endIndex = currentIndex + (newText?.length ?? 0); } } - return { startIndex, endIndex, @@ -309,7 +367,7 @@ function ComposerWithSuggestions( * Update the value of the comment in Onyx */ const updateComment = useCallback( - (commentValue: string | null, shouldDebounceSaveComment?: boolean) => { + (commentValue: string, shouldDebounceSaveComment?: boolean) => { raiseIsScrollLikelyLayoutTriggered(); const {startIndex, endIndex, diff} = findNewlyAddedChars(lastTextRef.current, commentValue); const isEmojiInserted = diff.length && endIndex > startIndex && diff.trim() === diff && EmojiUtils.containsOnlyEmojis(diff); @@ -317,7 +375,7 @@ function ComposerWithSuggestions( text: newComment, emojis, cursorPosition, - } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue, preferredSkinTone, preferredLocale); + } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue ?? '', preferredSkinTone, preferredLocale); if (emojis.length) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (newEmojis.length) { @@ -595,7 +653,6 @@ function ComposerWithSuggestions( const prevIsFocused = usePrevious(isFocused); useEffect(() => { if (modal?.isVisible && !prevIsModalVisible) { - // @ts-expect-error need to reassign this ref // eslint-disable-next-line no-param-reassign isNextModalWillOpenRef.current = false; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx index 94e65a48e46b..0fa535329d33 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx @@ -1,4 +1,4 @@ -import type {ForwardedRef, RefObject} from 'react'; +import type {ForwardedRef} from 'react'; import React, {forwardRef, useEffect} from 'react'; import E2EClient from '@libs/E2E/client'; import type {ComposerRef} from '@pages/home/report/ReportActionCompose/ReportActionCompose'; @@ -20,17 +20,17 @@ function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: Fo // Eventually Auto focus on e2e tests useEffect(() => { const testConfig = E2EClient.getCurrentActiveTestConfig(); - if (testConfig?.reportScreen && typeof testConfig.reportScreen !== 'string' && (testConfig?.reportScreen.autoFocus ?? false) === false) { + if (testConfig?.reportScreen && typeof testConfig.reportScreen !== 'string' && !testConfig?.reportScreen.autoFocus) { return; } // We need to wait for the component to be mounted before focusing setTimeout(() => { - if (!(ref as RefObject)?.current) { + if (!(ref && 'current' in ref)) { return; } - (ref as RefObject).current?.focus(true); + ref.current?.focus(true); }, 1); }, [ref]); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 02d4105c3500..98630ff88b23 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -69,38 +69,38 @@ type ReportActionComposeOnyxProps = { shouldShowComposeInput: OnyxEntry; }; -type ReportActionComposeProps = { - /** A method to call when the form is submitted */ - onSubmit: (newComment: string | undefined) => void; +type ReportActionComposeProps = ReportActionComposeOnyxProps & + WithCurrentUserPersonalDetailsProps & { + /** A method to call when the form is submitted */ + onSubmit: (newComment: string | undefined) => void; - /** The ID of the report actions will be created for */ - reportID: string; + /** The ID of the report actions will be created for */ + reportID: string; - /** The report currently being looked at */ - report: OnyxEntry; + /** The report currently being looked at */ + report: OnyxEntry; - /** Is composer full size */ - isComposerFullSize?: boolean; + /** Is composer full size */ + isComposerFullSize?: boolean; - /** Whether user interactions should be disabled */ - disabled?: boolean; + /** Whether user interactions should be disabled */ + disabled?: boolean; - /** Height of the list which the composer is part of */ - listHeight?: number; + /** Height of the list which the composer is part of */ + listHeight?: number; - /** The type of action that's pending */ - pendingAction?: OnyxCommon.PendingAction; + /** The type of action that's pending */ + pendingAction?: OnyxCommon.PendingAction; - /** Whether the report is ready for display */ - isReportReadyForDisplay?: boolean; + /** Whether the report is ready for display */ + isReportReadyForDisplay?: boolean; - /** Whether the chat is empty */ - isEmptyChat?: boolean; + /** Whether the chat is empty */ + isEmptyChat?: boolean; - /** The last report action */ - lastReportAction?: OnyxTypes.ReportAction; -} & ReportActionComposeOnyxProps & - WithCurrentUserPersonalDetailsProps; + /** The last report action */ + lastReportAction?: OnyxTypes.ReportAction; + }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will // prevent auto focus on existing chat for mobile device diff --git a/src/pages/home/report/ReportActionCompose/SendButton.tsx b/src/pages/home/report/ReportActionCompose/SendButton.tsx index feeff49ceb55..4726c1638f42 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.tsx +++ b/src/pages/home/report/ReportActionCompose/SendButton.tsx @@ -46,8 +46,6 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP ]} role={CONST.ROLE.BUTTON} accessibilityLabel={translate('common.send')} - accessible - onPress={() => {}} > {({pressed}) => ( { - updateComment(comment); + updateComment(comment ?? ''); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount }, []); diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index a1d591c97297..c84bd3786610 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -17,7 +17,7 @@ function SilentCommentUpdater({comment, commentRef, reportID, value, updateComme const prevPreferredLocale = usePrevious(preferredLocale); useEffect(() => { - updateComment(comment ?? null); + updateComment(comment ?? ''); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount }, []); @@ -32,7 +32,7 @@ function SilentCommentUpdater({comment, commentRef, reportID, value, updateComme return; } - updateComment(comment); + updateComment(comment ?? ''); }, [prevCommentProp, prevPreferredLocale, prevReportId, comment, preferredLocale, reportID, updateComment, value, commentRef]); return null; diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts index 1ba1689e7d6d..dbc23b0279c3 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/types.ts @@ -5,9 +5,9 @@ type SilentCommentUpdaterOnyxProps = { comment: OnyxEntry; }; -type SilentCommentUpdaterProps = { +type SilentCommentUpdaterProps = SilentCommentUpdaterOnyxProps & { /** Updates the comment */ - updateComment: (comment: OnyxEntry) => void; + updateComment: (comment: string) => void; /** The ID of the report associated with the comment */ reportID: string; @@ -17,6 +17,6 @@ type SilentCommentUpdaterProps = { /** The ref of the comment */ commentRef: React.RefObject; -} & SilentCommentUpdaterOnyxProps; +}; export type {SilentCommentUpdaterProps, SilentCommentUpdaterOnyxProps}; diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index d343616a206d..206c91ed9b08 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -6,7 +6,6 @@ import type {Emoji} from '@assets/emojis/types'; import EmojiSuggestions from '@components/EmojiSuggestions'; import useArrowKeyFocusManager from '@hooks/useArrowKeyFocusManager'; import useLocalize from '@hooks/useLocalize'; -import type {SupportedLanguage} from '@libs/EmojiTrie'; import * as EmojiUtils from '@libs/EmojiUtils'; import * as SuggestionsUtils from '@libs/SuggestionUtils'; import CONST from '@src/CONST'; @@ -26,11 +25,11 @@ type SuggestionEmojiOnyxProps = { preferredSkinTone: number; }; -type SuggestionEmojiProps = { - /** Function to clear the input */ - resetKeyboardInput: (() => void) | undefined; -} & SuggestionEmojiOnyxProps & - SuggestionProps; +type SuggestionEmojiProps = SuggestionProps & + SuggestionEmojiOnyxProps & { + /** Function to clear the input */ + resetKeyboardInput: (() => void) | undefined; + }; /** * Check if this piece of string looks like an emoji @@ -166,7 +165,7 @@ function SuggestionEmoji( colonIndex, shouldShowSuggestionMenu: false, }; - const newSuggestedEmojis = EmojiUtils.suggestEmojis(leftString, preferredLocale as SupportedLanguage); + const newSuggestedEmojis = EmojiUtils.suggestEmojis(leftString, preferredLocale); if (newSuggestedEmojis?.length && isCurrentlyShowingEmojiSuggestion) { nextState.suggestedEmojis = newSuggestedEmojis; diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 02bcd27093e7..1e3e80ad9d7e 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -15,8 +15,6 @@ import type {PersonalDetailsList} from '@src/types/onyx'; import type {SuggestionsRef} from './ReportActionCompose'; import type {SuggestionProps} from './Suggestions'; -type SuggestionMentionProps = {isAutoSuggestionPickerLarge: boolean} & SuggestionProps; - type SuggestionValues = { suggestedMentions: Mention[]; atSignIndex: number; @@ -37,7 +35,7 @@ const defaultSuggestionsValues: SuggestionValues = { }; function SuggestionMention( - {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused}: SuggestionMentionProps, + {value, selection, setSelection, updateComment, isAutoSuggestionPickerLarge, measureParentContainer, isComposerFocused}: SuggestionProps, ref: ForwardedRef, ) { const personalDetails = usePersonalDetails() ?? CONST.EMPTY_OBJECT; @@ -161,7 +159,7 @@ function SuggestionMention( sortedPersonalDetails.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length).forEach((detail) => { suggestions.push({ text: PersonalDetailsUtils.getDisplayNameOrDefault(detail), - alternateText: formatPhoneNumber(detail?.login ?? ''), + alternateText: formatPhoneNumber(detail?.login), login: detail?.login, icons: [ { @@ -200,7 +198,7 @@ function SuggestionMention( const leftString = value.substring(0, suggestionEndIndex); const words = leftString.split(CONST.REGEX.SPACE_OR_EMOJI); - const lastWord = words.at(-1) ?? ''; + const lastWord: string = words.at(-1) ?? ''; const secondToLastWord = words[words.length - 3]; let atSignIndex; @@ -297,7 +295,7 @@ function SuggestionMention( mentions={suggestionValues.suggestedMentions} prefix={suggestionValues.mentionPrefix} onSelect={insertSelectedMention} - isMentionPickerLarge={isAutoSuggestionPickerLarge} + isMentionPickerLarge={!!isAutoSuggestionPickerLarge} measureParentContainer={measureParentContainer} /> ); diff --git a/src/pages/home/report/ReportActionCompose/Suggestions.tsx b/src/pages/home/report/ReportActionCompose/Suggestions.tsx index 0968a61c3abb..61026a792919 100644 --- a/src/pages/home/report/ReportActionCompose/Suggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/Suggestions.tsx @@ -14,16 +14,37 @@ type Selection = { }; type SuggestionProps = { + /** The current input value */ value: string; + + /** Callback to update the current input value */ setValue: (newValue: string) => void; + + /** The current selection value */ selection: Selection; + + /** Callback to update the current selection */ setSelection: (newSelection: Selection) => void; + + /** Callback to update the comment draft */ updateComment: (newComment: string, shouldDebounceSaveComment?: boolean) => void; + + /** Meaures the parent container's position and dimensions. */ measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + + /** Whether the composer is expanded */ isComposerFullSize: boolean; + + /** Report composer focus state */ isComposerFocused?: boolean; + + /** Callback to reset the keyboard input */ resetKeyboardInput?: () => void; + + /** Whether the auto suggestion picker is large */ isAutoSuggestionPickerLarge?: boolean; + + /** The height of the composer */ composerHeight?: number; }; diff --git a/src/pages/home/report/ReportTypingIndicator.tsx b/src/pages/home/report/ReportTypingIndicator.tsx index 6b00a4520295..e484ce9886a7 100755 --- a/src/pages/home/report/ReportTypingIndicator.tsx +++ b/src/pages/home/report/ReportTypingIndicator.tsx @@ -15,10 +15,10 @@ type ReportTypingIndicatorOnyxProps = { userTypingStatuses: OnyxEntry; }; -type ReportTypingIndicatorProps = { +type ReportTypingIndicatorProps = ReportTypingIndicatorOnyxProps & { // eslint-disable-next-line react/no-unused-prop-types -- This is used by withOnyx reportID: string; -} & ReportTypingIndicatorOnyxProps; +}; function ReportTypingIndicator({userTypingStatuses}: ReportTypingIndicatorProps) { const {translate} = useLocalize(); From fad94907fbdbd62dc75a4668055132d0dec9825b Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 5 Feb 2024 12:02:47 +0100 Subject: [PATCH 23/29] fix: resolve comments --- .../ReportActionCompose/SuggestionMention.tsx | 21 +++++-------------- 1 file changed, 5 insertions(+), 16 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index 1e3e80ad9d7e..2b5080e788ab 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -1,3 +1,4 @@ +import lodashSortBy from 'lodash/sortBy'; import type {ForwardedRef} from 'react'; import React, {forwardRef, useCallback, useEffect, useImperativeHandle, useRef, useState} from 'react'; import * as Expensicons from '@components/Icon/Expensicons'; @@ -142,20 +143,8 @@ function SuggestionMention( return true; }); - const sortedPersonalDetails = filteredPersonalDetails.sort((a, b) => { - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null - const nameA = a?.displayName || a?.login || ''; - // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null - const nameB = b?.displayName || b?.login || ''; - - if (nameA < nameB) { - return -1; - } - if (nameA > nameB) { - return 1; - } - return 0; - }); + // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- nullish coalescing cannot be used if left side can be empty string + const sortedPersonalDetails = lodashSortBy(filteredPersonalDetails, (detail) => detail?.displayName || detail?.login); sortedPersonalDetails.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length).forEach((detail) => { suggestions.push({ text: PersonalDetailsUtils.getDisplayNameOrDefault(detail), @@ -201,9 +190,9 @@ function SuggestionMention( const lastWord: string = words.at(-1) ?? ''; const secondToLastWord = words[words.length - 3]; - let atSignIndex; + let atSignIndex: number | undefined; let suggestionWord = ''; - let prefix; + let prefix: string; // Detect if the last two words contain a mention (two words are needed to detect a mention with a space in it) if (lastWord.startsWith('@')) { From 5735f6163034bff9ad5b24adfbda21caaa132b9c Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Thu, 8 Feb 2024 18:40:53 +0100 Subject: [PATCH 24/29] fix: resolve comments --- src/components/AttachmentModal.tsx | 4 +- src/components/Composer/types.ts | 2 +- src/components/LocaleContextProvider.tsx | 2 +- src/libs/ComposerUtils/index.ts | 2 +- src/libs/EmojiUtils.ts | 2 +- .../home/report/ParticipantLocalTime.tsx | 13 +- .../AttachmentPickerWithMenuItems.tsx | 21 +-- .../ComposerWithSuggestions.tsx | 146 +++++++++--------- .../ComposerWithSuggestions/index.e2e.tsx | 2 +- .../ReportActionCompose.tsx | 4 +- .../report/ReportActionCompose/SendButton.tsx | 9 +- .../SilentCommentUpdater/index.android.tsx | 2 +- .../ReportActionCompose/SuggestionEmoji.tsx | 2 +- .../home/report/ReportTypingIndicator.tsx | 2 +- 14 files changed, 99 insertions(+), 114 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index adf5b7b232a2..f97da9fcd9b7 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -80,7 +80,7 @@ type ImagePickerResponse = { type FileObject = File | ImagePickerResponse; type ChildrenProps = { - displayFileInModal: (data: FileObject | undefined) => void; + displayFileInModal: (data: FileObject) => void; show: () => void; }; @@ -317,7 +317,7 @@ function AttachmentModal({ }, []); const validateAndDisplayFileToUpload = useCallback( - (data: FileObject | undefined) => { + (data: FileObject) => { if (!data || !isDirectoryCheck(data)) { return; } diff --git a/src/components/Composer/types.ts b/src/components/Composer/types.ts index f19feb94dd0a..6bc44aba69cd 100644 --- a/src/components/Composer/types.ts +++ b/src/components/Composer/types.ts @@ -31,7 +31,7 @@ type ComposerProps = TextInputProps & { onNumberOfLinesChange?: (numberOfLines: number) => void; /** Callback method to handle pasting a file */ - onPasteFile?: (file?: File) => void; + onPasteFile?: (file: File) => void; /** General styles to apply to the text input */ // eslint-disable-next-line react/forbid-prop-types diff --git a/src/components/LocaleContextProvider.tsx b/src/components/LocaleContextProvider.tsx index 6d819f4d6eaa..eca433025f71 100644 --- a/src/components/LocaleContextProvider.tsx +++ b/src/components/LocaleContextProvider.tsx @@ -94,7 +94,7 @@ function LocaleContextProvider({preferredLocale, currentUserPersonalDetails = {} const updateLocale = useMemo(() => () => DateUtils.setLocale(locale), [locale]); - const formatPhoneNumber = useMemo(() => (phoneNumber) => LocalePhoneNumber.formatPhoneNumber(phoneNumber ?? ''), []); + const formatPhoneNumber = useMemo(() => (phoneNumber) => LocalePhoneNumber.formatPhoneNumber(phoneNumber), []); const toLocaleDigit = useMemo(() => (digit) => LocaleDigitUtils.toLocaleDigit(locale, digit), [locale]); diff --git a/src/libs/ComposerUtils/index.ts b/src/libs/ComposerUtils/index.ts index 6018cad86e47..94bba5d0d00c 100644 --- a/src/libs/ComposerUtils/index.ts +++ b/src/libs/ComposerUtils/index.ts @@ -19,7 +19,7 @@ function insertText(text: string, selection: Selection, textToInsert: string): s * @param text - text that needs whitespace to be appended to */ function insertWhiteSpaceAtIndex(text: string, index: number) { - return `${text?.slice(0, index)} ${text?.slice(index)}`; + return `${text.slice(0, index)} ${text.slice(index)}`; } /** diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 05a1cb29141b..cab0f48d75fd 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -370,7 +370,7 @@ function replaceEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEF * Find all emojis in a text and replace them with their code. */ function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST.EMOJI_DEFAULT_SKIN_TONE, lang: Locale = CONST.LOCALES.DEFAULT): ReplacedEmoji { - const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text ?? '', preferredSkinTone, lang); + const {text: convertedText = '', emojis = [], cursorPosition} = replaceEmojis(text, preferredSkinTone, lang); return { text: convertedText, diff --git a/src/pages/home/report/ParticipantLocalTime.tsx b/src/pages/home/report/ParticipantLocalTime.tsx index 58d85c0662b2..f51032690a33 100644 --- a/src/pages/home/report/ParticipantLocalTime.tsx +++ b/src/pages/home/report/ParticipantLocalTime.tsx @@ -12,16 +12,13 @@ import type {PersonalDetails} from '@src/types/onyx'; type ParticipantLocalTimeProps = { /** Personal details of the participant */ participant: PersonalDetails; - - /** The user's preferred locale e.g. 'en', 'es-ES' */ - preferredLocale?: Locale; }; -function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locale | undefined) { +function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: Locale) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing -- Disabling this line for safeness as nullish coalescing works only if the value is undefined or null const reportRecipientTimezone = participant.timezone || CONST.DEFAULT_TIME_ZONE; - const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT, undefined, reportRecipientTimezone.selected); - const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale ?? CONST.LOCALES.DEFAULT); + const reportTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale, undefined, reportRecipientTimezone.selected); + const currentTimezone = DateUtils.getLocalDateFromDatetime(preferredLocale); const reportRecipientDay = DateUtils.formatToDayOfWeek(reportTimezone); const currentUserDay = DateUtils.formatToDayOfWeek(currentTimezone); if (reportRecipientDay !== currentUserDay) { @@ -30,8 +27,8 @@ function getParticipantLocalTime(participant: PersonalDetails, preferredLocale: return `${DateUtils.formatToLocalTime(reportTimezone)}`; } -function ParticipantLocalTime({participant, preferredLocale}: ParticipantLocalTimeProps) { - const {translate} = useLocalize(); +function ParticipantLocalTime({participant}: ParticipantLocalTimeProps) { + const {translate, preferredLocale} = useLocalize(); const styles = useThemeStyles(); const [localTime, setLocalTime] = useState(() => getParticipantLocalTime(participant, preferredLocale)); diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index e6a75e6f157b..1ae8e63bd0f1 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -1,15 +1,14 @@ import {useIsFocused} from '@react-navigation/native'; -import type {FC} from 'react'; import React, {useCallback, useEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {withOnyx} from 'react-native-onyx'; -import type {SvgProps} from 'react-native-svg'; import type {ValueOf} from 'type-fest'; import type {FileObject} from '@components/AttachmentModal'; import AttachmentPicker from '@components/AttachmentPicker'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; +import type {PopoverMenuItem} from '@components/PopoverMenu'; import PopoverMenu from '@components/PopoverMenu'; import PressableWithFeedback from '@components/Pressable/PressableWithFeedback'; import Tooltip from '@components/Tooltip/PopoverAnchorTooltip'; @@ -29,25 +28,19 @@ import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; -type MoneyRequestOption = { - icon: FC; - text: string; - onSelected: () => void; -}; - -type MoneyRequestOptions = Record, MoneyRequestOption>; +type MoneyRequestOptions = Record, PopoverMenuItem>; type AttachmentPickerWithMenuItemsOnyxProps = { /** The policy tied to the report */ policy: OnyxEntry; }; -type AttachmentPickerWithMenuItemsProps = { +type AttachmentPickerWithMenuItemsProps = AttachmentPickerWithMenuItemsOnyxProps & { /** The report currently being looked at */ report: OnyxEntry; /** Callback to open the file in the modal */ - displayFileInModal: (url: FileObject | undefined) => void; + displayFileInModal: (url: FileObject) => void; /** Whether or not the full size composer is available */ isFullComposerAvailable: boolean; @@ -59,7 +52,7 @@ type AttachmentPickerWithMenuItemsProps = { isBlockedFromConcierge: boolean; /** Whether or not the attachment picker is disabled */ - disabled: boolean; + disabled?: boolean; /** Sets the menu visibility */ setMenuVisibility: (isVisible: boolean) => void; @@ -93,7 +86,7 @@ type AttachmentPickerWithMenuItemsProps = { /** The personal details of everyone in the report */ reportParticipantIDs?: number[]; -} & AttachmentPickerWithMenuItemsOnyxProps; +}; /** * This includes the popover of options you see when pressing the + button in the composer. @@ -155,7 +148,7 @@ function AttachmentPickerWithMenuItems({ /** * Determines if we can show the task option */ - const taskOption: MoneyRequestOption[] = useMemo(() => { + const taskOption: PopoverMenuItem[] = useMemo(() => { if (!ReportUtils.canCreateTaskInReport(report)) { return []; } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index b54ccaf20518..6b89a591055a 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -78,99 +78,99 @@ type ComposerWithSuggestionsOnyxProps = { editFocused: OnyxEntry; }; -type ComposerWithSuggestionsProps = { - /** Report ID */ - reportID: string; +type ComposerWithSuggestionsProps = ComposerWithSuggestionsOnyxProps & + Partial & { + /** Report ID */ + reportID: string; - /** Callback to focus composer */ - onFocus: () => void; + /** Callback to focus composer */ + onFocus: () => void; - /** Callback to blur composer */ - onBlur: (event: NativeSyntheticEvent) => void; + /** Callback to blur composer */ + onBlur: (event: NativeSyntheticEvent) => void; - /** Callback to update the value of the composer */ - onValueChange: (value: string) => void; + /** Callback to update the value of the composer */ + onValueChange: (value: string) => void; - /** Whether the composer is full size */ - isComposerFullSize: boolean; + /** Whether the composer is full size */ + isComposerFullSize: boolean; - /** Whether the menu is visible */ - isMenuVisible: boolean; + /** Whether the menu is visible */ + isMenuVisible: boolean; - /** The placeholder for the input */ - inputPlaceholder: string; + /** The placeholder for the input */ + inputPlaceholder: string; - /** Function to display a file in a modal */ - displayFileInModal: (file: FileObject | undefined) => void; + /** Function to display a file in a modal */ + displayFileInModal: (file: FileObject) => void; - /** Whether the text input should clear */ - textInputShouldClear: boolean; + /** Whether the text input should clear */ + textInputShouldClear: boolean; - /** Function to set the text input should clear */ - setTextInputShouldClear: (shouldClear: boolean) => void; + /** Function to set the text input should clear */ + setTextInputShouldClear: (shouldClear: boolean) => void; - /** Whether the user is blocked from concierge */ - isBlockedFromConcierge: boolean; + /** Whether the user is blocked from concierge */ + isBlockedFromConcierge: boolean; - /** Whether the input is disabled */ - disabled: boolean; + /** Whether the input is disabled */ + disabled: boolean; - /** Whether the full composer is available */ - isFullComposerAvailable: boolean; + /** Whether the full composer is available */ + isFullComposerAvailable: boolean; - /** Function to set whether the full composer is available */ - setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; + /** Function to set whether the full composer is available */ + setIsFullComposerAvailable: (isFullComposerAvailable: boolean) => void; - /** Function to set whether the comment is empty */ - setIsCommentEmpty: (isCommentEmpty: boolean) => void; + /** Function to set whether the comment is empty */ + setIsCommentEmpty: (isCommentEmpty: boolean) => void; - /** Function to handle sending a message */ - handleSendMessage: () => void; + /** Function to handle sending a message */ + handleSendMessage: () => void; - /** Whether the compose input should show */ - shouldShowComposeInput: OnyxEntry; + /** Whether the compose input should show */ + shouldShowComposeInput: OnyxEntry; - /** Function to measure the parent container */ - measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; + /** Function to measure the parent container */ + measureParentContainer: (callback: MeasureInWindowOnSuccessCallback) => void; - /** The height of the list */ - listHeight: number; + /** The height of the list */ + listHeight: number; - /** Whether the scroll is likely to trigger a layout */ - isScrollLikelyLayoutTriggered: RefObject; + /** Whether the scroll is likely to trigger a layout */ + isScrollLikelyLayoutTriggered: RefObject; - /** Function to raise the scroll is likely layout triggered */ - raiseIsScrollLikelyLayoutTriggered: () => void; + /** Function to raise the scroll is likely layout triggered */ + raiseIsScrollLikelyLayoutTriggered: () => void; - /** The ref to the suggestions */ - suggestionsRef: React.RefObject; + /** The ref to the suggestions */ + suggestionsRef: React.RefObject; - /** The ref to the animated input */ - animatedRef: AnimatedRef; + /** The ref to the animated input */ + animatedRef: AnimatedRef; - /** The ref to the next modal will open */ - isNextModalWillOpenRef: MutableRefObject; + /** The ref to the next modal will open */ + isNextModalWillOpenRef: MutableRefObject; - /** Whether the edit is focused */ - editFocused: boolean; + /** Whether the edit is focused */ + editFocused: boolean; - /** Wheater chat is empty */ - isEmptyChat?: boolean; + /** Wheater chat is empty */ + isEmptyChat?: boolean; - /** The last report action */ - lastReportAction?: OnyxTypes.ReportAction; + /** The last report action */ + lastReportAction?: OnyxTypes.ReportAction; - /** Whether to include chronos */ - includeChronos?: boolean; + /** Whether to include chronos */ + includeChronos?: boolean; - /** The parent report action ID */ - parentReportActionID?: string; + /** The parent report action ID */ + parentReportActionID?: string; - /** The parent report ID */ - // eslint-disable-next-line react/no-unused-prop-types - parentReportID: string | undefined; -} & ComposerWithSuggestionsOnyxProps & - Partial; + /** The parent report ID */ + // eslint-disable-next-line react/no-unused-prop-types -- its used in the withOnyx HOC + parentReportID: string | undefined; + }; const {RNTextInputReset} = NativeModules; @@ -240,7 +240,7 @@ function ComposerWithSuggestions( // For testing children, }: ComposerWithSuggestionsProps, - ref: ForwardedRef, + ref: ForwardedRef, ) { const {isKeyboardShown} = useKeyboardState(); const theme = useTheme(); @@ -340,24 +340,24 @@ function ComposerWithSuggestions( let currentIndex = 0; // Find the first character mismatch with newText - while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText?.charAt(currentIndex) && selection.start > currentIndex) { + while (currentIndex < newText.length && prevText.charAt(currentIndex) === newText.charAt(currentIndex) && selection.start > currentIndex) { currentIndex++; } - if (currentIndex < (newText?.length ?? 0)) { + if (currentIndex < newText.length) { startIndex = currentIndex; - const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText ?? '', selection.end); + const commonSuffixLength = ComposerUtils.findCommonSuffixLength(prevText, newText, selection.end); // if text is getting pasted over find length of common suffix and subtract it from new text length if (commonSuffixLength > 0 || selection.end - selection.start > 0) { - endIndex = (newText?.length ?? 0) - commonSuffixLength; + endIndex = newText.length - commonSuffixLength; } else { - endIndex = currentIndex + (newText?.length ?? 0); + endIndex = currentIndex + newText.length; } } return { startIndex, endIndex, - diff: newText?.substring(startIndex, endIndex) ?? '', + diff: newText.substring(startIndex, endIndex), }; }, [selection.start, selection.end], @@ -375,7 +375,7 @@ function ComposerWithSuggestions( text: newComment, emojis, cursorPosition, - } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue ?? '', preferredSkinTone, preferredLocale); + } = EmojiUtils.replaceAndExtractEmojis(isEmojiInserted ? ComposerUtils.insertWhiteSpaceAtIndex(commentValue, endIndex) : commentValue, preferredSkinTone, preferredLocale); if (emojis.length) { const newEmojis = EmojiUtils.getAddedEmojis(emojis, emojisPresentBefore.current); if (newEmojis.length) { @@ -748,7 +748,7 @@ function ComposerWithSuggestions( isComposerFullSize={isComposerFullSize} value={value} testID="composer" - numberOfLines={numberOfLines ?? 0} + numberOfLines={numberOfLines ?? undefined} onNumberOfLinesChange={updateNumberOfLines} shouldCalculateCaretPosition onLayout={onLayout} diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx index 0fa535329d33..7f169ef15918 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/index.e2e.tsx @@ -16,7 +16,7 @@ function IncrementRenderCount() { return null; } -function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: ForwardedRef) { +function ComposerWithSuggestionsE2e(props: ComposerWithSuggestionsProps, ref: ForwardedRef) { // Eventually Auto focus on e2e tests useEffect(() => { const testConfig = E2EClient.getCurrentActiveTestConfig(); diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 98630ff88b23..283f0dfab18f 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -289,9 +289,7 @@ function ReportActionCompose({ */ const submitForm = useCallback( (event?: SyntheticEvent) => { - if (event) { - event.preventDefault(); - } + event?.preventDefault(); const newComment = composerRef.current?.prepareCommentAndResetComposer(); if (!newComment) { diff --git a/src/pages/home/report/ReportActionCompose/SendButton.tsx b/src/pages/home/report/ReportActionCompose/SendButton.tsx index 4726c1638f42..c505eb0e32e7 100644 --- a/src/pages/home/report/ReportActionCompose/SendButton.tsx +++ b/src/pages/home/report/ReportActionCompose/SendButton.tsx @@ -23,12 +23,9 @@ function SendButton({isDisabled: isDisabledProp, handleSendMessage}: SendButtonP const styles = useThemeStyles(); const {translate} = useLocalize(); - const Tap = Gesture.Tap() - // @ts-expect-error Enabled require argument but when passing something button is not working - .enabled() - .onEnd(() => { - handleSendMessage(); - }); + const Tap = Gesture.Tap().onEnd(() => { + handleSendMessage(); + }); return ( { updateComment(comment ?? ''); // eslint-disable-next-line react-hooks/exhaustive-deps -- We need to run this on mount diff --git a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx index 206c91ed9b08..0ae45d2d705d 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionEmoji.tsx @@ -28,7 +28,7 @@ type SuggestionEmojiOnyxProps = { type SuggestionEmojiProps = SuggestionProps & SuggestionEmojiOnyxProps & { /** Function to clear the input */ - resetKeyboardInput: (() => void) | undefined; + resetKeyboardInput?: () => void; }; /** diff --git a/src/pages/home/report/ReportTypingIndicator.tsx b/src/pages/home/report/ReportTypingIndicator.tsx index e484ce9886a7..3ff8f2b0eb8e 100755 --- a/src/pages/home/report/ReportTypingIndicator.tsx +++ b/src/pages/home/report/ReportTypingIndicator.tsx @@ -41,7 +41,7 @@ function ReportTypingIndicator({userTypingStatuses}: ReportTypingIndicatorProps) if (usersTyping.length === 1) { return ( Date: Wed, 14 Feb 2024 16:45:57 +0100 Subject: [PATCH 25/29] fix: resolve comments --- src/components/AttachmentModal.tsx | 4 ++-- .../AddCommentOrAttachementParams.ts | 2 +- src/libs/ReportUtils.ts | 2 +- src/libs/actions/Report.ts | 4 ++-- .../ComposerWithSuggestions.tsx | 10 ++++---- .../ReportActionCompose.tsx | 24 ++++--------------- .../SilentCommentUpdater/index.tsx | 1 + src/types/modules/pusher.d.ts | 2 +- src/types/onyx/ReportAction.ts | 2 +- 9 files changed, 19 insertions(+), 32 deletions(-) diff --git a/src/components/AttachmentModal.tsx b/src/components/AttachmentModal.tsx index 2b4afc037796..7f0178863fc9 100755 --- a/src/components/AttachmentModal.tsx +++ b/src/components/AttachmentModal.tsx @@ -89,7 +89,7 @@ type AttachmentModalProps = AttachmentModalOnyxProps & { source?: AvatarSource; /** Optional callback to fire when we want to preview an image and approve it for use. */ - onConfirm?: ((file: Partial) => void) | null; + onConfirm?: ((file: FileObject) => void) | null; /** Whether the modal should be open by default */ defaultOpen?: boolean; @@ -264,7 +264,7 @@ function AttachmentModal({ } if (onConfirm) { - onConfirm(Object.assign(file ?? {}, {source: sourceState})); + onConfirm(Object.assign(file ?? {}, {source: sourceState} as FileObject)); } setIsModalOpen(false); diff --git a/src/libs/API/parameters/AddCommentOrAttachementParams.ts b/src/libs/API/parameters/AddCommentOrAttachementParams.ts index 4eab35be7dd2..a705c92f7f27 100644 --- a/src/libs/API/parameters/AddCommentOrAttachementParams.ts +++ b/src/libs/API/parameters/AddCommentOrAttachementParams.ts @@ -5,7 +5,7 @@ type AddCommentOrAttachementParams = { reportActionID?: string; commentReportActionID?: string | null; reportComment?: string; - file?: Partial; + file?: FileObject; timezone?: string; shouldAllowActionableMentionWhispers?: boolean; clientCreatedTime?: string; diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 2b073b110184..ed3dc16b56a4 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -2631,7 +2631,7 @@ function getReportDescriptionText(report: Report): string { return parser.htmlToText(report.description); } -function buildOptimisticAddCommentReportAction(text?: string, file?: Partial, actorAccountID?: number): OptimisticReportAction { +function buildOptimisticAddCommentReportAction(text?: string, file?: FileObject, actorAccountID?: number): OptimisticReportAction { const parser = new ExpensiMark(); const commentText = getParsedComment(text ?? ''); const isAttachment = !text && file !== undefined; diff --git a/src/libs/actions/Report.ts b/src/libs/actions/Report.ts index bfae6ece1fa1..7127178daae7 100644 --- a/src/libs/actions/Report.ts +++ b/src/libs/actions/Report.ts @@ -353,7 +353,7 @@ function notifyNewAction(reportID: string, accountID?: number, reportActionID?: * - Adding one attachment * - Add both a comment and attachment simultaneously */ -function addActions(reportID: string, text = '', file?: Partial) { +function addActions(reportID: string, text = '', file?: FileObject) { let reportCommentText = ''; let reportCommentAction: OptimisticAddCommentReportAction | undefined; let attachmentAction: OptimisticAddCommentReportAction | undefined; @@ -512,7 +512,7 @@ function addActions(reportID: string, text = '', file?: Partial) { } /** Add an attachment and optional comment. */ -function addAttachment(reportID: string, file: Partial, text = '') { +function addAttachment(reportID: string, file: FileObject, text = '') { addActions(reportID, text, file); } diff --git a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx index cfe45e5a011b..a345926ad7d2 100644 --- a/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx +++ b/src/pages/home/report/ReportActionCompose/ComposerWithSuggestions/ComposerWithSuggestions.tsx @@ -214,6 +214,7 @@ function ComposerWithSuggestions( onFocus, onBlur, onValueChange, + // Composer isComposerFullSize, isMenuVisible, @@ -232,11 +233,13 @@ function ComposerWithSuggestions( listHeight, isScrollLikelyLayoutTriggered, raiseIsScrollLikelyLayoutTriggered, + // Refs suggestionsRef, animatedRef, isNextModalWillOpenRef, editFocused, + // For testing children, }: ComposerWithSuggestionsProps, @@ -278,7 +281,7 @@ function ComposerWithSuggestions( const syncSelectionWithOnChangeTextRef = useRef(null); - const suggestions = suggestionsRef.current?.getSuggestions() ?? (() => []); + const suggestions = suggestionsRef.current?.getSuggestions() ?? []; const hasEnoughSpaceForLargeSuggestion = SuggestionUtils.hasEnoughSpaceForLargeSuggestionMenu(listHeight, composerHeight, suggestions?.length ?? 0); @@ -457,7 +460,7 @@ function ComposerWithSuggestions( [reportID, numberOfLines], ); - const prepareCommentAndResetComposer = useCallback(() => { + const prepareCommentAndResetComposer = useCallback((): string => { const trimmedComment = commentRef.current.trim(); const commentLength = ReportUtils.getCommentLength(trimmedComment); @@ -575,7 +578,6 @@ function ComposerWithSuggestions( /** * Focus the composer text input * @param [shouldDelay=false] Impose delay before focusing the composer - * @memberof ReportActionCompose */ const focus = useCallback((shouldDelay = false) => { focusComposerWithDelay(textInputRef.current)(shouldDelay); @@ -614,7 +616,7 @@ function ComposerWithSuggestions( } // if we're typing on another input/text area, do not focus - if (['INPUT', 'TEXTAREA'].includes((e.target as Element)?.nodeName)) { + if (['INPUT', 'TEXTAREA'].includes((e.target as Element | null)?.nodeName ?? '')) { return; } diff --git a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx index 735fb323d638..84f7600f8ef0 100644 --- a/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx +++ b/src/pages/home/report/ReportActionCompose/ReportActionCompose.tsx @@ -43,6 +43,7 @@ import type * as OnyxCommon from '@src/types/onyx/OnyxCommon'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import AttachmentPickerWithMenuItems from './AttachmentPickerWithMenuItems'; import ComposerWithSuggestions from './ComposerWithSuggestions'; +import type {ComposerWithSuggestionsProps} from './ComposerWithSuggestions/ComposerWithSuggestions'; import SendButton from './SendButton'; type ComposerRef = { @@ -71,36 +72,19 @@ type ReportActionComposeOnyxProps = { }; type ReportActionComposeProps = ReportActionComposeOnyxProps & - WithCurrentUserPersonalDetailsProps & { + WithCurrentUserPersonalDetailsProps & + Pick & { /** A method to call when the form is submitted */ onSubmit: (newComment: string | undefined) => void; - /** The ID of the report actions will be created for */ - reportID: string; - /** The report currently being looked at */ report: OnyxEntry; - /** Is composer full size */ - isComposerFullSize?: boolean; - - /** Whether user interactions should be disabled */ - disabled?: boolean; - - /** Height of the list which the composer is part of */ - listHeight?: number; - /** The type of action that's pending */ pendingAction?: OnyxCommon.PendingAction; /** Whether the report is ready for display */ isReportReadyForDisplay?: boolean; - - /** Whether the chat is empty */ - isEmptyChat?: boolean; - - /** The last report action */ - lastReportAction?: OnyxTypes.ReportAction; }; // We want consistent auto focus behavior on input between native and mWeb so we have some auto focus management code that will @@ -268,7 +252,7 @@ function ReportActionCompose({ }, []); const addAttachment = useCallback( - (file: Partial) => { + (file: FileObject) => { playSound(SOUNDS.DONE); const newComment = composerRef?.current?.prepareCommentAndResetComposer(); Report.addAttachment(reportID, file, newComment); diff --git a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx index c84bd3786610..1abc6567bc7b 100644 --- a/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx +++ b/src/pages/home/report/ReportActionCompose/SilentCommentUpdater/index.tsx @@ -43,5 +43,6 @@ SilentCommentUpdater.displayName = 'SilentCommentUpdater'; export default withOnyx({ comment: { key: ({reportID}) => `${ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT}${reportID}`, + initialValue: '', }, })(SilentCommentUpdater); diff --git a/src/types/modules/pusher.d.ts b/src/types/modules/pusher.d.ts index 676d7a7ee2fc..e9aa50085e8d 100644 --- a/src/types/modules/pusher.d.ts +++ b/src/types/modules/pusher.d.ts @@ -9,6 +9,6 @@ declare global { // eslint-disable-next-line @typescript-eslint/consistent-type-definitions interface File { source: string; - uri: string; + uri?: string; } } diff --git a/src/types/onyx/ReportAction.ts b/src/types/onyx/ReportAction.ts index 7071c211c2e9..d339d666a411 100644 --- a/src/types/onyx/ReportAction.ts +++ b/src/types/onyx/ReportAction.ts @@ -169,7 +169,7 @@ type ReportActionBase = { isFirstItem?: boolean; /** Informations about attachments of report action */ - attachmentInfo?: Partial | EmptyObject; + attachmentInfo?: FileObject | EmptyObject; /** Receipt tied to report action */ receipt?: Receipt; From 2a71a34beacca8b099be1161eceb072b6aa821f0 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 21 Feb 2024 14:22:53 +0100 Subject: [PATCH 26/29] fix: typecheck --- .../ReportActionCompose/AttachmentPickerWithMenuItems.tsx | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx index 9888c5217e69..68c7f0883683 100644 --- a/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx +++ b/src/pages/home/report/ReportActionCompose/AttachmentPickerWithMenuItems.tsx @@ -18,14 +18,12 @@ import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import useWindowDimensions from '@hooks/useWindowDimensions'; import * as Browser from '@libs/Browser'; -import Navigation from '@libs/Navigation/Navigation'; import * as ReportUtils from '@libs/ReportUtils'; import * as IOU from '@userActions/IOU'; import * as Report from '@userActions/Report'; import * as Task from '@userActions/Task'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; import type * as OnyxTypes from '@src/types/onyx'; type MoneyRequestOptions = Record, PopoverMenuItem>; @@ -126,12 +124,12 @@ function AttachmentPickerWithMenuItems({ [CONST.IOU.TYPE.SPLIT]: { icon: Expensicons.Receipt, text: translate('iou.splitBill'), - onSelected: () => IOU.startMoneyRequest_temporaryForRefactor(CONST.IOU.TYPE.SPLIT, report.reportID), + onSelected: () => IOU.startMoneyRequest_temporaryForRefactor(CONST.IOU.TYPE.SPLIT, report?.reportID ?? ''), }, [CONST.IOU.TYPE.REQUEST]: { icon: Expensicons.MoneyCircle, text: translate('iou.requestMoney'), - onSelected: () => IOU.startMoneyRequest_temporaryForRefactor(CONST.IOU.TYPE.REQUEST, report.reportID), + onSelected: () => IOU.startMoneyRequest_temporaryForRefactor(CONST.IOU.TYPE.REQUEST, report?.reportID ?? ''), }, [CONST.IOU.TYPE.SEND]: { icon: Expensicons.Send, From 2e829a169702d72ec6a526ed432b917576baa810 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 23 Feb 2024 11:11:33 +0100 Subject: [PATCH 27/29] fix: typecheck --- src/pages/home/report/ReportActionCompose/SuggestionMention.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx index f284596040b7..54d21d637d09 100644 --- a/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx +++ b/src/pages/home/report/ReportActionCompose/SuggestionMention.tsx @@ -152,7 +152,7 @@ function SuggestionMention( sortedPersonalDetails.slice(0, CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS - suggestions.length).forEach((detail) => { suggestions.push({ text: PersonalDetailsUtils.getDisplayNameOrDefault(detail), - alternateText: formatPhoneNumber(detail?.login), + alternateText: formatPhoneNumber(detail?.login ?? ''), login: detail?.login, icons: [ { From 1e19123cf64f548463fc0ddc8d48baab4db3bc50 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Mon, 26 Feb 2024 11:30:48 +0100 Subject: [PATCH 28/29] fix: typecheck --- src/components/AvatarWithImagePicker.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index fa8a6d71516f..4388ebb8f815 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -220,7 +220,7 @@ function AvatarWithImagePicker({ setError(null, {}); setIsMenuVisible(false); setImageData({ - uri: image.uri, + uri: image.uri ?? '', name: image.name, type: image.type, }); From 64368d5784c3ad869251eb6845070fbfb5ef8f23 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Wed, 28 Feb 2024 10:49:28 +0100 Subject: [PATCH 29/29] fix: typecheck --- src/libs/EmojiUtils.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/libs/EmojiUtils.ts b/src/libs/EmojiUtils.ts index 353ced9e1e45..cab0f48d75fd 100644 --- a/src/libs/EmojiUtils.ts +++ b/src/libs/EmojiUtils.ts @@ -10,7 +10,6 @@ import ONYXKEYS from '@src/ONYXKEYS'; import type {FrequentlyUsedEmoji, Locale} from '@src/types/onyx'; import type {ReportActionReaction, UsersReactions} from '@src/types/onyx/ReportActionReactions'; import type IconAsset from '@src/types/utils/IconAsset'; -import {SupportedLanguage} from './EmojiTrie'; type HeaderIndice = {code: string; index: number; icon: IconAsset}; type EmojiSpacer = {code: string; spacer: boolean}; @@ -384,7 +383,7 @@ function replaceAndExtractEmojis(text: string, preferredSkinTone: number = CONST * Suggest emojis when typing emojis prefix after colon * @param [limit] - matching emojis limit */ -function suggestEmojis(text: string, lang: SupportedLanguage, limit: number = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { +function suggestEmojis(text: string, lang: Locale, limit: number = CONST.AUTO_COMPLETE_SUGGESTER.MAX_AMOUNT_OF_SUGGESTIONS): Emoji[] | undefined { // emojisTrie is importing the emoji JSON file on the app starting and we want to avoid it const emojisTrie = require('./EmojiTrie').default;