2017-04-22 04:05:35 +10:00
import PropTypes from 'prop-types' ;
2023-05-24 01:15:17 +10:00
import { PureComponent } from 'react' ;
2017-03-26 05:22:24 +11:00
import { injectIntl , defineMessages } from 'react-intl' ;
2023-05-24 01:15:17 +10:00
2017-10-01 21:20:00 +11:00
import classNames from 'classnames' ;
2023-05-24 01:15:17 +10:00
import { supportsPassiveEvents } from 'detect-passive-events' ;
import Overlay from 'react-overlays/Overlay' ;
2024-01-16 21:27:26 +11:00
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react' ;
2024-01-26 02:41:31 +11:00
import InfoIcon from '@/material-icons/400-24px/info.svg?react' ;
2024-01-16 21:27:26 +11:00
import LockIcon from '@/material-icons/400-24px/lock.svg?react' ;
import PublicIcon from '@/material-icons/400-24px/public.svg?react' ;
2024-01-26 02:41:31 +11:00
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react' ;
2023-05-09 11:11:56 +10:00
import { Icon } from 'mastodon/components/icon' ;
2017-03-25 10:01:43 +11:00
2017-03-26 05:22:24 +11:00
const messages = defineMessages ( {
public _short : { id : 'privacy.public.short' , defaultMessage : 'Public' } ,
2024-01-26 02:41:31 +11:00
public _long : { id : 'privacy.public.long' , defaultMessage : 'Anyone on and off Mastodon' } ,
unlisted _short : { id : 'privacy.unlisted.short' , defaultMessage : 'Quiet public' } ,
unlisted _long : { id : 'privacy.unlisted.long' , defaultMessage : 'Fewer algorithmic fanfares' } ,
private _short : { id : 'privacy.private.short' , defaultMessage : 'Followers' } ,
private _long : { id : 'privacy.private.long' , defaultMessage : 'Only your followers' } ,
direct _short : { id : 'privacy.direct.short' , defaultMessage : 'Specific people' } ,
direct _long : { id : 'privacy.direct.long' , defaultMessage : 'Everyone mentioned in the post' } ,
change _privacy : { id : 'privacy.change' , defaultMessage : 'Change post privacy' } ,
unlisted _extra : { id : 'privacy.unlisted.additional' , defaultMessage : 'This behaves exactly like public, except the post will not appear in live feeds or hashtags, explore, or Mastodon search, even if you are opted-in account-wide.' } ,
2017-03-26 05:22:24 +11:00
} ) ;
2023-05-22 23:48:01 +10:00
const listenerOptions = supportsPassiveEvents ? { passive : true , capture : true } : true ;
2017-10-01 21:20:00 +11:00
2023-05-23 18:52:27 +10:00
class PrivacyDropdownMenu extends PureComponent {
2017-10-01 21:20:00 +11:00
static propTypes = {
style : PropTypes . object ,
items : PropTypes . array . isRequired ,
value : PropTypes . string . isRequired ,
onClose : PropTypes . func . isRequired ,
onChange : PropTypes . func . isRequired ,
} ;
handleDocumentClick = e => {
if ( this . node && ! this . node . contains ( e . target ) ) {
this . props . onClose ( ) ;
2023-05-22 23:48:01 +10:00
e . stopPropagation ( ) ;
2017-10-01 21:20:00 +11:00
}
2023-01-30 11:45:35 +11:00
} ;
2017-10-01 21:20:00 +11:00
2018-05-05 06:13:26 +10:00
handleKeyDown = e => {
const { items } = this . props ;
const value = e . currentTarget . getAttribute ( 'data-index' ) ;
const index = items . findIndex ( item => {
return ( item . value === value ) ;
} ) ;
2020-04-21 23:13:26 +10:00
let element = null ;
2017-10-01 21:20:00 +11:00
2018-05-05 06:13:26 +10:00
switch ( e . key ) {
case 'Escape' :
2017-10-01 21:20:00 +11:00
this . props . onClose ( ) ;
2018-05-05 06:13:26 +10:00
break ;
case 'Enter' :
this . handleClick ( e ) ;
break ;
case 'ArrowDown' :
2020-04-21 23:13:26 +10:00
element = this . node . childNodes [ index + 1 ] || this . node . firstChild ;
2018-05-05 06:13:26 +10:00
break ;
case 'ArrowUp' :
2020-04-21 23:13:26 +10:00
element = this . node . childNodes [ index - 1 ] || this . node . lastChild ;
2018-05-05 06:13:26 +10:00
break ;
2019-08-06 19:59:58 +10:00
case 'Tab' :
if ( e . shiftKey ) {
element = this . node . childNodes [ index - 1 ] || this . node . lastChild ;
} else {
element = this . node . childNodes [ index + 1 ] || this . node . firstChild ;
}
break ;
2018-05-05 06:13:26 +10:00
case 'Home' :
element = this . node . firstChild ;
break ;
case 'End' :
element = this . node . lastChild ;
break ;
2017-10-01 21:20:00 +11:00
}
2020-04-21 23:13:26 +10:00
if ( element ) {
element . focus ( ) ;
this . props . onChange ( element . getAttribute ( 'data-index' ) ) ;
e . preventDefault ( ) ;
e . stopPropagation ( ) ;
}
2023-01-30 11:45:35 +11:00
} ;
2017-10-01 21:20:00 +11:00
2018-05-05 06:13:26 +10:00
handleClick = e => {
const value = e . currentTarget . getAttribute ( 'data-index' ) ;
e . preventDefault ( ) ;
this . props . onClose ( ) ;
this . props . onChange ( value ) ;
2023-01-30 11:45:35 +11:00
} ;
2018-05-05 06:13:26 +10:00
2017-10-01 21:20:00 +11:00
componentDidMount ( ) {
2023-05-22 23:48:01 +10:00
document . addEventListener ( 'click' , this . handleDocumentClick , { capture : true } ) ;
2017-10-01 21:20:00 +11:00
document . addEventListener ( 'touchend' , this . handleDocumentClick , listenerOptions ) ;
2020-04-28 21:19:39 +10:00
if ( this . focusedItem ) this . focusedItem . focus ( { preventScroll : true } ) ;
2017-10-01 21:20:00 +11:00
}
componentWillUnmount ( ) {
2023-05-22 23:48:01 +10:00
document . removeEventListener ( 'click' , this . handleDocumentClick , { capture : true } ) ;
2017-10-01 21:20:00 +11:00
document . removeEventListener ( 'touchend' , this . handleDocumentClick , listenerOptions ) ;
}
setRef = c => {
this . node = c ;
2023-01-30 11:45:35 +11:00
} ;
2017-10-01 21:20:00 +11:00
2018-05-05 06:13:26 +10:00
setFocusRef = c => {
this . focusedItem = c ;
2023-01-30 11:45:35 +11:00
} ;
2018-05-05 06:13:26 +10:00
2017-10-01 21:20:00 +11:00
render ( ) {
2023-01-12 07:58:46 +11:00
const { style , items , value } = this . props ;
2017-10-01 21:20:00 +11:00
return (
2023-01-12 07:58:46 +11:00
< div style = { { ... style } } role = 'listbox' ref = { this . setRef } >
{ items . map ( item => (
2023-04-05 00:33:44 +10:00
< div role = 'option' tabIndex = { 0 } key = { item . value } data - index = { item . value } onKeyDown = { this . handleKeyDown } onClick = { this . handleClick } className = { classNames ( 'privacy-dropdown__option' , { active : item . value === value } ) } aria - selected = { item . value === value } ref = { item . value === value ? this . setFocusRef : null } >
2023-01-12 07:58:46 +11:00
< div className = 'privacy-dropdown__option__icon' >
2023-10-25 04:45:08 +11:00
< Icon id = { item . icon } icon = { item . iconComponent } / >
2023-01-12 07:58:46 +11:00
< / div >
< div className = 'privacy-dropdown__option__content' >
< strong > { item . text } < / strong >
{ item . meta }
< / div >
2024-01-26 02:41:31 +11:00
{ item . extra && (
< div className = 'privacy-dropdown__option__additional' title = { item . extra } >
< Icon id = 'info-circle' icon = { InfoIcon } / >
< / div >
) }
2017-10-01 21:20:00 +11:00
< / div >
2023-01-12 07:58:46 +11:00
) ) }
< / div >
2017-10-01 21:20:00 +11:00
) ;
}
}
2017-04-23 22:18:58 +10:00
2023-05-23 18:52:27 +10:00
class PrivacyDropdown extends PureComponent {
2017-03-25 10:01:43 +11:00
2017-05-12 22:44:10 +10:00
static propTypes = {
2017-07-28 06:31:59 +10:00
isUserTouching : PropTypes . func ,
onModalOpen : PropTypes . func ,
onModalClose : PropTypes . func ,
2017-05-12 22:44:10 +10:00
value : PropTypes . string . isRequired ,
onChange : PropTypes . func . isRequired ,
2021-02-11 16:22:11 +11:00
noDirect : PropTypes . bool ,
2021-02-11 10:53:12 +11:00
container : PropTypes . func ,
2022-02-13 05:00:33 +11:00
disabled : PropTypes . bool ,
2017-05-21 01:31:47 +10:00
intl : PropTypes . object . isRequired ,
2017-05-12 22:44:10 +10:00
} ;
state = {
2017-05-21 01:31:47 +10:00
open : false ,
2018-10-19 09:00:19 +11:00
placement : 'bottom' ,
2017-05-12 22:44:10 +10:00
} ;
2017-03-25 10:01:43 +11:00
2023-01-12 07:58:46 +11:00
handleToggle = ( ) => {
2024-01-26 02:41:31 +11:00
if ( this . state . open && this . activeElement ) {
this . activeElement . focus ( { preventScroll : true } ) ;
2017-07-28 06:31:59 +10:00
}
2024-01-26 02:41:31 +11:00
this . setState ( { open : ! this . state . open } ) ;
2023-01-30 11:45:35 +11:00
} ;
2017-03-25 10:01:43 +11:00
2017-10-01 21:20:00 +11:00
handleKeyDown = e => {
switch ( e . key ) {
case 'Escape' :
this . handleClose ( ) ;
break ;
2017-07-28 12:37:30 +10:00
}
2023-01-30 11:45:35 +11:00
} ;
2017-03-25 10:01:43 +11:00
2019-08-06 19:59:58 +10:00
handleMouseDown = ( ) => {
if ( ! this . state . open ) {
this . activeElement = document . activeElement ;
}
2023-01-30 11:45:35 +11:00
} ;
2019-08-06 19:59:58 +10:00
handleButtonKeyDown = ( e ) => {
switch ( e . key ) {
case ' ' :
case 'Enter' :
this . handleMouseDown ( ) ;
break ;
}
2023-01-30 11:45:35 +11:00
} ;
2019-08-06 19:59:58 +10:00
2017-10-01 21:20:00 +11:00
handleClose = ( ) => {
2019-08-06 19:59:58 +10:00
if ( this . state . open && this . activeElement ) {
2020-08-21 22:14:28 +10:00
this . activeElement . focus ( { preventScroll : true } ) ;
2019-08-06 19:59:58 +10:00
}
2017-10-01 21:20:00 +11:00
this . setState ( { open : false } ) ;
2023-01-30 11:45:35 +11:00
} ;
2017-10-01 21:20:00 +11:00
handleChange = value => {
this . props . onChange ( value ) ;
2023-01-30 11:45:35 +11:00
} ;
2017-03-25 10:01:43 +11:00
2023-05-10 17:05:32 +10:00
UNSAFE _componentWillMount ( ) {
2017-07-28 06:31:59 +10:00
const { intl : { formatMessage } } = this . props ;
this . options = [
2023-10-25 04:45:08 +11:00
{ icon : 'globe' , iconComponent : PublicIcon , value : 'public' , text : formatMessage ( messages . public _short ) , meta : formatMessage ( messages . public _long ) } ,
2024-01-26 02:41:31 +11:00
{ icon : 'unlock' , iconComponent : QuietTimeIcon , value : 'unlisted' , text : formatMessage ( messages . unlisted _short ) , meta : formatMessage ( messages . unlisted _long ) , extra : formatMessage ( messages . unlisted _extra ) } ,
2023-10-25 04:45:08 +11:00
{ icon : 'lock' , iconComponent : LockIcon , value : 'private' , text : formatMessage ( messages . private _short ) , meta : formatMessage ( messages . private _long ) } ,
2017-07-28 06:31:59 +10:00
] ;
2021-02-11 10:53:12 +11:00
if ( ! this . props . noDirect ) {
this . options . push (
2023-10-25 04:45:08 +11:00
{ icon : 'at' , iconComponent : AlternateEmailIcon , value : 'direct' , text : formatMessage ( messages . direct _short ) , meta : formatMessage ( messages . direct _long ) } ,
2021-02-11 10:53:12 +11:00
) ;
}
2017-07-28 06:31:59 +10:00
}
2023-01-12 07:58:46 +11:00
setTargetRef = c => {
this . target = c ;
2023-01-30 11:45:35 +11:00
} ;
2023-01-12 07:58:46 +11:00
findTarget = ( ) => {
return this . target ;
2023-01-30 11:45:35 +11:00
} ;
2023-01-12 07:58:46 +11:00
handleOverlayEnter = ( state ) => {
this . setState ( { placement : state . placement } ) ;
2023-01-30 11:45:35 +11:00
} ;
2023-01-12 07:58:46 +11:00
2017-03-25 10:01:43 +11:00
render ( ) {
2022-02-13 05:00:33 +11:00
const { value , container , disabled , intl } = this . props ;
2018-04-12 04:42:50 +10:00
const { open , placement } = this . state ;
2017-03-25 10:01:43 +11:00
2017-07-28 06:31:59 +10:00
const valueOption = this . options . find ( item => item . value === value ) ;
2017-03-25 10:01:43 +11:00
return (
2023-10-25 04:45:08 +11:00
< div ref = { this . setTargetRef } onKeyDown = { this . handleKeyDown } >
2024-01-26 02:41:31 +11:00
< button
type = 'button'
2023-10-25 04:45:08 +11:00
title = { intl . formatMessage ( messages . change _privacy ) }
2024-01-26 02:41:31 +11:00
aria - expanded = { open }
2023-10-25 04:45:08 +11:00
onClick = { this . handleToggle }
onMouseDown = { this . handleMouseDown }
onKeyDown = { this . handleButtonKeyDown }
disabled = { disabled }
2024-01-26 02:41:31 +11:00
className = { classNames ( 'dropdown-button' , { active : open } ) }
>
< Icon id = { valueOption . icon } icon = { valueOption . iconComponent } / >
< span className = 'dropdown-button__label' > { valueOption . text } < / span >
< / button >
2023-10-25 04:45:08 +11:00
2024-01-26 02:41:31 +11:00
< Overlay show = { open } offset = { [ 5 , 5 ] } placement = { placement } flip target = { this . findTarget } container = { container } popperConfig = { { strategy : 'fixed' , onFirstUpdate : this . handleOverlayEnter } } >
2023-01-12 07:58:46 +11:00
{ ( { props , placement } ) => (
2023-01-13 02:43:02 +11:00
< div { ...props } >
2023-01-12 07:58:46 +11:00
< div className = { ` dropdown-animation privacy-dropdown__dropdown ${ placement } ` } >
< PrivacyDropdownMenu
items = { this . options }
value = { value }
onClose = { this . handleClose }
onChange = { this . handleChange }
/ >
< / div >
< / div >
) }
2017-10-01 21:20:00 +11:00
< / Overlay >
2017-03-25 10:01:43 +11:00
< / div >
) ;
}
2017-04-22 04:05:35 +10:00
}
2023-03-24 13:17:53 +11:00
export default injectIntl ( PrivacyDropdown ) ;