Merge pull request #1682 from ClearlyClaire/glitch-soc/fixes/dropdowns-modals
Refactor and fix dropdown/action dialog
This commit is contained in:
		
						commit
						77fec39bee
					
				
					 7 changed files with 174 additions and 214 deletions
				
			
		|  | @ -116,7 +116,7 @@ class DropdownMenu extends React.PureComponent { | |||
| 
 | ||||
|     if (typeof action === 'function') { | ||||
|       e.preventDefault(); | ||||
|       action(); | ||||
|       action(e); | ||||
|     } else if (to) { | ||||
|       e.preventDefault(); | ||||
|       this.context.router.history.push(to); | ||||
|  | @ -128,11 +128,11 @@ class DropdownMenu extends React.PureComponent { | |||
|       return <li key={`sep-${i}`} className='dropdown-menu__separator' />; | ||||
|     } | ||||
| 
 | ||||
|     const { text, href = '#' } = option; | ||||
|     const { text, href = '#', target = '_blank', method } = option; | ||||
| 
 | ||||
|     return ( | ||||
|       <li className='dropdown-menu__item' key={`${text}-${i}`}> | ||||
|         <a href={href} target='_blank' rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> | ||||
|         <a href={href} target={target} data-method={method} rel='noopener noreferrer' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyPress={this.handleItemKeyPress} data-index={i}> | ||||
|           {text} | ||||
|         </a> | ||||
|       </li> | ||||
|  | @ -149,7 +149,7 @@ class DropdownMenu extends React.PureComponent { | |||
|           // It should not be transformed when mounting because the resulting
 | ||||
|           // size will be used to determine the coordinate of the menu by
 | ||||
|           // react-overlays
 | ||||
|           <div className='dropdown-menu' style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> | ||||
|           <div className={`dropdown-menu ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> | ||||
|             <div className={`dropdown-menu__arrow ${placement}`} style={{ left: arrowOffsetLeft, top: arrowOffsetTop }} /> | ||||
| 
 | ||||
|             <ul> | ||||
|  | @ -236,7 +236,8 @@ export default class Dropdown extends React.PureComponent { | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleItemClick = (i, e) => { | ||||
|   handleItemClick = e => { | ||||
|     const i = Number(e.currentTarget.getAttribute('data-index')); | ||||
|     const { action, to } = this.props.items[i]; | ||||
| 
 | ||||
|     this.handleClose(); | ||||
|  |  | |||
|  | @ -14,15 +14,11 @@ const mapDispatchToProps = (dispatch, { status, items, scrollKey }) => ({ | |||
|   onOpen(id, onItemClick, dropdownPlacement, keyboard) { | ||||
|     dispatch(isUserTouching() ? openModal('ACTIONS', { | ||||
|       status, | ||||
|       actions: items.map( | ||||
|         (item, i) => item ? { | ||||
|           ...item, | ||||
|           name: `${item.text}-${i}`, | ||||
|           onClick: item.action ? ((e) => { return onItemClick(i, e) }) : null, | ||||
|         } : null | ||||
|       ), | ||||
|       actions: items, | ||||
|       onClick: onItemClick, | ||||
|     }) : openDropdownMenu(id, dropdownPlacement, keyboard, scrollKey)); | ||||
|   }, | ||||
| 
 | ||||
|   onClose(id) { | ||||
|     dispatch(closeModal('ACTIONS')); | ||||
|     dispatch(closeDropdownMenu(id)); | ||||
|  |  | |||
|  | @ -21,22 +21,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent { | |||
|     icon: PropTypes.string, | ||||
|     items: PropTypes.arrayOf(PropTypes.shape({ | ||||
|       icon: PropTypes.string, | ||||
|       meta: PropTypes.node, | ||||
|       meta: PropTypes.string, | ||||
|       name: PropTypes.string.isRequired, | ||||
|       on: PropTypes.bool, | ||||
|       text: PropTypes.node, | ||||
|       text: PropTypes.string, | ||||
|     })).isRequired, | ||||
|     onModalOpen: PropTypes.func, | ||||
|     onModalClose: PropTypes.func, | ||||
|     title: PropTypes.string, | ||||
|     value: PropTypes.string, | ||||
|     onChange: PropTypes.func, | ||||
|     noModal: PropTypes.bool, | ||||
|     container: PropTypes.func, | ||||
|     renderItemContents: PropTypes.func, | ||||
|     closeOnChange: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|     closeOnChange: true, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|     needsModalUpdate: false, | ||||
|     open: false, | ||||
|     openedViaKeyboard: undefined, | ||||
|     placement: 'bottom', | ||||
|  | @ -44,10 +47,10 @@ export default class ComposerOptionsDropdown extends React.PureComponent { | |||
| 
 | ||||
|   //  Toggles opening and closing the dropdown.
 | ||||
|   handleToggle = ({ target, type }) => { | ||||
|     const { onModalOpen, noModal } = this.props; | ||||
|     const { onModalOpen } = this.props; | ||||
|     const { open } = this.state; | ||||
| 
 | ||||
|     if (!noModal && isUserTouching()) { | ||||
|     if (isUserTouching()) { | ||||
|       if (this.state.open) { | ||||
|         this.props.onModalClose(); | ||||
|       } else { | ||||
|  | @ -107,9 +110,25 @@ export default class ComposerOptionsDropdown extends React.PureComponent { | |||
|     this.setState({ open: false }); | ||||
|   } | ||||
| 
 | ||||
|   handleItemClick = (e) => { | ||||
|     const { | ||||
|       items, | ||||
|       onChange, | ||||
|       onModalClose, | ||||
|       closeOnChange, | ||||
|     } = this.props; | ||||
| 
 | ||||
|     const i = Number(e.currentTarget.getAttribute('data-index')); | ||||
| 
 | ||||
|     const { name } = items[i]; | ||||
| 
 | ||||
|     e.preventDefault();  //  Prevents focus from changing
 | ||||
|     if (closeOnChange) onModalClose(); | ||||
|     onChange(name); | ||||
|   }; | ||||
| 
 | ||||
|   //  Creates an action modal object.
 | ||||
|   handleMakeModal = () => { | ||||
|     const component = this; | ||||
|     const { | ||||
|       items, | ||||
|       onChange, | ||||
|  | @ -125,6 +144,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent { | |||
| 
 | ||||
|     //  The object.
 | ||||
|     return { | ||||
|       renderItemContents: this.props.renderItemContents, | ||||
|       onClick: this.handleItemClick, | ||||
|       actions: items.map( | ||||
|         ({ | ||||
|           name, | ||||
|  | @ -133,48 +154,11 @@ export default class ComposerOptionsDropdown extends React.PureComponent { | |||
|           ...rest, | ||||
|           active: value && name === value, | ||||
|           name, | ||||
|           onClick (e) { | ||||
|             e.preventDefault();  //  Prevents focus from changing
 | ||||
|             onModalClose(); | ||||
|             onChange(name); | ||||
|           }, | ||||
|           onPassiveClick (e) { | ||||
|             e.preventDefault();  //  Prevents focus from changing
 | ||||
|             onChange(name); | ||||
|             component.setState({ needsModalUpdate: true }); | ||||
|           }, | ||||
|         }) | ||||
|       ), | ||||
|     }; | ||||
|   } | ||||
| 
 | ||||
|   //  If our modal is open and our props update, we need to also update
 | ||||
|   //  the modal.
 | ||||
|   handleUpdate = () => { | ||||
|     const { onModalOpen } = this.props; | ||||
|     const { needsModalUpdate } = this.state; | ||||
| 
 | ||||
|     //  Gets our modal object.
 | ||||
|     const modal = this.handleMakeModal(); | ||||
| 
 | ||||
|     //  Reopens the modal with the new object.
 | ||||
|     if (needsModalUpdate && modal && onModalOpen) { | ||||
|       onModalOpen(modal); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   //  Updates our modal as necessary.
 | ||||
|   componentDidUpdate (prevProps) { | ||||
|     const { items } = this.props; | ||||
|     const { needsModalUpdate } = this.state; | ||||
|     if (needsModalUpdate && items.find( | ||||
|       (item, i) => item.on !== prevProps.items[i].on | ||||
|     )) { | ||||
|       this.handleUpdate(); | ||||
|       this.setState({ needsModalUpdate: false }); | ||||
|     } | ||||
|   } | ||||
| 
 | ||||
|   //  Rendering.
 | ||||
|   render () { | ||||
|     const { | ||||
|  | @ -186,6 +170,8 @@ export default class ComposerOptionsDropdown extends React.PureComponent { | |||
|       onChange, | ||||
|       value, | ||||
|       container, | ||||
|       renderItemContents, | ||||
|       closeOnChange, | ||||
|     } = this.props; | ||||
|     const { open, placement } = this.state; | ||||
|     const computedClass = classNames('composer--options--dropdown', { | ||||
|  | @ -226,10 +212,12 @@ export default class ComposerOptionsDropdown extends React.PureComponent { | |||
|         > | ||||
|           <DropdownMenu | ||||
|             items={items} | ||||
|             renderItemContents={renderItemContents} | ||||
|             onChange={onChange} | ||||
|             onClose={this.handleClose} | ||||
|             value={value} | ||||
|             openedViaKeyboard={this.state.openedViaKeyboard} | ||||
|             closeOnChange={closeOnChange} | ||||
|           /> | ||||
|         </Overlay> | ||||
|       </div> | ||||
|  |  | |||
|  | @ -2,7 +2,6 @@ | |||
| import PropTypes from 'prop-types'; | ||||
| import React from 'react'; | ||||
| import spring from 'react-motion/lib/spring'; | ||||
| import Toggle from 'react-toggle'; | ||||
| import ImmutablePureComponent from 'react-immutable-pure-component'; | ||||
| import classNames from 'classnames'; | ||||
| 
 | ||||
|  | @ -28,18 +27,20 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent | |||
|       icon: PropTypes.string, | ||||
|       meta: PropTypes.node, | ||||
|       name: PropTypes.string.isRequired, | ||||
|       on: PropTypes.bool, | ||||
|       text: PropTypes.node, | ||||
|     })), | ||||
|     onChange: PropTypes.func.isRequired, | ||||
|     onClose: PropTypes.func.isRequired, | ||||
|     style: PropTypes.object, | ||||
|     value: PropTypes.string, | ||||
|     renderItemContents: PropTypes.func, | ||||
|     openedViaKeyboard: PropTypes.bool, | ||||
|     closeOnChange: PropTypes.bool, | ||||
|   }; | ||||
| 
 | ||||
|   static defaultProps = { | ||||
|     style: {}, | ||||
|     closeOnChange: true, | ||||
|   }; | ||||
| 
 | ||||
|   state = { | ||||
|  | @ -77,16 +78,19 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent | |||
|     document.removeEventListener('touchend', this.handleDocumentClick, withPassive); | ||||
|   } | ||||
| 
 | ||||
|   handleClick = (name, e) => { | ||||
|   handleClick = (e) => { | ||||
|     const i = Number(e.currentTarget.getAttribute('data-index')); | ||||
| 
 | ||||
|     const { | ||||
|       onChange, | ||||
|       onClose, | ||||
|       closeOnChange, | ||||
|       items, | ||||
|     } = this.props; | ||||
| 
 | ||||
|     const { on } = this.props.items.find(item => item.name === name); | ||||
|     const { name } = this.props.items[i]; | ||||
|     e.preventDefault();  //  Prevents change in focus on click
 | ||||
|     if ((on === null || typeof on === 'undefined')) { | ||||
|     if (closeOnChange) { | ||||
|       onClose(); | ||||
|     } | ||||
|     onChange(name); | ||||
|  | @ -101,11 +105,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent | |||
|     } | ||||
|   } | ||||
| 
 | ||||
|   handleKeyDown = (name, e) => { | ||||
|   handleKeyDown = (e) => { | ||||
|     const index = Number(e.currentTarget.getAttribute('data-index')); | ||||
|     const { items } = this.props; | ||||
|     const index = items.findIndex(item => { | ||||
|       return (item.name === name); | ||||
|     }); | ||||
|     let element = null; | ||||
| 
 | ||||
|     switch(e.key) { | ||||
|  | @ -139,7 +141,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent | |||
| 
 | ||||
|     if (element) { | ||||
|       element.focus(); | ||||
|       this.handleChange(element.getAttribute('data-index')); | ||||
|       this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name); | ||||
|       e.preventDefault(); | ||||
|       e.stopPropagation(); | ||||
|     } | ||||
|  | @ -149,44 +151,40 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent | |||
|     this.focusedItem = c; | ||||
|   } | ||||
| 
 | ||||
|   renderItem = (item) => { | ||||
|     const { name, icon, meta, on, text } = item; | ||||
|   renderItem = (item, i) => { | ||||
|     const { name, icon, meta, text } = item; | ||||
| 
 | ||||
|     const active = (name === (this.props.value || this.state.value)); | ||||
| 
 | ||||
|     const computedClass = classNames('composer--options--dropdown--content--item', { | ||||
|       active, | ||||
|       lengthy: meta, | ||||
|       'toggled-off': !on && on !== null && typeof on !== 'undefined', | ||||
|       'toggled-on': on, | ||||
|       'with-icon': icon, | ||||
|     }); | ||||
|     const computedClass = classNames('composer--options--dropdown--content--item', { active }); | ||||
| 
 | ||||
|     let prefix = null; | ||||
|     let contents = this.props.renderItemContents && this.props.renderItemContents(item, i); | ||||
| 
 | ||||
|     if (on !== null && typeof on !== 'undefined') { | ||||
|       prefix = <Toggle checked={on} onChange={this.handleClick.bind(this, name)} />; | ||||
|     } else if (icon) { | ||||
|       prefix = <Icon className='icon' fixedWidth id={icon} /> | ||||
|     if (!contents) { | ||||
|       contents = ( | ||||
|         <React.Fragment> | ||||
|           {icon && <Icon className='icon' fixedWidth id={icon} />} | ||||
| 
 | ||||
|           <div className='content'> | ||||
|             <strong>{text}</strong> | ||||
|             {meta} | ||||
|           </div> | ||||
|         </React.Fragment> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <div | ||||
|         className={computedClass} | ||||
|         onClick={this.handleClick.bind(this, name)} | ||||
|         onKeyDown={this.handleKeyDown.bind(this, name)} | ||||
|         onClick={this.handleClick} | ||||
|         onKeyDown={this.handleKeyDown} | ||||
|         role='option' | ||||
|         tabIndex='0' | ||||
|         key={name} | ||||
|         data-index={name} | ||||
|         data-index={i} | ||||
|         ref={active ? this.setFocusRef : null} | ||||
|       > | ||||
|         {prefix} | ||||
| 
 | ||||
|         <div className='content'> | ||||
|           <strong>{text}</strong> | ||||
|           {meta} | ||||
|         </div> | ||||
|         {contents} | ||||
|       </div> | ||||
|     ); | ||||
|   } | ||||
|  | @ -229,7 +227,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent | |||
|               transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, | ||||
|             }} | ||||
|           > | ||||
|             {!!items && items.map(item => this.renderItem(item))} | ||||
|             {!!items && items.map((item, i) => this.renderItem(item, i))} | ||||
|           </div> | ||||
|         )} | ||||
|       </Motion> | ||||
|  |  | |||
|  | @ -2,8 +2,10 @@ | |||
| import PropTypes from 'prop-types'; | ||||
| import React from 'react'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; | ||||
| import { defineMessages, injectIntl } from 'react-intl'; | ||||
| import spring from 'react-motion/lib/spring'; | ||||
| import Toggle from 'react-toggle'; | ||||
| import { connect } from 'react-redux'; | ||||
| 
 | ||||
| //  Components.
 | ||||
| import IconButton from 'flavours/glitch/components/icon_button'; | ||||
|  | @ -80,6 +82,36 @@ const messages = defineMessages({ | |||
|   }, | ||||
| }); | ||||
| 
 | ||||
| @connect((state, { name }) => ({ checked: state.getIn(['compose', 'advanced_options', name]) })) | ||||
| class ToggleOption extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     name: PropTypes.string.isRequired, | ||||
|     checked: PropTypes.bool, | ||||
|     onChangeAdvancedOption: PropTypes.func.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   handleChange = () => { | ||||
|     this.props.onChangeAdvancedOption(this.props.name); | ||||
|   }; | ||||
| 
 | ||||
|   render() { | ||||
|     const { meta, text, checked } = this.props; | ||||
| 
 | ||||
|     return ( | ||||
|       <React.Fragment> | ||||
|         <Toggle checked={checked} onChange={this.handleChange} /> | ||||
| 
 | ||||
|         <div className='content'> | ||||
|           <strong>{text}</strong> | ||||
|           {meta} | ||||
|         </div> | ||||
|       </React.Fragment> | ||||
|     ); | ||||
|   } | ||||
| 
 | ||||
| } | ||||
| 
 | ||||
| export default @injectIntl | ||||
| class ComposerOptions extends ImmutablePureComponent { | ||||
| 
 | ||||
|  | @ -141,6 +173,13 @@ class ComposerOptions extends ImmutablePureComponent { | |||
|     this.fileElement = fileElement; | ||||
|   } | ||||
| 
 | ||||
|   renderToggleItemContents = (item) => { | ||||
|     const { onChangeAdvancedOption } = this.props; | ||||
|     const { name, meta, text } = item; | ||||
| 
 | ||||
|     return <ToggleOption name={name} text={text} meta={meta} onChangeAdvancedOption={onChangeAdvancedOption} />; | ||||
|   }; | ||||
| 
 | ||||
|   //  Rendering.
 | ||||
|   render () { | ||||
|     const { | ||||
|  | @ -152,7 +191,6 @@ class ComposerOptions extends ImmutablePureComponent { | |||
|       hasMedia, | ||||
|       allowPoll, | ||||
|       hasPoll, | ||||
|       intl, | ||||
|       onChangeAdvancedOption, | ||||
|       onChangeContentType, | ||||
|       onChangeVisibility, | ||||
|  | @ -164,23 +202,24 @@ class ComposerOptions extends ImmutablePureComponent { | |||
|       resetFileKey, | ||||
|       spoiler, | ||||
|       showContentTypeChoice, | ||||
|       intl: { formatMessage }, | ||||
|     } = this.props; | ||||
| 
 | ||||
|     const contentTypeItems = { | ||||
|       plain: { | ||||
|         icon: 'file-text', | ||||
|         name: 'text/plain', | ||||
|         text: <FormattedMessage {...messages.plain} />, | ||||
|         text: formatMessage(messages.plain), | ||||
|       }, | ||||
|       html: { | ||||
|         icon: 'code', | ||||
|         name: 'text/html', | ||||
|         text: <FormattedMessage {...messages.html} />, | ||||
|         text: formatMessage(messages.html), | ||||
|       }, | ||||
|       markdown: { | ||||
|         icon: 'arrow-circle-down', | ||||
|         name: 'text/markdown', | ||||
|         text: <FormattedMessage {...messages.markdown} />, | ||||
|         text: formatMessage(messages.markdown), | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|  | @ -204,18 +243,18 @@ class ComposerOptions extends ImmutablePureComponent { | |||
|             { | ||||
|               icon: 'cloud-upload', | ||||
|               name: 'upload', | ||||
|               text: <FormattedMessage {...messages.upload} />, | ||||
|               text: formatMessage(messages.upload), | ||||
|             }, | ||||
|             { | ||||
|               icon: 'paint-brush', | ||||
|               name: 'doodle', | ||||
|               text: <FormattedMessage {...messages.doodle} />, | ||||
|               text: formatMessage(messages.doodle), | ||||
|             }, | ||||
|           ]} | ||||
|           onChange={this.handleClickAttach} | ||||
|           onModalClose={onModalClose} | ||||
|           onModalOpen={onModalOpen} | ||||
|           title={intl.formatMessage(messages.attach)} | ||||
|           title={formatMessage(messages.attach)} | ||||
|         /> | ||||
|         {!!pollLimits && ( | ||||
|           <IconButton | ||||
|  | @ -229,7 +268,7 @@ class ComposerOptions extends ImmutablePureComponent { | |||
|               height: null, | ||||
|               lineHeight: null, | ||||
|             }} | ||||
|             title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)} | ||||
|             title={formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)} | ||||
|           /> | ||||
|         )} | ||||
|         <hr /> | ||||
|  | @ -252,7 +291,7 @@ class ComposerOptions extends ImmutablePureComponent { | |||
|             onChange={onChangeContentType} | ||||
|             onModalClose={onModalClose} | ||||
|             onModalOpen={onModalOpen} | ||||
|             title={intl.formatMessage(messages.content_type)} | ||||
|             title={formatMessage(messages.content_type)} | ||||
|             value={contentType} | ||||
|           /> | ||||
|         )} | ||||
|  | @ -262,7 +301,7 @@ class ComposerOptions extends ImmutablePureComponent { | |||
|             ariaControls='glitch.composer.spoiler.input' | ||||
|             label='CW' | ||||
|             onClick={onToggleSpoiler} | ||||
|             title={intl.formatMessage(messages.spoiler)} | ||||
|             title={formatMessage(messages.spoiler)} | ||||
|           /> | ||||
|         )} | ||||
|         <Dropdown | ||||
|  | @ -271,22 +310,22 @@ class ComposerOptions extends ImmutablePureComponent { | |||
|           icon='ellipsis-h' | ||||
|           items={advancedOptions ? [ | ||||
|             { | ||||
|               meta: <FormattedMessage {...messages.local_only_long} />, | ||||
|               meta: formatMessage(messages.local_only_long), | ||||
|               name: 'do_not_federate', | ||||
|               on: advancedOptions.get('do_not_federate'), | ||||
|               text: <FormattedMessage {...messages.local_only_short} />, | ||||
|               text: formatMessage(messages.local_only_short), | ||||
|             }, | ||||
|             { | ||||
|               meta: <FormattedMessage {...messages.threaded_mode_long} />, | ||||
|               meta: formatMessage(messages.threaded_mode_long), | ||||
|               name: 'threaded_mode', | ||||
|               on: advancedOptions.get('threaded_mode'), | ||||
|               text: <FormattedMessage {...messages.threaded_mode_short} />, | ||||
|               text: formatMessage(messages.threaded_mode_short), | ||||
|             }, | ||||
|           ] : null} | ||||
|           onChange={onChangeAdvancedOption} | ||||
|           renderItemContents={this.renderToggleItemContents} | ||||
|           onModalClose={onModalClose} | ||||
|           onModalOpen={onModalOpen} | ||||
|           title={intl.formatMessage(messages.advanced_options_icon_title)} | ||||
|           title={formatMessage(messages.advanced_options_icon_title)} | ||||
|           closeOnChange={false} | ||||
|         /> | ||||
|       </div> | ||||
|     ); | ||||
|  |  | |||
|  | @ -1,46 +1,19 @@ | |||
| import PropTypes from 'prop-types'; | ||||
| import React from 'react'; | ||||
| import ImmutablePropTypes from 'react-immutable-proptypes'; | ||||
| import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; | ||||
| import { defineMessages, injectIntl } from 'react-intl'; | ||||
| import Dropdown from './dropdown'; | ||||
| 
 | ||||
| const messages = defineMessages({ | ||||
|   change_privacy: { | ||||
|     defaultMessage: 'Adjust status privacy', | ||||
|     id: 'privacy.change', | ||||
|   }, | ||||
|   direct_long: { | ||||
|     defaultMessage: 'Visible for mentioned users only', | ||||
|     id: 'privacy.direct.long', | ||||
|   }, | ||||
|   direct_short: { | ||||
|     defaultMessage: 'Direct', | ||||
|     id: 'privacy.direct.short', | ||||
|   }, | ||||
|   private_long: { | ||||
|     defaultMessage: 'Visible for followers only', | ||||
|     id: 'privacy.private.long', | ||||
|   }, | ||||
|   private_short: { | ||||
|     defaultMessage: 'Followers-only', | ||||
|     id: 'privacy.private.short', | ||||
|   }, | ||||
|   public_long: { | ||||
|     defaultMessage: 'Visible for all, shown in public timelines', | ||||
|     id: 'privacy.public.long', | ||||
|   }, | ||||
|   public_short: { | ||||
|     defaultMessage: 'Public', | ||||
|     id: 'privacy.public.short', | ||||
|   }, | ||||
|   unlisted_long: { | ||||
|     defaultMessage: 'Visible for all, but not in public timelines', | ||||
|     id: 'privacy.unlisted.long', | ||||
|   }, | ||||
|   unlisted_short: { | ||||
|     defaultMessage: 'Unlisted', | ||||
|     id: 'privacy.unlisted.short', | ||||
|   }, | ||||
|   public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, | ||||
|   public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all, shown in public timelines' }, | ||||
|   unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, | ||||
|   unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but not in public timelines' }, | ||||
|   private_short: { id: 'privacy.private.short', defaultMessage: 'Followers-only' }, | ||||
|   private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, | ||||
|   direct_short: { id: 'privacy.direct.short', defaultMessage: 'Direct' }, | ||||
|   direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, | ||||
|   change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, | ||||
| }); | ||||
| 
 | ||||
| export default @injectIntl | ||||
|  | @ -53,40 +26,39 @@ class PrivacyDropdown extends React.PureComponent { | |||
|     value: PropTypes.string.isRequired, | ||||
|     onChange: PropTypes.func.isRequired, | ||||
|     noDirect: PropTypes.bool, | ||||
|     noModal: PropTypes.bool, | ||||
|     container: PropTypes.func, | ||||
|     intl: PropTypes.object.isRequired, | ||||
|   }; | ||||
| 
 | ||||
|   render () { | ||||
|     const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, noModal, container, intl } = this.props; | ||||
|     const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, intl: { formatMessage } } = this.props; | ||||
| 
 | ||||
|     //  We predefine our privacy items so that we can easily pick the
 | ||||
|     //  dropdown icon later.
 | ||||
|     const privacyItems = { | ||||
|       direct: { | ||||
|         icon: 'envelope', | ||||
|         meta: <FormattedMessage {...messages.direct_long} />, | ||||
|         meta: formatMessage(messages.direct_long), | ||||
|         name: 'direct', | ||||
|         text: <FormattedMessage {...messages.direct_short} />, | ||||
|         text: formatMessage(messages.direct_short), | ||||
|       }, | ||||
|       private: { | ||||
|         icon: 'lock', | ||||
|         meta: <FormattedMessage {...messages.private_long} />, | ||||
|         meta: formatMessage(messages.private_long), | ||||
|         name: 'private', | ||||
|         text: <FormattedMessage {...messages.private_short} />, | ||||
|         text: formatMessage(messages.private_short), | ||||
|       }, | ||||
|       public: { | ||||
|         icon: 'globe', | ||||
|         meta: <FormattedMessage {...messages.public_long} />, | ||||
|         meta: formatMessage(messages.public_long), | ||||
|         name: 'public', | ||||
|         text: <FormattedMessage {...messages.public_short} />, | ||||
|         text: formatMessage(messages.public_short), | ||||
|       }, | ||||
|       unlisted: { | ||||
|         icon: 'unlock', | ||||
|         meta: <FormattedMessage {...messages.unlisted_long} />, | ||||
|         meta: formatMessage(messages.unlisted_long), | ||||
|         name: 'unlisted', | ||||
|         text: <FormattedMessage {...messages.unlisted_short} />, | ||||
|         text: formatMessage(messages.unlisted_short), | ||||
|       }, | ||||
|     }; | ||||
| 
 | ||||
|  | @ -104,9 +76,8 @@ class PrivacyDropdown extends React.PureComponent { | |||
|         onChange={onChange} | ||||
|         onModalClose={onModalClose} | ||||
|         onModalOpen={onModalOpen} | ||||
|         title={intl.formatMessage(messages.change_privacy)} | ||||
|         title={formatMessage(messages.change_privacy)} | ||||
|         container={container} | ||||
|         noModal={noModal} | ||||
|         value={value} | ||||
|       /> | ||||
|     ); | ||||
|  |  | |||
|  | @ -7,24 +7,22 @@ import Avatar from 'flavours/glitch/components/avatar'; | |||
| import RelativeTimestamp from 'flavours/glitch/components/relative_timestamp'; | ||||
| import DisplayName from 'flavours/glitch/components/display_name'; | ||||
| import classNames from 'classnames'; | ||||
| import Icon from 'flavours/glitch/components/icon'; | ||||
| import Link from 'flavours/glitch/components/link'; | ||||
| import Toggle from 'react-toggle'; | ||||
| import IconButton from 'flavours/glitch/components/icon_button'; | ||||
| 
 | ||||
| export default class ActionsModal extends ImmutablePureComponent { | ||||
| 
 | ||||
|   static propTypes = { | ||||
|     status: ImmutablePropTypes.map, | ||||
|     onClick: PropTypes.func, | ||||
|     actions: PropTypes.arrayOf(PropTypes.shape({ | ||||
|       active: PropTypes.bool, | ||||
|       href: PropTypes.string, | ||||
|       icon: PropTypes.string, | ||||
|       meta: PropTypes.node, | ||||
|       meta: PropTypes.string, | ||||
|       name: PropTypes.string, | ||||
|       on: PropTypes.bool, | ||||
|       onPassiveClick: PropTypes.func, | ||||
|       text: PropTypes.node, | ||||
|       text: PropTypes.string, | ||||
|     })), | ||||
|     renderItemContents: PropTypes.func, | ||||
|   }; | ||||
| 
 | ||||
|   renderAction = (action, i) => { | ||||
|  | @ -32,57 +30,26 @@ export default class ActionsModal extends ImmutablePureComponent { | |||
|       return <li key={`sep-${i}`} className='dropdown-menu__separator' />; | ||||
|     } | ||||
| 
 | ||||
|     const { | ||||
|       active, | ||||
|       href, | ||||
|       icon, | ||||
|       meta, | ||||
|       name, | ||||
|       on, | ||||
|       onClick, | ||||
|       onPassiveClick, | ||||
|       text, | ||||
|     } = action; | ||||
|     const { icon = null, text, meta = null, active = false, href = '#' } = action; | ||||
|     let contents = this.props.renderItemContents && this.props.renderItemContents(action, i); | ||||
| 
 | ||||
|     if (!contents) { | ||||
|       contents = ( | ||||
|         <React.Fragment> | ||||
|           {icon && <IconButton title={text} icon={icon} role='presentation' tabIndex='-1' inverted />} | ||||
|           <div> | ||||
|             <div className={classNames({ 'actions-modal__item-label': !!meta })}>{text}</div> | ||||
|             <div>{meta}</div> | ||||
|           </div> | ||||
|         </React.Fragment> | ||||
|       ); | ||||
|     } | ||||
| 
 | ||||
|     return ( | ||||
|       <li key={name || i}> | ||||
|         <Link | ||||
|           className={classNames('link', { active })} | ||||
|           href={href} | ||||
|           onClick={on !== null && typeof on !== 'undefined' && onPassiveClick || onClick} | ||||
|           role={onClick ? 'button' : null} | ||||
|         > | ||||
|           {function () { | ||||
| 
 | ||||
|             //  We render a `<Toggle>` if we were provided an `on`
 | ||||
|             //  property, and otherwise show an `<Icon>` if available.
 | ||||
|             switch (true) { | ||||
|             case on !== null && typeof on !== 'undefined': | ||||
|               return ( | ||||
|                 <Toggle | ||||
|                   checked={on} | ||||
|                   onChange={onPassiveClick || onClick} | ||||
|                 /> | ||||
|               ); | ||||
|             case !!icon: | ||||
|               return ( | ||||
|                 <Icon | ||||
|                   className='icon' | ||||
|                   fixedWidth | ||||
|                   id={icon} | ||||
|                 /> | ||||
|               ); | ||||
|             default: | ||||
|               return null; | ||||
|             } | ||||
|           }()} | ||||
|           {meta ? ( | ||||
|             <div> | ||||
|               <strong>{text}</strong> | ||||
|               {meta} | ||||
|             </div> | ||||
|           ) : <div>{text}</div>} | ||||
|         </Link> | ||||
|       <li key={`${text}-${i}`}> | ||||
|         <a href={href} target='_blank' rel='noopener noreferrer' onClick={this.props.onClick} data-index={i} className={classNames('link', { active })}> | ||||
|           {contents} | ||||
|         </a> | ||||
|       </li> | ||||
|     ); | ||||
|   } | ||||
|  |  | |||
		Loading…
	
		Reference in a new issue