diff options
Diffstat (limited to 'app/javascript')
32 files changed, 868 insertions, 790 deletions
diff --git a/app/javascript/flavours/glitch/components/dropdown_menu.js b/app/javascript/flavours/glitch/components/dropdown_menu.js index 519941dd6..706390c92 100644 --- a/app/javascript/flavours/glitch/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/components/dropdown_menu.js @@ -134,11 +134,12 @@ export default class Dropdown extends React.PureComponent { this.props.onModalOpen({ status, actions: items.map( - (item, i) => ({ + (item, i) => item ? { ...item, name: `${item.text}-${i}`, onClick: this.handleItemClick.bind(i), - }), + } : null + ), }); return; diff --git a/app/javascript/flavours/glitch/components/link.js b/app/javascript/flavours/glitch/components/link.js index c49fc487c..de99f7d42 100644 --- a/app/javascript/flavours/glitch/components/link.js +++ b/app/javascript/flavours/glitch/components/link.js @@ -45,7 +45,7 @@ export default class Link extends React.PureComponent { title, ...rest } = this.props; - const computedClass = classNames('link', className, role); + const computedClass = classNames('link', className, `role-${role}`); // We assume that our `onClick` is a routing function and give it // the qualities of a link even if no `href` is provided. However, 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, diff --git a/app/javascript/flavours/glitch/features/drawer/account/index.js b/app/javascript/flavours/glitch/features/drawer/account/index.js index 9afe8ba3e..168d0c2cf 100644 --- a/app/javascript/flavours/glitch/features/drawer/account/index.js +++ b/app/javascript/flavours/glitch/features/drawer/account/index.js @@ -27,7 +27,7 @@ export default function DrawerAccount ({ account }) { // We need an account to render. if (!account) { return ( - <div className='drawer--pager--account'> + <div className='drawer--account'> <a className='edit' href='/settings/profile' @@ -40,7 +40,7 @@ export default function DrawerAccount ({ account }) { // The result. return ( - <div className='drawer--pager--account'> + <div className='drawer--account'> <Permalink className='avatar' href={account.get('url')} @@ -67,4 +67,5 @@ export default function DrawerAccount ({ account }) { ); } +// Props. DrawerAccount.propTypes = { account: ImmutablePropTypes.map }; diff --git a/app/javascript/flavours/glitch/features/drawer/header/index.js b/app/javascript/flavours/glitch/features/drawer/header/index.js index fd79b6e18..6949cd028 100644 --- a/app/javascript/flavours/glitch/features/drawer/header/index.js +++ b/app/javascript/flavours/glitch/features/drawer/header/index.js @@ -51,7 +51,7 @@ export default function DrawerHeader ({ }) { // Only renders the component if the column isn't being shown. - const renderForColumn = conditionalRender.bind( + const renderForColumn = conditionalRender.bind(null, columnId => !columns || !columns.some( column => column.get('id') === columnId ) @@ -110,6 +110,7 @@ export default function DrawerHeader ({ ); } +// Props. DrawerHeader.propTypes = { columns: ImmutablePropTypes.list, intl: PropTypes.object, diff --git a/app/javascript/flavours/glitch/features/drawer/index.js b/app/javascript/flavours/glitch/features/drawer/index.js index d184dfd9b..9ade1f87a 100644 --- a/app/javascript/flavours/glitch/features/drawer/index.js +++ b/app/javascript/flavours/glitch/features/drawer/index.js @@ -34,23 +34,13 @@ const mapStateToProps = state => ({ }); // Dispatch mapping. -const mapDispatchToProps = dispatch => ({ - change (value) { - dispatch(changeSearch(value)); - }, - clear () { - dispatch(clearSearch()); - }, - show () { - dispatch(showSearch()); - }, - submit () { - dispatch(submitSearch()); - }, - openSettings () { - dispatch(openModal('SETTINGS', {})); - }, -}); +const mapDispatchToProps = { + onChange: changeSearch, + onClear: clearSearch, + onShow: showSearch, + onSubmit: submitSearch, + onOpenSettings: openModal.bind(null, 'SETTINGS', {}), +}; // The component. class Drawer extends React.Component { @@ -63,23 +53,19 @@ class Drawer extends React.Component { // Rendering. render () { const { - dispatch: { - change, - clear, - openSettings, - show, - submit, - }, + account, + columns, intl, multiColumn, - state: { - account, - columns, - results, - searchHidden, - searchValue, - submitted, - }, + onChange, + onClear, + onOpenSettings, + onShow, + onSubmit, + results, + searchHidden, + searchValue, + submitted, } = this.props; // The result. @@ -89,15 +75,15 @@ class Drawer extends React.Component { <DrawerHeader columns={columns} intl={intl} - onSettingsClick={openSettings} + onSettingsClick={onOpenSettings} /> ) : null} <DrawerSearch intl={intl} - onChange={change} - onClear={clear} - onShow={show} - onSubmit={submit} + onChange={onChange} + onClear={onClear} + onShow={onShow} + onSubmit={onSubmit} submitted={submitted} value={searchValue} /> @@ -117,23 +103,23 @@ class Drawer extends React.Component { // Props. Drawer.propTypes = { - dispatch: PropTypes.func.isRequired, intl: PropTypes.object.isRequired, multiColumn: PropTypes.bool, - state: PropTypes.shape({ - account: ImmutablePropTypes.map, - columns: ImmutablePropTypes.list, - results: ImmutablePropTypes.map, - searchHidden: PropTypes.bool, - searchValue: PropTypes.string, - submitted: PropTypes.bool, - }).isRequired, -}; -// Default props. -Drawer.defaultProps = { - dispatch: {}, - state: {}, + // State props. + account: ImmutablePropTypes.map, + columns: ImmutablePropTypes.list, + results: ImmutablePropTypes.map, + searchHidden: PropTypes.bool, + searchValue: PropTypes.string, + submitted: PropTypes.bool, + + // Dispatch props. + onChange: PropTypes.func, + onClear: PropTypes.func, + onShow: PropTypes.func, + onSubmit: PropTypes.func, + onOpenSettings: PropTypes.func, }; // Connecting and export. diff --git a/app/javascript/flavours/glitch/features/drawer/results/index.js b/app/javascript/flavours/glitch/features/drawer/results/index.js index 559d56da5..f2a79eb59 100644 --- a/app/javascript/flavours/glitch/features/drawer/results/index.js +++ b/app/javascript/flavours/glitch/features/drawer/results/index.js @@ -25,7 +25,7 @@ const messages = defineMessages({ }); // The component. -export default function DrawerPager ({ +export default function DrawerResults ({ results, visible, }) { @@ -33,6 +33,7 @@ export default function DrawerPager ({ const statuses = results ? results.get('statuses') : null; const hashtags = results ? results.get('hashtags') : null; + // This gets the total number of items. const count = [accounts, statuses, hashtags].reduce(function (size, item) { if (item && item.size) { return size + item.size; @@ -108,7 +109,8 @@ export default function DrawerPager ({ ); } -DrawerPager.propTypes = { +// Props. +DrawerResults.propTypes = { results: ImmutablePropTypes.map, visible: PropTypes.bool, }; diff --git a/app/javascript/flavours/glitch/features/drawer/search/index.js b/app/javascript/flavours/glitch/features/drawer/search/index.js index ed69f71ed..2d739349c 100644 --- a/app/javascript/flavours/glitch/features/drawer/search/index.js +++ b/app/javascript/flavours/glitch/features/drawer/search/index.js @@ -30,18 +30,18 @@ const messages = defineMessages({ // Handlers. const handlers = { - blur () { + handleBlur () { this.setState({ expanded: false }); }, - change ({ target: { value } }) { + handleChange ({ target: { value } }) { const { onChange } = this.props; if (onChange) { onChange(value); } }, - clear (e) { + handleClear (e) { const { onClear, submitted, @@ -53,7 +53,7 @@ const handlers = { } }, - focus () { + handleFocus () { const { onShow } = this.props; this.setState({ expanded: true }); if (onShow) { @@ -61,7 +61,7 @@ const handlers = { } }, - keyUp (e) { + handleKeyUp (e) { const { onSubmit } = this.props; switch (e.key) { case 'Enter': @@ -78,19 +78,21 @@ const handlers = { // The component. export default class DrawerSearch extends React.PureComponent { + // Constructor. constructor (props) { super(props); assignHandlers(this, handlers); this.state = { expanded: false }; } + // Rendering. render () { const { - blur, - change, - clear, - focus, - keyUp, + handleBlur, + handleChange, + handleClear, + handleFocus, + handleKeyUp, } = this.handlers; const { intl, @@ -110,23 +112,22 @@ export default class DrawerSearch extends React.PureComponent { type='text' placeholder={intl.formatMessage(messages.placeholder)} value={value || ''} - onChange={change} - onKeyUp={keyUp} - onFocus={focus} - onBlur={blur} + onChange={handleChange} + onKeyUp={handleKeyUp} + onFocus={handleFocus} + onBlur={handleBlur} /> </label> <div aria-label={intl.formatMessage(messages.placeholder)} className='icon' - onClick={clear} + onClick={handleClear} role='button' tabIndex='0' > <Icon icon='search' /> <Icon icon='fa-times-circle' /> </div> - <Overlay placement='bottom' show={expanded && !(value || '').length && !submitted} @@ -138,6 +139,7 @@ export default class DrawerSearch extends React.PureComponent { } +// Props. DrawerSearch.propTypes = { value: PropTypes.string, submitted: PropTypes.bool, diff --git a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js index bd36275f5..b5ea86ff1 100644 --- a/app/javascript/flavours/glitch/features/drawer/search/popout/index.js +++ b/app/javascript/flavours/glitch/features/drawer/search/popout/index.js @@ -34,9 +34,13 @@ const messages = defineMessages({ }, }); +// The spring used by our motion. const motionSpring = spring(1, { damping: 35, stiffness: 400 }); +// The component. export default function DrawerSearchPopout ({ style }) { + + // The result. return ( <Motion defaultStyle={{ diff --git a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js index 020cc0dd6..c8b040f95 100644 --- a/app/javascript/flavours/glitch/features/ui/components/actions_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/actions_modal.js @@ -50,7 +50,7 @@ export default class ActionsModal extends ImmutablePureComponent { <Link className={classNames('link', { active })} href={href} - onClick={onClick} + onClick={on !== null && typeof on !== 'undefined' && onPassiveClick || onClick} role={onClick ? 'button' : null} > {function () { diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index 91d4df93f..e4556899d 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -11,13 +11,13 @@ import BundleContainer from '../containers/bundle_container'; import ColumnLoading from './column_loading'; import DrawerLoading from './drawer_loading'; import BundleColumnError from './bundle_column_error'; -import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from 'flavours/glitch/util/async-components'; +import { Drawer, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, ListTimeline } from 'flavours/glitch/util/async-components'; import detectPassiveEvents from 'detect-passive-events'; import { scrollRight } from 'flavours/glitch/util/scroll'; const componentMap = { - 'COMPOSE': Compose, + 'COMPOSE': Drawer, 'HOME': HomeTimeline, 'NOTIFICATIONS': Notifications, 'PUBLIC': PublicTimeline, diff --git a/app/javascript/flavours/glitch/features/ui/index.js b/app/javascript/flavours/glitch/features/ui/index.js index 5c80ea07b..fae705deb 100644 --- a/app/javascript/flavours/glitch/features/ui/index.js +++ b/app/javascript/flavours/glitch/features/ui/index.js @@ -17,7 +17,7 @@ import UploadArea from './components/upload_area'; import ColumnsAreaContainer from './containers/columns_area_container'; import classNames from 'classnames'; import { - Compose, + Drawer, Status, GettingStarted, KeyboardShortcuts, @@ -56,7 +56,6 @@ const messages = defineMessages({ }); const mapStateToProps = state => ({ - isComposing: state.getIn(['compose', 'is_composing']), hasComposingText: state.getIn(['compose', 'text']) !== '', layout: state.getIn(['local_settings', 'layout']), isWide: state.getIn(['local_settings', 'stretch']), @@ -120,9 +119,9 @@ export default class UI extends React.Component { }; handleBeforeUnload = (e) => { - const { intl, isComposing, hasComposingText } = this.props; + const { intl, hasComposingText } = this.props; - if (isComposing && hasComposingText) { + if (hasComposingText) { // Setting returnValue to any string causes confirmation dialog. // Many browsers no longer display this text to users, // but we set user-friendly message for other browsers, e.g. Edge. @@ -227,9 +226,8 @@ export default class UI extends React.Component { } shouldComponentUpdate (nextProps) { - if (nextProps.isComposing !== this.props.isComposing) { + if (nextProps.navbarUnder !== this.props.navbarUnder) { // Avoid expensive update just to toggle a class - this.node.classList.toggle('is-composing', nextProps.isComposing); this.node.classList.toggle('navbar-under', nextProps.navbarUnder); return false; @@ -427,7 +425,7 @@ export default class UI extends React.Component { <WrappedRoute path='/favourites' component={FavouritedStatuses} content={children} /> <WrappedRoute path='/pinned' component={PinnedStatuses} content={children} /> - <WrappedRoute path='/statuses/new' component={Compose} content={children} /> + <WrappedRoute path='/statuses/new' component={Drawer} content={children} /> <WrappedRoute path='/statuses/:statusId' exact component={Status} content={children} /> <WrappedRoute path='/statuses/:statusId/reblogs' component={Reblogs} content={children} /> <WrappedRoute path='/statuses/:statusId/favourites' component={Favourites} content={children} /> diff --git a/app/javascript/flavours/glitch/reducers/compose.js b/app/javascript/flavours/glitch/reducers/compose.js index 17c5e3a3a..c5d7b03ac 100644 --- a/app/javascript/flavours/glitch/reducers/compose.js +++ b/app/javascript/flavours/glitch/reducers/compose.js @@ -47,7 +47,6 @@ const initialState = ImmutableMap({ focusDate: null, preselectDate: null, in_reply_to: null, - is_composing: false, is_submitting: false, is_uploading: false, progress: 0, @@ -180,9 +179,7 @@ export default function compose(state = initialState, action) { case COMPOSE_MOUNT: return state.set('mounted', true); case COMPOSE_UNMOUNT: - return state - .set('mounted', false) - .set('is_composing', false); + return state.set('mounted', false) case COMPOSE_ADVANCED_OPTIONS_CHANGE: return state .set('advanced_options', diff --git a/app/javascript/flavours/glitch/styles/components/composer.scss b/app/javascript/flavours/glitch/styles/components/composer.scss index ae9114644..4b09d80d6 100644 --- a/app/javascript/flavours/glitch/styles/components/composer.scss +++ b/app/javascript/flavours/glitch/styles/components/composer.scss @@ -1,22 +1,24 @@ .composer { padding: 10px } .composer--spoiler { - display: block; - box-sizing: border-box; - margin: 0; - border: none; - border-radius: 4px; - padding: 10px; - width: 100%; - outline: 0; - color: $ui-base-color; - background: $simple-background-color; - font-size: 14px; - font-family: inherit; - resize: vertical; + input { + display: block; + box-sizing: border-box; + margin: 0; + border: none; + border-radius: 4px; + padding: 10px; + width: 100%; + outline: 0; + color: $ui-base-color; + background: $simple-background-color; + font-size: 14px; + font-family: inherit; + resize: vertical; - &:focus { outline: 0 } - @include single-column('screen and (max-width: 630px)') { font-size: 16px } + &:focus { outline: 0 } + @include single-column('screen and (max-width: 630px)') { font-size: 16px } + } } .composer--warning { @@ -116,33 +118,33 @@ } .composer--textarea { - background: $simple-background-color; position: relative; - &:disabled { background: $ui-secondary-color } - - & > .textarea { - display: block; - box-sizing: border-box; - margin: 0; - border: none; - border-radius: 4px 4px 0 0; - padding: 10px 32px 0 10px; - width: 100%; - min-height: 100px; - outline: 0; - color: $ui-base-color; - background: $simple-background-color; - font-size: 14px; - font-family: inherit; - resize: none; + & > label { + .textarea { + display: block; + box-sizing: border-box; + margin: 0; + border: none; + border-radius: 4px 4px 0 0; + padding: 10px 32px 0 10px; + width: 100%; + min-height: 100px; + outline: 0; + color: $ui-base-color; + background: $simple-background-color; + font-size: 14px; + font-family: inherit; + resize: none; - &:focus { outline: 0 } - @include single-column('screen and (max-width: 630px)') { font-size: 16px } + &:disabled { background: $ui-secondary-color } + &:focus { outline: 0 } + @include single-column('screen and (max-width: 630px)') { font-size: 16px } - @include limited-single-column('screen and (max-width: 600px)') { - height: 100px !important; // prevent auto-resize textarea - resize: vertical; + @include limited-single-column('screen and (max-width: 600px)') { + height: 100px !important; // prevent auto-resize textarea + resize: vertical; + } } } } @@ -192,15 +194,18 @@ } .composer--upload_form { - display: flex; - flex-direction: row; - flex-wrap: wrap; padding: 5px; color: $ui-base-color; background: $simple-background-color; font-size: 14px; - font-family: inherit; - overflow: hidden; + + & > .content { + display: flex; + flex-direction: row; + flex-wrap: wrap; + font-family: inherit; + overflow: hidden; + } } .composer--upload_form--item { @@ -254,17 +259,61 @@ } } +.composer--upload_form--progress { + display: flex; + padding: 10px; + color: $ui-base-lighter-color; + overflow: hidden; + + & > .fa { + font-size: 34px; + margin-right: 10px; + } + + & > .message { + flex: 1 1 auto; + + & > span { + display: block; + font-size: 12px; + font-weight: 500; + text-transform: uppercase; + } + + & > .backdrop { + position: relative; + margin-top: 5px; + border-radius: 6px; + width: 100%; + height: 6px; + background: $ui-base-lighter-color; + + & > .tracker { + position: absolute; + top: 0; + left: 0; + height: 6px; + border-radius: 6px; + background: $ui-highlight-color; + } + } + } +} + .composer--options { padding: 10px; background: darken($simple-background-color, 8%); box-shadow: inset 0 5px 5px rgba($base-shadow-color, 0.05); border-radius: 0 0 4px 4px; + height: 27px; & > * { display: inline-block; box-sizing: content-box; padding: 0 3px; + height: 27px; line-height: 27px; + vertical-align: bottom; } & > hr { @@ -274,26 +323,26 @@ border-style: none none none solid; border-color: transparent transparent transparent darken($simple-background-color, 24%); padding: 0; + width: 0; + height: 27px; background: transparent; } } .composer--options--dropdown { - & > .value { transition: none } - - &.active { + &.open { & > .value { border-radius: 4px 4px 0 0; box-shadow: 0 -4px 4px rgba($base-shadow-color, 0.1); color: $primary-text-color; background: $ui-highlight-color; + transition: none; } } } -.composer--options--dropdown__dropdown { +.composer--options--dropdown--content { position: absolute; - margin-left: 40px; border-radius: 4px; box-shadow: 2px 4px 15px rgba($base-shadow-color, 0.4); background: $simple-background-color; @@ -301,11 +350,12 @@ transform-origin: 50% 0; } -.composer--options--dropdown--item { - color: $ui-base-color; +.composer--options--dropdown--content--item { + display: flex; + align-items: center; padding: 10px; + color: $ui-base-color; cursor: pointer; - display: flex; & > .content { flex: 1 1 auto; @@ -344,7 +394,6 @@ & > .count { display: inline-block; margin: 0 16px 0 8px; - padding-top: 10px; font-size: 16px; line-height: 36px; } diff --git a/app/javascript/flavours/glitch/styles/components/drawer.scss b/app/javascript/flavours/glitch/styles/components/drawer.scss index 8ad55c79b..ebf996907 100644 --- a/app/javascript/flavours/glitch/styles/components/drawer.scss +++ b/app/javascript/flavours/glitch/styles/components/drawer.scss @@ -4,7 +4,7 @@ box-sizing: border-box; padding: 10px 5px; width: 300px; - flex: 1 1 100%; + flex: none; contain: strict; &:first-child { @@ -15,10 +15,10 @@ padding-right: 10px; } - @include multi-columns('screen and (max-width: 630px)') { - &, &:first-child, &:last-child { - padding: 0; - } + @include single-column('screen and (max-width: 630px)') { flex: auto } + + @include limited-single-column('screen and (max-width: 630px)') { + &, &:first-child, &:last-child { padding: 0 } } .wide & { @@ -27,207 +27,196 @@ flex: 1 1 200px; } - .react-swipeable-view-container & { + @include single-column('screen and (max-width: 630px)') { + :root & { // Overrides `.wide` for single-column view + flex: auto; + width: 100%; + min-width: 0; + max-width: none; + padding: 0; + } + } + + .react-swipeable-view-container & { height: 100% } + + & > .contents { + position: relative; + padding: 0; + width: 100%; height: 100%; + background: lighten($ui-base-color, 13%); + overflow-x: hidden; + overflow-y: auto; + contain: strict; + } +} + +.drawer--header { + display: flex; + flex-direction: row; + margin-bottom: 10px; + flex: none; + background: lighten($ui-base-color, 8%); + font-size: 16px; + + & > * { + display: block; + box-sizing: border-box; + border-bottom: 2px solid transparent; + padding: 15px 5px 13px; + height: 48px; + flex: 1 1 auto; + color: $ui-primary-color; + text-align: center; + text-decoration: none; + cursor: pointer; } - .drawer--header { - display: flex; - flex-direction: row; - margin-bottom: 10px; - flex: none; - background: lighten($ui-base-color, 8%); - font-size: 16px; + a { + transition: background 100ms ease-in; - & > * { - display: block; - box-sizing: border-box; - border-bottom: 2px solid transparent; - padding: 15px 5px 13px; - height: 48px; - flex: 1 1 auto; - color: $ui-primary-color; - text-align: center; - text-decoration: none; - cursor: pointer; + &:focus, + &:hover { + outline: none; + background: lighten($ui-base-color, 3%); + transition: background 200ms ease-out; } + } +} - a { - transition: background 100ms ease-in; +.drawer--search { + position: relative; + margin-bottom: 10px; + flex: none; - &:focus, - &:hover { - outline: none; - background: lighten($ui-base-color, 3%); - transition: background 200ms ease-out; - } + @include limited-single-column('screen and (max-width: 360px)') { margin-bottom: 0 } + @include single-column('screen and (max-width: 630px)') { font-size: 16px } + + input { + display: block; + box-sizing: border-box; + margin: 0; + border: none; + padding: 10px 30px 10px 10px; + width: 100%; + height: 36px; + outline: 0; + color: $ui-primary-color; + background: $ui-base-color; + font-size: 14px; + font-family: inherit; + line-height: 16px; + + &:focus { + outline: 0; + background: lighten($ui-base-color, 4%); } } - .drawer--search { - position: relative; - margin-bottom: 10px; - flex: none; + & > .icon { + .fa { + display: inline-block; + position: absolute; + top: 10px; + right: 10px; + width: 18px; + height: 18px; + color: $ui-secondary-color; + font-size: 18px; + opacity: 0; + cursor: default; + pointer-events: none; + z-index: 2; + transition: all 100ms linear; + } - @include limited-single-column('screen and (max-width: 360px)') { - margin-bottom: 0; + .fa-search { + opacity: 0.3; + transform: rotate(0deg); } - input { - display: block; - box-sizing: border-box; - margin: 0; - border: none; - padding: 10px 30px 10px 10px; - width: 100%; - height: 36px; - outline: 0; - color: $ui-primary-color; - background: $ui-base-color; - font-size: 14px; - font-family: inherit; - line-height: 16px; + .fa-times-circle { + top: 11px; + transform: rotate(-90deg); + cursor: pointer; - &:focus { - outline: 0; - background: lighten($ui-base-color, 4%); - } + &:hover { color: $primary-text-color } } - & > .icon { - .fa { - display: inline-block; - position: absolute; - top: 10px; - right: 10px; - width: 18px; - height: 18px; - color: $ui-secondary-color; - font-size: 18px; + &.active { + .fa-search { opacity: 0; - cursor: default; - pointer-events: none; - z-index: 2; - transition: all 100ms linear; + transform: rotate(90deg); } - .fa-search { + .fa-times-circle { opacity: 0.3; + pointer-events: auto; transform: rotate(0deg); } - - .fa-times-circle { - top: 11px; - transform: rotate(-90deg); - cursor: pointer; - - &:hover { - color: $primary-text-color; - } - } - - &.active { - .fa-search { - opacity: 0; - transform: rotate(90deg); - } - - .fa-times-circle { - opacity: 0.3; - pointer-events: auto; - transform: rotate(0deg); - } - } } } +} - & > .contents { - position: relative; - padding: 0; - width: 100%; - height: 100%; - background: lighten($ui-base-color, 13%); - overflow-x: hidden; - overflow-y: auto; - contain: strict; - - .drawer--account { - padding: 10px; - color: $ui-primary-color; +.drawer--account { + padding: 10px; + color: $ui-primary-color; - & > a { - color: inherit; - text-decoration: none; - } + & > a { + color: inherit; + text-decoration: none; + } - & > .avatar { - float: left; - margin-right: 10px; - } + & > .avatar { + float: left; + margin-right: 10px; + } - & > .acct { - display: block; - color: $primary-text-color; - font-weight: 500; - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - } + & > .acct { + display: block; + color: $primary-text-color; + font-weight: 500; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } +} - .drawer--results { - position: absolute; - top: 0; - bottom: 0; - left: 0; - right: 0; - padding: 0; - background: $ui-base-color; - overflow-x: hidden; - overflow-y: auto; - contain: strict; - - & > header { - border-bottom: 1px solid darken($ui-base-color, 4%); - padding: 15px 10px; - color: $ui-base-lighter-color; - background: lighten($ui-base-color, 2%); - font-size: 14px; - font-weight: 500; - } +.drawer--results { + position: absolute; + top: 0; + bottom: 0; + left: 0; + right: 0; + padding: 0; + background: $ui-base-color; + overflow-x: hidden; + overflow-y: auto; + contain: strict; - & > section { - background: $ui-base-color; - - & > .hashtag { - display: block; - padding: 10px; - color: $ui-secondary-color; - text-decoration: none; - - &:hover, - &:active, - &:focus { - color: lighten($ui-secondary-color, 4%); - text-decoration: underline; - } - } - } - } + & > header { + border-bottom: 1px solid darken($ui-base-color, 4%); + padding: 15px 10px; + color: $ui-base-lighter-color; + background: lighten($ui-base-color, 2%); + font-size: 14px; + font-weight: 500; } -} -:root { // Overrides .wide stylings for mobile view - @include single-column('screen and (max-width: 630px)', $parent: null) { - .drawer { - flex: auto; - width: 100%; - min-width: 0; - max-width: none; - padding: 0; + & > section { + background: $ui-base-color; - .drawer--search input { - font-size: 16px; + & > .hashtag { + display: block; + padding: 10px; + color: $ui-secondary-color; + text-decoration: none; + + &:hover, + &:active, + &:focus { + color: lighten($ui-secondary-color, 4%); + text-decoration: underline; } } } diff --git a/app/javascript/flavours/glitch/styles/components/index.scss b/app/javascript/flavours/glitch/styles/components/index.scss index 98ed5d24a..0ce0dafc9 100644 --- a/app/javascript/flavours/glitch/styles/components/index.scss +++ b/app/javascript/flavours/glitch/styles/components/index.scss @@ -2704,47 +2704,6 @@ border-radius: 4px; } -.upload-progress { - padding: 10px; - color: $ui-base-lighter-color; - overflow: hidden; - display: flex; - - .fa { - font-size: 34px; - margin-right: 10px; - } - - span { - font-size: 12px; - text-transform: uppercase; - font-weight: 500; - display: block; - } -} - -.upload-progess__message { - flex: 1 1 auto; -} - -.upload-progress__backdrop { - width: 100%; - height: 6px; - border-radius: 6px; - background: $ui-base-lighter-color; - position: relative; - margin-top: 5px; -} - -.upload-progress__tracker { - position: absolute; - left: 0; - top: 0; - height: 6px; - background: $ui-highlight-color; - border-radius: 6px; -} - .emoji-button { display: block; font-size: 24px; @@ -3339,6 +3298,7 @@ max-width: 80vw; strong { + display: block; font-weight: 500; } @@ -3368,6 +3328,7 @@ color: $primary-text-color; } + & > .react-toggle, & > .icon { margin-right: 10px; } diff --git a/app/javascript/flavours/glitch/theme.yml b/app/javascript/flavours/glitch/theme.yml index 435fa2329..8ccd8fa65 100644 --- a/app/javascript/flavours/glitch/theme.yml +++ b/app/javascript/flavours/glitch/theme.yml @@ -11,8 +11,8 @@ pack: home: filename: packs/home.js preload: + - flavours/glitch/async/drawer - flavours/glitch/async/getting_started - - flavours/glitch/async/compose - flavours/glitch/async/home_timeline - flavours/glitch/async/notifications modal: diff --git a/app/javascript/flavours/glitch/util/async-components.js b/app/javascript/flavours/glitch/util/async-components.js index 5d21ccca2..b90f1b8c8 100644 --- a/app/javascript/flavours/glitch/util/async-components.js +++ b/app/javascript/flavours/glitch/util/async-components.js @@ -2,8 +2,8 @@ export function EmojiPicker () { return import(/* webpackChunkName: "flavours/glitch/async/emoji_picker" */'flavours/glitch/util/emoji/emoji_picker'); } -export function Compose () { - return import(/* webpackChunkName: "flavours/glitch/async/compose" */'flavours/glitch/features/compose'); +export function Drawer () { + return import(/* webpackChunkName: "flavours/glitch/async/drawer" */'flavours/glitch/features/drawer'); } export function Notifications () { diff --git a/app/javascript/flavours/glitch/util/react_helpers.js b/app/javascript/flavours/glitch/util/react_helpers.js index 087e3969d..082a58e62 100644 --- a/app/javascript/flavours/glitch/util/react_helpers.js +++ b/app/javascript/flavours/glitch/util/react_helpers.js @@ -6,8 +6,8 @@ export function assignHandlers (target, handlers) { // We just bind each handler to the `target`. const handle = target.handlers = {}; - handlers.keys().forEach( - key => handle.key = key.bind(target) + Object.keys(handlers).forEach( + key => handle[key] = handlers[key].bind(target) ); } diff --git a/app/javascript/flavours/glitch/util/redux_helpers.js b/app/javascript/flavours/glitch/util/redux_helpers.js index c0f5eeb28..8eb338da7 100644 --- a/app/javascript/flavours/glitch/util/redux_helpers.js +++ b/app/javascript/flavours/glitch/util/redux_helpers.js @@ -1,16 +1,8 @@ import { injectIntl } from 'react-intl'; import { connect } from 'react-redux'; -// Merges react-redux props. -export function mergeProps (stateProps, dispatchProps, ownProps) { - Object.assign({}, ownProps, { - dispatch: Object.assign({}, dispatchProps, ownProps.dispatch || {}), - state: Object.assign({}, stateProps, ownProps.state || {}), - }); -} - // Connects a component. export function wrap (Component, mapStateToProps, mapDispatchToProps, options) { const withIntl = typeof options === 'object' ? options.withIntl : !!options; - return (withIntl ? injectIntl : i => i)(connect(mapStateToProps, mapDispatchToProps, mergeProps)(Component)); + return (withIntl ? injectIntl : i => i)(connect(mapStateToProps, mapDispatchToProps)(Component)); } |