diff --git a/src/components/Avatar.tsx b/src/components/Avatar.tsx index 8ea8a1bb6f64..2b2d0a60f657 100644 --- a/src/components/Avatar.tsx +++ b/src/components/Avatar.tsx @@ -1,5 +1,5 @@ import React, {useEffect, useState} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import useNetwork from '@hooks/useNetwork'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -19,7 +19,7 @@ type AvatarProps = { source?: AvatarSource; /** Extra styles to pass to Image */ - imageStyles?: StyleProp; + imageStyles?: StyleProp; /** Additional styles to pass to Icon */ iconAdditionalStyles?: StyleProp; @@ -81,7 +81,7 @@ function Avatar({ const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE; const iconSize = StyleUtils.getAvatarSize(size); - const imageStyle = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; + const imageStyle: StyleProp = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius]; const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined; const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(name).fill : fill; @@ -92,7 +92,15 @@ function Avatar({ return ( - {typeof avatarSource === 'function' || typeof avatarSource === 'number' ? ( + {typeof avatarSource === 'string' ? ( + + setImageError(true)} + /> + + ) : ( - ) : ( - - setImageError(true)} - /> - )} ); diff --git a/src/components/AvatarWithImagePicker.tsx b/src/components/AvatarWithImagePicker.tsx index 4388ebb8f815..5755c69641c8 100644 --- a/src/components/AvatarWithImagePicker.tsx +++ b/src/components/AvatarWithImagePicker.tsx @@ -1,6 +1,6 @@ import React, {useEffect, useRef, useState} from 'react'; import {StyleSheet, View} from 'react-native'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; @@ -103,7 +103,7 @@ type AvatarWithImagePickerProps = { isFocused: boolean; /** Style applied to the avatar */ - avatarStyle: StyleProp; + avatarStyle: StyleProp; /** Indicates if picker feature should be disabled */ disabled?: boolean; @@ -279,8 +279,6 @@ function AvatarWithImagePicker({ vertical: y + height + variables.spacing2, }); }); - - // eslint-disable-next-line react-hooks/exhaustive-deps }, [isMenuVisible, windowWidth]); return ( diff --git a/src/components/Image/imagePropTypes.js b/src/components/Image/imagePropTypes.js deleted file mode 100644 index 78bd48ba47ec..000000000000 --- a/src/components/Image/imagePropTypes.js +++ /dev/null @@ -1,51 +0,0 @@ -import PropTypes from 'prop-types'; -import stylePropTypes from '@styles/stylePropTypes'; -import RESIZE_MODES from './resizeModes'; -import sourcePropTypes from './sourcePropTypes'; - -const imagePropTypes = { - /** Styles for the Image */ - style: stylePropTypes, - - /** The static asset or URI source of the image */ - source: sourcePropTypes.isRequired, - - /** Should an auth token be included in the image request */ - isAuthTokenRequired: PropTypes.bool, - - /** How should the image fit within its container */ - resizeMode: PropTypes.string, - - /** Event for when the image begins loading */ - onLoadStart: PropTypes.func, - - /** Event for when the image finishes loading */ - onLoadEnd: PropTypes.func, - - /** Event for when the image is fully loaded and returns the natural dimensions of the image */ - onLoad: PropTypes.func, - - /** Progress events while the image is downloading */ - onProgress: PropTypes.func, - - /* Onyx Props */ - /** Session info for the currently logged in user. */ - session: PropTypes.shape({ - /** Currently logged in user authToken */ - authToken: PropTypes.string, - }), -}; - -const defaultProps = { - style: [], - session: { - authToken: null, - }, - isAuthTokenRequired: false, - resizeMode: RESIZE_MODES.cover, - onLoadStart: () => {}, - onLoadEnd: () => {}, - onLoad: () => {}, -}; - -export {imagePropTypes, defaultProps}; diff --git a/src/components/Image/index.native.js b/src/components/Image/index.native.tsx similarity index 52% rename from src/components/Image/index.native.js rename to src/components/Image/index.native.tsx index f31cfb6936d9..63440ca96dc0 100644 --- a/src/components/Image/index.native.js +++ b/src/components/Image/index.native.tsx @@ -1,35 +1,26 @@ import {Image as ImageComponent} from 'expo-image'; -import lodashGet from 'lodash/get'; import React from 'react'; import {withOnyx} from 'react-native-onyx'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import {defaultProps, imagePropTypes} from './imagePropTypes'; -import RESIZE_MODES from './resizeModes'; +import type {ImageOnyxProps, ImageProps} from './types'; const dimensionsCache = new Map(); -function resolveDimensions(key) { - return dimensionsCache.get(key); -} - -function Image(props) { - // eslint-disable-next-line react/destructuring-assignment - const {source, isAuthTokenRequired, session, ...rest} = props; - +function Image({source, isAuthTokenRequired = false, session, onLoad, ...rest}: ImageProps) { let imageSource = source; - if (source && source.uri && typeof source.uri === 'number') { + if (typeof source === 'object' && 'uri' in source && typeof source.uri === 'number') { imageSource = source.uri; } - if (typeof imageSource !== 'number' && isAuthTokenRequired) { - const authToken = lodashGet(props, 'session.encryptedAuthToken', null); + if (typeof imageSource === 'object' && typeof source === 'object' && isAuthTokenRequired) { + const authToken = session?.encryptedAuthToken ?? null; imageSource = { ...source, headers: authToken ? { [CONST.CHAT_ATTACHMENT_TOKEN_KEY]: authToken, } - : null, + : undefined, }; } @@ -41,23 +32,20 @@ function Image(props) { onLoad={(evt) => { const {width, height, url} = evt.source; dimensionsCache.set(url, {width, height}); - if (props.onLoad) { - props.onLoad({nativeEvent: {width, height}}); + if (onLoad) { + onLoad({nativeEvent: {width, height}}); } }} /> ); } -Image.propTypes = imagePropTypes; -Image.defaultProps = defaultProps; Image.displayName = 'Image'; -const ImageWithOnyx = withOnyx({ + +const ImageWithOnyx = withOnyx({ session: { key: ONYXKEYS.SESSION, }, })(Image); -ImageWithOnyx.resizeMode = RESIZE_MODES; -ImageWithOnyx.resolveDimensions = resolveDimensions; export default ImageWithOnyx; diff --git a/src/components/Image/index.js b/src/components/Image/index.tsx similarity index 66% rename from src/components/Image/index.js rename to src/components/Image/index.tsx index 59fcde8273fd..a2198223c12e 100644 --- a/src/components/Image/index.js +++ b/src/components/Image/index.tsx @@ -1,15 +1,11 @@ -import lodashGet from 'lodash/get'; import React, {useEffect, useMemo} from 'react'; import {Image as RNImage} from 'react-native'; import {withOnyx} from 'react-native-onyx'; -import _ from 'underscore'; import useNetwork from '@hooks/useNetwork'; import ONYXKEYS from '@src/ONYXKEYS'; -import {defaultProps, imagePropTypes} from './imagePropTypes'; -import RESIZE_MODES from './resizeModes'; +import type {ImageOnyxProps, ImageOwnProps, ImageProps} from './types'; -function Image(props) { - const {source: propsSource, isAuthTokenRequired, onLoad, session} = props; +function Image({source: propsSource, isAuthTokenRequired = false, onLoad, session, ...forwardedProps}: ImageProps) { const {isOffline} = useNetwork(); /** @@ -17,12 +13,12 @@ function Image(props) { * to the source. */ const source = useMemo(() => { - if (isAuthTokenRequired) { + const authToken = session?.encryptedAuthToken ?? null; + if (isAuthTokenRequired && typeof propsSource === 'object' && 'uri' in propsSource && authToken) { // There is currently a `react-native-web` bug preventing the authToken being passed // in the headers of the image request so the authToken is added as a query param. // On native the authToken IS passed in the image request headers - const authToken = lodashGet(session, 'encryptedAuthToken', null); - return {uri: `${propsSource.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`}; + return {uri: `${propsSource?.uri}?encryptedAuthToken=${encodeURIComponent(authToken)}`}; } return propsSource; // The session prop is not required, as it causes the image to reload whenever the session changes. For more information, please refer to issue #26034. @@ -39,13 +35,13 @@ function Image(props) { if (onLoad == null) { return; } - RNImage.getSize(source.uri, (width, height) => { - onLoad({nativeEvent: {width, height}}); - }); - }, [onLoad, source, isOffline]); - // Omit the props which the underlying RNImage won't use - const forwardedProps = _.omit(props, ['source', 'onLoad', 'session', 'isAuthTokenRequired']); + if (typeof source === 'object' && 'uri' in source && source.uri) { + RNImage.getSize(source.uri, (width, height) => { + onLoad({nativeEvent: {width, height}}); + }); + } + }, [onLoad, source, isOffline]); return ( ({ session: { key: ONYXKEYS.SESSION, }, })(Image), imagePropsAreEqual, ); -ImageWithOnyx.resizeMode = RESIZE_MODES; + +ImageWithOnyx.displayName = 'Image'; export default ImageWithOnyx; diff --git a/src/components/Image/resizeModes.js b/src/components/Image/resizeModes.ts similarity index 92% rename from src/components/Image/resizeModes.js rename to src/components/Image/resizeModes.ts index e6cc699a2fe3..246793a9e3a3 100644 --- a/src/components/Image/resizeModes.js +++ b/src/components/Image/resizeModes.ts @@ -3,6 +3,6 @@ const RESIZE_MODES = { cover: 'cover', stretch: 'stretch', center: 'center', -}; +} as const; export default RESIZE_MODES; diff --git a/src/components/Image/types.ts b/src/components/Image/types.ts new file mode 100644 index 000000000000..e764b53706a2 --- /dev/null +++ b/src/components/Image/types.ts @@ -0,0 +1,51 @@ +import type {ImageSource} from 'expo-image'; +import type {ImageRequireSource, ImageResizeMode, ImageStyle, ImageURISource, StyleProp} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import type {Session} from '@src/types/onyx'; + +type ExpoImageSource = ImageSource | number | ImageSource[]; + +type ImageOnyxProps = { + /** Session info for the currently logged in user. */ + session: OnyxEntry; +}; + +type ImageOnLoadEvent = { + nativeEvent: { + width: number; + height: number; + }; +}; + +type ImageOwnProps = { + /** Styles for the Image */ + style?: StyleProp; + + /** The static asset or URI source of the image */ + source: ExpoImageSource | Omit | ImageRequireSource | undefined; + + /** Should an auth token be included in the image request */ + isAuthTokenRequired?: boolean; + + /** How should the image fit within its container */ + resizeMode?: ImageResizeMode; + + /** Event for when the image begins loading */ + onLoadStart?: () => void; + + /** Event for when the image finishes loading */ + onLoadEnd?: () => void; + + /** Error handler */ + onError?: () => void; + + /** Event for when the image is fully loaded and returns the natural dimensions of the image */ + onLoad?: (event: ImageOnLoadEvent) => void; + + /** Progress events while the image is downloading */ + onProgress?: () => void; +}; + +type ImageProps = ImageOnyxProps & ImageOwnProps; + +export type {ImageOwnProps, ImageOnyxProps, ImageProps, ExpoImageSource, ImageOnLoadEvent}; diff --git a/src/components/ImageView/index.native.tsx b/src/components/ImageView/index.native.tsx index 24f7ffb1dac3..c27869f0e281 100644 --- a/src/components/ImageView/index.native.tsx +++ b/src/components/ImageView/index.native.tsx @@ -1,7 +1,7 @@ import React from 'react'; import Lightbox from '@components/Lightbox'; import {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas'; -import type {ImageViewProps} from './types'; +import type ImageViewProps from './types'; function ImageView({isAuthTokenRequired = false, url, style, zoomRange = DEFAULT_ZOOM_RANGE, onError}: ImageViewProps) { return ( diff --git a/src/components/ImageView/index.tsx b/src/components/ImageView/index.tsx index ec37abf6d275..5d09e7abf41d 100644 --- a/src/components/ImageView/index.tsx +++ b/src/components/ImageView/index.tsx @@ -1,17 +1,18 @@ import type {SyntheticEvent} from 'react'; import React, {useCallback, useEffect, useRef, useState} from 'react'; -import type {GestureResponderEvent, LayoutChangeEvent, NativeSyntheticEvent} from 'react-native'; +import type {GestureResponderEvent, LayoutChangeEvent} from 'react-native'; import {View} from 'react-native'; import FullscreenLoadingIndicator from '@components/FullscreenLoadingIndicator'; import Image from '@components/Image'; import RESIZE_MODES from '@components/Image/resizeModes'; +import type {ImageOnLoadEvent} from '@components/Image/types'; import PressableWithoutFeedback from '@components/Pressable/PressableWithoutFeedback'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import * as DeviceCapabilities from '@libs/DeviceCapabilities'; import CONST from '@src/CONST'; import viewRef from '@src/types/utils/viewRef'; -import type {ImageLoadNativeEventData, ImageViewProps} from './types'; +import type ImageViewProps from './types'; type ZoomDelta = {offsetX: number; offsetY: number}; @@ -73,7 +74,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV setIsZoomed(false); }; - const imageLoad = ({nativeEvent}: NativeSyntheticEvent) => { + const imageLoad = ({nativeEvent}: ImageOnLoadEvent) => { setImageRegion(nativeEvent.width, nativeEvent.height); setIsLoading(false); }; diff --git a/src/components/ImageView/types.ts b/src/components/ImageView/types.ts index bb63373324cb..b19e6b228cbd 100644 --- a/src/components/ImageView/types.ts +++ b/src/components/ImageView/types.ts @@ -21,9 +21,4 @@ type ImageViewProps = { zoomRange?: ZoomRange; }; -type ImageLoadNativeEventData = { - width: number; - height: number; -}; - -export type {ImageViewProps, ImageLoadNativeEventData}; +export default ImageViewProps; diff --git a/src/components/ImageWithSizeCalculation.tsx b/src/components/ImageWithSizeCalculation.tsx index 0ca4a0456e33..f2dcc2cc3422 100644 --- a/src/components/ImageWithSizeCalculation.tsx +++ b/src/components/ImageWithSizeCalculation.tsx @@ -47,7 +47,7 @@ function ImageWithSizeCalculation({url, style, onMeasure, onLoadFailure, isAuthT const [isLoading, setIsLoading] = useState(false); const {isOffline} = useNetwork(); - const source = useMemo(() => ({uri: url}), [url]); + const source = useMemo(() => (typeof url === 'string' ? {uri: url} : url), [url]); const onError = () => { Log.hmmm('Unable to fetch image to calculate size', {url}); diff --git a/src/components/Lightbox/index.tsx b/src/components/Lightbox/index.tsx index 69fa0d5e6e41..a7ed6946fb28 100644 --- a/src/components/Lightbox/index.tsx +++ b/src/components/Lightbox/index.tsx @@ -1,9 +1,10 @@ import React, {useCallback, useContext, useEffect, useMemo, useState} from 'react'; -import type {LayoutChangeEvent, NativeSyntheticEvent, StyleProp, ViewStyle} from 'react-native'; +import type {LayoutChangeEvent, StyleProp, ViewStyle} from 'react-native'; import {ActivityIndicator, PixelRatio, StyleSheet, View} from 'react-native'; import {useSharedValue} from 'react-native-reanimated'; import AttachmentCarouselPagerContext from '@components/Attachments/AttachmentCarousel/Pager/AttachmentCarouselPagerContext'; import Image from '@components/Image'; +import type {ImageOnLoadEvent} from '@components/Image/types'; import MultiGestureCanvas, {DEFAULT_ZOOM_RANGE} from '@components/MultiGestureCanvas'; import type {CanvasSize, ContentSize, OnScaleChangedCallback, ZoomRange} from '@components/MultiGestureCanvas/types'; import {getCanvasFitScale} from '@components/MultiGestureCanvas/utils'; @@ -13,8 +14,6 @@ import NUMBER_OF_CONCURRENT_LIGHTBOXES from './numberOfConcurrentLightboxes'; const DEFAULT_IMAGE_SIZE = 200; const DEFAULT_IMAGE_DIMENSION: ContentSize = {width: DEFAULT_IMAGE_SIZE, height: DEFAULT_IMAGE_SIZE}; -type ImageOnLoadEvent = NativeSyntheticEvent; - const cachedImageDimensions = new Map(); type LightboxProps = { diff --git a/src/components/MultipleAvatars.tsx b/src/components/MultipleAvatars.tsx index 8b606bd4429d..ba1a5351117b 100644 --- a/src/components/MultipleAvatars.tsx +++ b/src/components/MultipleAvatars.tsx @@ -1,5 +1,5 @@ import React, {memo, useMemo} from 'react'; -import type {StyleProp, ViewStyle} from 'react-native'; +import type {ImageStyle, StyleProp, ViewStyle} from 'react-native'; import {View} from 'react-native'; import type {ValueOf} from 'type-fest'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -57,8 +57,8 @@ type MultipleAvatarsProps = { }; type AvatarStyles = { - singleAvatarStyle: ViewStyle; - secondAvatarStyles: ViewStyle; + singleAvatarStyle: ViewStyle & ImageStyle; + secondAvatarStyles: ViewStyle & ImageStyle; }; type AvatarSizeToStyles = typeof CONST.AVATAR_SIZE.SMALL | typeof CONST.AVATAR_SIZE.LARGE | typeof CONST.AVATAR_SIZE.DEFAULT; diff --git a/src/components/ReportActionItem/ReportActionItemImage.tsx b/src/components/ReportActionItem/ReportActionItemImage.tsx index ecf852a2bcee..5ebd1bf88ffb 100644 --- a/src/components/ReportActionItem/ReportActionItemImage.tsx +++ b/src/components/ReportActionItem/ReportActionItemImage.tsx @@ -1,5 +1,5 @@ import Str from 'expensify-common/lib/str'; -import React from 'react'; +import React, {useMemo} from 'react'; import type {ReactElement} from 'react'; import type {ImageSourcePropType, ViewStyle} from 'react-native'; import {View} from 'react-native'; @@ -63,12 +63,20 @@ function ReportActionItemImage({ }: ReportActionItemImageProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); - const imageSource = tryResolveUrlFromApiRoot(image ?? ''); + const attachmentModalSource = tryResolveUrlFromApiRoot(image ?? ''); const thumbnailSource = tryResolveUrlFromApiRoot(thumbnail ?? ''); const isEReceipt = transaction && TransactionUtils.hasEReceipt(transaction); let receiptImageComponent: ReactElement; + const imageSource = useMemo(() => { + if (thumbnail) { + return typeof thumbnail === 'string' ? {uri: thumbnail} : thumbnail; + } + + return typeof image === 'string' ? {uri: image} : image; + }, [image, thumbnail]); + if (isEReceipt) { receiptImageComponent = ( @@ -78,7 +86,7 @@ function ReportActionItemImage({ /> ); - } else if (thumbnail && !isLocalFile && !Str.isPDF(imageSource as string)) { + } else if (thumbnail && !isLocalFile && !Str.isPDF(attachmentModalSource as string)) { receiptImageComponent = ( ); @@ -103,7 +111,7 @@ function ReportActionItemImage({ {({report}) => (