2023-11-15 20:13:53 +09:00
import { useState , useMemo , useCallback , createRef } from 'react' ;
import { useIntl , defineMessages , FormattedMessage } from 'react-intl' ;
import classNames from 'classnames' ;
import { useHistory } from 'react-router-dom' ;
2023-11-17 19:37:04 +09:00
import { useDispatch } from 'react-redux' ;
2023-11-15 20:13:53 +09:00
2024-01-12 19:31:24 +09:00
import AddPhotoAlternateIcon from '@material-symbols/svg-600/outlined/add_photo_alternate.svg?react' ;
import EditIcon from '@material-symbols/svg-600/outlined/edit.svg?react' ;
2023-11-15 20:13:53 +09:00
import Toggle from 'react-toggle' ;
import { updateAccount } from 'mastodon/actions/accounts' ;
import { Button } from 'mastodon/components/button' ;
import { ColumnBackButton } from 'mastodon/components/column_back_button' ;
import { Icon } from 'mastodon/components/icon' ;
import { LoadingIndicator } from 'mastodon/components/loading_indicator' ;
import { me } from 'mastodon/initial_state' ;
import { useAppSelector } from 'mastodon/store' ;
import { unescapeHTML } from 'mastodon/utils/html' ;
const messages = defineMessages ( {
uploadHeader : { id : 'onboarding.profile.upload_header' , defaultMessage : 'Upload profile header' } ,
uploadAvatar : { id : 'onboarding.profile.upload_avatar' , defaultMessage : 'Upload profile picture' } ,
} ) ;
2023-12-21 17:32:25 +09:00
const nullIfMissing = path => path . endsWith ( 'missing.png' ) ? null : path ;
2023-11-15 20:13:53 +09:00
export const Profile = ( ) => {
const account = useAppSelector ( state => state . getIn ( [ 'accounts' , me ] ) ) ;
const [ displayName , setDisplayName ] = useState ( account . get ( 'display_name' ) ) ;
const [ note , setNote ] = useState ( unescapeHTML ( account . get ( 'note' ) ) ) ;
const [ avatar , setAvatar ] = useState ( null ) ;
const [ header , setHeader ] = useState ( null ) ;
const [ discoverable , setDiscoverable ] = useState ( account . get ( 'discoverable' ) ) ;
const [ isSaving , setIsSaving ] = useState ( false ) ;
const [ errors , setErrors ] = useState ( ) ;
const avatarFileRef = createRef ( ) ;
const headerFileRef = createRef ( ) ;
const dispatch = useDispatch ( ) ;
const intl = useIntl ( ) ;
const history = useHistory ( ) ;
const handleDisplayNameChange = useCallback ( e => {
setDisplayName ( e . target . value ) ;
} , [ setDisplayName ] ) ;
const handleNoteChange = useCallback ( e => {
setNote ( e . target . value ) ;
} , [ setNote ] ) ;
const handleDiscoverableChange = useCallback ( e => {
setDiscoverable ( e . target . checked ) ;
} , [ setDiscoverable ] ) ;
const handleAvatarChange = useCallback ( e => {
setAvatar ( e . target ? . files ? . [ 0 ] ) ;
} , [ setAvatar ] ) ;
const handleHeaderChange = useCallback ( e => {
setHeader ( e . target ? . files ? . [ 0 ] ) ;
} , [ setHeader ] ) ;
2023-12-21 17:32:25 +09:00
const avatarPreview = useMemo ( ( ) => avatar ? URL . createObjectURL ( avatar ) : nullIfMissing ( account . get ( 'avatar' ) ) , [ avatar , account ] ) ;
const headerPreview = useMemo ( ( ) => header ? URL . createObjectURL ( header ) : nullIfMissing ( account . get ( 'header' ) ) , [ header , account ] ) ;
2023-11-15 20:13:53 +09:00
const handleSubmit = useCallback ( ( ) => {
setIsSaving ( true ) ;
dispatch ( updateAccount ( {
displayName ,
note ,
avatar ,
header ,
discoverable ,
2023-11-17 19:37:04 +09:00
indexable : discoverable ,
2023-11-15 20:13:53 +09:00
} ) ) . then ( ( ) => history . push ( '/start/follows' ) ) . catch ( err => {
setIsSaving ( false ) ;
setErrors ( err . response . data . details ) ;
} ) ;
2023-11-17 19:37:04 +09:00
} , [ dispatch , displayName , note , avatar , header , discoverable , history ] ) ;
2023-11-15 20:13:53 +09:00
return (
< >
< ColumnBackButton / >
< div className = 'scrollable privacy-policy' >
< div className = 'column-title' >
< h3 > < FormattedMessage id = 'onboarding.profile.title' defaultMessage = 'Profile setup' / > < / h3 >
< p > < FormattedMessage id = 'onboarding.profile.lead' defaultMessage = 'You can always complete this later in the settings, where even more customization options are available.' / > < / p >
< / div >
< div className = 'simple_form' >
< div className = 'onboarding__profile' >
< label className = { classNames ( 'app-form__header-input' , { selected : ! ! headerPreview , invalid : ! ! errors ? . header } ) } title = { intl . formatMessage ( messages . uploadHeader ) } >
< input
type = 'file'
hidden
ref = { headerFileRef }
accept = 'image/*'
onChange = { handleHeaderChange }
/ >
{ headerPreview && < img src = { headerPreview } alt = '' / > }
< Icon icon = { headerPreview ? EditIcon : AddPhotoAlternateIcon } / >
< / label >
< label className = { classNames ( 'app-form__avatar-input' , { selected : ! ! avatarPreview , invalid : ! ! errors ? . avatar } ) } title = { intl . formatMessage ( messages . uploadAvatar ) } >
< input
type = 'file'
hidden
ref = { avatarFileRef }
accept = 'image/*'
onChange = { handleAvatarChange }
/ >
{ avatarPreview && < img src = { avatarPreview } alt = '' / > }
< Icon icon = { avatarPreview ? EditIcon : AddPhotoAlternateIcon } / >
< / label >
< / div >
< div className = { classNames ( 'input with_block_label' , { field _with _errors : ! ! errors ? . display _name } ) } >
< label htmlFor = 'display_name' > < FormattedMessage id = 'onboarding.profile.display_name' defaultMessage = 'Display name' / > < / label >
< span className = 'hint' > < FormattedMessage id = 'onboarding.profile.display_name_hint' defaultMessage = 'Your full name or your fun name…' / > < / span >
< div className = 'label_input' >
< input id = 'display_name' type = 'text' value = { displayName } onChange = { handleDisplayNameChange } maxLength = { 30 } / >
< / div >
< / div >
< div className = { classNames ( 'input with_block_label' , { field _with _errors : ! ! errors ? . note } ) } >
< label htmlFor = 'note' > < FormattedMessage id = 'onboarding.profile.note' defaultMessage = 'Bio' / > < / label >
< span className = 'hint' > < FormattedMessage id = 'onboarding.profile.note_hint' defaultMessage = 'You can @mention other people or #hashtags…' / > < / span >
< div className = 'label_input' >
< textarea id = 'note' value = { note } onChange = { handleNoteChange } maxLength = { 500 } / >
< / div >
< / div >
2023-11-17 19:37:04 +09:00
< label className = 'app-form__toggle' >
< div className = 'app-form__toggle__label' >
< strong > < FormattedMessage id = 'onboarding.profile.discoverable' defaultMessage = 'Make my profile discoverable' / > < / strong > < span className = 'recommended' > < FormattedMessage id = 'recommended' defaultMessage = 'Recommended' / > < / span >
< span className = 'hint' > < FormattedMessage id = 'onboarding.profile.discoverable_hint' defaultMessage = 'When you opt in to discoverability on Mastodon, your posts may appear in search results and trending, and your profile may be suggested to people with similar interests to you.' / > < / span >
< / div >
2023-11-15 20:13:53 +09:00
2023-11-17 19:37:04 +09:00
< div className = 'app-form__toggle__toggle' >
< div >
< Toggle checked = { discoverable } onChange = { handleDiscoverableChange } / >
< / div >
< / div >
< / label >
< / div >
2023-11-15 20:13:53 +09:00
< div className = 'onboarding__footer' >
< Button block onClick = { handleSubmit } disabled = { isSaving } > { isSaving ? < LoadingIndicator / > : < FormattedMessage id = 'onboarding.profile.save_and_continue' defaultMessage = 'Save and continue' / > } < / Button >
< / div >
< / div >
< / >
) ;
} ;