1
- import lodashGet from 'lodash/get' ;
2
- import PropTypes from 'prop-types' ;
3
1
import React , { useEffect , useRef , useState } from 'react' ;
4
2
import { StyleSheet , View } from 'react-native' ;
5
- import _ from 'underscore ' ;
3
+ import type { StyleProp , ViewStyle } from 'react-native ' ;
6
4
import useLocalize from '@hooks/useLocalize' ;
7
5
import useTheme from '@hooks/useTheme' ;
8
6
import useThemeStyles from '@hooks/useThemeStyles' ;
9
7
import useWindowDimensions from '@hooks/useWindowDimensions' ;
10
8
import * as Browser from '@libs/Browser' ;
11
9
import * as FileUtils from '@libs/fileDownload/FileUtils' ;
12
10
import getImageResolution from '@libs/fileDownload/getImageResolution' ;
13
- import stylePropTypes from '@styles/stylePropTypes ' ;
11
+ import type { AvatarSource } from '@libs/UserUtils ' ;
14
12
import variables from '@styles/variables' ;
15
13
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' ;
16
17
import AttachmentModal from './AttachmentModal' ;
17
18
import AttachmentPicker from './AttachmentPicker' ;
18
19
import Avatar from './Avatar' ;
19
20
import AvatarCropModal from './AvatarCropModal/AvatarCropModal' ;
20
21
import DotIndicatorMessage from './DotIndicatorMessage' ;
21
22
import Icon from './Icon' ;
22
23
import * as Expensicons from './Icon/Expensicons' ;
23
- import sourcePropTypes from './Image/sourcePropTypes' ;
24
24
import OfflineWithFeedback from './OfflineWithFeedback' ;
25
25
import PopoverMenu from './PopoverMenu' ;
26
26
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback' ;
27
27
import Tooltip from './Tooltip' ;
28
28
import withNavigationFocus from './withNavigationFocus' ;
29
29
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 = {
31
47
/** Avatar source to display */
32
- source : PropTypes . oneOfType ( [ PropTypes . string , sourcePropTypes ] ) ,
48
+ source ?: AvatarSource ;
33
49
34
50
/** Additional style props */
35
- style : stylePropTypes ,
51
+ style ?: StyleProp < ViewStyle > ;
36
52
37
53
/** Additional style props for disabled picker */
38
- disabledStyle : stylePropTypes ,
54
+ disabledStyle ?: StyleProp < ViewStyle > ;
39
55
40
56
/** Executed once an image has been selected */
41
- onImageSelected : PropTypes . func ,
57
+ onImageSelected ?: ( ) => void ;
42
58
43
59
/** Execute when the user taps "remove" */
44
- onImageRemoved : PropTypes . func ,
60
+ onImageRemoved ?: ( ) => void ;
45
61
46
62
/** A default avatar component to display when there is no source */
47
- DefaultAvatar : PropTypes . func ,
63
+ DefaultAvatar ?: ( ) => React . ReactNode ;
48
64
49
65
/** Whether we are using the default avatar */
50
- isUsingDefaultAvatar : PropTypes . bool ,
66
+ isUsingDefaultAvatar ?: boolean ;
51
67
52
68
/** 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 ;
54
70
55
71
/** A fallback avatar icon to display when there is an error on loading avatar from remote URL. */
56
- fallbackIcon : sourcePropTypes ,
72
+ fallbackIcon ?: AvatarSource ;
57
73
58
74
/** 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 ;
60
76
61
77
/** Image crop vector mask */
62
- editorMaskImage : sourcePropTypes ,
78
+ editorMaskImage ?: IconAsset ;
63
79
64
80
/** Additional style object for the error row */
65
- errorRowStyles : stylePropTypes ,
81
+ errorRowStyles ?: StyleProp < ViewStyle > ;
66
82
67
83
/** A function to run when the X button next to the error is clicked */
68
- onErrorClose : PropTypes . func ,
84
+ onErrorClose ?: ( ) => void ;
69
85
70
86
/** The type of action that's pending */
71
- pendingAction : PropTypes . oneOf ( [ 'add' , 'update' , 'delete' ] ) ,
87
+ pendingAction ?: OnyxCommon . PendingAction ;
72
88
73
89
/** The errors to display */
74
- // eslint-disable-next-line react/forbid-prop-types
75
- errors : PropTypes . object ,
90
+ errors ?: OnyxCommon . Errors ;
76
91
77
92
/** Title for avatar preview modal */
78
- headerTitle : PropTypes . string ,
93
+ headerTitle ?: string ;
79
94
80
95
/** Avatar source for avatar preview modal */
81
- previewSource : PropTypes . oneOfType ( [ PropTypes . string , sourcePropTypes ] ) ,
96
+ previewSource ?: AvatarSource ;
82
97
83
98
/** File name of the avatar */
84
- originalFileName : PropTypes . string ,
99
+ originalFileName ?: string ;
85
100
86
101
/** Whether navigation is focused */
87
- isFocused : PropTypes . bool . isRequired ,
102
+ isFocused : boolean ;
88
103
89
104
/** Style applied to the avatar */
90
- avatarStyle : stylePropTypes . isRequired ,
105
+ avatarStyle : StyleProp < ViewStyle > ;
91
106
92
107
/** Indicates if picker feature should be disabled */
93
- disabled : PropTypes . bool ,
108
+ disabled ?: boolean ;
94
109
95
110
/** 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 ;
130
112
} ;
131
113
132
114
function AvatarWithImagePicker ( {
133
115
isFocused,
134
- DefaultAvatar,
116
+ DefaultAvatar = ( ) => null ,
135
117
style,
136
118
disabledStyle,
137
119
pendingAction,
138
120
errors,
139
121
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 = ( ) => { } ,
151
133
editorMaskImage,
152
134
avatarStyle,
153
- disabled,
135
+ disabled = false ,
154
136
onViewPhotoPress,
155
- } ) {
137
+ } : AvatarWithImagePickerProps ) {
156
138
const theme = useTheme ( ) ;
157
139
const styles = useThemeStyles ( ) ;
158
140
const { windowWidth} = useWindowDimensions ( ) ;
159
141
const [ popoverPosition , setPopoverPosition ] = useState ( { horizontal : 0 , vertical : 0 } ) ;
160
142
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 : { } } ) ;
165
144
const [ isAvatarCropModalOpen , setIsAvatarCropModalOpen ] = useState ( false ) ;
166
145
const [ imageData , setImageData ] = useState ( {
167
146
uri : '' ,
168
147
name : '' ,
169
148
type : '' ,
170
149
} ) ;
171
- const anchorRef = useRef ( ) ;
150
+ const anchorRef = useRef < View > ( null ) ;
172
151
const { translate} = useLocalize ( ) ;
173
152
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 > ) => {
179
154
setErrorData ( {
180
155
validationError : error ,
181
156
phraseParam,
@@ -193,40 +168,29 @@ function AvatarWithImagePicker({
193
168
194
169
/**
195
170
* Check if the attachment extension is allowed.
196
- *
197
- * @param {Object } image
198
- * @returns {Boolean }
199
171
*/
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 ( ) ) ;
203
175
} ;
204
176
205
177
/**
206
178
* Check if the attachment size is less than allowed size.
207
- *
208
- * @param {Object } image
209
- * @returns {Boolean }
210
179
*/
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 ;
212
181
213
182
/**
214
183
* Check if the attachment resolution matches constraints.
215
- *
216
- * @param {Object } image
217
- * @returns {Promise }
218
184
*/
219
- const isValidResolution = ( image ) =>
185
+ const isValidResolution = ( image : File ) : Promise < boolean > =>
220
186
getImageResolution ( image ) . then (
221
187
( { 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 ,
222
188
) ;
223
189
224
190
/**
225
191
* Validates if an image has a valid resolution and opens an avatar crop modal
226
- *
227
- * @param {Object } image
228
192
*/
229
- const showAvatarCropModal = ( image ) => {
193
+ const showAvatarCropModal = ( image : File ) => {
230
194
if ( ! isValidExtension ( image ) ) {
231
195
setError ( 'avatarWithImagePicker.notAllowedExtension' , { allowedExtensions : CONST . AVATAR_ALLOWED_EXTENSIONS } ) ;
232
196
return ;
@@ -264,11 +228,8 @@ function AvatarWithImagePicker({
264
228
265
229
/**
266
230
* Create menu items list for avatar menu
267
- *
268
- * @param {Function } openPicker
269
- * @returns {Array }
270
231
*/
271
- const createMenuItems = ( openPicker ) => {
232
+ const createMenuItems = ( openPicker : OpenPicker ) : MenuItem [ ] => {
272
233
const menuItems = [
273
234
{
274
235
icon : Expensicons . Upload ,
@@ -313,6 +274,7 @@ function AvatarWithImagePicker({
313
274
vertical : y + height + variables . spacing2 ,
314
275
} ) ;
315
276
} ) ;
277
+
316
278
// eslint-disable-next-line react-hooks/exhaustive-deps
317
279
} , [ isMenuVisible , windowWidth ] ) ;
318
280
@@ -372,7 +334,11 @@ function AvatarWithImagePicker({
372
334
maybeIcon = { isUsingDefaultAvatar }
373
335
>
374
336
{ ( { 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. */ }
376
342
{ ( { openPicker} ) => {
377
343
const menuItems = createMenuItems ( openPicker ) ;
378
344
@@ -421,7 +387,8 @@ function AvatarWithImagePicker({
421
387
{ errorData . validationError && (
422
388
< DotIndicatorMessage
423
389
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 ) } }
425
392
type = "error"
426
393
/>
427
394
) }
@@ -438,8 +405,6 @@ function AvatarWithImagePicker({
438
405
) ;
439
406
}
440
407
441
- AvatarWithImagePicker . propTypes = propTypes ;
442
- AvatarWithImagePicker . defaultProps = defaultProps ;
443
408
AvatarWithImagePicker . displayName = 'AvatarWithImagePicker' ;
444
409
445
410
export default withNavigationFocus ( AvatarWithImagePicker ) ;
0 commit comments