From 924ffe81d477a8cf890c8117efb94b908760bccc Mon Sep 17 00:00:00 2001 From: kibigo! Date: Sat, 23 Dec 2017 22:16:45 -0800 Subject: WIPgit status Refactor; ed. --- .../flavours/glitch/features/composer/index.js | 440 +++++++++++++++++++++ 1 file changed, 440 insertions(+) create mode 100644 app/javascript/flavours/glitch/features/composer/index.js (limited to 'app/javascript/flavours/glitch/features/composer/index.js') diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js new file mode 100644 index 000000000..25c2622d8 --- /dev/null +++ b/app/javascript/flavours/glitch/features/composer/index.js @@ -0,0 +1,440 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl } from 'react-intl'; +import { connect } from 'react-redux'; +import { withRouter } from 'react-router'; + +// Actions. +import { + cancelReplyCompose, + changeCompose, + changeComposeSensitivity, + changeComposeSpoilerText, + changeComposeSpoilerness, + changeComposeVisibility, + changeUploadCompose, + clearComposeSuggestions, + fetchComposeSuggestions, + insertEmojiCompose, + selectComposeSuggestion, + submitCompose, + toggleComposeAdvancedOption, + undoUploadCompose, + uploadCompose, +} from 'flavours/glitch/actions/compose'; +import { + closeModal, + openModal, +} from 'flavours/glitch/actions/modal'; + +// Components. +import ComposerOptions from './options'; +import ComposerPublisher from './publisher'; +import ComposerReply from './reply'; +import ComposerSpoiler from './spoiler'; +import ComposerTextarea from './textarea'; +import ComposerUploadForm from './upload_form'; +import ComposerWarning from './warning'; + +// Utils. +import { countableText } from 'flavours/glitch/util/counter'; +import { me } from 'flavours/glitch/util/initial_state'; +import { isMobile } from 'flavours/glitch/util/is_mobile'; +import { assignHandlers } from 'flavours/glitch/util/react_helpers'; +import { mergeProps } from 'flavours/glitch/util/redux_helpers'; + +// State mapping. +function mapStateToProps (state) { + const inReplyTo = state.getIn(['compose', 'in_reply_to']); + return { + acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','), + amUnlocked: !state.getIn(['accounts', me, 'locked']), + doNotFederate: state.getIn(['compose', 'advanced_options', 'do_not_federate']), + focusDate: state.getIn(['compose', 'focusDate']), + isSubmitting: state.getIn(['compose', 'is_submitting']), + isUploading: state.getIn(['compose', 'is_uploading']), + media: state.getIn(['compose', 'media_attachments']), + preselectDate: state.getIn(['compose', 'preselectDate']), + privacy: state.getIn(['compose', 'privacy']), + progress: state.getIn(['compose', 'progress']), + replyAccount: inReplyTo ? state.getIn(['accounts', state.getIn(['statuses', inReplyTo, 'account'])]) : null, + replyContent: inReplyTo ? state.getIn(['statuses', inReplyTo, 'contentHtml']) : null, + resetFileKey: state.getIn(['compose', 'resetFileKey']), + sideArm: state.getIn(['local_settings', 'side_arm']), + sensitive: state.getIn(['compose', 'sensitive']), + showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), + spoiler: state.getIn(['compose', 'spoiler']), + spoilerText: state.getIn(['compose', 'spoiler_text']), + suggestionToken: state.getIn(['compose', 'suggestion_token']), + suggestions: state.getIn(['compose', 'suggestions']), + text: state.getIn(['compose', 'text']), + }; +}; + +// 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)); + }, +}); + +// Handlers. +const handlers = { + + // Changes the text value of the spoiler. + changeSpoiler ({ target: { value } }) { + const { dispatch: { changeSpoilerText } } = this.props; + if (changeSpoilerText) { + changeSpoilerText(value); + } + }, + + // Inserts an emoji at the caret. + emoji (data) { + const { textarea: { selectionStart } } = this; + const { dispatch: { insertEmoji } } = this.props; + this.caretPos = selectionStart + data.native.length + 1; + if (insertEmoji) { + insertEmoji(selectionStart, data); + } + }, + + // Handles the secondary submit button. + secondarySubmit () { + const { submit } = this.handlers; + const { + dispatch: { changeVisibility }, + side_arm, + } = this.props; + if (changeVisibility) { + changeVisibility(side_arm); + } + submit(); + }, + + // Selects a suggestion from the autofill. + select (tokenStart, token, value) { + const { dispatch: { selectSuggestion } } = this.props; + this.caretPos = null; + if (selectSuggestion) { + selectSuggestion(tokenStart, token, value); + } + }, + + // Submits the status. + submit () { + const { textarea: { value } } = this; + const { + dispatch: { + changeText, + submit, + }, + state: { text }, + } = this.props; + + // If something changes inside the textarea, then we update the + // state before submitting. + if (changeText && text !== value) { + changeText(value); + } + + // Submits the status. + if (submit) { + submit(); + } + }, + + // Sets a reference to the textarea. + refTextarea ({ textarea }) { + this.textarea = textarea; + }, +}; + +// The component. +@injectIntl +@connect(mapStateToProps, mapDispatchToProps, mergeProps) +export default class Composer extends React.Component { + + // Constructor. + constructor (props) { + super(props); + assignHandlers(this, handlers); + + // Instance variables. + this.caretPos = null; + this.textarea = null; + } + + // 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; + } + } + + // This statement does several things: + // - If we're beginning a reply, and, + // - Replying to zero or one users, places the cursor at the end + // of the textbox. + // - Replying to more than one user, selects any usernames past + // the first; this provides a convenient shortcut to drop + // everyone else from the conversation. + // - If we've just finished uploading an image, and have a saved + // caret position, restores the cursor to that position after the + // text changes. + componentDidUpdate (prevProps) { + const { + caretPos, + textarea, + } = this; + const { + state: { + 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)) { + switch (true) { + case preselectDate !== prevProps.state.preselectDate: + selectionStart = text.search(/\s/) + 1; + selectionEnd = text.length; + break; + case !isNaN(caretPos) && caretPos !== null: + selectionStart = selectionEnd = caretPos; + break; + default: + selectionStart = selectionEnd = text.length; + } + textarea.setSelectionRange(selectionStart, selectionEnd); + textarea.focus(); + + // Refocuses the textarea after submitting. + } else if (prevProps.state.isSubmitting && !isSubmitting) { + textarea.focus(); + } + } + + render () { + const { + changeSpoiler, + emoji, + secondarySubmit, + select, + submit, + refTextarea, + } = this.handlers; + const { history } = this.context; + const { + dispatch: { + cancelReply, + changeDescription, + changeSensitivity, + changeText, + changeVisibility, + clearSuggestions, + closeModal, + fetchSuggestions, + openActionsModal, + openDoodleModal, + toggleAdvancedOption, + undoUpload, + upload, + }, + intl, + state: { + acceptContentTypes, + amUnlocked, + doNotFederate, + isSubmitting, + isUploading, + media, + privacy, + progress, + replyAccount, + replyContent, + resetFileKey, + sensitive, + showSearch, + sideArm, + spoiler, + spoilerText, + suggestions, + text, + }, + } = this.props; + + return ( +
+
+ ); + } + +} + +// Context +Composer.contextTypes = { + history: PropTypes.object, +} + +// 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, +}; -- cgit