diff options
author | Reverite <github@reverite.sh> | 2019-05-06 18:17:05 -0700 |
---|---|---|
committer | Reverite <github@reverite.sh> | 2019-05-06 18:17:05 -0700 |
commit | 5b85256b334b13fad26a2bc073a874750a3cdc2e (patch) | |
tree | 2d523aa8266e42ae31ab82c7fc2533cf4a90ff0d /app/javascript/flavours/glitch/features/composer | |
parent | e10a9794f4ed7c90e3190f285359f55dd00da435 (diff) | |
parent | 89d2859296bc5a57a8db07be86239cc938a3f691 (diff) |
Merge remote-tracking branch 'glitch/master' into production
Diffstat (limited to 'app/javascript/flavours/glitch/features/composer')
20 files changed, 0 insertions, 2980 deletions
diff --git a/app/javascript/flavours/glitch/features/composer/direct_warning/index.js b/app/javascript/flavours/glitch/features/composer/direct_warning/index.js deleted file mode 100644 index 3b1369acd..000000000 --- a/app/javascript/flavours/glitch/features/composer/direct_warning/index.js +++ /dev/null @@ -1,55 +0,0 @@ -import React from 'react'; -import Motion from 'flavours/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { defineMessages, FormattedMessage } from 'react-intl'; -import { termsLink} from 'flavours/glitch/util/backend_links'; - -// This is the spring used with our motion. -const motionSpring = spring(1, { damping: 35, stiffness: 400 }); - -// Messages. -const messages = defineMessages({ - disclaimer: { - defaultMessage: 'This toot will only be sent to all the mentioned users.', - id: 'compose_form.direct_message_warning', - }, - learn_more: { - defaultMessage: 'Learn more', - id: 'compose_form.direct_message_warning_learn_more' - } -}); - -// The component. -export default function ComposerDirectWarning () { - return ( - <Motion - defaultStyle={{ - opacity: 0, - scaleX: 0.85, - scaleY: 0.75, - }} - style={{ - opacity: motionSpring, - scaleX: motionSpring, - scaleY: motionSpring, - }} - > - {({ opacity, scaleX, scaleY }) => ( - <div - className='composer--warning' - style={{ - opacity: opacity, - transform: `scale(${scaleX}, ${scaleY})`, - }} - > - <span> - <FormattedMessage {...messages.disclaimer} /> - { termsLink !== undefined && <a href={termsLink} target='_blank'><FormattedMessage {...messages.learn_more} /></a> } - </span> - </div> - )} - </Motion> - ); -} - -ComposerDirectWarning.propTypes = {}; diff --git a/app/javascript/flavours/glitch/features/composer/hashtag_warning/index.js b/app/javascript/flavours/glitch/features/composer/hashtag_warning/index.js deleted file mode 100644 index 716028e4c..000000000 --- a/app/javascript/flavours/glitch/features/composer/hashtag_warning/index.js +++ /dev/null @@ -1,49 +0,0 @@ -import React from 'react'; -import Motion from 'flavours/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { defineMessages, FormattedMessage } from 'react-intl'; - -// This is the spring used with our motion. -const motionSpring = spring(1, { damping: 35, stiffness: 400 }); - -// Messages. -const messages = defineMessages({ - disclaimer: { - defaultMessage: 'This toot won\'t be listed under any hashtag as it is unlisted. Only public toots can be searched by hashtag.', - id: 'compose_form.hashtag_warning', - }, -}); - -// The component. -export default function ComposerHashtagWarning () { - return ( - <Motion - defaultStyle={{ - opacity: 0, - scaleX: 0.85, - scaleY: 0.75, - }} - style={{ - opacity: motionSpring, - scaleX: motionSpring, - scaleY: motionSpring, - }} - > - {({ opacity, scaleX, scaleY }) => ( - <div - className='composer--warning' - style={{ - opacity: opacity, - transform: `scale(${scaleX}, ${scaleY})`, - }} - > - <FormattedMessage - {...messages.disclaimer} - /> - </div> - )} - </Motion> - ); -} - -ComposerHashtagWarning.propTypes = {}; diff --git a/app/javascript/flavours/glitch/features/composer/index.js b/app/javascript/flavours/glitch/features/composer/index.js deleted file mode 100644 index 9d2e0b3da..000000000 --- a/app/javascript/flavours/glitch/features/composer/index.js +++ /dev/null @@ -1,600 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages } from 'react-intl'; - -const APPROX_HASHTAG_RE = /(?:^|[^\/\)\w])#(\S+)/i; - -// Actions. -import { - cancelReplyCompose, - changeCompose, - changeComposeAdvancedOption, - changeComposeSensitivity, - changeComposeSpoilerText, - changeComposeSpoilerness, - changeComposeVisibility, - changeUploadCompose, - clearComposeSuggestions, - fetchComposeSuggestions, - insertEmojiCompose, - mountCompose, - selectComposeSuggestion, - submitCompose, - undoUploadCompose, - unmountCompose, - uploadCompose, -} from 'flavours/glitch/actions/compose'; -import { - closeModal, - openModal, -} from 'flavours/glitch/actions/modal'; -import { changeLocalSetting } from 'flavours/glitch/actions/local_settings'; -import { addPoll, removePoll } from 'flavours/glitch/actions/compose'; - -// 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 ComposerPollForm from './poll_form'; -import ComposerWarning from './warning'; -import ComposerHashtagWarning from './hashtag_warning'; -import ComposerDirectWarning from './direct_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 { wrap } from 'flavours/glitch/util/redux_helpers'; -import { privacyPreference } from 'flavours/glitch/util/privacy_preference'; - -const messages = defineMessages({ - missingDescriptionMessage: { id: 'confirmations.missing_media_description.message', - defaultMessage: 'At least one media attachment is lacking a description. Consider describing all media attachments for the visually impaired before sending your toot.' }, - missingDescriptionConfirm: { id: 'confirmations.missing_media_description.confirm', - defaultMessage: 'Send anyway' }, -}); - -// State mapping. -function mapStateToProps (state) { - const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']); - const inReplyTo = state.getIn(['compose', 'in_reply_to']); - const replyPrivacy = inReplyTo ? state.getIn(['statuses', inReplyTo, 'visibility']) : null; - const sideArmBasePrivacy = state.getIn(['local_settings', 'side_arm']); - const sideArmRestrictedPrivacy = replyPrivacy ? privacyPreference(replyPrivacy, sideArmBasePrivacy) : null; - let sideArmPrivacy = null; - switch (state.getIn(['local_settings', 'side_arm_reply_mode'])) { - case 'copy': - sideArmPrivacy = replyPrivacy; - break; - case 'restrict': - sideArmPrivacy = sideArmRestrictedPrivacy; - break; - } - sideArmPrivacy = sideArmPrivacy || sideArmBasePrivacy; - return { - acceptContentTypes: state.getIn(['media_attachments', 'accept_content_types']).toArray().join(','), - advancedOptions: state.getIn(['compose', 'advanced_options']), - amUnlocked: !state.getIn(['accounts', me, 'locked']), - focusDate: state.getIn(['compose', 'focusDate']), - caretPosition: state.getIn(['compose', 'caretPosition']), - isSubmitting: state.getIn(['compose', 'is_submitting']), - isChangingUpload: state.getIn(['compose', 'is_changing_upload']), - 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']), - progress: state.getIn(['compose', 'progress']), - inReplyTo: inReplyTo ? state.getIn(['statuses', inReplyTo]) : null, - replyAccount: inReplyTo ? state.getIn(['statuses', inReplyTo, 'account']) : null, - replyContent: inReplyTo ? state.getIn(['statuses', inReplyTo, 'contentHtml']) : null, - resetFileKey: state.getIn(['compose', 'resetFileKey']), - sideArm: sideArmPrivacy, - sensitive: state.getIn(['compose', 'sensitive']), - showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']), - spoiler: spoilersAlwaysOn || 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']), - anyMedia: state.getIn(['compose', 'media_attachments']).size > 0, - poll: state.getIn(['compose', 'poll']), - spoilersAlwaysOn: spoilersAlwaysOn, - mediaDescriptionConfirmation: state.getIn(['local_settings', 'confirm_missing_media_description']), - preselectOnReply: state.getIn(['local_settings', 'preselect_on_reply']), - }; -}; - -// Dispatch mapping. -const mapDispatchToProps = (dispatch, { intl }) => ({ - onCancelReply() { - dispatch(cancelReplyCompose()); - }, - onChangeAdvancedOption(option, value) { - dispatch(changeComposeAdvancedOption(option, value)); - }, - onChangeDescription(id, description) { - dispatch(changeUploadCompose(id, { description })); - }, - onChangeSensitivity() { - dispatch(changeComposeSensitivity()); - }, - onChangeSpoilerText(text) { - dispatch(changeComposeSpoilerText(text)); - }, - onChangeSpoilerness() { - dispatch(changeComposeSpoilerness()); - }, - onChangeText(text) { - dispatch(changeCompose(text)); - }, - onChangeVisibility(value) { - dispatch(changeComposeVisibility(value)); - }, - onTogglePoll() { - dispatch((_, getState) => { - if (getState().getIn(['compose', 'poll'])) { - dispatch(removePoll()); - } else { - dispatch(addPoll()); - } - }); - }, - onClearSuggestions() { - dispatch(clearComposeSuggestions()); - }, - onCloseModal() { - dispatch(closeModal()); - }, - onFetchSuggestions(token) { - dispatch(fetchComposeSuggestions(token)); - }, - onInsertEmoji(position, emoji) { - dispatch(insertEmojiCompose(position, emoji)); - }, - onMount() { - dispatch(mountCompose()); - }, - onOpenActionsModal(props) { - dispatch(openModal('ACTIONS', props)); - }, - onOpenDoodleModal() { - dispatch(openModal('DOODLE', { noEsc: true })); - }, - onOpenFocalPointModal(id) { - dispatch(openModal('FOCAL_POINT', { id })); - }, - onSelectSuggestion(position, token, suggestion) { - dispatch(selectComposeSuggestion(position, token, suggestion)); - }, - onMediaDescriptionConfirm(routerHistory) { - dispatch(openModal('CONFIRM', { - message: intl.formatMessage(messages.missingDescriptionMessage), - confirm: intl.formatMessage(messages.missingDescriptionConfirm), - onConfirm: () => dispatch(submitCompose(routerHistory)), - onDoNotAsk: () => dispatch(changeLocalSetting(['confirm_missing_media_description'], false)), - })); - }, - onSubmit(routerHistory) { - dispatch(submitCompose(routerHistory)); - }, - onUndoUpload(id) { - dispatch(undoUploadCompose(id)); - }, - onUnmount() { - dispatch(unmountCompose()); - }, - onUpload(files) { - dispatch(uploadCompose(files)); - }, -}); - -// Handlers. -const handlers = { - - // Changes the text value of the spoiler. - handleChangeSpoiler ({ target: { value } }) { - const { onChangeSpoilerText } = this.props; - if (onChangeSpoilerText) { - onChangeSpoilerText(value); - } - }, - - // Inserts an emoji at the caret. - handleEmoji (data) { - const { textarea: { selectionStart } } = this; - const { onInsertEmoji } = this.props; - if (onInsertEmoji) { - onInsertEmoji(selectionStart, data); - } - }, - - // Handles the secondary submit button. - handleSecondarySubmit () { - const { handleSubmit } = this.handlers; - const { - onChangeVisibility, - sideArm, - } = this.props; - if (sideArm !== 'none' && onChangeVisibility) { - onChangeVisibility(sideArm); - } - handleSubmit(); - }, - - // Selects a suggestion from the autofill. - handleSelect (tokenStart, token, value) { - const { onSelectSuggestion } = this.props; - if (onSelectSuggestion) { - onSelectSuggestion(tokenStart, token, value); - } - }, - - // Submits the status. - handleSubmit () { - const { textarea: { value }, uploadForm } = this; - const { - onChangeText, - onSubmit, - isSubmitting, - isChangingUpload, - isUploading, - media, - anyMedia, - text, - mediaDescriptionConfirmation, - onMediaDescriptionConfirm, - } = this.props; - - // If something changes inside the textarea, then we update the - // state before submitting. - if (onChangeText && text !== value) { - onChangeText(value); - } - - // Submit disabled: - if (isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia)) { - return; - } - - // Submit unless there are media with missing descriptions - if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) { - const firstWithoutDescription = media.findIndex(item => !item.get('description')); - if (uploadForm) { - const inputs = uploadForm.querySelectorAll('.composer--upload_form--item input'); - if (inputs.length == media.size && firstWithoutDescription !== -1) { - inputs[firstWithoutDescription].focus(); - } - } - onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null); - } else if (onSubmit) { - onSubmit(this.context.router ? this.context.router.history : null); - } - }, - - // Sets a reference to the upload form. - handleRefUploadForm (uploadFormComponent) { - this.uploadForm = uploadFormComponent; - }, - - // Sets a reference to the textarea. - handleRefTextarea (textareaComponent) { - if (textareaComponent) { - this.textarea = textareaComponent.textarea; - } - }, - - // Sets a reference to the CW field. - handleRefSpoilerText (spoilerComponent) { - if (spoilerComponent) { - this.spoilerText = spoilerComponent.spoilerText; - } - } -}; - -// The component. -class Composer extends React.Component { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - - // Instance variables. - this.textarea = null; - this.spoilerText = null; - } - - // Tells our state the composer has been mounted. - componentDidMount () { - const { onMount } = this.props; - if (onMount) { - onMount(); - } - } - - // Tells our state the composer has been unmounted. - componentWillUnmount () { - const { onUnmount } = this.props; - if (onUnmount) { - onUnmount(); - } - } - - // 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. - componentDidUpdate (prevProps) { - const { - textarea, - spoilerText, - } = this; - const { - focusDate, - caretPosition, - isSubmitting, - preselectDate, - text, - preselectOnReply, - } = this.props; - let selectionEnd, selectionStart; - - // Caret/selection handling. - if (focusDate !== prevProps.focusDate) { - switch (true) { - case preselectDate !== prevProps.preselectDate && preselectOnReply: - selectionStart = text.search(/\s/) + 1; - selectionEnd = text.length; - break; - case !isNaN(caretPosition) && caretPosition !== null: - selectionStart = selectionEnd = caretPosition; - break; - default: - selectionStart = selectionEnd = text.length; - } - if (textarea) { - textarea.setSelectionRange(selectionStart, selectionEnd); - textarea.focus(); - textarea.scrollIntoView(); - } - - // Refocuses the textarea after submitting. - } else if (textarea && prevProps.isSubmitting && !isSubmitting) { - textarea.focus(); - } else if (this.props.spoiler !== prevProps.spoiler) { - if (this.props.spoiler) { - if (spoilerText) { - spoilerText.focus(); - } - } else { - if (textarea) { - textarea.focus(); - } - } - } - } - - render () { - const { - handleChangeSpoiler, - handleEmoji, - handleSecondarySubmit, - handleSelect, - handleSubmit, - handleRefUploadForm, - handleRefTextarea, - handleRefSpoilerText, - } = this.handlers; - const { - acceptContentTypes, - advancedOptions, - amUnlocked, - anyMedia, - intl, - isSubmitting, - isChangingUpload, - isUploading, - layout, - media, - poll, - onCancelReply, - onChangeAdvancedOption, - onChangeDescription, - onChangeSensitivity, - onChangeSpoilerness, - onChangeText, - onChangeVisibility, - onTogglePoll, - onClearSuggestions, - onCloseModal, - onFetchSuggestions, - onOpenActionsModal, - onOpenDoodleModal, - onOpenFocalPointModal, - onUndoUpload, - onUpload, - privacy, - progress, - inReplyTo, - resetFileKey, - sensitive, - showSearch, - sideArm, - spoiler, - spoilerText, - suggestions, - text, - spoilersAlwaysOn, - } = this.props; - - let disabledButton = isSubmitting || isUploading || isChangingUpload || (!text.trim().length && !anyMedia); - - return ( - <div className='composer'> - {privacy === 'direct' ? <ComposerDirectWarning /> : null} - {privacy === 'private' && amUnlocked ? <ComposerWarning /> : null} - {privacy !== 'public' && APPROX_HASHTAG_RE.test(text) ? <ComposerHashtagWarning /> : null} - {inReplyTo && ( - <ComposerReply - status={inReplyTo} - intl={intl} - onCancel={onCancelReply} - /> - )} - <ComposerSpoiler - hidden={!spoiler} - intl={intl} - onChange={handleChangeSpoiler} - onSubmit={handleSubmit} - onSecondarySubmit={handleSecondarySubmit} - text={spoilerText} - ref={handleRefSpoilerText} - /> - <ComposerTextarea - advancedOptions={advancedOptions} - autoFocus={!showSearch && !isMobile(window.innerWidth, layout)} - disabled={isSubmitting} - intl={intl} - onChange={onChangeText} - onPaste={onUpload} - onPickEmoji={handleEmoji} - onSubmit={handleSubmit} - onSecondarySubmit={handleSecondarySubmit} - onSuggestionsClearRequested={onClearSuggestions} - onSuggestionsFetchRequested={onFetchSuggestions} - onSuggestionSelected={handleSelect} - ref={handleRefTextarea} - suggestions={suggestions} - value={text} - /> - <div className='compose-form__modifiers'> - {isUploading || media && media.size ? ( - <ComposerUploadForm - intl={intl} - media={media} - onChangeDescription={onChangeDescription} - onOpenFocalPointModal={onOpenFocalPointModal} - onRemove={onUndoUpload} - progress={progress} - uploading={isUploading} - handleRef={handleRefUploadForm} - /> - ) : null} - {!!poll && ( - <ComposerPollForm /> - )} - </div> - <ComposerOptions - acceptContentTypes={acceptContentTypes} - advancedOptions={advancedOptions} - disabled={isSubmitting} - allowMedia={!poll && (media ? media.size < 4 && !media.some( - item => item.get('type') === 'video' - ) : true)} - hasMedia={media && !!media.size} - allowPoll={!(media && !!media.size)} - hasPoll={!!poll} - intl={intl} - onChangeAdvancedOption={onChangeAdvancedOption} - onChangeSensitivity={onChangeSensitivity} - onChangeVisibility={onChangeVisibility} - onTogglePoll={onTogglePoll} - onDoodleOpen={onOpenDoodleModal} - onModalClose={onCloseModal} - onModalOpen={onOpenActionsModal} - onToggleSpoiler={spoilersAlwaysOn ? null : onChangeSpoilerness} - onUpload={onUpload} - privacy={privacy} - resetFileKey={resetFileKey} - sensitive={sensitive || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0)} - spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} - /> - <ComposerPublisher - countText={`${spoilerText}${countableText(text)}${advancedOptions && advancedOptions.get('do_not_federate') ? ' 👁️' : ''}`} - disabled={disabledButton} - intl={intl} - onSecondarySubmit={handleSecondarySubmit} - onSubmit={handleSubmit} - privacy={privacy} - sideArm={sideArm} - /> - </div> - ); - } - -} - -// Props. -Composer.propTypes = { - intl: PropTypes.object.isRequired, - - // State props. - acceptContentTypes: PropTypes.string, - advancedOptions: ImmutablePropTypes.map, - amUnlocked: PropTypes.bool, - focusDate: PropTypes.instanceOf(Date), - caretPosition: PropTypes.number, - isSubmitting: PropTypes.bool, - isChangingUpload: PropTypes.bool, - isUploading: PropTypes.bool, - layout: PropTypes.string, - media: ImmutablePropTypes.list, - preselectDate: PropTypes.instanceOf(Date), - privacy: PropTypes.string, - progress: PropTypes.number, - inReplyTo: ImmutablePropTypes.map, - 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, - anyMedia: PropTypes.bool, - spoilersAlwaysOn: PropTypes.bool, - mediaDescriptionConfirmation: PropTypes.bool, - preselectOnReply: PropTypes.bool, - - // Dispatch props. - onCancelReply: PropTypes.func, - onChangeAdvancedOption: 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, - onMount: PropTypes.func, - onOpenActionsModal: PropTypes.func, - onOpenDoodleModal: PropTypes.func, - onSelectSuggestion: PropTypes.func, - onSubmit: PropTypes.func, - onUndoUpload: PropTypes.func, - onUnmount: PropTypes.func, - onUpload: PropTypes.func, - onMediaDescriptionConfirm: PropTypes.func, -}; - -Composer.contextTypes = { - router: PropTypes.object, -}; - -// Connecting and export. -export { Composer as WrappedComponent }; -export default wrap(Composer, mapStateToProps, mapDispatchToProps, true); 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 deleted file mode 100644 index b76410561..000000000 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/content/index.js +++ /dev/null @@ -1,146 +0,0 @@ -// 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; - - this.state = { - mounted: false, - }; - } - - // On mounting, we add our listeners. - componentDidMount () { - const { handleDocumentClick } = this.handlers; - document.addEventListener('click', handleDocumentClick, false); - document.addEventListener('touchend', handleDocumentClick, withPassive); - this.setState({ mounted: true }); - } - - // On unmounting, we remove our listeners. - componentWillUnmount () { - const { handleDocumentClick } = this.handlers; - document.removeEventListener('click', handleDocumentClick, false); - document.removeEventListener('touchend', handleDocumentClick, withPassive); - } - - // Rendering. - render () { - const { mounted } = this.state; - 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 }) => ( - // It should not be transformed when mounting because the resulting - // size will be used to determine the coordinate of the menu by - // react-overlays - <div - className='composer--options--dropdown--content' - ref={handleRef} - style={{ - ...style, - opacity: opacity, - transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, - }} - > - {items ? items.map( - ({ - name, - ...rest - }) => ( - <ComposerOptionsDropdownContentItem - active={name === value} - key={name} - name={name} - onChange={onChange} - onClose={onClose} - options={rest} - /> - ) - ) : null} - </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, - })), - 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/content/item/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js deleted file mode 100644 index 68a52083f..000000000 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/content/item/index.js +++ /dev/null @@ -1,129 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Toggle from 'react-toggle'; - -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import { assignHandlers } from 'flavours/glitch/util/react_helpers'; - -// Handlers. -const handlers = { - - // This function activates the dropdown item. - handleActivate (e) { - const { - name, - onChange, - onClose, - options: { on }, - } = this.props; - - // If the escape key was pressed, we close the dropdown. - if (e.key === 'Escape' && onClose) { - onClose(); - - // Otherwise, we both close the dropdown and change the value. - } else if (onChange && (!e.key || e.key === 'Enter')) { - e.preventDefault(); // Prevents change in focus on click - if ((on === null || typeof on === 'undefined') && onClose) { - onClose(); - } - onChange(name); - } - }, -}; - -// The component. -export default class ComposerOptionsDropdownContentItem extends React.PureComponent { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - } - - // Rendering. - render () { - const { handleActivate } = this.handlers; - const { - active, - options: { - icon, - meta, - on, - text, - }, - } = this.props; - const computedClass = classNames('composer--options--dropdown--content--item', { - active, - lengthy: meta, - 'toggled-off': !on && on !== null && typeof on !== 'undefined', - 'toggled-on': on, - 'with-icon': icon, - }); - - // The result. - return ( - <div - className={computedClass} - onClick={handleActivate} - onKeyDown={handleActivate} - role='button' - tabIndex='0' - > - {function () { - - // We render a `<Toggle>` if we were provided an `on` - // property, and otherwise show an `<Icon>` if available. - switch (true) { - case on !== null && typeof on !== 'undefined': - return ( - <Toggle - checked={on} - onChange={handleActivate} - /> - ); - case !!icon: - return ( - <Icon - className='icon' - fullwidth - icon={icon} - /> - ); - default: - return null; - } - }()} - {meta ? ( - <div className='content'> - <strong>{text}</strong> - {meta} - </div> - ) : - <div className='content'> - <strong>{text}</strong> - </div>} - </div> - ); - } - -}; - -// Props. -ComposerOptionsDropdownContentItem.propTypes = { - active: PropTypes.bool, - name: PropTypes.string, - onChange: PropTypes.func, - onClose: PropTypes.func, - options: PropTypes.shape({ - icon: PropTypes.string, - meta: PropTypes.node, - on: PropTypes.bool, - text: PropTypes.node, - }), -}; diff --git a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js b/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js deleted file mode 100644 index 7817cc964..000000000 --- a/app/javascript/flavours/glitch/features/composer/options/dropdown/index.js +++ /dev/null @@ -1,229 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Overlay from 'react-overlays/lib/Overlay'; - -// Components. -import IconButton from 'flavours/glitch/components/icon_button'; -import ComposerOptionsDropdownContent from './content'; - -// Utils. -import { isUserTouching } from 'flavours/glitch/util/is_mobile'; -import { assignHandlers } from 'flavours/glitch/util/react_helpers'; - -// Handlers. -const handlers = { - - // Closes the dropdown. - handleClose () { - this.setState({ open: false }); - }, - - // The enter key toggles the dropdown's open state, and the escape - // key closes it. - handleKeyDown ({ key }) { - const { - handleClose, - handleToggle, - } = this.handlers; - switch (key) { - case 'Enter': - handleToggle(key); - break; - case 'Escape': - handleClose(); - break; - } - }, - - // Creates an action modal object. - handleMakeModal () { - const component = this; - const { - items, - onChange, - 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 ({ target }) { - 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 (isUserTouching()) { - - // This gets the modal to open. - const modal = handleMakeModal(); - - // If we can, we then open the modal. - if (modal && onModalOpen) { - onModalOpen(modal); - return; - } - } - - const { top } = target.getBoundingClientRect(); - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); - // Otherwise, we just set our state to open. - this.setState({ open: !open }); - }, - - // 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); - } - }, -}; - -// The component. -export default class ComposerOptionsDropdown extends React.PureComponent { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - this.state = { - needsModalUpdate: false, - open: false, - placement: 'bottom', - }; - } - - // 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 { - handleClose, - handleKeyDown, - handleToggle, - } = this.handlers; - const { - active, - disabled, - title, - icon, - items, - onChange, - value, - } = this.props; - const { open, placement } = this.state; - const computedClass = classNames('composer--options--dropdown', { - active, - open, - top: placement === 'top', - }); - - // The result. - return ( - <div - className={computedClass} - onKeyDown={handleKeyDown} - > - <IconButton - active={open || active} - className='value' - disabled={disabled} - icon={icon} - onClick={handleToggle} - size={18} - style={{ - height: null, - lineHeight: '27px', - }} - title={title} - /> - <Overlay - containerPadding={20} - placement={placement} - show={open} - target={this} - > - <ComposerOptionsDropdownContent - items={items} - onChange={onChange} - onClose={handleClose} - value={value} - /> - </Overlay> - </div> - ); - } - -} - -// Props. -ComposerOptionsDropdown.propTypes = { - active: PropTypes.bool, - disabled: PropTypes.bool, - icon: PropTypes.string, - 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, - onModalClose: PropTypes.func, - onModalOpen: PropTypes.func, - title: PropTypes.string, - value: PropTypes.string, -}; diff --git a/app/javascript/flavours/glitch/features/composer/options/index.js b/app/javascript/flavours/glitch/features/composer/options/index.js deleted file mode 100644 index 7c7f01dc2..000000000 --- a/app/javascript/flavours/glitch/features/composer/options/index.js +++ /dev/null @@ -1,377 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { - FormattedMessage, - defineMessages, -} from 'react-intl'; -import spring from 'react-motion/lib/spring'; - -// Components. -import IconButton from 'flavours/glitch/components/icon_button'; -import TextIconButton from 'flavours/glitch/components/text_icon_button'; -import Dropdown from './dropdown'; - -// Utils. -import Motion from 'flavours/glitch/util/optional_motion'; -import { - assignHandlers, - hiddenComponent, -} from 'flavours/glitch/util/react_helpers'; -import { pollLimits } from 'flavours/glitch/util/initial_state'; - -// Messages. -const messages = defineMessages({ - advanced_options_icon_title: { - defaultMessage: 'Advanced options', - id: 'advanced_options.icon_title', - }, - attach: { - defaultMessage: 'Attach...', - id: 'compose.attach', - }, - change_privacy: { - defaultMessage: 'Adjust status privacy', - id: 'privacy.change', - }, - direct_long: { - defaultMessage: 'Post to mentioned users only', - id: 'privacy.direct.long', - }, - direct_short: { - defaultMessage: 'Direct', - id: 'privacy.direct.short', - }, - doodle: { - defaultMessage: 'Draw something', - id: 'compose.attach.doodle', - }, - local_only_long: { - defaultMessage: 'Do not post to other instances', - id: 'advanced_options.local-only.long', - }, - local_only_short: { - defaultMessage: 'Local-only', - id: 'advanced_options.local-only.short', - }, - private_long: { - defaultMessage: 'Post to followers only', - id: 'privacy.private.long', - }, - private_short: { - defaultMessage: 'Followers-only', - id: 'privacy.private.short', - }, - public_long: { - defaultMessage: 'Post to public timelines', - id: 'privacy.public.long', - }, - public_short: { - defaultMessage: 'Public', - id: 'privacy.public.short', - }, - sensitive: { - defaultMessage: 'Mark media as sensitive', - id: 'compose_form.sensitive', - }, - spoiler: { - defaultMessage: 'Hide text behind warning', - id: 'compose_form.spoiler', - }, - threaded_mode_long: { - defaultMessage: 'Automatically opens a reply on posting', - id: 'advanced_options.threaded_mode.long', - }, - threaded_mode_short: { - defaultMessage: 'Threaded mode', - id: 'advanced_options.threaded_mode.short', - }, - unlisted_long: { - defaultMessage: 'Do not show in public timelines', - id: 'privacy.unlisted.long', - }, - unlisted_short: { - defaultMessage: 'Unlisted', - id: 'privacy.unlisted.short', - }, - upload: { - defaultMessage: 'Upload a file', - id: 'compose.attach.upload', - }, - add_poll: { - defaultMessage: 'Add a poll', - id: 'poll_button.add_poll', - }, - remove_poll: { - defaultMessage: 'Remove poll', - id: 'poll_button.remove_poll', - }, -}); - -// Handlers. -const handlers = { - - // Handles file selection. - handleChangeFiles ({ target: { files } }) { - const { onUpload } = this.props; - if (files.length && onUpload) { - onUpload(files); - } - }, - - // Handles attachment clicks. - handleClickAttach (name) { - const { fileElement } = this; - const { onDoodleOpen } = this.props; - - // We switch over the name of the option. - switch (name) { - case 'upload': - if (fileElement) { - fileElement.click(); - } - return; - case 'doodle': - if (onDoodleOpen) { - onDoodleOpen(); - } - return; - } - }, - - // Handles a ref to the file input. - handleRefFileElement (fileElement) { - this.fileElement = fileElement; - }, -}; - -// The component. -export default class ComposerOptions extends React.PureComponent { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - - // Instance variables. - this.fileElement = null; - } - - // Rendering. - render () { - const { - handleChangeFiles, - handleClickAttach, - handleRefFileElement, - } = this.handlers; - const { - acceptContentTypes, - advancedOptions, - disabled, - allowMedia, - hasMedia, - allowPoll, - hasPoll, - intl, - onChangeAdvancedOption, - onChangeSensitivity, - onChangeVisibility, - onTogglePoll, - onModalClose, - onModalOpen, - onToggleSpoiler, - privacy, - resetFileKey, - sensitive, - spoiler, - } = this.props; - - // We predefine our privacy items so that we can easily pick the - // dropdown icon later. - const privacyItems = { - direct: { - icon: 'envelope', - meta: <FormattedMessage {...messages.direct_long} />, - name: 'direct', - text: <FormattedMessage {...messages.direct_short} />, - }, - private: { - icon: 'lock', - meta: <FormattedMessage {...messages.private_long} />, - name: 'private', - text: <FormattedMessage {...messages.private_short} />, - }, - public: { - icon: 'globe', - meta: <FormattedMessage {...messages.public_long} />, - name: 'public', - text: <FormattedMessage {...messages.public_short} />, - }, - unlisted: { - icon: 'unlock', - meta: <FormattedMessage {...messages.unlisted_long} />, - name: 'unlisted', - text: <FormattedMessage {...messages.unlisted_short} />, - }, - }; - - // The result. - return ( - <div className='composer--options'> - <input - accept={acceptContentTypes} - disabled={disabled || !allowMedia} - key={resetFileKey} - onChange={handleChangeFiles} - ref={handleRefFileElement} - type='file' - multiple - {...hiddenComponent} - /> - <Dropdown - disabled={disabled || !allowMedia} - icon='paperclip' - items={[ - { - icon: 'cloud-upload', - name: 'upload', - text: <FormattedMessage {...messages.upload} />, - }, - { - icon: 'paint-brush', - name: 'doodle', - text: <FormattedMessage {...messages.doodle} />, - }, - ]} - onChange={handleClickAttach} - onModalClose={onModalClose} - onModalOpen={onModalOpen} - title={intl.formatMessage(messages.attach)} - /> - {!!pollLimits && ( - <IconButton - active={hasPoll} - disabled={disabled || !allowPoll} - icon='tasks' - inverted - onClick={onTogglePoll} - size={18} - style={{ - height: null, - lineHeight: null, - }} - title={intl.formatMessage(hasPoll ? messages.remove_poll : messages.add_poll)} - /> - )} - <Motion - defaultStyle={{ scale: 0.87 }} - style={{ - scale: spring(hasMedia ? 1 : 0.87, { - stiffness: 200, - damping: 3, - }), - }} - > - {({ scale }) => ( - <div - style={{ - display: hasMedia ? null : 'none', - transform: `scale(${scale})`, - }} - > - <IconButton - active={sensitive} - className='sensitive' - disabled={spoiler} - icon={sensitive ? 'eye-slash' : 'eye'} - inverted - onClick={onChangeSensitivity} - size={18} - style={{ - height: null, - lineHeight: null, - }} - title={intl.formatMessage(messages.sensitive)} - /> - </div> - )} - </Motion> - <hr /> - <Dropdown - disabled={disabled} - icon={(privacyItems[privacy] || {}).icon} - items={[ - privacyItems.public, - privacyItems.unlisted, - privacyItems.private, - privacyItems.direct, - ]} - onChange={onChangeVisibility} - onModalClose={onModalClose} - onModalOpen={onModalOpen} - title={intl.formatMessage(messages.change_privacy)} - value={privacy} - /> - {onToggleSpoiler && ( - <TextIconButton - active={spoiler} - ariaControls='glitch.composer.spoiler.input' - label='CW' - onClick={onToggleSpoiler} - title={intl.formatMessage(messages.spoiler)} - /> - )} - <Dropdown - active={advancedOptions && advancedOptions.some(value => !!value)} - disabled={disabled} - icon='ellipsis-h' - items={advancedOptions ? [ - { - meta: <FormattedMessage {...messages.local_only_long} />, - name: 'do_not_federate', - on: advancedOptions.get('do_not_federate'), - text: <FormattedMessage {...messages.local_only_short} />, - }, - { - meta: <FormattedMessage {...messages.threaded_mode_long} />, - name: 'threaded_mode', - on: advancedOptions.get('threaded_mode'), - text: <FormattedMessage {...messages.threaded_mode_short} />, - }, - ] : null} - onChange={onChangeAdvancedOption} - onModalClose={onModalClose} - onModalOpen={onModalOpen} - title={intl.formatMessage(messages.advanced_options_icon_title)} - /> - </div> - ); - } - -} - -// Props. -ComposerOptions.propTypes = { - acceptContentTypes: PropTypes.string, - advancedOptions: ImmutablePropTypes.map, - disabled: PropTypes.bool, - allowMedia: PropTypes.bool, - hasMedia: PropTypes.bool, - allowPoll: PropTypes.bool, - hasPoll: PropTypes.bool, - intl: PropTypes.object.isRequired, - onChangeAdvancedOption: PropTypes.func, - onChangeSensitivity: PropTypes.func, - onChangeVisibility: PropTypes.func, - onTogglePoll: PropTypes.func, - onDoodleOpen: PropTypes.func, - onModalClose: PropTypes.func, - onModalOpen: PropTypes.func, - onToggleSpoiler: PropTypes.func, - onUpload: PropTypes.func, - privacy: PropTypes.string, - resetFileKey: PropTypes.number, - sensitive: PropTypes.bool, - spoiler: PropTypes.bool, -}; diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js b/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js deleted file mode 100644 index 1915b62d5..000000000 --- a/app/javascript/flavours/glitch/features/composer/poll_form/components/poll_form.js +++ /dev/null @@ -1,135 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import IconButton from 'flavours/glitch/components/icon_button'; -import Icon from 'flavours/glitch/components/icon'; -import classNames from 'classnames'; -import { pollLimits } from 'flavours/glitch/util/initial_state'; - -const messages = defineMessages({ - option_placeholder: { id: 'compose_form.poll.option_placeholder', defaultMessage: 'Choice {number}' }, - add_option: { id: 'compose_form.poll.add_option', defaultMessage: 'Add a choice' }, - remove_option: { id: 'compose_form.poll.remove_option', defaultMessage: 'Remove this choice' }, - poll_duration: { id: 'compose_form.poll.duration', defaultMessage: 'Poll duration' }, - single_choice: { id: 'compose_form.poll.single_choice', defaultMessage: 'Allow one choice' }, - multiple_choices: { id: 'compose_form.poll.multiple_choices', defaultMessage: 'Allow multiple choices' }, - minutes: { id: 'intervals.full.minutes', defaultMessage: '{number, plural, one {# minute} other {# minutes}}' }, - hours: { id: 'intervals.full.hours', defaultMessage: '{number, plural, one {# hour} other {# hours}}' }, - days: { id: 'intervals.full.days', defaultMessage: '{number, plural, one {# day} other {# days}}' }, -}); - -@injectIntl -class Option extends React.PureComponent { - - static propTypes = { - title: PropTypes.string.isRequired, - index: PropTypes.number.isRequired, - isPollMultiple: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onRemove: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleOptionTitleChange = e => { - this.props.onChange(this.props.index, e.target.value); - }; - - handleOptionRemove = () => { - this.props.onRemove(this.props.index); - }; - - render () { - const { isPollMultiple, title, index, intl } = this.props; - - return ( - <li> - <label className='poll__text editable'> - <span className={classNames('poll__input', { checkbox: isPollMultiple })} /> - - <input - type='text' - placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })} - maxLength={pollLimits.max_option_chars} - value={title} - onChange={this.handleOptionTitleChange} - /> - </label> - - <div className='poll__cancel'> - <IconButton disabled={index <= 1} title={intl.formatMessage(messages.remove_option)} icon='times' onClick={this.handleOptionRemove} /> - </div> - </li> - ); - } - -} - -export default -@injectIntl -class PollForm extends ImmutablePureComponent { - - static propTypes = { - options: ImmutablePropTypes.list, - expiresIn: PropTypes.number, - isMultiple: PropTypes.bool, - onChangeOption: PropTypes.func.isRequired, - onAddOption: PropTypes.func.isRequired, - onRemoveOption: PropTypes.func.isRequired, - onChangeSettings: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleAddOption = () => { - this.props.onAddOption(''); - }; - - handleSelectDuration = e => { - this.props.onChangeSettings(e.target.value, this.props.isMultiple); - }; - - handleSelectMultiple = e => { - this.props.onChangeSettings(this.props.expiresIn, e.target.value === 'true'); - }; - - render () { - const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl } = this.props; - - if (!options) { - return null; - } - - return ( - <div className='compose-form__poll-wrapper'> - <ul> - {options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} />)} - {options.size < pollLimits.max_options && ( - <label className='poll__text editable'> - <span className={classNames('poll__input')} style={{ opacity: 0 }} /> - <button className='button button-secondary' onClick={this.handleAddOption}><Icon icon='plus' /> <FormattedMessage {...messages.add_option} /></button> - </label> - )} - </ul> - - <div className='poll__footer'> - <select value={isMultiple ? 'true' : 'false'} onChange={this.handleSelectMultiple}> - <option value='false'>{intl.formatMessage(messages.single_choice)}</option> - <option value='true'>{intl.formatMessage(messages.multiple_choices)}</option> - </select> - - <select value={expiresIn} onChange={this.handleSelectDuration}> - <option value={300}>{intl.formatMessage(messages.minutes, { number: 5 })}</option> - <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option> - <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option> - <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option> - <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option> - <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option> - <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option> - </select> - </div> - </div> - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/composer/poll_form/index.js b/app/javascript/flavours/glitch/features/composer/poll_form/index.js deleted file mode 100644 index 5232c3b31..000000000 --- a/app/javascript/flavours/glitch/features/composer/poll_form/index.js +++ /dev/null @@ -1,29 +0,0 @@ -import { connect } from 'react-redux'; -import PollForm from './components/poll_form'; -import { addPollOption, removePollOption, changePollOption, changePollSettings } from '../../../actions/compose'; - -const mapStateToProps = state => ({ - options: state.getIn(['compose', 'poll', 'options']), - expiresIn: state.getIn(['compose', 'poll', 'expires_in']), - isMultiple: state.getIn(['compose', 'poll', 'multiple']), -}); - -const mapDispatchToProps = dispatch => ({ - onAddOption(title) { - dispatch(addPollOption(title)); - }, - - onRemoveOption(index) { - dispatch(removePollOption(index)); - }, - - onChangeOption(index, title) { - dispatch(changePollOption(index, title)); - }, - - onChangeSettings(expiresIn, isMultiple) { - dispatch(changePollSettings(expiresIn, isMultiple)); - }, -}); - -export default connect(mapStateToProps, mapDispatchToProps)(PollForm); diff --git a/app/javascript/flavours/glitch/features/composer/publisher/index.js b/app/javascript/flavours/glitch/features/composer/publisher/index.js deleted file mode 100644 index dc9c8f8eb..000000000 --- a/app/javascript/flavours/glitch/features/composer/publisher/index.js +++ /dev/null @@ -1,122 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { - defineMessages, - FormattedMessage, -} from 'react-intl'; -import { length } from 'stringz'; - -// Components. -import Button from 'flavours/glitch/components/button'; -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import { maxChars } from 'flavours/glitch/util/initial_state'; - -// Messages. -const messages = defineMessages({ - publish: { - defaultMessage: 'Toot', - id: 'compose_form.publish', - }, - publishLoud: { - defaultMessage: '{publish}!', - id: 'compose_form.publish_loud', - }, -}); - -// The component. -export default function ComposerPublisher ({ - countText, - disabled, - intl, - onSecondarySubmit, - onSubmit, - privacy, - sideArm, -}) { - const diff = maxChars - length(countText || ''); - const computedClass = classNames('composer--publisher', { - disabled: disabled || diff < 0, - over: diff < 0, - }); - - // The result. - return ( - <div className={computedClass}> - <span className='count'>{diff}</span> - {sideArm && sideArm !== 'none' ? ( - <Button - className='side_arm' - disabled={disabled || diff < 0} - onClick={onSecondarySubmit} - style={{ padding: null }} - text={ - <span> - <Icon - icon={{ - public: 'globe', - unlisted: 'unlock', - private: 'lock', - direct: 'envelope', - }[sideArm]} - /> - </span> - } - title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${sideArm}.short` })}`} - /> - ) : null} - <Button - className='primary' - text={function () { - switch (true) { - case !!sideArm && sideArm !== 'none': - case privacy === 'direct': - case privacy === 'private': - return ( - <span> - <Icon - icon={{ - direct: 'envelope', - private: 'lock', - public: 'globe', - unlisted: 'unlock', - }[privacy]} - /> - {' '} - <FormattedMessage {...messages.publish} /> - </span> - ); - case privacy === 'public': - return ( - <span> - <FormattedMessage - {...messages.publishLoud} - values={{ publish: <FormattedMessage {...messages.publish} /> }} - /> - </span> - ); - default: - return <span><FormattedMessage {...messages.publish} /></span>; - } - }()} - title={`${intl.formatMessage(messages.publish)}: ${intl.formatMessage({ id: `privacy.${privacy}.short` })}`} - onClick={onSubmit} - disabled={disabled || diff < 0} - /> - </div> - ); -} - -// Props. -ComposerPublisher.propTypes = { - countText: PropTypes.string, - disabled: PropTypes.bool, - intl: PropTypes.object.isRequired, - onSecondarySubmit: PropTypes.func, - onSubmit: PropTypes.func, - privacy: PropTypes.oneOf(['direct', 'private', 'unlisted', 'public']), - sideArm: PropTypes.oneOf(['none', 'direct', 'private', 'unlisted', 'public']), -}; diff --git a/app/javascript/flavours/glitch/features/composer/reply/index.js b/app/javascript/flavours/glitch/features/composer/reply/index.js deleted file mode 100644 index 56e9e96a5..000000000 --- a/app/javascript/flavours/glitch/features/composer/reply/index.js +++ /dev/null @@ -1,96 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages } from 'react-intl'; - -// Components. -import AccountContainer from 'flavours/glitch/containers/account_container'; -import IconButton from 'flavours/glitch/components/icon_button'; -import AttachmentList from 'flavours/glitch/components/attachment_list'; - -// Utils. -import { assignHandlers } from 'flavours/glitch/util/react_helpers'; -import { isRtl } from 'flavours/glitch/util/rtl'; - -// Messages. -const messages = defineMessages({ - cancel: { - defaultMessage: 'Cancel', - id: 'reply_indicator.cancel', - }, -}); - -// Handlers. -const handlers = { - - // Handles a click on the "close" button. - handleClick () { - const { onCancel } = this.props; - if (onCancel) { - onCancel(); - } - }, -}; - -// The component. -export default class ComposerReply extends React.PureComponent { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - } - - // Rendering. - render () { - const { handleClick } = this.handlers; - const { - status, - intl, - } = this.props; - - const account = status.get('account'); - const content = status.get('content'); - const attachments = status.get('media_attachments'); - - // The result. - return ( - <article className='composer--reply'> - <header> - <IconButton - className='cancel' - icon='times' - onClick={handleClick} - title={intl.formatMessage(messages.cancel)} - inverted - /> - {account && ( - <AccountContainer - id={account} - small - /> - )} - </header> - <div - className='content' - dangerouslySetInnerHTML={{ __html: content || '' }} - style={{ direction: isRtl(content) ? 'rtl' : 'ltr' }} - /> - {attachments.size > 0 && ( - <AttachmentList - compact - media={attachments} - /> - )} - </article> - ); - } - -} - -ComposerReply.propTypes = { - status: ImmutablePropTypes.map.isRequired, - intl: PropTypes.object.isRequired, - onCancel: PropTypes.func, -}; diff --git a/app/javascript/flavours/glitch/features/composer/spoiler/index.js b/app/javascript/flavours/glitch/features/composer/spoiler/index.js deleted file mode 100644 index e2f9c7021..000000000 --- a/app/javascript/flavours/glitch/features/composer/spoiler/index.js +++ /dev/null @@ -1,107 +0,0 @@ -// Package imports. -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, FormattedMessage } from 'react-intl'; - -// Utils. -import { - assignHandlers, - hiddenComponent, -} from 'flavours/glitch/util/react_helpers'; - -// Messages. -const messages = defineMessages({ - placeholder: { - defaultMessage: 'Write your warning here', - id: 'compose_form.spoiler_placeholder', - }, -}); - -// Handlers. -const handlers = { - - // Handles a keypress. - handleKeyDown ({ - ctrlKey, - keyCode, - metaKey, - altKey, - }) { - const { onSubmit, onSecondarySubmit } = this.props; - - // We submit the status on control/meta + enter. - if (onSubmit && keyCode === 13 && (ctrlKey || metaKey)) { - onSubmit(); - } - - // Submit the status with secondary visibility on alt + enter. - if (onSecondarySubmit && keyCode === 13 && altKey) { - onSecondarySubmit(); - } - }, - - handleRefSpoilerText (spoilerText) { - this.spoilerText = spoilerText; - }, - - // When the escape key is released, we focus the UI. - handleKeyUp ({ key }) { - if (key === 'Escape') { - document.querySelector('.ui').parentElement.focus(); - } - }, -}; - -// The component. -export default class ComposerSpoiler extends React.PureComponent { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - } - - // Rendering. - render () { - const { handleKeyDown, handleKeyUp, handleRefSpoilerText } = this.handlers; - const { - hidden, - intl, - onChange, - text, - } = this.props; - - // The result. - return ( - <div className={`composer--spoiler ${hidden ? '' : 'composer--spoiler--visible'}`}> - <label> - <span {...hiddenComponent}> - <FormattedMessage {...messages.placeholder} /> - </span> - <input - id='glitch.composer.spoiler.input' - onChange={onChange} - onKeyDown={handleKeyDown} - onKeyUp={handleKeyUp} - placeholder={intl.formatMessage(messages.placeholder)} - type='text' - value={text} - ref={handleRefSpoilerText} - disabled={hidden} - /> - </label> - </div> - ); - } - -} - -// Props. -ComposerSpoiler.propTypes = { - hidden: PropTypes.bool, - intl: PropTypes.object.isRequired, - onChange: PropTypes.func, - onSubmit: PropTypes.func, - onSecondarySubmit: PropTypes.func, - text: PropTypes.string, -}; diff --git a/app/javascript/flavours/glitch/features/composer/textarea/icons/index.js b/app/javascript/flavours/glitch/features/composer/textarea/icons/index.js deleted file mode 100644 index 049cdd5cd..000000000 --- a/app/javascript/flavours/glitch/features/composer/textarea/icons/index.js +++ /dev/null @@ -1,60 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages } from 'react-intl'; - -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Messages. -const messages = defineMessages({ - localOnly: { - defaultMessage: 'This post is local-only', - id: 'advanced_options.local-only.tooltip', - }, - threadedMode: { - defaultMessage: 'Threaded mode enabled', - id: 'advanced_options.threaded_mode.tooltip', - }, -}); - -// We use an array of tuples here instead of an object because it -// preserves order. -const iconMap = [ - ['do_not_federate', 'home', messages.localOnly], - ['threaded_mode', 'comments', messages.threadedMode], -]; - -// The component. -export default function ComposerTextareaIcons ({ - advancedOptions, - intl, -}) { - - // The result. We just map every active option to its icon. - return ( - <div className='composer--textarea--icons'> - {advancedOptions ? iconMap.map( - ([key, icon, message]) => advancedOptions.get(key) ? ( - <span - className='textarea_icon' - key={key} - title={intl.formatMessage(message)} - > - <Icon - fullwidth - icon={icon} - /> - </span> - ) : null - ) : null} - </div> - ); -} - -// Props. -ComposerTextareaIcons.propTypes = { - advancedOptions: ImmutablePropTypes.map, - intl: PropTypes.object.isRequired, -}; diff --git a/app/javascript/flavours/glitch/features/composer/textarea/index.js b/app/javascript/flavours/glitch/features/composer/textarea/index.js deleted file mode 100644 index 50e46fc78..000000000 --- a/app/javascript/flavours/glitch/features/composer/textarea/index.js +++ /dev/null @@ -1,312 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { - defineMessages, - FormattedMessage, -} from 'react-intl'; -import Textarea from 'react-textarea-autosize'; - -// Components. -import EmojiPicker from 'flavours/glitch/features/emoji_picker'; -import ComposerTextareaIcons from './icons'; -import ComposerTextareaSuggestions from './suggestions'; - -// Utils. -import { isRtl } from 'flavours/glitch/util/rtl'; -import { - assignHandlers, - hiddenComponent, -} from 'flavours/glitch/util/react_helpers'; - -// Messages. -const messages = defineMessages({ - placeholder: { - defaultMessage: 'What is on your mind?', - id: 'compose_form.placeholder', - }, -}); - -// Handlers. -const handlers = { - - // When blurring the textarea, suggestions are hidden. - 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. - handleChange ({ - target: { - selectionStart, - value, - }, - }) { - const { - onChange, - onSuggestionsFetchRequested, - onSuggestionsClearRequested, - } = this.props; - const { lastToken } = this.state; - - // This gets the token at the caret location, if it begins with an - // `@` (mentions) or `:` (shortcodes). - const left = value.slice(0, selectionStart).search(/[^\s\u200B]+$/); - const right = value.slice(selectionStart).search(/[\s\u200B]/); - const token = function () { - switch (true) { - case left < 0 || !/[@:#]/.test(value[left]): - return null; - case right < 0: - return value.slice(left); - default: - return value.slice(left, right + selectionStart).trim().toLowerCase(); - } - }(); - - // We only request suggestions for tokens which are at least 3 - // characters long. - if (onSuggestionsFetchRequested && token && token.length >= 3) { - if (lastToken !== token) { - this.setState({ - lastToken: token, - selectedSuggestion: 0, - tokenStart: left, - }); - onSuggestionsFetchRequested(token); - } - } else { - this.setState({ lastToken: null }); - if (onSuggestionsClearRequested) { - onSuggestionsClearRequested(); - } - } - - // Updates the value of the textarea. - if (onChange) { - onChange(value); - } - }, - - // Handles a click on an autosuggestion. - handleClickSuggestion (index) { - const { textarea } = this; - const { - onSuggestionSelected, - suggestions, - } = this.props; - const { - lastToken, - tokenStart, - } = this.state; - onSuggestionSelected(tokenStart, lastToken, suggestions.get(index)); - textarea.focus(); - }, - - // Handles a keypress. If the autosuggestions are visible, we need - // to allow keypresses to navigate and sleect them. - handleKeyDown (e) { - const { - disabled, - onSubmit, - onSecondarySubmit, - onSuggestionSelected, - suggestions, - } = this.props; - const { - lastToken, - suggestionsHidden, - selectedSuggestion, - tokenStart, - } = this.state; - - // Keypresses do nothing if the composer is disabled. - if (disabled) { - e.preventDefault(); - return; - } - - // We submit the status on control/meta + enter. - if (onSubmit && e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - onSubmit(); - } - - // Submit the status with secondary visibility on alt + enter. - if (onSecondarySubmit && e.keyCode === 13 && e.altKey) { - onSecondarySubmit(); - } - - // Switches over the pressed key. - switch(e.key) { - - // On arrow down, we pick the next suggestion. - case 'ArrowDown': - if (suggestions && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.min(selectedSuggestion + 1, suggestions.size - 1) }); - } - return; - - // On arrow up, we pick the previous suggestion. - case 'ArrowUp': - if (suggestions && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - this.setState({ selectedSuggestion: Math.max(selectedSuggestion - 1, 0) }); - } - return; - - // On enter or tab, we select the suggestion. - case 'Enter': - case 'Tab': - if (onSuggestionSelected && lastToken !== null && suggestions && suggestions.size > 0 && !suggestionsHidden) { - e.preventDefault(); - e.stopPropagation(); - onSuggestionSelected(tokenStart, lastToken, suggestions.get(selectedSuggestion)); - } - return; - } - }, - - // When the escape key is released, we either close the suggestions - // window or focus the UI. - handleKeyUp ({ key }) { - const { suggestionsHidden } = this.state; - if (key === 'Escape') { - if (!suggestionsHidden) { - this.setState({ suggestionsHidden: true }); - } else { - document.querySelector('.ui').parentElement.focus(); - } - } - }, - - // Handles the pasting of images into the composer. - handlePaste (e) { - const { onPaste } = this.props; - let d; - if (onPaste && (d = e.clipboardData) && (d = d.files).length === 1) { - onPaste(d); - e.preventDefault(); - } - }, - - // Saves a reference to the textarea. - handleRefTextarea (textarea) { - this.textarea = textarea; - }, -}; - -// The component. -export default class ComposerTextarea extends React.Component { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - this.state = { - suggestionsHidden: false, - selectedSuggestion: 0, - lastToken: null, - tokenStart: 0, - }; - - // Instance variables. - this.textarea = null; - } - - // When we receive new suggestions, we unhide the suggestions window - // if we didn't have any suggestions before. - componentWillReceiveProps (nextProps) { - const { suggestions } = this.props; - const { suggestionsHidden } = this.state; - if (nextProps.suggestions && nextProps.suggestions !== suggestions && nextProps.suggestions.size > 0 && suggestionsHidden) { - this.setState({ suggestionsHidden: false }); - } - } - - // Rendering. - render () { - const { - handleBlur, - handleChange, - handleClickSuggestion, - handleKeyDown, - handleKeyUp, - handlePaste, - handleRefTextarea, - } = this.handlers; - const { - advancedOptions, - autoFocus, - disabled, - intl, - onPickEmoji, - suggestions, - value, - } = this.props; - const { - selectedSuggestion, - suggestionsHidden, - } = this.state; - - // The result. - return ( - <div className='composer--textarea'> - <label> - <span {...hiddenComponent}><FormattedMessage {...messages.placeholder} /></span> - <ComposerTextareaIcons - advancedOptions={advancedOptions} - intl={intl} - /> - <Textarea - aria-autocomplete='list' - autoFocus={autoFocus} - className='textarea' - disabled={disabled} - 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' }} - /> - </label> - <EmojiPicker onPickEmoji={onPickEmoji} /> - <ComposerTextareaSuggestions - hidden={suggestionsHidden} - onSuggestionClick={handleClickSuggestion} - suggestions={suggestions} - value={selectedSuggestion} - /> - </div> - ); - } - -} - -// Props. -ComposerTextarea.propTypes = { - advancedOptions: ImmutablePropTypes.map, - autoFocus: PropTypes.bool, - disabled: PropTypes.bool, - intl: PropTypes.object.isRequired, - onChange: PropTypes.func, - onPaste: PropTypes.func, - onPickEmoji: PropTypes.func, - onSubmit: PropTypes.func, - onSecondarySubmit: PropTypes.func, - onSuggestionsClearRequested: PropTypes.func, - onSuggestionsFetchRequested: PropTypes.func, - onSuggestionSelected: PropTypes.func, - suggestions: ImmutablePropTypes.list, - value: PropTypes.string, -}; - -// Default props. -ComposerTextarea.defaultProps = { autoFocus: true }; diff --git a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js b/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js deleted file mode 100644 index dc72585f2..000000000 --- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/index.js +++ /dev/null @@ -1,43 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -// Components. -import ComposerTextareaSuggestionsItem from './item'; - -// The component. -export default function ComposerTextareaSuggestions ({ - hidden, - onSuggestionClick, - suggestions, - value, -}) { - - // The result. - return ( - <div - className='composer--textarea--suggestions' - hidden={hidden || !suggestions || suggestions.isEmpty()} - > - {!hidden && suggestions ? suggestions.map( - (suggestion, index) => ( - <ComposerTextareaSuggestionsItem - index={index} - key={typeof suggestion === 'object' ? suggestion.id : suggestion} - onClick={onSuggestionClick} - selected={index === value} - suggestion={suggestion} - /> - ) - ) : null} - </div> - ); -} - -ComposerTextareaSuggestions.propTypes = { - hidden: PropTypes.bool, - onSuggestionClick: PropTypes.func, - suggestions: ImmutablePropTypes.list, - 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 deleted file mode 100644 index 1b7ae8904..000000000 --- a/app/javascript/flavours/glitch/features/composer/textarea/suggestions/item/index.js +++ /dev/null @@ -1,118 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; - -// Components. -import AccountContainer from 'flavours/glitch/containers/account_container'; - -// Utils. -import { unicodeMapping } from 'flavours/glitch/util/emoji'; -import { assignHandlers } from 'flavours/glitch/util/react_helpers'; - -// Gets our asset host from the environment, if available. -const assetHost = process.env.CDN_HOST || ''; - -// Handlers. -const handlers = { - - // Handles a click on a suggestion. - handleClick (e) { - const { - index, - onClick, - } = this.props; - if (onClick) { - e.preventDefault(); - e.stopPropagation(); // Prevents following account links - onClick(index); - } - }, - - // This prevents the focus from changing, which would mess with - // our suggestion code. - handleMouseDown (e) { - e.preventDefault(); - }, -}; - -// The component. -export default class ComposerTextareaSuggestionsItem extends React.Component { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - } - - // Rendering. - render () { - const { - handleMouseDown, - handleClick, - } = this.handlers; - const { - selected, - suggestion, - } = this.props; - const computedClass = classNames('composer--textarea--suggestions--item', { selected }); - - // If the suggestion is an object, then we render an emoji. - // Otherwise, we render a hashtag if it starts with #, or an account. - let inner; - if (typeof suggestion === 'object') { - let url; - if (suggestion.custom) { - url = suggestion.imageUrl; - } else { - const mapping = unicodeMapping[suggestion.native] || unicodeMapping[suggestion.native.replace(/\uFE0F$/, '')]; - if (mapping) { - url = `${assetHost}/emoji/${mapping.filename}.svg`; - } - } - if (url) { - inner = ( - <div className='emoji'> - <img - alt={suggestion.native || suggestion.colons} - className='emojione' - src={url} - /> - {suggestion.colons} - </div> - ); - } - } else if (suggestion[0] === '#') { - inner = suggestion; - } else { - inner = ( - <AccountContainer - id={suggestion} - small - /> - ); - } - - // The result. - return ( - <div - className={computedClass} - onMouseDown={handleMouseDown} - onClickCapture={handleClick} // Jumps in front of contents - role='button' - tabIndex='0' - > - { inner } - </div> - ); - } - -} - -// Props. -ComposerTextareaSuggestionsItem.propTypes = { - index: PropTypes.number, - onClick: PropTypes.func, - selected: PropTypes.bool, - suggestion: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), -}; diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/index.js deleted file mode 100644 index c2ff66623..000000000 --- a/app/javascript/flavours/glitch/features/composer/upload_form/index.js +++ /dev/null @@ -1,60 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; - -// Components. -import ComposerUploadFormItem from './item'; -import ComposerUploadFormProgress from './progress'; - -// The component. -export default function ComposerUploadForm ({ - intl, - media, - onChangeDescription, - onOpenFocalPointModal, - onRemove, - progress, - uploading, - handleRef, -}) { - const computedClass = classNames('composer--upload_form', { uploading }); - - // The result. - return ( - <div className={computedClass} ref={handleRef}> - {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} - focusX={item.getIn(['meta', 'focus', 'x'])} - focusY={item.getIn(['meta', 'focus', 'y'])} - mediaType={item.get('type')} - preview={item.get('preview_url')} - onChangeDescription={onChangeDescription} - onOpenFocalPointModal={onOpenFocalPointModal} - onRemove={onRemove} - /> - ))} - </div> - ) : null} - </div> - ); -} - -// Props. -ComposerUploadForm.propTypes = { - intl: PropTypes.object.isRequired, - media: ImmutablePropTypes.list, - onChangeDescription: PropTypes.func.isRequired, - onRemove: PropTypes.func.isRequired, - progress: PropTypes.number, - uploading: PropTypes.bool, - handleRef: PropTypes.func, -}; 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 deleted file mode 100644 index 4f5f66f04..000000000 --- a/app/javascript/flavours/glitch/features/composer/upload_form/item/index.js +++ /dev/null @@ -1,202 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { - FormattedMessage, - defineMessages, -} from 'react-intl'; -import spring from 'react-motion/lib/spring'; - -// Components. -import IconButton from 'flavours/glitch/components/icon_button'; - -// Utils. -import Motion from 'flavours/glitch/util/optional_motion'; -import { assignHandlers } from 'flavours/glitch/util/react_helpers'; -import { isUserTouching } from 'flavours/glitch/util/is_mobile'; - -// Messages. -const messages = defineMessages({ - undo: { - defaultMessage: 'Undo', - id: 'upload_form.undo', - }, - description: { - defaultMessage: 'Describe for the visually impaired', - id: 'upload_form.description', - }, - crop: { - defaultMessage: 'Crop', - id: 'upload_form.focus', - }, -}); - -// Handlers. -const handlers = { - - // On blur, we save the description for the media item. - handleBlur () { - const { - id, - onChangeDescription, - } = this.props; - const { dirtyDescription } = this.state; - - this.setState({ dirtyDescription: null, focused: false }); - - if (id && onChangeDescription && dirtyDescription !== null) { - onChangeDescription(id, dirtyDescription); - } - }, - - // When the value of our description changes, we store it in the - // temp value `dirtyDescription` in our state. - handleChange ({ target: { value } }) { - this.setState({ dirtyDescription: value }); - }, - - // Records focus on the media item. - handleFocus () { - this.setState({ focused: true }); - }, - - // Records the start of a hover over the media item. - handleMouseEnter () { - this.setState({ hovered: true }); - }, - - // Records the end of a hover over the media item. - handleMouseLeave () { - this.setState({ hovered: false }); - }, - - // Removes the media item. - handleRemove () { - const { - id, - onRemove, - } = this.props; - if (id && onRemove) { - onRemove(id); - } - }, - - // Opens the focal point modal. - handleFocalPointClick () { - const { - id, - onOpenFocalPointModal, - } = this.props; - if (id && onOpenFocalPointModal) { - onOpenFocalPointModal(id); - } - }, -}; - -// The component. -export default class ComposerUploadFormItem extends React.PureComponent { - - // Constructor. - constructor (props) { - super(props); - assignHandlers(this, handlers); - this.state = { - hovered: false, - focused: false, - dirtyDescription: null, - }; - } - - // Rendering. - render () { - const { - handleBlur, - handleChange, - handleFocus, - handleMouseEnter, - handleMouseLeave, - handleRemove, - handleFocalPointClick, - } = this.handlers; - const { - intl, - preview, - focusX, - focusY, - mediaType, - } = this.props; - const { - focused, - hovered, - dirtyDescription, - } = this.state; - const active = hovered || focused || isUserTouching(); - const computedClass = classNames('composer--upload_form--item', { active }); - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; - const description = dirtyDescription || (dirtyDescription !== '' && this.props.description) || ''; - - // The result. - return ( - <div - className={computedClass} - onMouseEnter={handleMouseEnter} - onMouseLeave={handleMouseLeave} - > - <Motion - defaultStyle={{ scale: 0.8 }} - style={{ - scale: spring(1, { - stiffness: 180, - damping: 12, - }), - }} - > - {({ scale }) => ( - <div - style={{ - transform: `scale(${scale})`, - backgroundImage: preview ? `url(${preview})` : null, - backgroundPosition: `${x}% ${y}%` - }} - > - <div className={classNames('composer--upload_form--actions', { active })}> - <button className='icon-button' onClick={handleRemove}> - <i className='fa fa-times' /> <FormattedMessage {...messages.undo} /> - </button> - {mediaType === 'image' && <button className='icon-button' onClick={handleFocalPointClick}><i className='fa fa-crosshairs' /> <FormattedMessage {...messages.crop} /></button>} - </div> - <label> - <span style={{ display: 'none' }}><FormattedMessage {...messages.description} /></span> - <textarea - maxLength={420} - onBlur={handleBlur} - onChange={handleChange} - onFocus={handleFocus} - placeholder={intl.formatMessage(messages.description)} - value={description} - /> - </label> - </div> - )} - </Motion> - </div> - ); - } - -} - -// Props. -ComposerUploadFormItem.propTypes = { - description: PropTypes.string, - id: PropTypes.string, - intl: PropTypes.object.isRequired, - onChangeDescription: PropTypes.func.isRequired, - onOpenFocalPointModal: PropTypes.func.isRequired, - onRemove: PropTypes.func.isRequired, - focusX: PropTypes.number, - focusY: PropTypes.number, - mediaType: PropTypes.string, - preview: PropTypes.string, -}; diff --git a/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js b/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js deleted file mode 100644 index 8c4b0eea6..000000000 --- a/app/javascript/flavours/glitch/features/composer/upload_form/progress/index.js +++ /dev/null @@ -1,52 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import { - defineMessages, - FormattedMessage, -} from 'react-intl'; -import spring from 'react-motion/lib/spring'; - -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import Motion from 'flavours/glitch/util/optional_motion'; - -// Messages. -const messages = defineMessages({ - upload: { - defaultMessage: 'Uploading...', - id: 'upload_progress.label', - }, -}); - -// The component. -export default function ComposerUploadFormProgress ({ progress }) { - - // The result. - return ( - <div className='composer--upload_form--progress'> - <Icon icon='upload' /> - <div className='message'> - <FormattedMessage {...messages.upload} /> - <div className='backdrop'> - <Motion - defaultStyle={{ width: 0 }} - style={{ width: spring(progress) }} - > - {({ width }) => - (<div - className='tracker' - style={{ width: `${width}%` }} - />) - } - </Motion> - </div> - </div> - </div> - ); -} - -// Props. -ComposerUploadFormProgress.propTypes = { progress: PropTypes.number }; diff --git a/app/javascript/flavours/glitch/features/composer/warning/index.js b/app/javascript/flavours/glitch/features/composer/warning/index.js deleted file mode 100644 index 8be8acbce..000000000 --- a/app/javascript/flavours/glitch/features/composer/warning/index.js +++ /dev/null @@ -1,59 +0,0 @@ -import React from 'react'; -import Motion from 'flavours/glitch/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import { defineMessages, FormattedMessage } from 'react-intl'; -import { profileLink } from 'flavours/glitch/util/backend_links'; - -// This is the spring used with our motion. -const motionSpring = spring(1, { damping: 35, stiffness: 400 }); - -// Messages. -const messages = defineMessages({ - disclaimer: { - defaultMessage: 'Your account is not {locked}. Anyone can follow you to view your follower-only posts.', - id: 'compose_form.lock_disclaimer', - }, - locked: { - defaultMessage: 'locked', - id: 'compose_form.lock_disclaimer.lock', - }, -}); - -// The component. -export default function ComposerWarning () { - let lockedLink = <FormattedMessage {...messages.locked} />; - if (profileLink !== undefined) { - lockedLink = <a href={profileLink}>{lockedLink}</a>; - } - return ( - <Motion - defaultStyle={{ - opacity: 0, - scaleX: 0.85, - scaleY: 0.75, - }} - style={{ - opacity: motionSpring, - scaleX: motionSpring, - scaleY: motionSpring, - }} - > - {({ opacity, scaleX, scaleY }) => ( - <div - className='composer--warning' - style={{ - opacity: opacity, - transform: `scale(${scaleX}, ${scaleY})`, - }} - > - <FormattedMessage - {...messages.disclaimer} - values={{ locked: lockedLink }} - /> - </div> - )} - </Motion> - ); -} - -ComposerWarning.propTypes = {}; |