Skip to content

Commit

Permalink
Merge pull request #31296 from JKobrynski/migrateImageToTypeScript
Browse files Browse the repository at this point in the history
[TS Migration] Migrate Image to TypeScript
  • Loading branch information
roryabraham authored Mar 1, 2024
2 parents a3f40b6 + 3476ef1 commit 65086be
Show file tree
Hide file tree
Showing 14 changed files with 116 additions and 133 deletions.
24 changes: 12 additions & 12 deletions src/components/Avatar.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -19,7 +19,7 @@ type AvatarProps = {
source?: AvatarSource;

/** Extra styles to pass to Image */
imageStyles?: StyleProp<ViewStyle>;
imageStyles?: StyleProp<ViewStyle & ImageStyle>;

/** Additional styles to pass to Icon */
iconAdditionalStyles?: StyleProp<ViewStyle>;
Expand Down Expand Up @@ -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<ImageStyle> = [StyleUtils.getAvatarStyle(size), imageStyles, styles.noBorderRadius];
const iconStyle = imageStyles ? [StyleUtils.getAvatarStyle(size), styles.bgTransparent, imageStyles] : undefined;

const iconFillColor = isWorkspace ? StyleUtils.getDefaultWorkspaceAvatarColor(name).fill : fill;
Expand All @@ -92,7 +92,15 @@ function Avatar({

return (
<View style={[containerStyles, styles.pointerEventsNone]}>
{typeof avatarSource === 'function' || typeof avatarSource === 'number' ? (
{typeof avatarSource === 'string' ? (
<View style={[iconStyle, StyleUtils.getAvatarBorderStyle(size, type), iconAdditionalStyles]}>
<Image
source={{uri: avatarSource}}
style={imageStyle}
onError={() => setImageError(true)}
/>
</View>
) : (
<View style={iconStyle}>
<Icon
testID={fallbackAvatarTestID}
Expand All @@ -108,14 +116,6 @@ function Avatar({
]}
/>
</View>
) : (
<View style={[iconStyle, StyleUtils.getAvatarBorderStyle(size, type), iconAdditionalStyles]}>
<Image
source={{uri: avatarSource}}
style={imageStyle}
onError={() => setImageError(true)}
/>
</View>
)}
</View>
);
Expand Down
6 changes: 2 additions & 4 deletions src/components/AvatarWithImagePicker.tsx
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -103,7 +103,7 @@ type AvatarWithImagePickerProps = {
isFocused: boolean;

/** Style applied to the avatar */
avatarStyle: StyleProp<ViewStyle>;
avatarStyle: StyleProp<ViewStyle & ImageStyle>;

/** Indicates if picker feature should be disabled */
disabled?: boolean;
Expand Down Expand Up @@ -279,8 +279,6 @@ function AvatarWithImagePicker({
vertical: y + height + variables.spacing2,
});
});

// eslint-disable-next-line react-hooks/exhaustive-deps
}, [isMenuVisible, windowWidth]);

return (
Expand Down
51 changes: 0 additions & 51 deletions src/components/Image/imagePropTypes.js

This file was deleted.

Original file line number Diff line number Diff line change
@@ -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,
};
}

Expand All @@ -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<ImageProps, ImageOnyxProps>({
session: {
key: ONYXKEYS.SESSION,
},
})(Image);
ImageWithOnyx.resizeMode = RESIZE_MODES;
ImageWithOnyx.resolveDimensions = resolveDimensions;

export default ImageWithOnyx;
36 changes: 15 additions & 21 deletions src/components/Image/index.js → src/components/Image/index.tsx
Original file line number Diff line number Diff line change
@@ -1,28 +1,24 @@
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();

/**
* Check if the image source is a URL - if so the `encryptedAuthToken` is appended
* 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.
Expand All @@ -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 (
<RNImage
Expand All @@ -56,21 +52,19 @@ function Image(props) {
);
}

function imagePropsAreEqual(prevProps, nextProps) {
function imagePropsAreEqual(prevProps: ImageOwnProps, nextProps: ImageOwnProps) {
return prevProps.source === nextProps.source;
}

Image.propTypes = imagePropTypes;
Image.defaultProps = defaultProps;

const ImageWithOnyx = React.memo(
withOnyx({
withOnyx<ImageProps, ImageOnyxProps>({
session: {
key: ONYXKEYS.SESSION,
},
})(Image),
imagePropsAreEqual,
);
ImageWithOnyx.resizeMode = RESIZE_MODES;

ImageWithOnyx.displayName = 'Image';

export default ImageWithOnyx;
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,6 @@ const RESIZE_MODES = {
cover: 'cover',
stretch: 'stretch',
center: 'center',
};
} as const;

export default RESIZE_MODES;
51 changes: 51 additions & 0 deletions src/components/Image/types.ts
Original file line number Diff line number Diff line change
@@ -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<Session>;
};

type ImageOnLoadEvent = {
nativeEvent: {
width: number;
height: number;
};
};

type ImageOwnProps = {
/** Styles for the Image */
style?: StyleProp<ImageStyle>;

/** The static asset or URI source of the image */
source: ExpoImageSource | Omit<ImageURISource, 'cache'> | 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};
2 changes: 1 addition & 1 deletion src/components/ImageView/index.native.tsx
Original file line number Diff line number Diff line change
@@ -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 (
Expand Down
7 changes: 4 additions & 3 deletions src/components/ImageView/index.tsx
Original file line number Diff line number Diff line change
@@ -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};

Expand Down Expand Up @@ -73,7 +74,7 @@ function ImageView({isAuthTokenRequired = false, url, fileName, onError}: ImageV
setIsZoomed(false);
};

const imageLoad = ({nativeEvent}: NativeSyntheticEvent<ImageLoadNativeEventData>) => {
const imageLoad = ({nativeEvent}: ImageOnLoadEvent) => {
setImageRegion(nativeEvent.width, nativeEvent.height);
setIsLoading(false);
};
Expand Down
7 changes: 1 addition & 6 deletions src/components/ImageView/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,4 @@ type ImageViewProps = {
zoomRange?: ZoomRange;
};

type ImageLoadNativeEventData = {
width: number;
height: number;
};

export type {ImageViewProps, ImageLoadNativeEventData};
export default ImageViewProps;
Loading

0 comments on commit 65086be

Please sign in to comment.