Skip to content

Commit 80fc188

Browse files
committed
[TS migration] Migrate 'AvatarWithImagePicker.js' component to TypeScript
1 parent 9463e22 commit 80fc188

File tree

2 files changed

+77
-112
lines changed

2 files changed

+77
-112
lines changed

src/components/AvatarWithImagePicker.js renamed to src/components/AvatarWithImagePicker.tsx

Lines changed: 76 additions & 111 deletions
Original file line numberDiff line numberDiff line change
@@ -1,181 +1,156 @@
1-
import lodashGet from 'lodash/get';
2-
import PropTypes from 'prop-types';
31
import React, {useEffect, useRef, useState} from 'react';
42
import {StyleSheet, View} from 'react-native';
5-
import _ from 'underscore';
3+
import type {StyleProp, ViewStyle} from 'react-native';
64
import useLocalize from '@hooks/useLocalize';
75
import useTheme from '@hooks/useTheme';
86
import useThemeStyles from '@hooks/useThemeStyles';
97
import useWindowDimensions from '@hooks/useWindowDimensions';
108
import * as Browser from '@libs/Browser';
119
import * as FileUtils from '@libs/fileDownload/FileUtils';
1210
import getImageResolution from '@libs/fileDownload/getImageResolution';
13-
import stylePropTypes from '@styles/stylePropTypes';
11+
import type {AvatarSource} from '@libs/UserUtils';
1412
import variables from '@styles/variables';
1513
import CONST from '@src/CONST';
14+
import type {TranslationPaths} from '@src/languages/types';
15+
import type * as OnyxCommon from '@src/types/onyx/OnyxCommon';
16+
import type IconAsset from '@src/types/utils/IconAsset';
1617
import AttachmentModal from './AttachmentModal';
1718
import AttachmentPicker from './AttachmentPicker';
1819
import Avatar from './Avatar';
1920
import AvatarCropModal from './AvatarCropModal/AvatarCropModal';
2021
import DotIndicatorMessage from './DotIndicatorMessage';
2122
import Icon from './Icon';
2223
import * as Expensicons from './Icon/Expensicons';
23-
import sourcePropTypes from './Image/sourcePropTypes';
2424
import OfflineWithFeedback from './OfflineWithFeedback';
2525
import PopoverMenu from './PopoverMenu';
2626
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
2727
import Tooltip from './Tooltip';
2828
import withNavigationFocus from './withNavigationFocus';
2929

30-
const propTypes = {
30+
type ErrorData = {
31+
validationError?: TranslationPaths | null | '';
32+
phraseParam: Record<string, unknown>;
33+
};
34+
35+
type OpenPickerParams = {
36+
onPicked: (image: File) => void;
37+
};
38+
type OpenPicker = (args: OpenPickerParams) => void;
39+
40+
type MenuItem = {
41+
icon: IconAsset;
42+
text: string;
43+
onSelected: () => void;
44+
};
45+
46+
type AvatarWithImagePickerProps = {
3147
/** Avatar source to display */
32-
source: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]),
48+
source?: AvatarSource;
3349

3450
/** Additional style props */
35-
style: stylePropTypes,
51+
style?: StyleProp<ViewStyle>;
3652

3753
/** Additional style props for disabled picker */
38-
disabledStyle: stylePropTypes,
54+
disabledStyle?: StyleProp<ViewStyle>;
3955

4056
/** Executed once an image has been selected */
41-
onImageSelected: PropTypes.func,
57+
onImageSelected?: () => void;
4258

4359
/** Execute when the user taps "remove" */
44-
onImageRemoved: PropTypes.func,
60+
onImageRemoved?: () => void;
4561

4662
/** A default avatar component to display when there is no source */
47-
DefaultAvatar: PropTypes.func,
63+
DefaultAvatar?: () => React.ReactNode;
4864

4965
/** Whether we are using the default avatar */
50-
isUsingDefaultAvatar: PropTypes.bool,
66+
isUsingDefaultAvatar?: boolean;
5167

5268
/** Size of Indicator */
53-
size: PropTypes.oneOf([CONST.AVATAR_SIZE.XLARGE, CONST.AVATAR_SIZE.LARGE, CONST.AVATAR_SIZE.DEFAULT]),
69+
size?: typeof CONST.AVATAR_SIZE.XLARGE | typeof CONST.AVATAR_SIZE.LARGE | typeof CONST.AVATAR_SIZE.DEFAULT;
5470

5571
/** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */
56-
fallbackIcon: sourcePropTypes,
72+
fallbackIcon?: AvatarSource;
5773

5874
/** Denotes whether it is an avatar or a workspace avatar */
59-
type: PropTypes.oneOf([CONST.ICON_TYPE_AVATAR, CONST.ICON_TYPE_WORKSPACE]),
75+
type?: typeof CONST.ICON_TYPE_AVATAR | typeof CONST.ICON_TYPE_WORKSPACE;
6076

6177
/** Image crop vector mask */
62-
editorMaskImage: sourcePropTypes,
78+
editorMaskImage?: IconAsset;
6379

6480
/** Additional style object for the error row */
65-
errorRowStyles: stylePropTypes,
81+
errorRowStyles?: StyleProp<ViewStyle>;
6682

6783
/** A function to run when the X button next to the error is clicked */
68-
onErrorClose: PropTypes.func,
84+
onErrorClose?: () => void;
6985

7086
/** The type of action that's pending */
71-
pendingAction: PropTypes.oneOf(['add', 'update', 'delete']),
87+
pendingAction?: OnyxCommon.PendingAction;
7288

7389
/** The errors to display */
74-
// eslint-disable-next-line react/forbid-prop-types
75-
errors: PropTypes.object,
90+
errors?: OnyxCommon.Errors;
7691

7792
/** Title for avatar preview modal */
78-
headerTitle: PropTypes.string,
93+
headerTitle?: string;
7994

8095
/** Avatar source for avatar preview modal */
81-
previewSource: PropTypes.oneOfType([PropTypes.string, sourcePropTypes]),
96+
previewSource?: AvatarSource;
8297

8398
/** File name of the avatar */
84-
originalFileName: PropTypes.string,
99+
originalFileName?: string;
85100

86101
/** Whether navigation is focused */
87-
isFocused: PropTypes.bool.isRequired,
102+
isFocused: boolean;
88103

89104
/** Style applied to the avatar */
90-
avatarStyle: stylePropTypes.isRequired,
105+
avatarStyle: StyleProp<ViewStyle>;
91106

92107
/** Indicates if picker feature should be disabled */
93-
disabled: PropTypes.bool,
108+
disabled?: boolean;
94109

95110
/** Executed once click on view photo option */
96-
onViewPhotoPress: PropTypes.func,
97-
98-
/** Where the popover should be positioned relative to the anchor points. */
99-
anchorAlignment: PropTypes.shape({
100-
horizontal: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL)),
101-
vertical: PropTypes.oneOf(_.values(CONST.MODAL.ANCHOR_ORIGIN_VERTICAL)),
102-
}),
103-
};
104-
105-
const defaultProps = {
106-
source: '',
107-
onImageSelected: () => {},
108-
onImageRemoved: () => {},
109-
style: [],
110-
disabledStyle: [],
111-
DefaultAvatar: () => {},
112-
isUsingDefaultAvatar: false,
113-
size: CONST.AVATAR_SIZE.DEFAULT,
114-
fallbackIcon: Expensicons.FallbackAvatar,
115-
type: CONST.ICON_TYPE_AVATAR,
116-
editorMaskImage: undefined,
117-
errorRowStyles: [],
118-
onErrorClose: () => {},
119-
pendingAction: null,
120-
errors: null,
121-
headerTitle: '',
122-
previewSource: '',
123-
originalFileName: '',
124-
disabled: false,
125-
onViewPhotoPress: undefined,
126-
anchorAlignment: {
127-
horizontal: CONST.MODAL.ANCHOR_ORIGIN_HORIZONTAL.LEFT,
128-
vertical: CONST.MODAL.ANCHOR_ORIGIN_VERTICAL.TOP,
129-
},
111+
onViewPhotoPress?: () => void;
130112
};
131113

132114
function AvatarWithImagePicker({
133115
isFocused,
134-
DefaultAvatar,
116+
DefaultAvatar = () => null,
135117
style,
136118
disabledStyle,
137119
pendingAction,
138120
errors,
139121
errorRowStyles,
140-
onErrorClose,
141-
source,
142-
fallbackIcon,
143-
size,
144-
type,
145-
headerTitle,
146-
previewSource,
147-
originalFileName,
148-
isUsingDefaultAvatar,
149-
onImageRemoved,
150-
onImageSelected,
122+
onErrorClose = () => {},
123+
source = '',
124+
fallbackIcon = Expensicons.FallbackAvatar,
125+
size = CONST.AVATAR_SIZE.DEFAULT,
126+
type = CONST.ICON_TYPE_AVATAR,
127+
headerTitle = '',
128+
previewSource = '',
129+
originalFileName = '',
130+
isUsingDefaultAvatar = false,
131+
onImageSelected = () => {},
132+
onImageRemoved = () => {},
151133
editorMaskImage,
152134
avatarStyle,
153-
disabled,
135+
disabled = false,
154136
onViewPhotoPress,
155-
}) {
137+
}: AvatarWithImagePickerProps) {
156138
const theme = useTheme();
157139
const styles = useThemeStyles();
158140
const {windowWidth} = useWindowDimensions();
159141
const [popoverPosition, setPopoverPosition] = useState({horizontal: 0, vertical: 0});
160142
const [isMenuVisible, setIsMenuVisible] = useState(false);
161-
const [errorData, setErrorData] = useState({
162-
validationError: null,
163-
phraseParam: {},
164-
});
143+
const [errorData, setErrorData] = useState<ErrorData>({validationError: null, phraseParam: {}});
165144
const [isAvatarCropModalOpen, setIsAvatarCropModalOpen] = useState(false);
166145
const [imageData, setImageData] = useState({
167146
uri: '',
168147
name: '',
169148
type: '',
170149
});
171-
const anchorRef = useRef();
150+
const anchorRef = useRef<View>(null);
172151
const {translate} = useLocalize();
173152

174-
/**
175-
* @param {String} error
176-
* @param {Object} phraseParam
177-
*/
178-
const setError = (error, phraseParam) => {
153+
const setError = (error: TranslationPaths | null, phraseParam: Record<string, unknown>) => {
179154
setErrorData({
180155
validationError: error,
181156
phraseParam,
@@ -193,40 +168,29 @@ function AvatarWithImagePicker({
193168

194169
/**
195170
* Check if the attachment extension is allowed.
196-
*
197-
* @param {Object} image
198-
* @returns {Boolean}
199171
*/
200-
const isValidExtension = (image) => {
201-
const {fileExtension} = FileUtils.splitExtensionFromFileName(lodashGet(image, 'name', ''));
202-
return _.contains(CONST.AVATAR_ALLOWED_EXTENSIONS, fileExtension.toLowerCase());
172+
const isValidExtension = (image: File): boolean => {
173+
const {fileExtension} = FileUtils.splitExtensionFromFileName(image?.name ?? '');
174+
return CONST.AVATAR_ALLOWED_EXTENSIONS.some((extension) => extension === fileExtension.toLowerCase());
203175
};
204176

205177
/**
206178
* Check if the attachment size is less than allowed size.
207-
*
208-
* @param {Object} image
209-
* @returns {Boolean}
210179
*/
211-
const isValidSize = (image) => image && lodashGet(image, 'size', 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE;
180+
const isValidSize = (image: File): boolean => (image?.size ?? 0) < CONST.AVATAR_MAX_ATTACHMENT_SIZE;
212181

213182
/**
214183
* Check if the attachment resolution matches constraints.
215-
*
216-
* @param {Object} image
217-
* @returns {Promise}
218184
*/
219-
const isValidResolution = (image) =>
185+
const isValidResolution = (image: File): Promise<boolean> =>
220186
getImageResolution(image).then(
221187
({height, width}) => height >= CONST.AVATAR_MIN_HEIGHT_PX && width >= CONST.AVATAR_MIN_WIDTH_PX && height <= CONST.AVATAR_MAX_HEIGHT_PX && width <= CONST.AVATAR_MAX_WIDTH_PX,
222188
);
223189

224190
/**
225191
* Validates if an image has a valid resolution and opens an avatar crop modal
226-
*
227-
* @param {Object} image
228192
*/
229-
const showAvatarCropModal = (image) => {
193+
const showAvatarCropModal = (image: File) => {
230194
if (!isValidExtension(image)) {
231195
setError('avatarWithImagePicker.notAllowedExtension', {allowedExtensions: CONST.AVATAR_ALLOWED_EXTENSIONS});
232196
return;
@@ -264,11 +228,8 @@ function AvatarWithImagePicker({
264228

265229
/**
266230
* Create menu items list for avatar menu
267-
*
268-
* @param {Function} openPicker
269-
* @returns {Array}
270231
*/
271-
const createMenuItems = (openPicker) => {
232+
const createMenuItems = (openPicker: OpenPicker): MenuItem[] => {
272233
const menuItems = [
273234
{
274235
icon: Expensicons.Upload,
@@ -313,6 +274,7 @@ function AvatarWithImagePicker({
313274
vertical: y + height + variables.spacing2,
314275
});
315276
});
277+
316278
// eslint-disable-next-line react-hooks/exhaustive-deps
317279
}, [isMenuVisible, windowWidth]);
318280

@@ -372,7 +334,11 @@ function AvatarWithImagePicker({
372334
maybeIcon={isUsingDefaultAvatar}
373335
>
374336
{({show}) => (
375-
<AttachmentPicker type={CONST.ATTACHMENT_PICKER_TYPE.IMAGE}>
337+
<AttachmentPicker
338+
// @ts-expect-error TODO: Remove this once AttachmentPicker (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript.
339+
type={CONST.ATTACHMENT_PICKER_TYPE.IMAGE}
340+
>
341+
{/* @ts-expect-error TODO: Remove this once AttachmentPicker (https://github.com/Expensify/App/issues/25134) is migrated to TypeScript. */}
376342
{({openPicker}) => {
377343
const menuItems = createMenuItems(openPicker);
378344

@@ -421,7 +387,8 @@ function AvatarWithImagePicker({
421387
{errorData.validationError && (
422388
<DotIndicatorMessage
423389
style={[styles.mt6]}
424-
messages={{0: [errorData.validationError, errorData.phraseParam]}}
390+
// eslint-disable-next-line @typescript-eslint/naming-convention
391+
messages={{0: translate(errorData.validationError, errorData.phraseParam as never)}}
425392
type="error"
426393
/>
427394
)}
@@ -438,8 +405,6 @@ function AvatarWithImagePicker({
438405
);
439406
}
440407

441-
AvatarWithImagePicker.propTypes = propTypes;
442-
AvatarWithImagePicker.defaultProps = defaultProps;
443408
AvatarWithImagePicker.displayName = 'AvatarWithImagePicker';
444409

445410
export default withNavigationFocus(AvatarWithImagePicker);

src/components/PopoverMenu.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ type PopoverMenuProps = Partial<PopoverModalProps> & {
6969
anchorPosition: AnchorPosition;
7070

7171
/** Ref of the anchor */
72-
anchorRef: RefObject<HTMLDivElement>;
72+
anchorRef: RefObject<View | HTMLDivElement>;
7373

7474
/** Where the popover should be positioned relative to the anchor points. */
7575
anchorAlignment?: AnchorAlignment;

0 commit comments

Comments
 (0)