From e275a7decf6d5f98387c8f63f9f907e1538e9aa6 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 24 Nov 2023 15:25:55 +0100 Subject: [PATCH 1/8] ref: started migration of AttachmentView component, migrated useCarouselArrows and extractAttachmentsFromReport to Typescript --- ...ort.js => extractAttachmentsFromReport.ts} | 45 ++++++----- ...arouselArrows.js => useCarouselArrows.tsx} | 4 +- .../{index.native.js => index.native.tsx} | 30 ++++--- .../{index.js => index.tsx} | 21 ++--- .../AttachmentViewImage/types.ts | 9 +++ ...ntViewPdf.js => BaseAttachmentViewPdf.tsx} | 18 ++--- .../{index.android.js => index.android.tsx} | 22 +++-- .../{index.ios.js => index.ios.tsx} | 11 ++- .../AttachmentViewPdf/{index.js => index.tsx} | 7 +- .../AttachmentView/AttachmentViewPdf/types.ts | 16 ++++ .../AttachmentView/{index.js => index.tsx} | 80 +++++++++---------- .../Attachments/AttachmentView/types.ts | 14 ++++ 12 files changed, 152 insertions(+), 125 deletions(-) rename src/components/Attachments/AttachmentCarousel/{extractAttachmentsFromReport.js => extractAttachmentsFromReport.ts} (63%) rename src/components/Attachments/AttachmentCarousel/{useCarouselArrows.js => useCarouselArrows.tsx} (91%) rename src/components/Attachments/AttachmentView/AttachmentViewImage/{index.native.js => index.native.tsx} (65%) rename src/components/Attachments/AttachmentView/AttachmentViewImage/{index.js => index.tsx} (64%) create mode 100644 src/components/Attachments/AttachmentView/AttachmentViewImage/types.ts rename src/components/Attachments/AttachmentView/AttachmentViewPdf/{BaseAttachmentViewPdf.js => BaseAttachmentViewPdf.tsx} (74%) rename src/components/Attachments/AttachmentView/AttachmentViewPdf/{index.android.js => index.android.tsx} (80%) rename src/components/Attachments/AttachmentView/AttachmentViewPdf/{index.ios.js => index.ios.tsx} (50%) rename src/components/Attachments/AttachmentView/AttachmentViewPdf/{index.js => index.tsx} (58%) create mode 100644 src/components/Attachments/AttachmentView/AttachmentViewPdf/types.ts rename src/components/Attachments/AttachmentView/{index.js => index.tsx} (80%) create mode 100644 src/components/Attachments/AttachmentView/types.ts diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts similarity index 63% rename from src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js rename to src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts index 0f1fa15c99ca..2eec9ee1f995 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.js +++ b/src/components/Attachments/AttachmentCarousel/extractAttachmentsFromReport.ts @@ -1,22 +1,25 @@ import {Parser as HtmlParser} from 'htmlparser2'; -import lodashGet from 'lodash/get'; -import _ from 'underscore'; +import {OnyxEntry} from 'react-native-onyx'; import * as ReceiptUtils from '@libs/ReceiptUtils'; import * as ReportActionsUtils from '@libs/ReportActionsUtils'; import * as TransactionUtils from '@libs/TransactionUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; +import {ReportAction, ReportActions, Transaction} from '@src/types/onyx'; -/** - * Constructs the initial component state from report actions - * @param {Object} parentReportAction - * @param {Object} reportActions - * @param {Object} transaction - * @returns {Array} - */ -function extractAttachmentsFromReport(parentReportAction, reportActions, transaction) { - const actions = [parentReportAction, ...ReportActionsUtils.getSortedReportActions(_.values(reportActions))]; - const attachments = []; +type Attachment = { + reportActionID?: string; + source: string; + isAuthTokenRequired: boolean; + file: {name: string}; + isReceipt: boolean; + hasBeenFlagged?: boolean; + transactionID?: string; +}; + +function extractAttachmentsFromReport(parentReportAction: OnyxEntry, reportActions: OnyxEntry, transaction: OnyxEntry) { + const actions = [parentReportAction, ...ReportActionsUtils.getSortedReportActions(Object.values(reportActions ?? {}))]; + const attachments: Attachment[] = []; const htmlParser = new HtmlParser({ onopentag: (name, attribs) => { @@ -39,26 +42,26 @@ function extractAttachmentsFromReport(parentReportAction, reportActions, transac }, }); - _.forEach(actions, (action, key) => { + actions.forEach((action, key) => { if (!ReportActionsUtils.shouldReportActionBeVisible(action, key)) { return; } // We're handling receipts differently here because receipt images are not // part of the report action message, the images are constructed client-side - if (ReportActionsUtils.isMoneyRequestAction(action)) { - const transactionID = lodashGet(action, ['originalMessage', 'IOUTransactionID']); + if (ReportActionsUtils.isMoneyRequestAction(action) && action?.actionName === CONST.REPORT.ACTIONS.TYPE.IOU) { + const transactionID = action?.originalMessage?.IOUTransactionID; if (!transactionID) { return; } - if (TransactionUtils.hasReceipt(transaction)) { + if (TransactionUtils.hasReceipt(transaction) && transaction) { const {image} = ReceiptUtils.getThumbnailAndImageURIs(transaction); - const isLocalFile = typeof image === 'string' && _.some(CONST.ATTACHMENT_LOCAL_URL_PREFIX, (prefix) => image.startsWith(prefix)); + const isLocalFile = typeof image === 'string' && CONST.ATTACHMENT_LOCAL_URL_PREFIX.some((prefix) => image.startsWith(prefix)); attachments.unshift({ - source: tryResolveUrlFromApiRoot(image), + source: tryResolveUrlFromApiRoot(image as string), isAuthTokenRequired: !isLocalFile, - file: {name: transaction.filename}, + file: {name: transaction.filename ?? ''}, isReceipt: true, transactionID, }); @@ -66,9 +69,9 @@ function extractAttachmentsFromReport(parentReportAction, reportActions, transac } } - const decision = _.get(action, ['message', 0, 'moderationDecision', 'decision'], ''); + const decision = action?.message?.[0].moderationDecision?.decision ?? ''; const hasBeenFlagged = decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_HIDE || decision === CONST.MODERATION.MODERATOR_DECISION_HIDDEN; - const html = _.get(action, ['message', 0, 'html'], '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action.reportActionID}"/>`); + const html = (action?.message?.[0].html ?? '').replace('/>', `data-flagged="${hasBeenFlagged}" data-id="${action?.reportActionID}"/>`); htmlParser.write(html); }); htmlParser.end(); diff --git a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.tsx similarity index 91% rename from src/components/Attachments/AttachmentCarousel/useCarouselArrows.js rename to src/components/Attachments/AttachmentCarousel/useCarouselArrows.tsx index 0c55c3ae519d..93d8bb8d35a1 100644 --- a/src/components/Attachments/AttachmentCarousel/useCarouselArrows.js +++ b/src/components/Attachments/AttachmentCarousel/useCarouselArrows.tsx @@ -5,12 +5,12 @@ import CONST from '@src/CONST'; function useCarouselArrows() { const canUseTouchScreen = DeviceCapabilities.canUseTouchScreen(); const [shouldShowArrows, setShouldShowArrowsInternal] = useState(canUseTouchScreen); - const autoHideArrowTimeout = useRef(null); + const autoHideArrowTimeout = useRef(null); /** * Cancels the automatic hiding of the arrows. */ - const cancelAutoHideArrows = useCallback(() => clearTimeout(autoHideArrowTimeout.current), []); + const cancelAutoHideArrows = useCallback(() => clearTimeout(autoHideArrowTimeout.current ?? undefined), []); /** * Automatically hide the arrows if there is no interaction for 3 seconds. diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.tsx similarity index 65% rename from src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js rename to src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.tsx index fc443e5ea17b..0fd6222182bc 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.native.tsx @@ -1,20 +1,26 @@ import React, {memo} from 'react'; +import {Role} from 'react-native'; import AttachmentCarouselPage from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPage'; import ImageView from '@components/ImageView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import compose from '@libs/compose'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import {attachmentViewImageDefaultProps, attachmentViewImagePropTypes} from './propTypes'; +import AttachmentViewImageProps from './types'; -const propTypes = { - ...attachmentViewImagePropTypes, - ...withLocalizePropTypes, -}; - -function AttachmentViewImage({source, file, isAuthTokenRequired, isFocused, isUsedInCarousel, loadComplete, onPress, isImage, onScaleChanged, translate}) { +function AttachmentViewImage({ + source, + file = {name: ''}, + isAuthTokenRequired = false, + isFocused = false, + isUsedInCarousel = false, + loadComplete = false, + onPress = undefined, + isImage = false, + onScaleChanged = () => {}, +}: AttachmentViewImageProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); const children = isUsedInCarousel ? ( {children} @@ -46,8 +52,6 @@ function AttachmentViewImage({source, file, isAuthTokenRequired, isFocused, isUs ); } -AttachmentViewImage.propTypes = propTypes; -AttachmentViewImage.defaultProps = attachmentViewImageDefaultProps; AttachmentViewImage.displayName = 'AttachmentViewImage'; -export default compose(memo, withLocalize)(AttachmentViewImage); +export default memo(AttachmentViewImage); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx similarity index 64% rename from src/components/Attachments/AttachmentView/AttachmentViewImage/index.js rename to src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx index 22bcf259ed77..1316e872d753 100755 --- a/src/components/Attachments/AttachmentView/AttachmentViewImage/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/index.tsx @@ -1,19 +1,16 @@ import React, {memo} from 'react'; +import {Role} from 'react-native'; import ImageView from '@components/ImageView'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; -import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; -import compose from '@libs/compose'; +import useLocalize from '@hooks/useLocalize'; import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -import {attachmentViewImageDefaultProps, attachmentViewImagePropTypes} from './propTypes'; +import AttachmentViewImageProps from './types'; -const propTypes = { - ...attachmentViewImagePropTypes, - ...withLocalizePropTypes, -}; - -function AttachmentViewImage({source, file, isAuthTokenRequired, loadComplete, onPress, isImage, onScaleChanged, translate, onError}) { +function AttachmentViewImage({source, file, isAuthTokenRequired, loadComplete, onPress, isImage, onScaleChanged, onError}: AttachmentViewImageProps) { const styles = useThemeStyles(); + const {translate} = useLocalize(); + const children = ( {children} @@ -38,8 +35,6 @@ function AttachmentViewImage({source, file, isAuthTokenRequired, loadComplete, o ); } -AttachmentViewImage.propTypes = propTypes; -AttachmentViewImage.defaultProps = attachmentViewImageDefaultProps; AttachmentViewImage.displayName = 'AttachmentViewImage'; -export default compose(memo, withLocalize)(AttachmentViewImage); +export default memo(AttachmentViewImage); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewImage/types.ts b/src/components/Attachments/AttachmentView/AttachmentViewImage/types.ts new file mode 100644 index 000000000000..c4aceee4087d --- /dev/null +++ b/src/components/Attachments/AttachmentView/AttachmentViewImage/types.ts @@ -0,0 +1,9 @@ +import AttachmentProps from '@components/Attachments/AttachmentView/types'; + +type AttachmentViewImageProps = { + loadComplete: boolean; + isImage: boolean; + onError: () => void; +} & AttachmentProps; + +export default AttachmentViewImageProps; diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx similarity index 74% rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx index 40887ddee697..0ed6d5837a57 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/BaseAttachmentViewPdf.tsx @@ -1,10 +1,10 @@ import React, {memo, useCallback, useContext, useEffect} from 'react'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import PDFView from '@components/PDFView'; -import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; +import AttachmentViewPdfProps from './types'; function BaseAttachmentViewPdf({ - file, + file = {name: ''} as File, encryptedSourceUrl, isFocused, isUsedInCarousel, @@ -14,32 +14,32 @@ function BaseAttachmentViewPdf({ onLoadComplete, errorLabelStyles, style, -}) { +}: AttachmentViewPdfProps) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); useEffect(() => { if (!attachmentCarouselPagerContext) { return; } - attachmentCarouselPagerContext.onPinchGestureChange(false); + attachmentCarouselPagerContext?.onPinchGestureChange(false); // eslint-disable-next-line react-hooks/exhaustive-deps -- we just want to call this function when component is mounted }, []); const onScaleChanged = useCallback( - (scale) => { + (scale: number) => { onScaleChangedProp(scale); // When a pdf is shown in a carousel, we want to disable the pager scroll when the pdf is zoomed in if (isUsedInCarousel) { const shouldPagerScroll = scale === 1; - attachmentCarouselPagerContext.onPinchGestureChange(!shouldPagerScroll); + attachmentCarouselPagerContext?.onPinchGestureChange(!shouldPagerScroll); - if (attachmentCarouselPagerContext.shouldPagerScroll.value === shouldPagerScroll) { + if (attachmentCarouselPagerContext?.shouldPagerScroll.value === shouldPagerScroll) { return; } - attachmentCarouselPagerContext.shouldPagerScroll.value = shouldPagerScroll; + attachmentCarouselPagerContext?.shouldPagerScroll.value = shouldPagerScroll; } }, [attachmentCarouselPagerContext, isUsedInCarousel, onScaleChangedProp], @@ -60,8 +60,6 @@ function BaseAttachmentViewPdf({ ); } -BaseAttachmentViewPdf.propTypes = attachmentViewPdfPropTypes; -BaseAttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps; BaseAttachmentViewPdf.displayName = 'BaseAttachmentViewPdf'; export default memo(BaseAttachmentViewPdf); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx similarity index 80% rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx index 46afd23daa4c..0a9a8cda1182 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.android.tsx @@ -5,10 +5,9 @@ import Animated, {useSharedValue} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import styles from '@styles/styles'; import BaseAttachmentViewPdf from './BaseAttachmentViewPdf'; -import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; +import AttachmentViewPdfProps from './types'; -function AttachmentViewPdf(props) { - const {onScaleChanged, ...restProps} = props; +function AttachmentViewPdf({onScaleChanged, ...props}: AttachmentViewPdfProps) { const attachmentCarouselPagerContext = useContext(AttachmentCarouselPagerContext); const scaleRef = useSharedValue(1); const offsetX = useSharedValue(0); @@ -22,8 +21,10 @@ function AttachmentViewPdf(props) { // enable the pager scroll so that the user // can swipe to the next attachment otherwise disable it. if (Math.abs(evt.allTouches[0].absoluteX - offsetX.value) > Math.abs(evt.allTouches[0].absoluteY - offsetY.value) && scaleRef.value === 1) { - attachmentCarouselPagerContext.shouldPagerScroll.value = true; - } else { + if (attachmentCarouselPagerContext) { + attachmentCarouselPagerContext.shouldPagerScroll.value = true; + } + } else if (attachmentCarouselPagerContext) { attachmentCarouselPagerContext.shouldPagerScroll.value = false; } } @@ -32,7 +33,7 @@ function AttachmentViewPdf(props) { }); const updateScale = useCallback( - (scale) => { + (scale: number) => { scaleRef.value = scale; }, [scaleRef], @@ -41,7 +42,7 @@ function AttachmentViewPdf(props) { return ( { - updateScale(scale); + updateScale(scale ?? 0); onScaleChanged(); }} /> @@ -62,7 +63,4 @@ function AttachmentViewPdf(props) { ); } -AttachmentViewPdf.propTypes = attachmentViewPdfPropTypes; -AttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps; - export default memo(AttachmentViewPdf); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx similarity index 50% rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx index 103ff292760f..655df67a74e4 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.ios.tsx @@ -1,17 +1,16 @@ import React, {memo} from 'react'; import BaseAttachmentViewPdf from './BaseAttachmentViewPdf'; -import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; +import AttachmentViewPdfProps from './types'; -function AttachmentViewPdf(props) { +function AttachmentViewPdf(props: AttachmentViewPdfProps) { + const {file = {name: ''} as File, ...restProps} = props; return ( ); } -AttachmentViewPdf.propTypes = attachmentViewPdfPropTypes; -AttachmentViewPdf.defaultProps = attachmentViewPdfDefaultProps; - export default memo(AttachmentViewPdf); diff --git a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx similarity index 58% rename from src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js rename to src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx index c3d1423b17c9..f4cb55e60057 100644 --- a/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.js +++ b/src/components/Attachments/AttachmentView/AttachmentViewPdf/index.tsx @@ -1,8 +1,8 @@ import React, {memo} from 'react'; import PDFView from '@components/PDFView'; -import {attachmentViewPdfDefaultProps, attachmentViewPdfPropTypes} from './propTypes'; +import AttachmentViewPdfProps from './types'; -function AttachmentViewPdf({file, encryptedSourceUrl, isFocused, onPress, onScaleChanged, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}) { +function AttachmentViewPdf({file = {name: ''}, encryptedSourceUrl, isFocused, onPress, onScaleChanged, onToggleKeyboard, onLoadComplete, errorLabelStyles, style}: AttachmentViewPdfProps) { return ( void; + onLoadComplete: () => void; + + /** Additional style props */ + style: StyleProp; + + /** Styles for the error label */ + errorLabelStyles: StyleProp; +} & AttachmentProps; + +export default AttachmentViewPdfProps; diff --git a/src/components/Attachments/AttachmentView/index.js b/src/components/Attachments/AttachmentView/index.tsx similarity index 80% rename from src/components/Attachments/AttachmentView/index.js rename to src/components/Attachments/AttachmentView/index.tsx index e484abe041b9..c9574f0a84cb 100755 --- a/src/components/Attachments/AttachmentView/index.js +++ b/src/components/Attachments/AttachmentView/index.tsx @@ -1,8 +1,8 @@ import Str from 'expensify-common/lib/str'; import PropTypes from 'prop-types'; import React, {memo, useState} from 'react'; -import {ActivityIndicator, ScrollView, View} from 'react-native'; -import {withOnyx} from 'react-native-onyx'; +import {ActivityIndicator, ImageSourcePropType, ScrollView, StyleProp, View, ViewStyle} from 'react-native'; +import {OnyxEntry, withOnyx} from 'react-native-onyx'; import _ from 'underscore'; import DistanceEReceipt from '@components/DistanceEReceipt'; import EReceipt from '@components/EReceipt'; @@ -11,6 +11,7 @@ import * as Expensicons from '@components/Icon/Expensicons'; import Text from '@components/Text'; import Tooltip from '@components/Tooltip'; import withLocalize, {withLocalizePropTypes} from '@components/withLocalize'; +import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import addEncryptedAuthTokenToURL from '@libs/addEncryptedAuthTokenToURL'; import compose from '@libs/compose'; @@ -21,77 +22,73 @@ import useThemeStyles from '@styles/useThemeStyles'; import cursor from '@styles/utilities/cursor'; import variables from '@styles/variables'; import ONYXKEYS from '@src/ONYXKEYS'; +import {Transaction} from '@src/types/onyx'; import AttachmentViewImage from './AttachmentViewImage'; import AttachmentViewPdf from './AttachmentViewPdf'; -import {attachmentViewDefaultProps, attachmentViewPropTypes} from './propTypes'; +import AttachmentProps from './types'; -const propTypes = { - ...attachmentViewPropTypes, - ...withLocalizePropTypes, +type AttachmentViewOnyxProps = { + /** The transaction currently being looked at */ + transaction: OnyxEntry; +}; +type AttachmentViewProps = { /** Flag to show/hide download icon */ - shouldShowDownloadIcon: PropTypes.bool, + shouldShowDownloadIcon?: boolean; /** Flag to show the loading indicator */ - shouldShowLoadingSpinnerIcon: PropTypes.bool, + shouldShowLoadingSpinnerIcon?: boolean; /** Notify parent that the UI should be modified to accommodate keyboard */ - onToggleKeyboard: PropTypes.func, + onToggleKeyboard?: () => void; /** Extra styles to pass to View wrapper */ // eslint-disable-next-line react/forbid-prop-types - containerStyles: PropTypes.arrayOf(PropTypes.object), + containerStyles?: StyleProp; /** Denotes whether it is a workspace avatar or not */ - isWorkspaceAvatar: PropTypes.bool, + isWorkspaceAvatar?: boolean; /** The id of the transaction related to the attachment */ // eslint-disable-next-line react/no-unused-prop-types - transactionID: PropTypes.string, -}; + transactionID?: string; -const defaultProps = { - ...attachmentViewDefaultProps, - shouldShowDownloadIcon: false, - shouldShowLoadingSpinnerIcon: false, - onToggleKeyboard: () => {}, - containerStyles: [], - isWorkspaceAvatar: false, - transactionID: '', -}; + fallbackSource?: string | ImageSourcePropType; +} & AttachmentProps & + AttachmentViewOnyxProps; function AttachmentView({ source, - file, - isAuthTokenRequired, + file = {name: ''} as File, + isAuthTokenRequired = false, isUsedInCarousel, onPress, - shouldShowLoadingSpinnerIcon, - shouldShowDownloadIcon, + shouldShowLoadingSpinnerIcon = false, + shouldShowDownloadIcon = false, containerStyles, - onScaleChanged, - onToggleKeyboard, - translate, - isFocused, - isWorkspaceAvatar, + onScaleChanged = () => {}, + onToggleKeyboard = () => {}, + isFocused = false, + isWorkspaceAvatar = false, fallbackSource, transaction, - isUsedInAttachmentModal, -}) { + isUsedInAttachmentModal = false, +}: AttachmentViewProps) { const theme = useTheme(); const styles = useThemeStyles(); const [loadComplete, setLoadComplete] = useState(false); const [imageError, setImageError] = useState(false); + const {translate} = useLocalize(); useNetwork({onReconnect: () => setImageError(false)}); // Handles case where source is a component (ex: SVG) - if (_.isFunction(source)) { + if (typeof source === 'function') { let iconFillColor = ''; - let additionalStyles = []; + let additionalStyles: StyleProp = []; if (isWorkspaceAvatar) { const defaultWorkspaceAvatarColor = StyleUtils.getDefaultWorkspaceAvatarColor(file.name); - iconFillColor = defaultWorkspaceAvatarColor.fill; + iconFillColor = defaultWorkspaceAvatarColor.fill ?? ''; additionalStyles = [defaultWorkspaceAvatarColor]; } @@ -113,7 +110,7 @@ function AttachmentView({ style={styles.w100} contentContainerStyle={[styles.flexGrow1, styles.justifyContentCenter, styles.alignItemsCenter]} > - + ); @@ -121,7 +118,7 @@ function AttachmentView({ // Check both source and file.name since PDFs dragged into the text field // will appear with a source that is a blob - if ((_.isString(source) && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { + if ((typeof source === 'string' && Str.isPDF(source)) || (file && Str.isPDF(file.name || translate('attachmentView.unknownFilename')))) { const encryptedSourceUrl = isAuthTokenRequired ? addEncryptedAuthTokenToURL(source) : source; // We need the following View component on android native @@ -175,7 +172,7 @@ function AttachmentView({ } return ( - + @@ -202,14 +199,11 @@ function AttachmentView({ ); } -AttachmentView.propTypes = propTypes; -AttachmentView.defaultProps = defaultProps; AttachmentView.displayName = 'AttachmentView'; export default compose( memo, - withLocalize, - withOnyx({ + withOnyx({ transaction: { key: ({transactionID}) => `${ONYXKEYS.COLLECTION.TRANSACTION}${transactionID}`, }, diff --git a/src/components/Attachments/AttachmentView/types.ts b/src/components/Attachments/AttachmentView/types.ts new file mode 100644 index 000000000000..62b463e20c16 --- /dev/null +++ b/src/components/Attachments/AttachmentView/types.ts @@ -0,0 +1,14 @@ +import {ImageSourcePropType} from 'react-native'; + +type AttachmentProps = { + source: string | ImageSourcePropType; + file: {name: string}; + isAuthTokenRequired?: boolean; + isFocused?: boolean; + isUsedInCarousel?: boolean; + onPress?: () => void; + onScaleChanged: (scale?: number) => void; + isUsedInAttachmentModal?: boolean; +}; + +export default AttachmentProps; From 2762106eb2bbfd5559c814e89eb141e1297488a9 Mon Sep 17 00:00:00 2001 From: Jakub Butkiewicz Date: Fri, 24 Nov 2023 19:33:53 +0100 Subject: [PATCH 2/8] ref: migrate AttachmentCarouselCellRenderer, CarouselActions, CarouselButtons --- ....js => AttachmentCarouselCellRenderer.tsx} | 15 ++--- .../AttachmentCarousel/CarouselActions.js | 55 ----------------- .../AttachmentCarousel/CarouselActions.tsx | 33 +++++++++++ ...CarouselButtons.js => CarouselButtons.tsx} | 37 ++++-------- .../{CarouselItem.js => CarouselItem.tsx} | 59 +++++++------------ .../extractAttachmentsFromReport.ts | 11 +--- .../Attachments/AttachmentCarousel/index.js | 2 +- .../AttachmentCarousel/index.native.js | 2 +- .../Attachments/AttachmentCarousel/types.ts | 17 ++++++ .../Attachments/AttachmentView/types.ts | 2 +- src/hooks/useKeyboardShortcut.ts | 2 +- 11 files changed, 90 insertions(+), 145 deletions(-) rename src/components/Attachments/AttachmentCarousel/{AttachmentCarouselCellRenderer.js => AttachmentCarouselCellRenderer.tsx} (67%) delete mode 100644 src/components/Attachments/AttachmentCarousel/CarouselActions.js create mode 100644 src/components/Attachments/AttachmentCarousel/CarouselActions.tsx rename src/components/Attachments/AttachmentCarousel/{CarouselButtons.js => CarouselButtons.tsx} (74%) rename src/components/Attachments/AttachmentCarousel/{CarouselItem.js => CarouselItem.tsx} (66%) create mode 100644 src/components/Attachments/AttachmentCarousel/types.ts diff --git a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx similarity index 67% rename from src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js rename to src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx index dd2713a38b2b..d46dce1a3658 100644 --- a/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.js +++ b/src/components/Attachments/AttachmentCarousel/AttachmentCarouselCellRenderer.tsx @@ -1,19 +1,14 @@ -import PropTypes from 'prop-types'; import React from 'react'; -import {PixelRatio, View} from 'react-native'; +import {PixelRatio, StyleProp, View, ViewStyle} from 'react-native'; import useWindowDimensions from '@hooks/useWindowDimensions'; import useThemeStyles from '@styles/useThemeStyles'; -const propTypes = { +type AttachmentCarouselCellRendererProps = { /** Cell Container styles */ - style: PropTypes.oneOfType([PropTypes.arrayOf(PropTypes.object), PropTypes.object]), + style: StyleProp; }; -const defaultProps = { - style: [], -}; - -function AttachmentCarouselCellRenderer(props) { +function AttachmentCarouselCellRenderer(props: AttachmentCarouselCellRendererProps) { const styles = useThemeStyles(); const {windowWidth, isSmallScreenWidth} = useWindowDimensions(); const modalStyles = styles.centeredModalStyles(isSmallScreenWidth, true); @@ -28,8 +23,6 @@ function AttachmentCarouselCellRenderer(props) { ); } -AttachmentCarouselCellRenderer.propTypes = propTypes; -AttachmentCarouselCellRenderer.defaultProps = defaultProps; AttachmentCarouselCellRenderer.displayName = 'AttachmentCarouselCellRenderer'; export default React.memo(AttachmentCarouselCellRenderer); diff --git a/src/components/Attachments/AttachmentCarousel/CarouselActions.js b/src/components/Attachments/AttachmentCarousel/CarouselActions.js deleted file mode 100644 index cf5309222c4e..000000000000 --- a/src/components/Attachments/AttachmentCarousel/CarouselActions.js +++ /dev/null @@ -1,55 +0,0 @@ -import lodashGet from 'lodash/get'; -import PropTypes from 'prop-types'; -import {useEffect} from 'react'; -import KeyboardShortcut from '@libs/KeyboardShortcut'; -import CONST from '@src/CONST'; - -const propTypes = { - /** Callback to cycle through attachments */ - onCycleThroughAttachments: PropTypes.func.isRequired, -}; - -function CarouselActions({onCycleThroughAttachments}) { - useEffect(() => { - const shortcutLeftConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT; - const unsubscribeLeftKey = KeyboardShortcut.subscribe( - shortcutLeftConfig.shortcutKey, - (e) => { - if (lodashGet(e, 'target.blur')) { - // prevents focus from highlighting around the modal - e.target.blur(); - } - - onCycleThroughAttachments(-1); - }, - shortcutLeftConfig.descriptionKey, - shortcutLeftConfig.modifiers, - ); - - const shortcutRightConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT; - const unsubscribeRightKey = KeyboardShortcut.subscribe( - shortcutRightConfig.shortcutKey, - (e) => { - if (lodashGet(e, 'target.blur')) { - // prevents focus from highlighting around the modal - e.target.blur(); - } - - onCycleThroughAttachments(1); - }, - shortcutRightConfig.descriptionKey, - shortcutRightConfig.modifiers, - ); - - return () => { - unsubscribeLeftKey(); - unsubscribeRightKey(); - }; - }, [onCycleThroughAttachments]); - - return null; -} - -CarouselActions.propTypes = propTypes; - -export default CarouselActions; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselActions.tsx b/src/components/Attachments/AttachmentCarousel/CarouselActions.tsx new file mode 100644 index 000000000000..bdc181b6be9d --- /dev/null +++ b/src/components/Attachments/AttachmentCarousel/CarouselActions.tsx @@ -0,0 +1,33 @@ +import useKeyboardShortcut from '@hooks/useKeyboardShortcut'; +import CONST from '@src/CONST'; + +type CarouselActionsProps = { + onCycleThroughAttachments: (direction: number) => void; +}; + +const shortcutLeftConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_LEFT; +const shortcutRightConfig = CONST.KEYBOARD_SHORTCUTS.ARROW_RIGHT; + +function CarouselActions({onCycleThroughAttachments}: CarouselActionsProps) { + useKeyboardShortcut(shortcutLeftConfig, (e) => { + const target = e?.target as HTMLElement; + // prevents focus from highlighting around the modal + target?.blur(); + + onCycleThroughAttachments(-1); + }); + + useKeyboardShortcut(shortcutRightConfig, (e) => { + const target = e?.target as HTMLElement; + // prevents focus from highlighting around the modal + target?.blur(); + + onCycleThroughAttachments(1); + }); + + return null; +} + +CarouselActions.displayName = 'CarouselActions'; + +export default CarouselActions; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx similarity index 74% rename from src/components/Attachments/AttachmentCarousel/CarouselButtons.js rename to src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx index 14a6ea268468..7875aae69adc 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselButtons.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselButtons.tsx @@ -10,36 +10,23 @@ import useLocalize from '@hooks/useLocalize'; import useWindowDimensions from '@hooks/useWindowDimensions'; import useTheme from '@styles/themes/useTheme'; import useThemeStyles from '@styles/useThemeStyles'; +import {Attachment} from './CarouselItem'; -const propTypes = { - /** Where the arrows should be visible */ - shouldShowArrows: PropTypes.bool.isRequired, - - /** The current page index */ - page: PropTypes.number.isRequired, - - /** The attachments from the carousel */ - attachments: AttachmentCarouselViewPropTypes.attachmentsPropType.isRequired, - - /** Callback to go one page back */ - onBack: PropTypes.func.isRequired, - /** Callback to go one page forward */ - onForward: PropTypes.func.isRequired, - - autoHideArrow: PropTypes.func, - cancelAutoHideArrow: PropTypes.func, -}; - -const defaultProps = { - autoHideArrow: () => {}, - cancelAutoHideArrow: () => {}, +type CarouselButtonsProps = { + shouldShowArrows: boolean; + page: number; + attachments: Attachment[]; + onBack: () => void; + onForward: () => void; + autoHideArrow?: () => void; + cancelAutoHideArrow?: () => void; }; -function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow, autoHideArrow}) { +function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward, cancelAutoHideArrow = () => {}, autoHideArrow = () => {}}: CarouselButtonsProps) { const theme = useTheme(); const styles = useThemeStyles(); const isBackDisabled = page === 0; - const isForwardDisabled = page === _.size(attachments) - 1; + const isForwardDisabled = page === attachments.length - 1; const {translate} = useLocalize(); const {isSmallScreenWidth} = useWindowDimensions(); @@ -82,8 +69,6 @@ function CarouselButtons({page, attachments, shouldShowArrows, onBack, onForward ) : null; } -CarouselButtons.propTypes = propTypes; -CarouselButtons.defaultProps = defaultProps; CarouselButtons.displayName = 'CarouselButtons'; export default CarouselButtons; diff --git a/src/components/Attachments/AttachmentCarousel/CarouselItem.js b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx similarity index 66% rename from src/components/Attachments/AttachmentCarousel/CarouselItem.js rename to src/components/Attachments/AttachmentCarousel/CarouselItem.tsx index b6cc0cbf21a4..7677eb49450b 100644 --- a/src/components/Attachments/AttachmentCarousel/CarouselItem.js +++ b/src/components/Attachments/AttachmentCarousel/CarouselItem.tsx @@ -1,8 +1,6 @@ -import PropTypes from 'prop-types'; import React, {useContext, useState} from 'react'; -import {View} from 'react-native'; +import {Role, StyleProp, View, ViewStyle} from 'react-native'; import AttachmentView from '@components/Attachments/AttachmentView'; -import * as AttachmentsPropTypes from '@components/Attachments/propTypes'; import Button from '@components/Button'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import SafeAreaConsumer from '@components/SafeAreaConsumer'; @@ -12,50 +10,34 @@ import ReportAttachmentsContext from '@pages/home/report/ReportAttachmentsContex import useThemeStyles from '@styles/useThemeStyles'; import CONST from '@src/CONST'; -const propTypes = { - /** Attachment required information such as the source and file name */ - item: PropTypes.shape({ - /** Report action ID of the attachment */ - reportActionID: PropTypes.string, - - /** Whether source URL requires authentication */ - isAuthTokenRequired: PropTypes.bool, - - /** URL to full-sized attachment or SVG function */ - source: AttachmentsPropTypes.attachmentSourcePropType.isRequired, - - /** Additional information about the attachment file */ - file: PropTypes.shape({ - /** File name of the attachment */ - name: PropTypes.string, - }), - - /** Whether the attachment has been flagged */ - hasBeenFlagged: PropTypes.bool, +type Attachment = { + reportActionID?: string; + source: string; + isAuthTokenRequired: boolean; + file: {name: string}; + isReceipt: boolean; + hasBeenFlagged?: boolean; + transactionID?: string; +}; - /** The id of the transaction related to the attachment */ - transactionID: PropTypes.string, - }).isRequired, +type CarouselItemProps = { + /** Attachment required information such as the source and file name */ + item: Attachment; /** Whether the attachment is currently being viewed in the carousel */ - isFocused: PropTypes.bool.isRequired, + isFocused: boolean; /** onPress callback */ - onPress: PropTypes.func, -}; - -const defaultProps = { - onPress: undefined, + onPress?: () => void; }; -function CarouselItem({item, isFocused, onPress}) { +function CarouselItem({item, isFocused, onPress = undefined}: CarouselItemProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const {isAttachmentHidden} = useContext(ReportAttachmentsContext); - // eslint-disable-next-line es/no-nullish-coalescing-operators - const [isHidden, setIsHidden] = useState(() => isAttachmentHidden(item.reportActionID) ?? item.hasBeenFlagged); + const [isHidden, setIsHidden] = useState(() => isAttachmentHidden(item.reportActionID) ?? item.hasBeenFlagged); - const renderButton = (style) => ( + const renderButton = (style: StyleProp) => (