11import type { IUser , AvatarObject } from '@rocket.chat/core-typings' ;
2- import { Box , Button , Avatar , TextInput , IconButton , Label } from '@rocket.chat/fuselage' ;
2+ import { Box , Button , Avatar , IconButton } from '@rocket.chat/fuselage' ;
3+ import { Field , FieldLabel , FieldRow , FieldError , TextInput } from '@rocket.chat/fuselage-forms' ;
34import { UserAvatar } from '@rocket.chat/ui-avatar' ;
45import { useToastMessageDispatch , useSetting } from '@rocket.chat/ui-contexts' ;
56import type { ReactElement , ChangeEvent } from 'react' ;
6- import { useId , useState , useCallback } from 'react' ;
7+ import { useState , useCallback } from 'react' ;
78import { useTranslation } from 'react-i18next' ;
89
910import type { UserAvatarSuggestion } from './UserAvatarSuggestion' ;
1011import UserAvatarSuggestions from './UserAvatarSuggestions' ;
1112import { readFileAsDataURL } from './readFileAsDataURL' ;
1213import { useSingleFileInput } from '../../../hooks/useSingleFileInput' ;
14+ import { isSafeAvatarUrl } from '../../../lib/utils/isSafeAvatarUrl' ;
1315import { isValidImageFormat } from '../../../lib/utils/isValidImageFormat' ;
1416
1517type UserAvatarEditorProps = {
@@ -27,8 +29,8 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
2729 const rotateImages = useSetting ( 'FileUpload_RotateImages' ) ;
2830 const [ avatarFromUrl , setAvatarFromUrl ] = useState ( '' ) ;
2931 const [ newAvatarSource , setNewAvatarSource ] = useState < string > ( ) ;
30- const imageUrlField = useId ( ) ;
3132 const dispatchToastMessage = useToastMessageDispatch ( ) ;
33+ const [ avatarUrlError , setAvatarUrlError ] = useState < string | undefined > ( undefined ) ;
3234
3335 const setUploadedPreview = useCallback (
3436 async ( file : File , avatarObj : AvatarObject ) => {
@@ -48,9 +50,21 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
4850
4951 const [ clickUpload ] = useSingleFileInput ( setUploadedPreview ) ;
5052
51- const handleAddUrl = ( ) : void => {
53+ const handleAddUrl = async ( ) : Promise < void > => {
54+ if ( ! isSafeAvatarUrl ( avatarFromUrl ) ) {
55+ setAvatarUrlError ( t ( 'error-invalid-image-url' ) ) ;
56+ return ;
57+ }
58+
59+ if ( ! ( await isValidImageFormat ( avatarFromUrl ) ) ) {
60+ setAvatarUrlError ( t ( 'error-invalid-image-url' ) ) ;
61+ return ;
62+ }
63+
5264 setNewAvatarSource ( avatarFromUrl ) ;
5365 setAvatarObj ( { avatarUrl : avatarFromUrl } ) ;
66+ setAvatarUrlError ( undefined ) ;
67+ dispatchToastMessage ( { type : 'info' , message : t ( 'Avatar_preview_updated' ) } ) ;
5468 } ;
5569
5670 const clickReset = ( ) : void => {
@@ -61,7 +75,11 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
6175 const url = newAvatarSource ;
6276
6377 const handleAvatarFromUrlChange = ( event : ChangeEvent < HTMLInputElement > ) : void => {
64- setAvatarFromUrl ( event . currentTarget . value ) ;
78+ if ( avatarUrlError ) {
79+ setAvatarUrlError ( undefined ) ;
80+ }
81+ const { value } = event . currentTarget ;
82+ setAvatarFromUrl ( value ) ;
6583 } ;
6684
6785 const handleSelectSuggestion = useCallback (
@@ -87,35 +105,45 @@ function UserAvatarEditor({ currentUsername, username, setAvatarObj, name, disab
87105 imageOrientation : rotateImages ? 'from-image' : 'none' ,
88106 objectFit : 'contain' ,
89107 } }
90- onError = { ( ) => dispatchToastMessage ( { type : 'error' , message : t ( 'error-invalid-image-url' ) } ) }
108+ onError = { ( ) => setAvatarUrlError ( t ( 'error-invalid-image-url' ) ) }
91109 />
92- < Box display = 'flex' flexDirection = 'column' flexGrow = '1' justifyContent = 'space-between' mis = { 4 } >
110+ < Box display = 'flex' flexDirection = 'column' flexGrow = '1' mis = { 4 } >
93111 < Box display = 'flex' flexDirection = 'row' mbs = 'none' >
94112 < Button square disabled = { disabled } mi = { 4 } title = { t ( 'Accounts_SetDefaultAvatar' ) } onClick = { clickReset } >
95113 < Avatar url = { `/avatar/%40${ useFullNameForDefaultAvatar ? name : username } ` } />
96114 </ Button >
97115 < IconButton icon = 'upload' secondary disabled = { disabled } title = { t ( 'Upload' ) } mi = { 4 } onClick = { clickUpload } />
98- < IconButton
99- icon = 'permalink'
100- secondary
101- disabled = { disabled || ! avatarFromUrl }
102- title = { t ( 'Add_URL' ) }
103- mi = { 4 }
104- onClick = { handleAddUrl }
105- />
106116 < UserAvatarSuggestions disabled = { disabled } onSelectOne = { handleSelectSuggestion } />
107117 </ Box >
108- < Label htmlFor = { imageUrlField } mis = { 4 } >
109- { t ( 'Use_url_for_avatar' ) }
110- </ Label >
111- < TextInput
112- id = { imageUrlField }
113- flexGrow = { 0 }
114- placeholder = { t ( 'Use_url_for_avatar' ) }
115- value = { avatarFromUrl }
116- mis = { 4 }
117- onChange = { handleAvatarFromUrlChange }
118- />
118+ < Field pis = { 4 } mbs = { 12 } >
119+ < FieldLabel > { t ( 'Use_url_for_avatar' ) } </ FieldLabel >
120+ < FieldRow >
121+ < TextInput
122+ placeholder = { t ( 'Use_url_for_avatar' ) }
123+ addon = {
124+ < IconButton
125+ icon = 'permalink'
126+ secondary
127+ small
128+ disabled = { disabled || ! avatarFromUrl || ! ! avatarUrlError }
129+ title = { t ( 'Add_URL' ) }
130+ onClick = { handleAddUrl }
131+ mb = { - 4 }
132+ mie = { - 4 }
133+ />
134+ }
135+ value = { avatarFromUrl }
136+ onChange = { handleAvatarFromUrlChange }
137+ error = { avatarUrlError }
138+ onKeyDown = { ( event ) : void => {
139+ if ( event . key === 'Enter' ) {
140+ handleAddUrl ( ) ;
141+ }
142+ } }
143+ />
144+ </ FieldRow >
145+ { avatarUrlError && < FieldError > { avatarUrlError } </ FieldError > }
146+ </ Field >
119147 </ Box >
120148 </ Box >
121149 </ Box >
0 commit comments