diff options
author | kibigo! <marrus-sh@users.noreply.github.com> | 2018-01-03 12:36:21 -0800 |
---|---|---|
committer | kibigo! <marrus-sh@users.noreply.github.com> | 2018-01-04 18:33:13 -0800 |
commit | 42f50049ff29ccdc484c22f8a5a19cda2dd03a5b (patch) | |
tree | 5e04c0bc0106f634316202eb8dc2f8c356d1b556 /app/javascript/flavours/glitch/features/composer | |
parent | b4a3792201ccc01713b536e50428e027bd094d2b (diff) |
WIP <Compose> Refactor; 1000 tiny edits
Diffstat (limited to 'app/javascript/flavours/glitch/features/composer')
13 files changed, 516 insertions, 421 deletions
diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js index c3e6c987c..d64bee7ee 100644 --- a/app/javascript/flavours/glitch/features/composer/index.js +++ b/app/javascript/flavours/glitch/features/composer/index.js @@ -52,6 +52,7 @@ function mapStateToProps (state) { focusDate: state.getIn(['compose', 'focusDate']), isSubmitting: state.getIn(['compose', 'is_submitting']), isUploading: state.getIn(['compose', 'is_uploading']), + layout: state.getIn(['local_settings', 'layout']), media: state.getIn(['compose', 'media_attachments']), preselectDate: state.getIn(['compose', 'preselectDate']), privacy: state.getIn(['compose', 'privacy']), @@ -71,132 +72,96 @@ function mapStateToProps (state) { }; // Dispatch mapping. -const mapDispatchToProps = dispatch => ({ - cancelReply () { - dispatch(cancelReplyCompose()); - }, - changeDescription (mediaId, description) { - dispatch(changeUploadCompose(mediaId, description)); - }, - changeSensitivity () { - dispatch(changeComposeSensitivity()); - }, - changeSpoilerText (checked) { - dispatch(changeComposeSpoilerText(checked)); - }, - changeSpoilerness () { - dispatch(changeComposeSpoilerness()); - }, - changeText (text) { - dispatch(changeCompose(text)); - }, - changeVisibility (value) { - dispatch(changeComposeVisibility(value)); - }, - clearSuggestions () { - dispatch(clearComposeSuggestions()); - }, - closeModal () { - dispatch(closeModal()); - }, - fetchSuggestions (token) { - dispatch(fetchComposeSuggestions(token)); - }, - insertEmoji (position, data) { - dispatch(insertEmojiCompose(position, data)); - }, - openActionsModal (data) { - dispatch(openModal('ACTIONS', data)); - }, - openDoodleModal () { - dispatch(openModal('DOODLE', { noEsc: true })); - }, - selectSuggestion (position, token, accountId) { - dispatch(selectComposeSuggestion(position, token, accountId)); - }, - submit () { - dispatch(submitCompose()); - }, - toggleAdvancedOption (option) { - dispatch(toggleComposeAdvancedOption(option)); - }, - undoUpload (mediaId) { - dispatch(undoUploadCompose(mediaId)); - }, - upload (files) { - dispatch(uploadCompose(files)); - }, -}); +const mapDispatchToProps = { + onCancelReply: cancelReplyCompose, + onChangeDescription: changeUploadCompose, + onChangeSensitivity: changeComposeSensitivity, + onChangeSpoilerText: changeComposeSpoilerText, + onChangeSpoilerness: changeComposeSpoilerness, + onChangeText: changeCompose, + onChangeVisibility: changeComposeVisibility, + onClearSuggestions: clearComposeSuggestions, + onCloseModal: closeModal, + onFetchSuggestions: fetchComposeSuggestions, + onInsertEmoji: insertEmojiCompose, + onOpenActionsModal: openModal.bind(null, 'ACTIONS'), + onOpenDoodleModal: openModal.bind(null, 'DOODLE', { noEsc: true }), + onSelectSuggestion: selectComposeSuggestion, + onSubmit: submitCompose, + onToggleAdvancedOption: toggleComposeAdvancedOption, + onUndoUpload: undoUploadCompose, + onUpload: uploadCompose, +}; // Handlers. const handlers = { // Changes the text value of the spoiler. - changeSpoiler ({ target: { value } }) { - const { dispatch: { changeSpoilerText } } = this.props; - if (changeSpoilerText) { - changeSpoilerText(value); + handleChangeSpoiler ({ target: { value } }) { + const { onChangeSpoilerText } = this.props; + if (onChangeSpoilerText) { + onChangeSpoilerText(value); } }, // Inserts an emoji at the caret. - emoji (data) { + handleEmoji (data) { const { textarea: { selectionStart } } = this; - const { dispatch: { insertEmoji } } = this.props; + const { onInsertEmoji } = this.props; this.caretPos = selectionStart + data.native.length + 1; - if (insertEmoji) { - insertEmoji(selectionStart, data); + if (onInsertEmoji) { + onInsertEmoji(selectionStart, data); } }, // Handles the secondary submit button. - secondarySubmit () { - const { submit } = this.handlers; + handleSecondarySubmit () { + const { handleSubmit } = this.handlers; const { - dispatch: { changeVisibility }, - side_arm, + onChangeVisibility, + sideArm, } = this.props; - if (changeVisibility) { - changeVisibility(side_arm); + if (sideArm !== 'none' && onChangeVisibility) { + onChangeVisibility(sideArm); } - submit(); + handleSubmit(); }, // Selects a suggestion from the autofill. - select (tokenStart, token, value) { - const { dispatch: { selectSuggestion } } = this.props; + handleSelect (tokenStart, token, value) { + const { onSelectSuggestion } = this.props; this.caretPos = null; - if (selectSuggestion) { - selectSuggestion(tokenStart, token, value); + if (onSelectSuggestion) { + onSelectSuggestion(tokenStart, token, value); } }, // Submits the status. - submit () { + handleSubmit () { const { textarea: { value } } = this; const { - dispatch: { - changeText, - submit, - }, - state: { text }, + onChangeText, + onSubmit, + text, } = this.props; // If something changes inside the textarea, then we update the // state before submitting. - if (changeText && text !== value) { - changeText(value); + if (onChangeText && text !== value) { + onChangeText(value); } // Submits the status. - if (submit) { - submit(); + if (onSubmit) { + onSubmit(); } }, // Sets a reference to the textarea. - refTextarea ({ textarea }) { - this.textarea = textarea; + handleRefTextarea (textareaComponent) { + if (textareaComponent) { + this.textarea = textareaComponent.textarea; + } }, }; @@ -216,10 +181,10 @@ class Composer extends React.Component { // If this is the update where we've finished uploading, // save the last caret position so we can restore it below! componentWillReceiveProps (nextProps) { - const { textarea: { selectionStart } } = this; - const { state: { isUploading } } = this.props; - if (isUploading && !nextProps.state.isUploading) { - this.caretPos = selectionStart; + const { textarea } = this; + const { isUploading } = this.props; + if (textarea && isUploading && !nextProps.isUploading) { + this.caretPos = textarea.selectionStart; } } @@ -239,20 +204,18 @@ class Composer extends React.Component { textarea, } = this; const { - state: { - focusDate, - isUploading, - isSubmitting, - preselectDate, - text, - }, + focusDate, + isUploading, + isSubmitting, + preselectDate, + text, } = this.props; let selectionEnd, selectionStart; // Caret/selection handling. - if (focusDate !== prevProps.state.focusDate || (prevProps.state.isUploading && !isUploading && !isNaN(caretPos) && caretPos !== null)) { + if (focusDate !== prevProps.focusDate || (prevProps.isUploading && !isUploading && !isNaN(caretPos) && caretPos !== null)) { switch (true) { - case preselectDate !== prevProps.state.preselectDate: + case preselectDate !== prevProps.preselectDate: selectionStart = text.search(/\s/) + 1; selectionEnd = text.length; break; @@ -262,71 +225,71 @@ class Composer extends React.Component { default: selectionStart = selectionEnd = text.length; } - textarea.setSelectionRange(selectionStart, selectionEnd); - textarea.focus(); + if (textarea) { + textarea.setSelectionRange(selectionStart, selectionEnd); + textarea.focus(); + } // Refocuses the textarea after submitting. - } else if (prevProps.state.isSubmitting && !isSubmitting) { + } else if (textarea && prevProps.isSubmitting && !isSubmitting) { textarea.focus(); } } render () { const { - changeSpoiler, - emoji, - secondarySubmit, - select, - submit, - refTextarea, + handleChangeSpoiler, + handleEmoji, + handleSecondarySubmit, + handleSelect, + handleSubmit, + handleRefTextarea, } = this.handlers; const { history } = this.context; const { - dispatch: { - cancelReply, - changeDescription, - changeSensitivity, - changeText, - changeVisibility, - clearSuggestions, - closeModal, - fetchSuggestions, - openActionsModal, - openDoodleModal, - toggleAdvancedOption, - undoUpload, - upload, - }, + acceptContentTypes, + amUnlocked, + doNotFederate, intl, - state: { - acceptContentTypes, - amUnlocked, - doNotFederate, - isSubmitting, - isUploading, - media, - privacy, - progress, - replyAccount, - replyContent, - resetFileKey, - sensitive, - showSearch, - sideArm, - spoiler, - spoilerText, - suggestions, - text, - }, + isSubmitting, + isUploading, + layout, + media, + onCancelReply, + onChangeDescription, + onChangeSensitivity, + onChangeSpoilerness, + onChangeText, + onChangeVisibility, + onClearSuggestions, + onCloseModal, + onFetchSuggestions, + onOpenActionsModal, + onOpenDoodleModal, + onToggleAdvancedOption, + onUndoUpload, + onUpload, + privacy, + progress, + replyAccount, + replyContent, + resetFileKey, + sensitive, + showSearch, + sideArm, + spoiler, + spoilerText, + suggestions, + text, } = this.props; return ( - <div className='compose'> + <div className='composer'> <ComposerSpoiler hidden={!spoiler} intl={intl} - onChange={changeSpoiler} - onSubmit={submit} + onChange={handleChangeSpoiler} + onSubmit={handleSubmit} text={spoilerText} /> {privacy === 'private' && amUnlocked ? <ComposerWarning /> : null} @@ -336,32 +299,32 @@ class Composer extends React.Component { content={replyContent} history={history} intl={intl} - onCancel={cancelReply} + onCancel={onCancelReply} /> ) : null} <ComposerTextarea - autoFocus={!showSearch && !isMobile(window.innerWidth)} + autoFocus={!showSearch && !isMobile(window.innerWidth, layout)} disabled={isSubmitting} intl={intl} - onChange={changeText} - onPaste={upload} - onPickEmoji={emoji} - onSubmit={submit} - onSuggestionsClearRequested={clearSuggestions} - onSuggestionsFetchRequested={fetchSuggestions} - onSuggestionSelected={select} - ref={refTextarea} + onChange={onChangeText} + onPaste={onUpload} + onPickEmoji={handleEmoji} + onSubmit={handleSubmit} + onSuggestionsClearRequested={onClearSuggestions} + onSuggestionsFetchRequested={onFetchSuggestions} + onSuggestionSelected={handleSelect} + ref={handleRefTextarea} suggestions={suggestions} value={text} /> - {media && media.size ? ( + {isUploading || media && media.size ? ( <ComposerUploadForm - active={isUploading} intl={intl} media={media} - onChangeDescription={changeDescription} - onRemove={undoUpload} + onChangeDescription={onChangeDescription} + onRemove={onUndoUpload} progress={progress} + uploading={isUploading} /> ) : null} <ComposerOptions @@ -373,13 +336,14 @@ class Composer extends React.Component { )} hasMedia={!!media.size} intl={intl} - onChangeSensitivity={changeSensitivity} - onChangeVisibility={changeVisibility} - onDoodleOpen={openDoodleModal} - onModalClose={closeModal} - onModalOpen={openActionsModal} - onToggleAdvancedOption={toggleAdvancedOption} - onUpload={upload} + onChangeSensitivity={onChangeSensitivity} + onChangeVisibility={onChangeVisibility} + onDoodleOpen={onOpenDoodleModal} + onModalClose={onCloseModal} + onModalOpen={onOpenActionsModal} + onToggleAdvancedOption={onToggleAdvancedOption} + onToggleSpoiler={onChangeSpoilerness} + onUpload={onUpload} privacy={privacy} resetFileKey={resetFileKey} sensitive={sensitive} @@ -387,10 +351,10 @@ class Composer extends React.Component { /> <ComposerPublisher countText={`${spoilerText}${countableText(text)}${doNotFederate ? ' 👁️' : ''}`} - disabled={isSubmitting || isUploading || text.length && text.trim().length === 0} + disabled={isSubmitting || isUploading || !!text.length && !text.trim().length} intl={intl} - onSecondarySubmit={secondarySubmit} - onSubmit={submit} + onSecondarySubmit={handleSecondarySubmit} + onSubmit={handleSubmit} privacy={privacy} sideArm={sideArm} /> @@ -407,37 +371,51 @@ Composer.contextTypes = { // Props. Composer.propTypes = { - dispatch: PropTypes.objectOf(PropTypes.func).isRequired, intl: PropTypes.object.isRequired, - state: PropTypes.shape({ - acceptContentTypes: PropTypes.string, - amUnlocked: PropTypes.bool, - doNotFederate: PropTypes.bool, - focusDate: PropTypes.instanceOf(Date), - isSubmitting: PropTypes.bool, - isUploading: PropTypes.bool, - media: PropTypes.list, - preselectDate: PropTypes.instanceOf(Date), - privacy: PropTypes.string, - progress: PropTypes.number, - replyAccount: ImmutablePropTypes.map, - replyContent: PropTypes.string, - resetFileKey: PropTypes.string, - sideArm: PropTypes.string, - sensitive: PropTypes.bool, - showSearch: PropTypes.bool, - spoiler: PropTypes.bool, - spoilerText: PropTypes.string, - suggestionToken: PropTypes.string, - suggestions: ImmutablePropTypes.list, - text: PropTypes.string, - }).isRequired, -}; -// Default props. -Composer.defaultProps = { - dispatch: {}, - state: {}, + // State props. + acceptContentTypes: PropTypes.string, + amUnlocked: PropTypes.bool, + doNotFederate: PropTypes.bool, + focusDate: PropTypes.instanceOf(Date), + isSubmitting: PropTypes.bool, + isUploading: PropTypes.bool, + layout: PropTypes.string, + media: ImmutablePropTypes.list, + preselectDate: PropTypes.instanceOf(Date), + privacy: PropTypes.string, + progress: PropTypes.number, + replyAccount: ImmutablePropTypes.map, + replyContent: PropTypes.string, + resetFileKey: PropTypes.number, + sideArm: PropTypes.string, + sensitive: PropTypes.bool, + showSearch: PropTypes.bool, + spoiler: PropTypes.bool, + spoilerText: PropTypes.string, + suggestionToken: PropTypes.string, + suggestions: ImmutablePropTypes.list, + text: PropTypes.string, + + // Dispatch props. + onCancelReply: PropTypes.func, + onChangeDescription: PropTypes.func, + onChangeSensitivity: PropTypes.func, + onChangeSpoilerText: PropTypes.func, + onChangeSpoilerness: PropTypes.func, + onChangeText: PropTypes.func, + onChangeVisibility: PropTypes.func, + onClearSuggestions: PropTypes.func, + onCloseModal: PropTypes.func, + onFetchSuggestions: PropTypes.func, + onInsertEmoji: PropTypes.func, + onOpenActionsModal: PropTypes.func, + onOpenDoodleModal: PropTypes.func, + onSelectSuggestion: PropTypes.func, + onSubmit: PropTypes.func, + onToggleAdvancedOption: PropTypes.func, + onUndoUpload: PropTypes.func, + onUpload: PropTypes.func, }; // Connecting and export. diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js new file mode 100644 index 000000000..28bdfc0db --- /dev/null +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js @@ -0,0 +1,138 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import spring from 'react-motion/lib/spring'; + +// Components. +import ComposerOptionsDropdownContentItem from './item'; + +// Utils. +import { withPassive } from 'flavours/glitch/util/dom_helpers'; +import Motion from 'flavours/glitch/util/optional_motion'; +import { assignHandlers } from 'flavours/glitch/util/react_helpers'; + +// Handlers. +const handlers = { + + // When the document is clicked elsewhere, we close the dropdown. + handleDocumentClick ({ target }) { + const { node } = this; + const { onClose } = this.props; + if (onClose && node && !node.contains(target)) { + onClose(); + } + }, + + // Stores our node in `this.node`. + handleRef (node) { + this.node = node; + }, +}; + +// The spring to use with our motion. +const springMotion = spring(1, { + damping: 35, + stiffness: 400, +}); + +// The component. +export default class ComposerOptionsDropdownContent extends React.PureComponent { + + // Constructor. + constructor (props) { + super(props); + assignHandlers(this, handlers); + + // Instance variables. + this.node = null; + } + + // On mounting, we add our listeners. + componentDidMount () { + const { handleDocumentClick } = this.handlers; + document.addEventListener('click', handleDocumentClick, false); + document.addEventListener('touchend', handleDocumentClick, withPassive); + } + + // On unmounting, we remove our listeners. + componentWillUnmount () { + const { handleDocumentClick } = this.handlers; + document.removeEventListener('click', handleDocumentClick, false); + document.removeEventListener('touchend', handleDocumentClick, withPassive); + } + + // Rendering. + render () { + const { handleRef } = this.handlers; + const { + items, + onChange, + onClose, + style, + value, + } = this.props; + + // The result. + return ( + <Motion + defaultStyle={{ + opacity: 0, + scaleX: 0.85, + scaleY: 0.75, + }} + style={{ + opacity: springMotion, + scaleX: springMotion, + scaleY: springMotion, + }} + > + {({ opacity, scaleX, scaleY }) => ( + <div + className='composer--options--dropdown--content' + ref={handleRef} + style={{ + ...style, + opacity: opacity, + transform: `scale(${scaleX}, ${scaleY})`, + }} + > + {items.map( + ({ + name, + ...rest + }) => ( + <ComposerOptionsDropdownContentItem + active={name === value} + key={name} + name={name} + onChange={onChange} + onClose={onClose} + options={rest} + /> + ) + )} + </div> + )} + </Motion> + ); + } + +} + +// Props. +ComposerOptionsDropdownContent.propTypes = { + items: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.string, + meta: PropTypes.node, + name: PropTypes.string.isRequired, + on: PropTypes.bool, + text: PropTypes.node, + })).isRequired, + onChange: PropTypes.func, + onClose: PropTypes.func, + style: PropTypes.object, + value: PropTypes.string, +}; + +// Default props. +ComposerOptionsDropdownContent.defaultProps = { style: {} }; diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js index e9047dc50..605c945bd 100644 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/item/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js @@ -14,7 +14,7 @@ import { assignHandlers } from 'flavours/glitch/util/react_helpers'; const handlers = { // This function activates the dropdown item. - activate (e) { + handleActivate (e) { const { name, onChange, @@ -35,11 +35,10 @@ const handlers = { onChange(name); } }, - }; // The component. -export default class ComposerOptionsDropdownItem extends React.PureComponent { +export default class ComposerOptionsDropdownContentItem extends React.PureComponent { // Constructor. constructor (props) { @@ -49,7 +48,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { // Rendering. render () { - const { activate } = this.handlers; + const { handleActivate } = this.handlers; const { active, options: { @@ -59,7 +58,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { text, }, } = this.props; - const computedClass = classNames('composer--options--dropdown_item', { + const computedClass = classNames('composer--options--dropdown--content--item', { active, lengthy: meta, 'toggled-off': !on && on !== null && typeof on !== 'undefined', @@ -71,8 +70,8 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { return ( <div className={computedClass} - onClick={activate} - onKeyDown={activate} + onClick={handleActivate} + onKeyDown={handleActivate} role='button' tabIndex='0' > @@ -85,7 +84,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { return ( <Toggle checked={on} - onChange={activate} + onChange={handleActivate} /> ); case !!icon: @@ -113,7 +112,7 @@ export default class ComposerOptionsDropdownItem extends React.PureComponent { }; // Props. -ComposerOptionsDropdownItem.propTypes = { +ComposerOptionsDropdownContentItem.propTypes = { active: PropTypes.bool, name: PropTypes.string, onChange: PropTypes.func, diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js index daed4ec8a..d63d90a9f 100644 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js @@ -2,108 +2,120 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import spring from 'react-motion/lib/spring'; import Overlay from 'react-overlays/lib/Overlay'; // Components. import IconButton from 'flavours/glitch/components/icon_button'; -import ComposerOptionsDropdownItem from './item'; +import ComposerOptionsDropdownContent from './content'; // Utils. -import { withPassive } from 'flavours/glitch/util/dom_helpers'; import { isUserTouching } from 'flavours/glitch/util/is_mobile'; -import Motion from 'flavours/glitch/util/optional_motion'; import { assignHandlers } from 'flavours/glitch/util/react_helpers'; -// We'll use this to define our various transitions. -const springMotion = spring(1, { - damping: 35, - stiffness: 400, -}); - // Handlers. const handlers = { // Closes the dropdown. - close () { + handleClose () { this.setState({ open: false }); }, - // When the document is clicked elsewhere, we close the dropdown. - documentClick ({ target }) { - const { node } = this; - const { onClose } = this.props; - if (onClose && node && !node.contains(target)) { - onClose(); - } - }, - // The enter key toggles the dropdown's open state, and the escape // key closes it. - keyDown ({ key }) { + handleKeyDown ({ key }) { const { - close, - toggle, + handleClose, + handleToggle, } = this.handlers; switch (key) { case 'Enter': - toggle(); + handleToggle(); break; case 'Escape': - close(); + handleClose(); break; } }, - // Toggles opening and closing the dropdown. - toggle () { + // Creates an action modal object. + handleMakeModal () { + const component = this; const { items, onChange, - onModalClose, onModalOpen, + onModalClose, value, } = this.props; + + // Required props. + if (!(onChange && onModalOpen && onModalClose && items)) { + return null; + } + + // The object. + return { + actions: items.map( + ({ + name, + ...rest + }) => ({ + ...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 }); + }, + }) + ), + }; + }, + + // Toggles opening and closing the dropdown. + handleToggle () { + const { handleMakeModal } = this.handlers; + const { onModalOpen } = this.props; const { open } = this.state; // If this is a touch device, we open a modal instead of the // dropdown. - if (onModalClose && isUserTouching()) { - if (open) { - onModalClose(); - } else if (onChange && onModalOpen) { - onModalOpen({ - actions: items.map( - ({ - name, - ...rest - }) => ({ - ...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); - }, - }) - ), - }); + if (isUserTouching()) { + + // This gets the modal to open. + const modal = handleMakeModal(); + + // If we can, we then open the modal. + if (modal && onModalOpen) { + onModalOpen(modal); + return; } + } // Otherwise, we just set our state to open. - } else { - this.setState({ open: !open }); - } + this.setState({ open: !open }); }, - // Stores our node in `this.node`. - ref (node) { - this.node = node; + // If our modal is open and our props update, we need to also update + // the modal. + handleUpdate () { + const { handleMakeModal } = this.handlers; + const { onModalOpen } = this.props; + const { needsModalUpdate } = this.state; + + // Gets our modal object. + const modal = handleMakeModal(); + + // Reopens the modal with the new object. + if (needsModalUpdate && modal && onModalOpen) { + onModalOpen(modal); + } }, }; @@ -114,33 +126,31 @@ export default class ComposerOptionsDropdown extends React.PureComponent { constructor (props) { super(props); assignHandlers(this, handlers); - this.state = { open: false }; - - // Instance variables. - this.node = null; + this.state = { + needsModalUpdate: false, + open: false, + }; } - // On mounting, we add our listeners. - componentDidMount () { - const { documentClick } = this.handlers; - document.addEventListener('click', documentClick, false); - document.addEventListener('touchend', documentClick, withPassive); - } - - // On unmounting, we remove our listeners. - componentWillUnmount () { - const { documentClick } = this.handlers; - document.removeEventListener('click', documentClick, false); - document.removeEventListener('touchend', documentClick, withPassive); + // Updates our modal as necessary. + componentDidUpdate (prevProps) { + const { handleUpdate } = this.handlers; + const { items } = this.props; + const { needsModalUpdate } = this.state; + if (needsModalUpdate && items.find( + (item, i) => item.on !== prevProps.items[i].on + )) { + handleUpdate(); + this.setState({ needsModalUpdate: false }); + } } // Rendering. render () { const { - close, - keyDown, - ref, - toggle, + handleClose, + handleKeyDown, + handleToggle, } = this.handlers; const { active, @@ -154,22 +164,21 @@ export default class ComposerOptionsDropdown extends React.PureComponent { const { open } = this.state; const computedClass = classNames('composer--options--dropdown', { active, - open: open || active, + open, }); // The result. return ( <div className={computedClass} - onKeyDown={keyDown} - ref={ref} + onKeyDown={handleKeyDown} > <IconButton active={open || active} className='value' disabled={disabled} icon={icon} - onClick={toggle} + onClick={handleToggle} size={18} style={{ height: null, @@ -178,49 +187,17 @@ export default class ComposerOptionsDropdown extends React.PureComponent { title={title} /> <Overlay + containerPadding={20} placement='bottom' show={open} target={this} > - <Motion - defaultStyle={{ - opacity: 0, - scaleX: 0.85, - scaleY: 0.75, - }} - style={{ - opacity: springMotion, - scaleX: springMotion, - scaleY: springMotion, - }} - > - {({ opacity, scaleX, scaleY }) => ( - <div - className='composer--options--dropdown__dropdown' - ref={this.setRef} - style={{ - opacity: opacity, - transform: `scale(${scaleX}, ${scaleY})`, - }} - > - {items.map( - ({ - name, - ...rest - }) => ( - <ComposerOptionsDropdownItem - active={name === value} - key={name} - name={name} - onChange={onChange} - onClose={close} - options={rest} - /> - ) - )} - </div> - )} - </Motion> + <ComposerOptionsDropdownContent + items={items} + onChange={onChange} + onClose={handleClose} + value={value} + /> </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js index ea998a421..e805372ab 100644 --- a/app/javascript/flavours/glitch/features/composer/options/index.js +++ b/app/javascript/flavours/glitch/features/composer/options/index.js @@ -95,7 +95,7 @@ const messages = defineMessages({ const handlers = { // Handles file selection. - changeFiles ({ target: { files } }) { + handleChangeFiles ({ target: { files } }) { const { onUpload } = this.props; if (files.length && onUpload) { onUpload(files); @@ -103,7 +103,7 @@ const handlers = { }, // Handles attachment clicks. - clickAttach (name) { + handleClickAttach (name) { const { fileElement } = this; const { onDoodleOpen } = this.props; @@ -123,7 +123,7 @@ const handlers = { }, // Handles a ref to the file input. - refFileElement (fileElement) { + handleRefFileElement (fileElement) { this.fileElement = fileElement; }, }; @@ -143,9 +143,9 @@ export default class ComposerOptions extends React.PureComponent { // Rendering. render () { const { - changeFiles, - clickAttach, - refFileElement, + handleChangeFiles, + handleClickAttach, + handleRefFileElement, } = this.handlers; const { acceptContentTypes, @@ -159,6 +159,7 @@ export default class ComposerOptions extends React.PureComponent { onModalClose, onModalOpen, onToggleAdvancedOption, + onToggleSpoiler, privacy, resetFileKey, sensitive, @@ -201,8 +202,8 @@ export default class ComposerOptions extends React.PureComponent { accept={acceptContentTypes} disabled={disabled || full} key={resetFileKey} - onChange={changeFiles} - ref={refFileElement} + onChange={handleChangeFiles} + ref={handleRefFileElement} type='file' {...hiddenComponent} /> @@ -221,10 +222,10 @@ export default class ComposerOptions extends React.PureComponent { text: <FormattedMessage {...messages.doodle} />, }, ]} - onChange={clickAttach} + onChange={handleClickAttach} onModalClose={onModalClose} onModalOpen={onModalOpen} - title={messages.attach} + title={intl.formatMessage(messages.attach)} /> <Motion defaultStyle={{ scale: 0.87 }} @@ -279,6 +280,7 @@ export default class ComposerOptions extends React.PureComponent { active={spoiler} ariaControls='glitch.composer.spoiler.input' label='CW' + onClick={onToggleSpoiler} title={intl.formatMessage(messages.spoiler)} /> <Dropdown @@ -318,9 +320,10 @@ ComposerOptions.propTypes = { onModalClose: PropTypes.func, onModalOpen: PropTypes.func, onToggleAdvancedOption: PropTypes.func, + onToggleSpoiler: PropTypes.func, onUpload: PropTypes.func, privacy: PropTypes.string, - resetFileKey: PropTypes.string, + resetFileKey: PropTypes.number, sensitive: PropTypes.bool, spoiler: PropTypes.bool, }; diff --git a/app/javascript/flavours/glitch/features/composer/publisher/index.js b/app/javascript/flavours/glitch/features/composer/publisher/index.js index 79337100f..f54fd68b7 100644 --- a/app/javascript/flavours/glitch/features/composer/publisher/index.js +++ b/app/javascript/flavours/glitch/features/composer/publisher/index.js @@ -46,10 +46,13 @@ export default function ComposerPublisher ({ // The result. return ( <div className={computedClass}> - <span class='count'>{diff}</span> + <span className='count'>{diff}</span> {sideArm && sideArm !== 'none' ? ( <Button className='side_arm' + disabled={disabled || diff < 0} + onClick={onSecondarySubmit} + style={{ padding: null }} text={ <span> <Icon @@ -63,8 +66,6 @@ export default function ComposerPublisher ({ </span> } title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`} - onClick={onSecondarySubmit} - disabled={disabled || diff < 0} /> ) : null} <Button diff --git a/app/javascript/flavours/glitch/features/composer/reply/index.js b/app/javascript/flavours/glitch/features/composer/reply/index.js index 4a52cddb4..568705aff 100644 --- a/app/javascript/flavours/glitch/features/composer/reply/index.js +++ b/app/javascript/flavours/glitch/features/composer/reply/index.js @@ -25,7 +25,7 @@ const messages = defineMessages({ const handlers = { // Handles a click on the "close" button. - click () { + handleClick () { const { onCancel } = this.props; if (onCancel) { onCancel(); @@ -33,7 +33,7 @@ const handlers = { }, // Handles a click on the status's account. - clickAccount () { + handleClickAccount () { const { account, history, @@ -56,8 +56,8 @@ export default class ComposerReply extends React.PureComponent { // Rendering. render () { const { - click, - clickAccount, + handleClick, + handleClickAccount, } = this.handlers; const { account, @@ -72,14 +72,14 @@ export default class ComposerReply extends React.PureComponent { <IconButton className='cancel' icon='times' - onClick={click} + onClick={handleClick} title={intl.formatMessage(messages.cancel)} /> {account ? ( <a className='account' href={account.get('url')} - onClick={clickAccount} + onClick={handleClickAccount} > <Avatar account={account} diff --git a/app/javascript/flavours/glitch/features/composer/spoiler/index.js b/app/javascript/flavours/glitch/features/composer/spoiler/index.js index 730ab2205..a49b0e10f 100644 --- a/app/javascript/flavours/glitch/features/composer/spoiler/index.js +++ b/app/javascript/flavours/glitch/features/composer/spoiler/index.js @@ -24,7 +24,7 @@ const messages = defineMessages({ const handlers = { // Handles a keypress. - keyDown ({ + handleKeyDown ({ ctrlKey, keyCode, metaKey, @@ -49,7 +49,7 @@ export default class ComposerSpoiler extends React.PureComponent { // Rendering. render () { - const { keyDown } = this.handlers; + const { handleKeyDown } = this.handlers; const { hidden, intl, @@ -70,7 +70,7 @@ export default class ComposerSpoiler extends React.PureComponent { <input id='glitch.composer.spoiler.input' onChange={onChange} - onKeyDown={keyDown} + onKeyDown={handleKeyDown} placeholder={intl.formatMessage(messages.placeholder)} type='text' value={text} diff --git a/app/javascript/flavours/glitch/features/composer/textarea/index.js b/app/javascript/flavours/glitch/features/composer/textarea/index.js index 2299757df..1b6f79bba 100644 --- a/app/javascript/flavours/glitch/features/composer/textarea/index.js +++ b/app/javascript/flavours/glitch/features/composer/textarea/index.js @@ -31,14 +31,14 @@ const messages = defineMessages({ const handlers = { // When blurring the textarea, suggestions are hidden. - blur () { + handleBlur () { this.setState({ suggestionsHidden: true }); }, // When the contents of the textarea change, we have to pull up new // autosuggest suggestions if applicable, and also change the value // of the textarea in our store. - change ({ + handleChange ({ target: { selectionStart, value, @@ -91,7 +91,7 @@ const handlers = { }, // Handles a click on an autosuggestion. - clickSuggestion (index) { + handleClickSuggestion (index) { const { textarea } = this; const { onSuggestionSelected, @@ -107,7 +107,7 @@ const handlers = { // Handles a keypress. If the autosuggestions are visible, we need // to allow keypresses to navigate and sleect them. - keyDown (e) { + handleKeyDown (e) { const { disabled, onSubmit, @@ -165,7 +165,7 @@ const handlers = { // When the escape key is released, we either close the suggestions // window or focus the UI. - keyUp ({ key }) { + handleKeyUp ({ key }) { const { suggestionsHidden } = this.state; if (key === 'Escape') { if (!suggestionsHidden) { @@ -177,7 +177,7 @@ const handlers = { }, // Handles the pasting of images into the composer. - paste (e) { + handlePaste (e) { const { onPaste } = this.props; let d; if (onPaste && (d = e.clipboardData) && (d = d.files).length === 1) { @@ -187,7 +187,7 @@ const handlers = { }, // Saves a reference to the textarea. - refTextarea (textarea) { + handleRefTextarea (textarea) { this.textarea = textarea; }, }; @@ -223,13 +223,13 @@ export default class ComposerTextarea extends React.Component { // Rendering. render () { const { - blur, - change, - clickSuggestion, - keyDown, - keyUp, - paste, - refTextarea, + handleBlur, + handleChange, + handleClickSuggestion, + handleKeyDown, + handleKeyUp, + handlePaste, + handleRefTextarea, } = this.handlers; const { autoFocus, @@ -254,12 +254,12 @@ export default class ComposerTextarea extends React.Component { autoFocus={autoFocus} className='textarea' disabled={disabled} - inputRef={refTextarea} - onBlur={blur} - onChange={change} - onKeyDown={keyDown} - onKeyUp={keyUp} - onPaste={paste} + inputRef={handleRefTextarea} + onBlur={handleBlur} + onChange={handleChange} + onKeyDown={handleKeyDown} + onKeyUp={handleKeyUp} + onPaste={handlePaste} placeholder={intl.formatMessage(messages.placeholder)} value={value} style={{ direction: isRtl(value) ? 'rtl' : 'ltr' }} @@ -268,7 +268,7 @@ export default class ComposerTextarea extends React.Component { <EmojiPicker onPickEmoji={onPickEmoji} /> <ComposerTextareaSuggestions hidden={suggestionsHidden} - onSuggestionClick={clickSuggestion} + onSuggestionClick={handleClickSuggestion} suggestions={suggestions} value={selectedSuggestion} /> diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js index 65ed2c18a..dc72585f2 100644 --- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js +++ b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js @@ -18,9 +18,9 @@ export default function ComposerTextareaSuggestions ({ return ( <div className='composer--textarea--suggestions' - hidden={hidden || suggestions.isEmpty()} + hidden={hidden || !suggestions || suggestions.isEmpty()} > - {!hidden ? suggestions.map( + {!hidden && suggestions ? suggestions.map( (suggestion, index) => ( <ComposerTextareaSuggestionsItem index={index} @@ -39,5 +39,5 @@ ComposerTextareaSuggestions.propTypes = { hidden: PropTypes.bool, onSuggestionClick: PropTypes.func, suggestions: ImmutablePropTypes.list, - value: PropTypes.string, + value: PropTypes.number, }; diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js index b78f99ee9..dc057e679 100644 --- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js +++ b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js @@ -17,7 +17,7 @@ const assetHost = ((process || {}).env || {}).CDN_HOST || ''; const handlers = { // Handles a click on a suggestion. - click (e) { + handleClick (e) { const { index, onClick, @@ -40,7 +40,7 @@ export default class ComposerTextareaSuggestionsItem extends React.Component { // Rendering. render () { - const { click } = this.handlers; + const { handleClick } = this.handlers; const { selected, suggestion, @@ -51,7 +51,7 @@ export default class ComposerTextareaSuggestionsItem extends React.Component { return ( <div className={computedClass} - onMouseDown={click} + onMouseDown={handleClick} role='button' tabIndex='0' > diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/index.js index ab46a3046..53b14acc7 100644 --- a/app/javascript/flavours/glitch/features/composer/upload_form/index.js +++ b/app/javascript/flavours/glitch/features/composer/upload_form/index.js @@ -10,45 +10,44 @@ import ComposerUploadFormProgress from './progress'; // The component. export default function ComposerUploadForm ({ - active, intl, media, onChangeDescription, onRemove, progress, + uploading, }) { - const computedClass = classNames('composer--upload_form', { uploading: active }); - - // We need `media` in order to be able to render. - if (!media) { - return null; - } + const computedClass = classNames('composer--upload_form', { uploading }); // The result. return ( <div className={computedClass}> - {active ? <ComposerUploadFormProgress progress={progress} /> : null} - {media.map(item => ( - <ComposerUploadFormItem - description={item.get('description')} - key={item.get('id')} - id={item.get('id')} - intl={intl} - preview={item.get('preview_url')} - onChangeDescription={onChangeDescription} - onRemove={onRemove} - /> - ))} + {uploading ? <ComposerUploadFormProgress progress={progress} /> : null} + {media ? ( + <div className='content'> + {media.map(item => ( + <ComposerUploadFormItem + description={item.get('description')} + key={item.get('id')} + id={item.get('id')} + intl={intl} + preview={item.get('preview_url')} + onChangeDescription={onChangeDescription} + onRemove={onRemove} + /> + ))} + </div> + ) : null} </div> ); } // Props. ComposerUploadForm.propTypes = { - active: PropTypes.bool, intl: PropTypes.object.isRequired, media: ImmutablePropTypes.list, onChangeDescription: PropTypes.func, onRemove: PropTypes.func, progress: PropTypes.number, + uploading: PropTypes.bool, }; diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js index cbec5ecd9..ec67b8ef8 100644 --- a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js +++ b/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js @@ -31,7 +31,7 @@ const messages = defineMessages({ const handlers = { // On blur, we save the description for the media item. - blur () { + handleBlur () { const { id, onChangeDescription, @@ -48,27 +48,27 @@ const handlers = { // When the value of our description changes, we store it in the // temp value `dirtyDescription` in our state. - change ({ target: { value } }) { + handleChange ({ target: { value } }) { this.setState({ dirtyDescription: value }); }, // Records focus on the media item. - focus () { + handleFocus () { this.setState({ focused: true }); }, // Records the start of a hover over the media item. - mouseEnter () { + handleMouseEnter () { this.setState({ hovered: true }); }, // Records the end of a hover over the media item. - mouseLeave () { + handleMouseLeave () { this.setState({ hovered: false }); }, // Removes the media item. - remove () { + handleRemove () { const { id, onRemove, @@ -85,7 +85,7 @@ export default class ComposerUploadFormItem extends React.PureComponent { // Constructor. constructor (props) { super(props); - assignHandlers(handlers); + assignHandlers(this, handlers); this.state = { hovered: false, focused: false, @@ -96,12 +96,12 @@ export default class ComposerUploadFormItem extends React.PureComponent { // Rendering. render () { const { - blur, - change, - focus, - mouseEnter, - mouseLeave, - remove, + handleBlur, + handleChange, + handleFocus, + handleMouseEnter, + handleMouseLeave, + handleRemove, } = this.handlers; const { description, @@ -119,8 +119,8 @@ export default class ComposerUploadFormItem extends React.PureComponent { return ( <div className={computedClass} - onMouseEnter={mouseEnter} - onMouseLeave={mouseLeave} + onMouseEnter={handleMouseEnter} + onMouseLeave={handleMouseLeave} > <Motion defaultStyle={{ scale: 0.8 }} @@ -141,7 +141,7 @@ export default class ComposerUploadFormItem extends React.PureComponent { <IconButton className='close' icon='times' - onClick={remove} + onClick={handleRemove} size={36} title={intl.formatMessage(messages.undo)} /> @@ -149,9 +149,9 @@ export default class ComposerUploadFormItem extends React.PureComponent { <span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span> <input maxLength={420} - onBlur={blur} - onChange={change} - onFocus={focus} + onBlur={handleBlur} + onChange={handleChange} + onFocus={handleFocus} placeholder={intl.formatMessage(messages.description)} type='text' value={dirtyDescription || description || ''} @@ -169,7 +169,7 @@ export default class ComposerUploadFormItem extends React.PureComponent { // Props. ComposerUploadFormItem.propTypes = { description: PropTypes.string, - id: PropTypes.number, + id: PropTypes.string, intl: PropTypes.object.isRequired, onChangeDescription: PropTypes.func, onRemove: PropTypes.func, |