diff options
Diffstat (limited to 'app/javascript/glitch/components/compose')
5 files changed, 542 insertions, 0 deletions
diff --git a/app/javascript/glitch/components/compose/advanced_options/container.js b/app/javascript/glitch/components/compose/advanced_options/container.js new file mode 100644 index 000000000..160f22737 --- /dev/null +++ b/app/javascript/glitch/components/compose/advanced_options/container.js @@ -0,0 +1,66 @@ +/* + +`<ComposeAdvancedOptionsContainer>` +=================================== + +This container connects `<ComposeAdvancedOptions>` to the Redux store. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + +// Package imports // +import { connect } from 'react-redux'; + +// Mastodon imports // +import { toggleComposeAdvancedOption } from '../../../../mastodon/actions/compose'; + +// Our imports // +import ComposeAdvancedOptions from '.'; + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +State mapping: +-------------- + +The `mapStateToProps()` function maps various state properties to the +props of our component. The only property we care about is +`compose.advanced_options`. + +*/ + +const mapStateToProps = state => ({ + values: state.getIn(['compose', 'advanced_options']), +}); + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Dispatch mapping: +----------------- + +The `mapDispatchToProps()` function maps dispatches to our store to the +various props of our component. We just need to provide a dispatch for +when an advanced option toggle changes. + +*/ + +const mapDispatchToProps = dispatch => ({ + + onChange (option) { + dispatch(toggleComposeAdvancedOption(option)); + }, + +}); + +export default connect(mapStateToProps, mapDispatchToProps)(ComposeAdvancedOptions); diff --git a/app/javascript/glitch/components/compose/advanced_options/index.js b/app/javascript/glitch/components/compose/advanced_options/index.js new file mode 100644 index 000000000..8251baf4d --- /dev/null +++ b/app/javascript/glitch/components/compose/advanced_options/index.js @@ -0,0 +1,163 @@ +/* + +`<ComposeAdvancedOptions>` +========================== + +> For more information on the contents of this file, please contact: +> +> - surinna [@srn@dev.glitch.social] + +This adds an advanced options dropdown to the toot compose box, for +toggles that don't necessarily fit elsewhere. + +__Props:__ + + - __`values` (`ImmutablePropTypes.contains(…).isRequired`) :__ + An Immutable map with the following values: + + - __`do_not_federate` (`PropTypes.bool.isRequired`) :__ + Specifies whether or not to federate the status. + + - __`onChange` (`PropTypes.func.isRequired`) :__ + The function to call when a toggle is changed. We pass this from + our container to the toggle. + + - __`intl` (`PropTypes.object.isRequired`) :__ + Our internationalization object, inserted by `@injectIntl`. + +__State:__ + + - __`open` :__ + This tells whether the dropdown is currently open or closed. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + +// Package imports // +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, defineMessages } from 'react-intl'; + +// Our imports // +import ComposeAdvancedOptionsToggle from './toggle'; +import ComposeDropdown from '../dropdown/index'; + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Inital setup: +------------- + +The `messages` constant is used to define any messages that we need +from inside props. These are the various titles and labels on our +toggles. + +`iconStyle` styles the icon used for the dropdown button. + +*/ + +const messages = defineMessages({ + local_only_short : + { id: 'advanced-options.local-only.short', defaultMessage: 'Local-only' }, + local_only_long : + { id: 'advanced-options.local-only.long', defaultMessage: 'Do not post to other instances' }, + advanced_options_icon_title : + { id: 'advanced_options.icon_title', defaultMessage: 'Advanced options' }, +}); + +/* + +Implementation: +--------------- + +*/ + +@injectIntl +export default class ComposeAdvancedOptions extends React.PureComponent { + + static propTypes = { + values : ImmutablePropTypes.contains({ + do_not_federate : PropTypes.bool.isRequired, + }).isRequired, + onChange : PropTypes.func.isRequired, + intl : PropTypes.object.isRequired, + }; + + +/* + +### `render()` + +`render()` actually puts our component on the screen. + +*/ + + render () { + const { intl, values } = this.props; + +/* + +The `options` array provides all of the available advanced options +alongside their icon, text, and name. + +*/ + const options = [ + { icon: 'wifi', shortText: messages.local_only_short, longText: messages.local_only_long, name: 'do_not_federate' }, + ]; + +/* + +`anyEnabled` tells us if any of our advanced options have been enabled. + +*/ + + const anyEnabled = values.some((enabled) => enabled); + +/* + +`optionElems` takes our `options` and creates +`<ComposeAdvancedOptionsToggle>`s out of them. We use the `name` of the +toggle as its `key` so that React can keep track of it. + +*/ + + const optionElems = options.map((option) => { + return ( + <ComposeAdvancedOptionsToggle + onChange={this.props.onChange} + active={values.get(option.name)} + key={option.name} + name={option.name} + shortText={intl.formatMessage(option.shortText)} + longText={intl.formatMessage(option.longText)} + /> + ); + }); + +/* + +Finally, we can render our component. + +*/ + return ( + <ComposeDropdown + title={intl.formatMessage(messages.advanced_options_icon_title)} + icon='home' + highlight={anyEnabled} + > + {optionElems} + </ComposeDropdown> + ); + } + +} diff --git a/app/javascript/glitch/components/compose/advanced_options/toggle.js b/app/javascript/glitch/components/compose/advanced_options/toggle.js new file mode 100644 index 000000000..d6907472a --- /dev/null +++ b/app/javascript/glitch/components/compose/advanced_options/toggle.js @@ -0,0 +1,103 @@ +/* + +`<ComposeAdvancedOptionsToggle>` +================================ + +> For more information on the contents of this file, please contact: +> +> - surinna [@srn@dev.glitch.social] + +This creates the toggle used by `<ComposeAdvancedOptions>`. + +__Props:__ + + - __`onChange` (`PropTypes.func`) :__ + This provides the function to call when the toggle is + (de-?)activated. + + - __`active` (`PropTypes.bool`) :__ + This prop controls whether the toggle is currently active or not. + + - __`name` (`PropTypes.string`) :__ + This identifies the toggle, and is sent to `onChange()` when it is + called. + + - __`shortText` (`PropTypes.string`) :__ + This is a short string used as the title of the toggle. + + - __`longText` (`PropTypes.string`) :__ + This is a longer string used as a subtitle for the toggle. + +*/ + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Imports: +-------- + +*/ + +// Package imports // +import React from 'react'; +import PropTypes from 'prop-types'; +import Toggle from 'react-toggle'; + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +/* + +Implementation: +--------------- + +*/ + +export default class ComposeAdvancedOptionsToggle extends React.PureComponent { + + static propTypes = { + onChange: PropTypes.func.isRequired, + active: PropTypes.bool.isRequired, + name: PropTypes.string.isRequired, + shortText: PropTypes.string.isRequired, + longText: PropTypes.string.isRequired, + } + +/* + +### `onToggle()` + +The `onToggle()` function simply calls the `onChange()` prop with the +toggle's `name`. + +*/ + + onToggle = () => { + this.props.onChange(this.props.name); + } + +/* + +### `render()` + +The `render()` function is used to render our component. We just render +a `<Toggle>` and place next to it our text. + +*/ + + render() { + const { active, shortText, longText } = this.props; + return ( + <div role='button' tabIndex='0' className='advanced-options-dropdown__option' onClick={this.onToggle}> + <div className='advanced-options-dropdown__option__toggle'> + <Toggle checked={active} onChange={this.onToggle} /> + </div> + <div className='advanced-options-dropdown__option__content'> + <strong>{shortText}</strong> + {longText} + </div> + </div> + ); + } + +} diff --git a/app/javascript/glitch/components/compose/attach_options/index.js b/app/javascript/glitch/components/compose/attach_options/index.js new file mode 100644 index 000000000..4340972f0 --- /dev/null +++ b/app/javascript/glitch/components/compose/attach_options/index.js @@ -0,0 +1,133 @@ +// Package imports // +import React from 'react'; +import PropTypes from 'prop-types'; +import { connect } from 'react-redux'; +import { injectIntl, defineMessages } from 'react-intl'; + +// Our imports // +import ComposeDropdown from '../dropdown/index'; +import { uploadCompose } from '../../../../mastodon/actions/compose'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { openModal } from '../../../../mastodon/actions/modal'; + +// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * + +const messages = defineMessages({ + upload : + { id: 'compose.attach.upload', defaultMessage: 'Upload a file' }, + doodle : + { id: 'compose.attach.doodle', defaultMessage: 'Draw something' }, + attach : + { id: 'compose.attach', defaultMessage: 'Attach...' }, +}); + +const mapStateToProps = state => ({ + // This horrible expression is copied from vanilla upload_button_container + disabled: state.getIn(['compose', 'is_uploading']) || (state.getIn(['compose', 'media_attachments']).size > 3 || state.getIn(['compose', 'media_attachments']).some(m => m.get('type') === 'video')), + resetFileKey: state.getIn(['compose', 'resetFileKey']), + acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']), +}); + +const mapDispatchToProps = dispatch => ({ + onSelectFile (files) { + dispatch(uploadCompose(files)); + }, + onOpenDoodle () { + dispatch(openModal('DOODLE', { noEsc: true })); + }, +}); + +@injectIntl +@connect(mapStateToProps, mapDispatchToProps) +export default class ComposeAttachOptions extends ImmutablePureComponent { + + static propTypes = { + intl : PropTypes.object.isRequired, + resetFileKey: PropTypes.number, + acceptContentTypes: ImmutablePropTypes.listOf(PropTypes.string).isRequired, + disabled: PropTypes.bool, + onSelectFile: PropTypes.func.isRequired, + onOpenDoodle: PropTypes.func.isRequired, + }; + + handleItemClick = bt => { + if (bt === 'upload') { + this.fileElement.click(); + } + + if (bt === 'doodle') { + this.props.onOpenDoodle(); + } + + this.dropdown.setState({ open: false }); + }; + + handleFileChange = (e) => { + if (e.target.files.length > 0) { + this.props.onSelectFile(e.target.files); + } + } + + setFileRef = (c) => { + this.fileElement = c; + } + + setDropdownRef = (c) => { + this.dropdown = c; + } + + render () { + const { intl, resetFileKey, disabled, acceptContentTypes } = this.props; + + const options = [ + { icon: 'cloud-upload', text: messages.upload, name: 'upload' }, + { icon: 'paint-brush', text: messages.doodle, name: 'doodle' }, + ]; + + const optionElems = options.map((item) => { + const hdl = () => this.handleItemClick(item.name); + return ( + <div + role='button' + tabIndex='0' + key={item.name} + onClick={hdl} + className='privacy-dropdown__option' + > + <div className='privacy-dropdown__option__icon'> + <i className={`fa fa-fw fa-${item.icon}`} /> + </div> + + <div className='privacy-dropdown__option__content'> + <strong>{intl.formatMessage(item.text)}</strong> + </div> + </div> + ); + }); + + return ( + <div> + <ComposeDropdown + title={intl.formatMessage(messages.attach)} + icon='paperclip' + disabled={disabled} + ref={this.setDropdownRef} + > + {optionElems} + </ComposeDropdown> + <input + key={resetFileKey} + ref={this.setFileRef} + type='file' + multiple={false} + accept={acceptContentTypes.toArray().join(',')} + onChange={this.handleFileChange} + disabled={disabled} + style={{ display: 'none' }} + /> + </div> + ); + } + +} diff --git a/app/javascript/glitch/components/compose/dropdown/index.js b/app/javascript/glitch/components/compose/dropdown/index.js new file mode 100644 index 000000000..5f6467155 --- /dev/null +++ b/app/javascript/glitch/components/compose/dropdown/index.js @@ -0,0 +1,77 @@ +// Package imports // +import React from 'react'; +import PropTypes from 'prop-types'; + +// Mastodon imports // +import IconButton from '../../../../mastodon/components/icon_button'; + +const iconStyle = { + height : null, + lineHeight : '27px', +}; + +export default class ComposeDropdown extends React.PureComponent { + + static propTypes = { + title: PropTypes.string.isRequired, + icon: PropTypes.string, + highlight: PropTypes.bool, + disabled: PropTypes.bool, + children: PropTypes.arrayOf(PropTypes.node).isRequired, + }; + + state = { + open: false, + }; + + onGlobalClick = (e) => { + if (e.target !== this.node && !this.node.contains(e.target) && this.state.open) { + this.setState({ open: false }); + } + }; + + componentDidMount () { + window.addEventListener('click', this.onGlobalClick); + window.addEventListener('touchstart', this.onGlobalClick); + } + componentWillUnmount () { + window.removeEventListener('click', this.onGlobalClick); + window.removeEventListener('touchstart', this.onGlobalClick); + } + + onToggleDropdown = () => { + if (this.props.disabled) return; + this.setState({ open: !this.state.open }); + }; + + setRef = (c) => { + this.node = c; + }; + + render () { + const { open } = this.state; + let { highlight, title, icon, disabled } = this.props; + + if (!icon) icon = 'ellipsis-h'; + + return ( + <div ref={this.setRef} className={`advanced-options-dropdown ${open ? 'open' : ''} ${highlight ? 'active' : ''} `}> + <div className='advanced-options-dropdown__value'> + <IconButton + className={'inverted'} + title={title} + icon={icon} active={open || highlight} + size={18} + style={iconStyle} + disabled={disabled} + onClick={this.onToggleDropdown} + /> + </div> + <div className='advanced-options-dropdown__dropdown'> + {this.props.children} + </div> + </div> + ); + } + +} |