From 81ef21a0c802f1d905f37a2a818544a8b400793c Mon Sep 17 00:00:00 2001 From: Renaud Chaput Date: Sat, 25 Feb 2023 14:34:32 +0100 Subject: [Glitch] Rename JSX files with proper `.jsx` extension Port 44a7d87cb1f5df953b6c14c16c59e2e4ead1bcb9 to glitch-soc Signed-off-by: Claire --- .../features/compose/components/action_bar.js | 68 ---- .../features/compose/components/action_bar.jsx | 68 ++++ .../compose/components/autosuggest_account.js | 24 -- .../compose/components/autosuggest_account.jsx | 24 ++ .../compose/components/character_counter.js | 25 -- .../compose/components/character_counter.jsx | 25 ++ .../features/compose/components/compose_form.js | 391 ------------------- .../features/compose/components/compose_form.jsx | 391 +++++++++++++++++++ .../glitch/features/compose/components/dropdown.js | 243 ------------ .../features/compose/components/dropdown.jsx | 243 ++++++++++++ .../features/compose/components/dropdown_menu.js | 199 ---------- .../features/compose/components/dropdown_menu.jsx | 199 ++++++++++ .../compose/components/emoji_picker_dropdown.js | 413 --------------------- .../compose/components/emoji_picker_dropdown.jsx | 413 +++++++++++++++++++++ .../glitch/features/compose/components/header.js | 136 ------- .../glitch/features/compose/components/header.jsx | 136 +++++++ .../compose/components/language_dropdown.js | 327 ---------------- .../compose/components/language_dropdown.jsx | 327 ++++++++++++++++ .../features/compose/components/navigation_bar.js | 46 --- .../features/compose/components/navigation_bar.jsx | 46 +++ .../glitch/features/compose/components/options.js | 317 ---------------- .../glitch/features/compose/components/options.jsx | 317 ++++++++++++++++ .../features/compose/components/poll_form.js | 170 --------- .../features/compose/components/poll_form.jsx | 170 +++++++++ .../compose/components/privacy_dropdown.js | 88 ----- .../compose/components/privacy_dropdown.jsx | 88 +++++ .../features/compose/components/publisher.js | 99 ----- .../features/compose/components/publisher.jsx | 99 +++++ .../features/compose/components/reply_indicator.js | 82 ---- .../compose/components/reply_indicator.jsx | 82 ++++ .../glitch/features/compose/components/search.js | 168 --------- .../glitch/features/compose/components/search.jsx | 168 +++++++++ .../features/compose/components/search_results.js | 141 ------- .../features/compose/components/search_results.jsx | 141 +++++++ .../compose/components/text_icon_button.js | 38 -- .../compose/components/text_icon_button.jsx | 38 ++ .../features/compose/components/textarea_icons.js | 60 --- .../features/compose/components/textarea_icons.jsx | 60 +++ .../glitch/features/compose/components/upload.js | 67 ---- .../glitch/features/compose/components/upload.jsx | 67 ++++ .../features/compose/components/upload_form.js | 34 -- .../features/compose/components/upload_form.jsx | 34 ++ .../features/compose/components/upload_progress.js | 52 --- .../compose/components/upload_progress.jsx | 52 +++ .../glitch/features/compose/components/warning.js | 26 -- .../glitch/features/compose/components/warning.jsx | 26 ++ .../containers/sensitive_button_container.js | 75 ---- .../containers/sensitive_button_container.jsx | 75 ++++ .../compose/containers/warning_container.js | 68 ---- .../compose/containers/warning_container.jsx | 68 ++++ .../flavours/glitch/features/compose/index.js | 116 ------ .../flavours/glitch/features/compose/index.jsx | 116 ++++++ 52 files changed, 3473 insertions(+), 3473 deletions(-) delete mode 100644 app/javascript/flavours/glitch/features/compose/components/action_bar.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/action_bar.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/character_counter.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/character_counter.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/compose_form.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/compose_form.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/dropdown.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/dropdown.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/header.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/header.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/language_dropdown.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/navigation_bar.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/options.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/options.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/poll_form.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/poll_form.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/publisher.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/publisher.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/reply_indicator.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/search.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/search.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/search_results.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/search_results.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/text_icon_button.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/text_icon_button.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/textarea_icons.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/textarea_icons.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/upload.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/upload.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/upload_form.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/upload_form.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/upload_progress.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/components/warning.js create mode 100644 app/javascript/flavours/glitch/features/compose/components/warning.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/containers/warning_container.js create mode 100644 app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx delete mode 100644 app/javascript/flavours/glitch/features/compose/index.js create mode 100644 app/javascript/flavours/glitch/features/compose/index.jsx (limited to 'app/javascript/flavours/glitch/features/compose') diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.js b/app/javascript/flavours/glitch/features/compose/components/action_bar.js deleted file mode 100644 index 1843fdacb..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.js +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; -import { defineMessages, injectIntl } from 'react-intl'; -import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links'; - -const messages = defineMessages({ - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, - preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, - follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, - lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, - followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, - blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, - mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, - filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, - logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, - bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, -}); - -export default @injectIntl -class ActionBar extends React.PureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onLogout: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - handleLogout = () => { - this.props.onLogout(); - }; - - render () { - const { intl } = this.props; - - let menu = []; - - menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink }); - menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink }); - menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); - menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); - menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }); - menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); - menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); - menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); - menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); - menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' }); - menu.push(null); - menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout }); - - return ( -
-
- -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx new file mode 100644 index 000000000..1843fdacb --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; +import { defineMessages, injectIntl } from 'react-intl'; +import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links'; + +const messages = defineMessages({ + edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, + pins: { id: 'navigation_bar.pins', defaultMessage: 'Pinned posts' }, + preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' }, + follow_requests: { id: 'navigation_bar.follow_requests', defaultMessage: 'Follow requests' }, + favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favourites' }, + lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, + followed_tags: { id: 'navigation_bar.followed_tags', defaultMessage: 'Followed hashtags' }, + blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, + domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' }, + mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, + filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' }, + logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' }, + bookmarks: { id: 'navigation_bar.bookmarks', defaultMessage: 'Bookmarks' }, +}); + +export default @injectIntl +class ActionBar extends React.PureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + onLogout: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleLogout = () => { + this.props.onLogout(); + }; + + render () { + const { intl } = this.props; + + let menu = []; + + menu.push({ text: intl.formatMessage(messages.edit_profile), href: profileLink }); + menu.push({ text: intl.formatMessage(messages.preferences), href: preferencesLink }); + menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); + menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' }); + menu.push({ text: intl.formatMessage(messages.bookmarks), to: '/bookmarks' }); + menu.push({ text: intl.formatMessage(messages.lists), to: '/lists' }); + menu.push({ text: intl.formatMessage(messages.followed_tags), to: '/followed_tags' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.mutes), to: '/mutes' }); + menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' }); + menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' }); + menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' }); + menu.push(null); + menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout }); + + return ( +
+
+ +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js deleted file mode 100644 index fb9bb5035..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.js +++ /dev/null @@ -1,24 +0,0 @@ -import React from 'react'; -import Avatar from 'flavours/glitch/components/avatar'; -import DisplayName from 'flavours/glitch/components/display_name'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -export default class AutosuggestAccount extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - }; - - render () { - const { account } = this.props; - - return ( -
-
- -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx new file mode 100644 index 000000000..fb9bb5035 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx @@ -0,0 +1,24 @@ +import React from 'react'; +import Avatar from 'flavours/glitch/components/avatar'; +import DisplayName from 'flavours/glitch/components/display_name'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +export default class AutosuggestAccount extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + }; + + render () { + const { account } = this.props; + + return ( +
+
+ +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/character_counter.js b/app/javascript/flavours/glitch/features/compose/components/character_counter.js deleted file mode 100644 index 0ecfc9141..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/character_counter.js +++ /dev/null @@ -1,25 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { length } from 'stringz'; - -export default class CharacterCounter extends React.PureComponent { - - static propTypes = { - text: PropTypes.string.isRequired, - max: PropTypes.number.isRequired, - }; - - checkRemainingText (diff) { - if (diff < 0) { - return {diff}; - } - - return {diff}; - } - - render () { - const diff = this.props.max - length(this.props.text); - return this.checkRemainingText(diff); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx new file mode 100644 index 000000000..0ecfc9141 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/character_counter.jsx @@ -0,0 +1,25 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { length } from 'stringz'; + +export default class CharacterCounter extends React.PureComponent { + + static propTypes = { + text: PropTypes.string.isRequired, + max: PropTypes.number.isRequired, + }; + + checkRemainingText (diff) { + if (diff < 0) { + return {diff}; + } + + return {diff}; + } + + render () { + const diff = this.props.max - length(this.props.text); + return this.checkRemainingText(diff); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js deleted file mode 100644 index 696188f31..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ /dev/null @@ -1,391 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import ReplyIndicatorContainer from '../containers/reply_indicator_container'; -import AutosuggestTextarea from '../../../components/autosuggest_textarea'; -import AutosuggestInput from '../../../components/autosuggest_input'; -import { defineMessages, injectIntl } from 'react-intl'; -import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; -import PollFormContainer from '../containers/poll_form_container'; -import UploadFormContainer from '../containers/upload_form_container'; -import WarningContainer from '../containers/warning_container'; -import { isMobile } from 'flavours/glitch/is_mobile'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { countableText } from '../util/counter'; -import OptionsContainer from '../containers/options_container'; -import Publisher from './publisher'; -import TextareaIcons from './textarea_icons'; -import { maxChars } from 'flavours/glitch/initial_state'; -import CharacterCounter from './character_counter'; -import { length } from 'stringz'; - -const messages = defineMessages({ - placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, - 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', - }, - spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, -}); - -export default @injectIntl -class ComposeForm extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - intl: PropTypes.object.isRequired, - text: PropTypes.string, - suggestions: ImmutablePropTypes.list, - spoiler: PropTypes.bool, - privacy: PropTypes.string, - spoilerText: PropTypes.string, - focusDate: PropTypes.instanceOf(Date), - caretPosition: PropTypes.number, - preselectDate: PropTypes.instanceOf(Date), - isSubmitting: PropTypes.bool, - isChangingUpload: PropTypes.bool, - isEditing: PropTypes.bool, - isUploading: PropTypes.bool, - onChange: PropTypes.func, - onSubmit: PropTypes.func, - onClearSuggestions: PropTypes.func, - onFetchSuggestions: PropTypes.func, - onSuggestionSelected: PropTypes.func, - onChangeSpoilerText: PropTypes.func, - onPaste: PropTypes.func, - onPickEmoji: PropTypes.func, - showSearch: PropTypes.bool, - anyMedia: PropTypes.bool, - isInReply: PropTypes.bool, - singleColumn: PropTypes.bool, - lang: PropTypes.string, - - advancedOptions: ImmutablePropTypes.map, - layout: PropTypes.string, - media: ImmutablePropTypes.list, - sideArm: PropTypes.string, - sensitive: PropTypes.bool, - spoilersAlwaysOn: PropTypes.bool, - mediaDescriptionConfirmation: PropTypes.bool, - preselectOnReply: PropTypes.bool, - onChangeSpoilerness: PropTypes.func, - onChangeVisibility: PropTypes.func, - onPaste: PropTypes.func, - onMediaDescriptionConfirm: PropTypes.func, - }; - - static defaultProps = { - showSearch: false, - }; - - handleChange = (e) => { - this.props.onChange(e.target.value); - }; - - getFulltextForCharacterCounting = () => { - return [ - this.props.spoiler? this.props.spoilerText: '', - countableText(this.props.text), - this.props.advancedOptions && this.props.advancedOptions.get('do_not_federate') ? ' šŸ‘ļø' : '', - ].join(''); - }; - - canSubmit = () => { - const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props; - const fulltext = this.getFulltextForCharacterCounting(); - - return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (!fulltext.trim().length && !anyMedia)); - }; - - handleSubmit = (overriddenVisibility = null) => { - const { - onSubmit, - media, - mediaDescriptionConfirmation, - onMediaDescriptionConfirm, - onChangeVisibility, - } = this.props; - - if (this.props.text !== this.textarea.value) { - // Something changed the text inside the textarea (e.g. browser extensions like Grammarly) - // Update the state to match the current text - this.props.onChange(this.textarea.value); - } - - if (!this.canSubmit()) { - return; - } - - // Submit unless there are media with missing descriptions - if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) { - const firstWithoutDescription = media.find(item => !item.get('description')); - onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id'), overriddenVisibility); - } else if (onSubmit) { - if (onChangeVisibility && overriddenVisibility) { - onChangeVisibility(overriddenVisibility); - } - onSubmit(this.context.router ? this.context.router.history : null); - } - }; - - // Changes the text value of the spoiler. - handleChangeSpoiler = ({ target: { value } }) => { - const { onChangeSpoilerText } = this.props; - if (onChangeSpoilerText) { - onChangeSpoilerText(value); - } - }; - - setRef = c => { - this.composeForm = c; - }; - - // Inserts an emoji at the caret. - handleEmojiPick = (data) => { - const { textarea: { selectionStart } } = this; - const { onPickEmoji } = this.props; - if (onPickEmoji) { - onPickEmoji(selectionStart, data); - } - }; - - // Handles the secondary submit button. - handleSecondarySubmit = () => { - const { - sideArm, - } = this.props; - this.handleSubmit(sideArm === 'none' ? null : sideArm); - }; - - // Selects a suggestion from the autofill. - onSuggestionSelected = (tokenStart, token, value) => { - this.props.onSuggestionSelected(tokenStart, token, value, ['text']); - }; - - onSpoilerSuggestionSelected = (tokenStart, token, value) => { - this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); - }; - - handleKeyDown = (e) => { - if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { - this.handleSubmit(); - } - - if (e.keyCode == 13 && e.altKey) { - this.handleSecondarySubmit(); - } - }; - - // Sets a reference to the textarea. - setAutosuggestTextarea = (textareaComponent) => { - if (textareaComponent) { - this.textarea = textareaComponent.textarea; - } - }; - - // Sets a reference to the CW field. - handleRefSpoilerText = (spoilerComponent) => { - if (spoilerComponent) { - this.spoilerText = spoilerComponent.input; - } - }; - - handleFocus = () => { - if (this.composeForm && !this.props.singleColumn) { - const { left, right } = this.composeForm.getBoundingClientRect(); - if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { - this.composeForm.scrollIntoView(); - } - } - }; - - componentDidMount () { - this._updateFocusAndSelection({ }); - } - - componentDidUpdate (prevProps) { - this._updateFocusAndSelection(prevProps); - } - - // 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. - _updateFocusAndSelection = (prevProps) => { - const { - textarea, - spoilerText, - } = this; - const { - focusDate, - caretPosition, - isSubmitting, - preselectDate, - text, - preselectOnReply, - singleColumn, - } = this.props; - let selectionEnd, selectionStart; - - // Caret/selection handling. - if (focusDate !== prevProps.focusDate) { - switch (true) { - case preselectDate !== prevProps.preselectDate && this.props.isInReply && 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) { - // Because of the wicg-inert polyfill, the activeElement may not be - // immediately selectable, we have to wait for observers to run, as - // described in https://github.com/WICG/inert#performance-and-gotchas - Promise.resolve().then(() => { - textarea.setSelectionRange(selectionStart, selectionEnd); - textarea.focus(); - if (!singleColumn) textarea.scrollIntoView(); - }).catch(console.error); - } - - // 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 { - handleEmojiPick, - handleSecondarySubmit, - handleSelect, - handleSubmit, - handleRefTextarea, - } = this; - const { - advancedOptions, - intl, - isSubmitting, - layout, - onChangeSpoilerness, - onChangeVisibility, - onClearSuggestions, - onFetchSuggestions, - onPaste, - privacy, - sensitive, - showSearch, - sideArm, - spoiler, - spoilerText, - suggestions, - spoilersAlwaysOn, - isEditing, - } = this.props; - - const countText = this.getFulltextForCharacterCounting(); - - return ( -
- - - - -
- -
- - - - -
- - -
-
- -
- 0)} - spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} - /> -
- -
-
- - -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx new file mode 100644 index 000000000..696188f31 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -0,0 +1,391 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import ReplyIndicatorContainer from '../containers/reply_indicator_container'; +import AutosuggestTextarea from '../../../components/autosuggest_textarea'; +import AutosuggestInput from '../../../components/autosuggest_input'; +import { defineMessages, injectIntl } from 'react-intl'; +import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; +import PollFormContainer from '../containers/poll_form_container'; +import UploadFormContainer from '../containers/upload_form_container'; +import WarningContainer from '../containers/warning_container'; +import { isMobile } from 'flavours/glitch/is_mobile'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { countableText } from '../util/counter'; +import OptionsContainer from '../containers/options_container'; +import Publisher from './publisher'; +import TextareaIcons from './textarea_icons'; +import { maxChars } from 'flavours/glitch/initial_state'; +import CharacterCounter from './character_counter'; +import { length } from 'stringz'; + +const messages = defineMessages({ + placeholder: { id: 'compose_form.placeholder', defaultMessage: 'What is on your mind?' }, + 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', + }, + spoiler_placeholder: { id: 'compose_form.spoiler_placeholder', defaultMessage: 'Write your warning here' }, +}); + +export default @injectIntl +class ComposeForm extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + intl: PropTypes.object.isRequired, + text: PropTypes.string, + suggestions: ImmutablePropTypes.list, + spoiler: PropTypes.bool, + privacy: PropTypes.string, + spoilerText: PropTypes.string, + focusDate: PropTypes.instanceOf(Date), + caretPosition: PropTypes.number, + preselectDate: PropTypes.instanceOf(Date), + isSubmitting: PropTypes.bool, + isChangingUpload: PropTypes.bool, + isEditing: PropTypes.bool, + isUploading: PropTypes.bool, + onChange: PropTypes.func, + onSubmit: PropTypes.func, + onClearSuggestions: PropTypes.func, + onFetchSuggestions: PropTypes.func, + onSuggestionSelected: PropTypes.func, + onChangeSpoilerText: PropTypes.func, + onPaste: PropTypes.func, + onPickEmoji: PropTypes.func, + showSearch: PropTypes.bool, + anyMedia: PropTypes.bool, + isInReply: PropTypes.bool, + singleColumn: PropTypes.bool, + lang: PropTypes.string, + + advancedOptions: ImmutablePropTypes.map, + layout: PropTypes.string, + media: ImmutablePropTypes.list, + sideArm: PropTypes.string, + sensitive: PropTypes.bool, + spoilersAlwaysOn: PropTypes.bool, + mediaDescriptionConfirmation: PropTypes.bool, + preselectOnReply: PropTypes.bool, + onChangeSpoilerness: PropTypes.func, + onChangeVisibility: PropTypes.func, + onPaste: PropTypes.func, + onMediaDescriptionConfirm: PropTypes.func, + }; + + static defaultProps = { + showSearch: false, + }; + + handleChange = (e) => { + this.props.onChange(e.target.value); + }; + + getFulltextForCharacterCounting = () => { + return [ + this.props.spoiler? this.props.spoilerText: '', + countableText(this.props.text), + this.props.advancedOptions && this.props.advancedOptions.get('do_not_federate') ? ' šŸ‘ļø' : '', + ].join(''); + }; + + canSubmit = () => { + const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props; + const fulltext = this.getFulltextForCharacterCounting(); + + return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (!fulltext.trim().length && !anyMedia)); + }; + + handleSubmit = (overriddenVisibility = null) => { + const { + onSubmit, + media, + mediaDescriptionConfirmation, + onMediaDescriptionConfirm, + onChangeVisibility, + } = this.props; + + if (this.props.text !== this.textarea.value) { + // Something changed the text inside the textarea (e.g. browser extensions like Grammarly) + // Update the state to match the current text + this.props.onChange(this.textarea.value); + } + + if (!this.canSubmit()) { + return; + } + + // Submit unless there are media with missing descriptions + if (mediaDescriptionConfirmation && onMediaDescriptionConfirm && media && media.some(item => !item.get('description'))) { + const firstWithoutDescription = media.find(item => !item.get('description')); + onMediaDescriptionConfirm(this.context.router ? this.context.router.history : null, firstWithoutDescription.get('id'), overriddenVisibility); + } else if (onSubmit) { + if (onChangeVisibility && overriddenVisibility) { + onChangeVisibility(overriddenVisibility); + } + onSubmit(this.context.router ? this.context.router.history : null); + } + }; + + // Changes the text value of the spoiler. + handleChangeSpoiler = ({ target: { value } }) => { + const { onChangeSpoilerText } = this.props; + if (onChangeSpoilerText) { + onChangeSpoilerText(value); + } + }; + + setRef = c => { + this.composeForm = c; + }; + + // Inserts an emoji at the caret. + handleEmojiPick = (data) => { + const { textarea: { selectionStart } } = this; + const { onPickEmoji } = this.props; + if (onPickEmoji) { + onPickEmoji(selectionStart, data); + } + }; + + // Handles the secondary submit button. + handleSecondarySubmit = () => { + const { + sideArm, + } = this.props; + this.handleSubmit(sideArm === 'none' ? null : sideArm); + }; + + // Selects a suggestion from the autofill. + onSuggestionSelected = (tokenStart, token, value) => { + this.props.onSuggestionSelected(tokenStart, token, value, ['text']); + }; + + onSpoilerSuggestionSelected = (tokenStart, token, value) => { + this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']); + }; + + handleKeyDown = (e) => { + if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { + this.handleSubmit(); + } + + if (e.keyCode == 13 && e.altKey) { + this.handleSecondarySubmit(); + } + }; + + // Sets a reference to the textarea. + setAutosuggestTextarea = (textareaComponent) => { + if (textareaComponent) { + this.textarea = textareaComponent.textarea; + } + }; + + // Sets a reference to the CW field. + handleRefSpoilerText = (spoilerComponent) => { + if (spoilerComponent) { + this.spoilerText = spoilerComponent.input; + } + }; + + handleFocus = () => { + if (this.composeForm && !this.props.singleColumn) { + const { left, right } = this.composeForm.getBoundingClientRect(); + if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { + this.composeForm.scrollIntoView(); + } + } + }; + + componentDidMount () { + this._updateFocusAndSelection({ }); + } + + componentDidUpdate (prevProps) { + this._updateFocusAndSelection(prevProps); + } + + // 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. + _updateFocusAndSelection = (prevProps) => { + const { + textarea, + spoilerText, + } = this; + const { + focusDate, + caretPosition, + isSubmitting, + preselectDate, + text, + preselectOnReply, + singleColumn, + } = this.props; + let selectionEnd, selectionStart; + + // Caret/selection handling. + if (focusDate !== prevProps.focusDate) { + switch (true) { + case preselectDate !== prevProps.preselectDate && this.props.isInReply && 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) { + // Because of the wicg-inert polyfill, the activeElement may not be + // immediately selectable, we have to wait for observers to run, as + // described in https://github.com/WICG/inert#performance-and-gotchas + Promise.resolve().then(() => { + textarea.setSelectionRange(selectionStart, selectionEnd); + textarea.focus(); + if (!singleColumn) textarea.scrollIntoView(); + }).catch(console.error); + } + + // 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 { + handleEmojiPick, + handleSecondarySubmit, + handleSelect, + handleSubmit, + handleRefTextarea, + } = this; + const { + advancedOptions, + intl, + isSubmitting, + layout, + onChangeSpoilerness, + onChangeVisibility, + onClearSuggestions, + onFetchSuggestions, + onPaste, + privacy, + sensitive, + showSearch, + sideArm, + spoiler, + spoilerText, + suggestions, + spoilersAlwaysOn, + isEditing, + } = this.props; + + const countText = this.getFulltextForCharacterCounting(); + + return ( +
+ + + + +
+ +
+ + + + +
+ + +
+
+ +
+ 0)} + spoiler={spoilersAlwaysOn ? (spoilerText && spoilerText.length > 0) : spoiler} + /> +
+ +
+
+ + +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js deleted file mode 100644 index fe4ab36f5..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ /dev/null @@ -1,243 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import Overlay from 'react-overlays/Overlay'; - -// Components. -import IconButton from 'flavours/glitch/components/icon_button'; -import DropdownMenu from './dropdown_menu'; - -// Utils. -import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; - -// The component. -export default class ComposerOptionsDropdown extends React.PureComponent { - - static propTypes = { - isUserTouching: PropTypes.func, - disabled: PropTypes.bool, - icon: PropTypes.string, - items: PropTypes.arrayOf(PropTypes.shape({ - icon: PropTypes.string, - meta: PropTypes.string, - name: PropTypes.string.isRequired, - text: PropTypes.string, - })).isRequired, - onModalOpen: PropTypes.func, - onModalClose: PropTypes.func, - title: PropTypes.string, - value: PropTypes.string, - onChange: PropTypes.func, - container: PropTypes.func, - renderItemContents: PropTypes.func, - closeOnChange: PropTypes.bool, - }; - - static defaultProps = { - closeOnChange: true, - }; - - state = { - open: false, - openedViaKeyboard: undefined, - placement: 'bottom', - }; - - // Toggles opening and closing the dropdown. - handleToggle = ({ type }) => { - const { onModalOpen } = this.props; - const { open } = this.state; - - if (this.props.isUserTouching && this.props.isUserTouching()) { - if (this.state.open) { - this.props.onModalClose(); - } else { - const modal = this.handleMakeModal(); - if (modal && onModalOpen) { - onModalOpen(modal); - } - } - } else { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' }); - } - }; - - handleKeyDown = (e) => { - switch (e.key) { - case 'Escape': - this.handleClose(); - break; - } - }; - - handleMouseDown = () => { - if (!this.state.open) { - this.activeElement = document.activeElement; - } - }; - - handleButtonKeyDown = (e) => { - switch(e.key) { - case ' ': - case 'Enter': - this.handleMouseDown(); - break; - } - }; - - handleKeyPress = (e) => { - switch(e.key) { - case ' ': - case 'Enter': - this.handleToggle(e); - e.stopPropagation(); - e.preventDefault(); - break; - } - }; - - handleClose = () => { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - this.setState({ open: false }); - }; - - handleItemClick = (e) => { - const { - items, - onChange, - onModalClose, - closeOnChange, - } = this.props; - - const i = Number(e.currentTarget.getAttribute('data-index')); - - const { name } = items[i]; - - e.preventDefault(); // Prevents focus from changing - if (closeOnChange) onModalClose(); - onChange(name); - }; - - // Creates an action modal object. - handleMakeModal = () => { - const { - items, - onChange, - onModalOpen, - onModalClose, - value, - } = this.props; - - // Required props. - if (!(onChange && onModalOpen && onModalClose && items)) { - return null; - } - - // The object. - return { - renderItemContents: this.props.renderItemContents, - onClick: this.handleItemClick, - actions: items.map( - ({ - name, - ...rest - }) => ({ - ...rest, - active: value && name === value, - name, - }), - ), - }; - }; - - setTargetRef = c => { - this.target = c; - }; - - findTarget = () => { - return this.target; - }; - - handleOverlayEnter = (state) => { - this.setState({ placement: state.placement }); - }; - - // Rendering. - render () { - const { - disabled, - title, - icon, - items, - onChange, - value, - container, - renderItemContents, - closeOnChange, - } = this.props; - const { open, placement } = this.state; - - const active = value && items.findIndex(({ name }) => name === value) === (placement === 'bottom' ? 0 : (items.length - 1)); - - return ( -
-
- -
- - - {({ props, placement }) => ( -
-
- -
-
- )} -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx new file mode 100644 index 000000000..fe4ab36f5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx @@ -0,0 +1,243 @@ +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import Overlay from 'react-overlays/Overlay'; + +// Components. +import IconButton from 'flavours/glitch/components/icon_button'; +import DropdownMenu from './dropdown_menu'; + +// Utils. +import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; + +// The component. +export default class ComposerOptionsDropdown extends React.PureComponent { + + static propTypes = { + isUserTouching: PropTypes.func, + disabled: PropTypes.bool, + icon: PropTypes.string, + items: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.string, + meta: PropTypes.string, + name: PropTypes.string.isRequired, + text: PropTypes.string, + })).isRequired, + onModalOpen: PropTypes.func, + onModalClose: PropTypes.func, + title: PropTypes.string, + value: PropTypes.string, + onChange: PropTypes.func, + container: PropTypes.func, + renderItemContents: PropTypes.func, + closeOnChange: PropTypes.bool, + }; + + static defaultProps = { + closeOnChange: true, + }; + + state = { + open: false, + openedViaKeyboard: undefined, + placement: 'bottom', + }; + + // Toggles opening and closing the dropdown. + handleToggle = ({ type }) => { + const { onModalOpen } = this.props; + const { open } = this.state; + + if (this.props.isUserTouching && this.props.isUserTouching()) { + if (this.state.open) { + this.props.onModalClose(); + } else { + const modal = this.handleMakeModal(); + if (modal && onModalOpen) { + onModalOpen(modal); + } + } + } else { + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } + this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' }); + } + }; + + handleKeyDown = (e) => { + switch (e.key) { + case 'Escape': + this.handleClose(); + break; + } + }; + + handleMouseDown = () => { + if (!this.state.open) { + this.activeElement = document.activeElement; + } + }; + + handleButtonKeyDown = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleMouseDown(); + break; + } + }; + + handleKeyPress = (e) => { + switch(e.key) { + case ' ': + case 'Enter': + this.handleToggle(e); + e.stopPropagation(); + e.preventDefault(); + break; + } + }; + + handleClose = () => { + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } + this.setState({ open: false }); + }; + + handleItemClick = (e) => { + const { + items, + onChange, + onModalClose, + closeOnChange, + } = this.props; + + const i = Number(e.currentTarget.getAttribute('data-index')); + + const { name } = items[i]; + + e.preventDefault(); // Prevents focus from changing + if (closeOnChange) onModalClose(); + onChange(name); + }; + + // Creates an action modal object. + handleMakeModal = () => { + const { + items, + onChange, + onModalOpen, + onModalClose, + value, + } = this.props; + + // Required props. + if (!(onChange && onModalOpen && onModalClose && items)) { + return null; + } + + // The object. + return { + renderItemContents: this.props.renderItemContents, + onClick: this.handleItemClick, + actions: items.map( + ({ + name, + ...rest + }) => ({ + ...rest, + active: value && name === value, + name, + }), + ), + }; + }; + + setTargetRef = c => { + this.target = c; + }; + + findTarget = () => { + return this.target; + }; + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + }; + + // Rendering. + render () { + const { + disabled, + title, + icon, + items, + onChange, + value, + container, + renderItemContents, + closeOnChange, + } = this.props; + const { open, placement } = this.state; + + const active = value && items.findIndex(({ name }) => name === value) === (placement === 'bottom' ? 0 : (items.length - 1)); + + return ( +
+
+ +
+ + + {({ props, placement }) => ( +
+
+ +
+
+ )} +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js deleted file mode 100644 index 1ea0df536..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ /dev/null @@ -1,199 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import classNames from 'classnames'; - -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import { withPassive } from 'flavours/glitch/utils/dom_helpers'; -import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; - -// The component. -export default class ComposerOptionsDropdownContent extends React.PureComponent { - - static propTypes = { - items: PropTypes.arrayOf(PropTypes.shape({ - icon: PropTypes.string, - meta: PropTypes.node, - name: PropTypes.string.isRequired, - text: PropTypes.node, - })), - onChange: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - style: PropTypes.object, - value: PropTypes.string, - renderItemContents: PropTypes.func, - openedViaKeyboard: PropTypes.bool, - closeOnChange: PropTypes.bool, - }; - - static defaultProps = { - style: {}, - closeOnChange: true, - }; - - state = { - value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined, - }; - - // When the document is clicked elsewhere, we close the dropdown. - handleDocumentClick = (e) => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - }; - - // Stores our node in `this.node`. - setRef = (node) => { - this.node = node; - }; - - // On mounting, we add our listeners. - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, withPassive); - if (this.focusedItem) { - this.focusedItem.focus({ preventScroll: true }); - } else { - this.node.firstChild.focus({ preventScroll: true }); - } - } - - // On unmounting, we remove our listeners. - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, withPassive); - } - - handleClick = (e) => { - const i = Number(e.currentTarget.getAttribute('data-index')); - - const { - onChange, - onClose, - closeOnChange, - items, - } = this.props; - - const { name } = this.props.items[i]; - e.preventDefault(); // Prevents change in focus on click - if (closeOnChange) { - onClose(); - } - onChange(name); - }; - - // Handle changes differently whether the dropdown is a list of options or actions - handleChange = (name) => { - if (this.props.value) { - this.props.onChange(name); - } else { - this.setState({ value: name }); - } - }; - - handleKeyDown = (e) => { - const index = Number(e.currentTarget.getAttribute('data-index')); - const { items } = this.props; - let element = null; - - switch(e.key) { - case 'Escape': - this.props.onClose(); - break; - case 'Enter': - case ' ': - this.handleClick(e); - break; - case 'ArrowDown': - element = this.node.childNodes[index + 1] || this.node.firstChild; - break; - case 'ArrowUp': - element = this.node.childNodes[index - 1] || this.node.lastChild; - break; - case 'Tab': - if (e.shiftKey) { - element = this.node.childNodes[index - 1] || this.node.lastChild; - } else { - element = this.node.childNodes[index + 1] || this.node.firstChild; - } - break; - case 'Home': - element = this.node.firstChild; - break; - case 'End': - element = this.node.lastChild; - break; - } - - if (element) { - element.focus(); - this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name); - e.preventDefault(); - e.stopPropagation(); - } - }; - - setFocusRef = c => { - this.focusedItem = c; - }; - - renderItem = (item, i) => { - const { name, icon, meta, text } = item; - - const active = (name === (this.props.value || this.state.value)); - - const computedClass = classNames('privacy-dropdown__option', { active }); - - let contents = this.props.renderItemContents && this.props.renderItemContents(item, i); - - if (!contents) { - contents = ( - - {icon && } - -
- {text} - {meta} -
-
- ); - } - - return ( -
- {contents} -
- ); - }; - - // Rendering. - render () { - const { - items, - onChange, - onClose, - style, - } = this.props; - - // The result. - return ( -
- {!!items && items.map((item, i) => this.renderItem(item, i))} -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx new file mode 100644 index 000000000..1ea0df536 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx @@ -0,0 +1,199 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import classNames from 'classnames'; + +// Components. +import Icon from 'flavours/glitch/components/icon'; + +// Utils. +import { withPassive } from 'flavours/glitch/utils/dom_helpers'; +import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; + +// The component. +export default class ComposerOptionsDropdownContent extends React.PureComponent { + + static propTypes = { + items: PropTypes.arrayOf(PropTypes.shape({ + icon: PropTypes.string, + meta: PropTypes.node, + name: PropTypes.string.isRequired, + text: PropTypes.node, + })), + onChange: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + style: PropTypes.object, + value: PropTypes.string, + renderItemContents: PropTypes.func, + openedViaKeyboard: PropTypes.bool, + closeOnChange: PropTypes.bool, + }; + + static defaultProps = { + style: {}, + closeOnChange: true, + }; + + state = { + value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined, + }; + + // When the document is clicked elsewhere, we close the dropdown. + handleDocumentClick = (e) => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + }; + + // Stores our node in `this.node`. + setRef = (node) => { + this.node = node; + }; + + // On mounting, we add our listeners. + componentDidMount () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, withPassive); + if (this.focusedItem) { + this.focusedItem.focus({ preventScroll: true }); + } else { + this.node.firstChild.focus({ preventScroll: true }); + } + } + + // On unmounting, we remove our listeners. + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, withPassive); + } + + handleClick = (e) => { + const i = Number(e.currentTarget.getAttribute('data-index')); + + const { + onChange, + onClose, + closeOnChange, + items, + } = this.props; + + const { name } = this.props.items[i]; + e.preventDefault(); // Prevents change in focus on click + if (closeOnChange) { + onClose(); + } + onChange(name); + }; + + // Handle changes differently whether the dropdown is a list of options or actions + handleChange = (name) => { + if (this.props.value) { + this.props.onChange(name); + } else { + this.setState({ value: name }); + } + }; + + handleKeyDown = (e) => { + const index = Number(e.currentTarget.getAttribute('data-index')); + const { items } = this.props; + let element = null; + + switch(e.key) { + case 'Escape': + this.props.onClose(); + break; + case 'Enter': + case ' ': + this.handleClick(e); + break; + case 'ArrowDown': + element = this.node.childNodes[index + 1] || this.node.firstChild; + break; + case 'ArrowUp': + element = this.node.childNodes[index - 1] || this.node.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = this.node.childNodes[index - 1] || this.node.lastChild; + } else { + element = this.node.childNodes[index + 1] || this.node.firstChild; + } + break; + case 'Home': + element = this.node.firstChild; + break; + case 'End': + element = this.node.lastChild; + break; + } + + if (element) { + element.focus(); + this.handleChange(this.props.items[Number(element.getAttribute('data-index'))].name); + e.preventDefault(); + e.stopPropagation(); + } + }; + + setFocusRef = c => { + this.focusedItem = c; + }; + + renderItem = (item, i) => { + const { name, icon, meta, text } = item; + + const active = (name === (this.props.value || this.state.value)); + + const computedClass = classNames('privacy-dropdown__option', { active }); + + let contents = this.props.renderItemContents && this.props.renderItemContents(item, i); + + if (!contents) { + contents = ( + + {icon && } + +
+ {text} + {meta} +
+
+ ); + } + + return ( +
+ {contents} +
+ ); + }; + + // Rendering. + render () { + const { + items, + onChange, + onClose, + style, + } = this.props; + + // The result. + return ( +
+ {!!items && items.map((item, i) => this.renderItem(item, i))} +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js deleted file mode 100644 index 66355e088..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js +++ /dev/null @@ -1,413 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; -import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; -import Overlay from 'react-overlays/Overlay'; -import classNames from 'classnames'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; -import { useSystemEmojiFont } from 'flavours/glitch/initial_state'; -import { assetHost } from 'flavours/glitch/utils/config'; - -const messages = defineMessages({ - emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, - emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, - custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' }, - recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' }, - search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' }, - people: { id: 'emoji_button.people', defaultMessage: 'People' }, - nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' }, - food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' }, - activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' }, - travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' }, - objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' }, - symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' }, - flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }, -}); - -let EmojiPicker, Emoji; // load asynchronously - -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`; - -const notFoundFn = () => ( -
- - -
- -
-
-); - -class ModifierPickerMenu extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - onSelect: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - }; - - handleClick = e => { - this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); - }; - - componentWillReceiveProps (nextProps) { - if (nextProps.active) { - this.attachListeners(); - } else { - this.removeListeners(); - } - } - - componentWillUnmount () { - this.removeListeners(); - } - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - }; - - attachListeners () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - removeListeners () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - }; - - render () { - const { active } = this.props; - - return ( -
- - - - - - -
- ); - } - -} - -class ModifierPicker extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - modifier: PropTypes.number, - onChange: PropTypes.func, - onClose: PropTypes.func, - onOpen: PropTypes.func, - }; - - handleClick = () => { - if (this.props.active) { - this.props.onClose(); - } else { - this.props.onOpen(); - } - }; - - handleSelect = modifier => { - this.props.onChange(modifier); - this.props.onClose(); - }; - - render () { - const { active, modifier } = this.props; - - return ( -
- - -
- ); - } - -} - -@injectIntl -class EmojiPickerMenu extends React.PureComponent { - - static propTypes = { - custom_emojis: ImmutablePropTypes.list, - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - loading: PropTypes.bool, - onClose: PropTypes.func.isRequired, - onPick: PropTypes.func.isRequired, - style: PropTypes.object, - intl: PropTypes.object.isRequired, - skinTone: PropTypes.number.isRequired, - onSkinTone: PropTypes.func.isRequired, - }; - - static defaultProps = { - style: {}, - loading: true, - frequentlyUsedEmojis: [], - }; - - state = { - modifierOpen: false, - readyToFocus: false, - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - }; - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - - // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need - // to wait for a frame before focusing - requestAnimationFrame(() => { - this.setState({ readyToFocus: true }); - if (this.node) { - const element = this.node.querySelector('input[type="search"]'); - if (element) element.focus(); - } - }); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - }; - - getI18n = () => { - const { intl } = this.props; - - return { - search: intl.formatMessage(messages.emoji_search), - categories: { - search: intl.formatMessage(messages.search_results), - recent: intl.formatMessage(messages.recent), - people: intl.formatMessage(messages.people), - nature: intl.formatMessage(messages.nature), - foods: intl.formatMessage(messages.food), - activity: intl.formatMessage(messages.activity), - places: intl.formatMessage(messages.travel), - objects: intl.formatMessage(messages.objects), - symbols: intl.formatMessage(messages.symbols), - flags: intl.formatMessage(messages.flags), - custom: intl.formatMessage(messages.custom), - }, - }; - }; - - handleClick = (emoji, event) => { - if (!emoji.native) { - emoji.native = emoji.colons; - } - if (!(event.ctrlKey || event.metaKey)) { - this.props.onClose(); - } - this.props.onPick(emoji); - }; - - handleModifierOpen = () => { - this.setState({ modifierOpen: true }); - }; - - handleModifierClose = () => { - this.setState({ modifierOpen: false }); - }; - - handleModifierChange = modifier => { - this.props.onSkinTone(modifier); - }; - - render () { - const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props; - - if (loading) { - return
; - } - - const title = intl.formatMessage(messages.emoji); - - const { modifierOpen } = this.state; - - const categoriesSort = [ - 'recent', - 'people', - 'nature', - 'foods', - 'activity', - 'places', - 'objects', - 'symbols', - 'flags', - ]; - - categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort()); - - return ( -
- - - -
- ); - } - -} - -export default @injectIntl -class EmojiPickerDropdown extends React.PureComponent { - - static propTypes = { - custom_emojis: ImmutablePropTypes.list, - frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), - intl: PropTypes.object.isRequired, - onPickEmoji: PropTypes.func.isRequired, - onSkinTone: PropTypes.func.isRequired, - skinTone: PropTypes.number.isRequired, - button: PropTypes.node, - }; - - state = { - active: false, - loading: false, - }; - - setRef = (c) => { - this.dropdown = c; - }; - - onShowDropdown = () => { - this.setState({ active: true }); - - if (!EmojiPicker) { - this.setState({ loading: true }); - - EmojiPickerAsync().then(EmojiMart => { - EmojiPicker = EmojiMart.Picker; - Emoji = EmojiMart.Emoji; - - this.setState({ loading: false }); - }).catch(() => { - this.setState({ loading: false, active: false }); - }); - } - }; - - onHideDropdown = () => { - this.setState({ active: false }); - }; - - onToggle = (e) => { - if (!this.state.loading && (!e.key || e.key === 'Enter')) { - if (this.state.active) { - this.onHideDropdown(); - } else { - this.onShowDropdown(e); - } - } - }; - - handleKeyDown = e => { - if (e.key === 'Escape') { - this.onHideDropdown(); - } - }; - - setTargetRef = c => { - this.target = c; - }; - - findTarget = () => { - return this.target; - }; - - render () { - const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; - const title = intl.formatMessage(messages.emoji); - const { active, loading } = this.state; - - return ( -
-
- {button || šŸ™‚} -
- - - {({ props, placement })=> ( -
-
- -
-
- )} -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx new file mode 100644 index 000000000..66355e088 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.jsx @@ -0,0 +1,413 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { defineMessages, injectIntl, FormattedMessage } from 'react-intl'; +import { EmojiPicker as EmojiPickerAsync } from '../../ui/util/async-components'; +import Overlay from 'react-overlays/Overlay'; +import classNames from 'classnames'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import { buildCustomEmojis, categoriesFromEmojis } from '../../emoji/emoji'; +import { useSystemEmojiFont } from 'flavours/glitch/initial_state'; +import { assetHost } from 'flavours/glitch/utils/config'; + +const messages = defineMessages({ + emoji: { id: 'emoji_button.label', defaultMessage: 'Insert emoji' }, + emoji_search: { id: 'emoji_button.search', defaultMessage: 'Search...' }, + custom: { id: 'emoji_button.custom', defaultMessage: 'Custom' }, + recent: { id: 'emoji_button.recent', defaultMessage: 'Frequently used' }, + search_results: { id: 'emoji_button.search_results', defaultMessage: 'Search results' }, + people: { id: 'emoji_button.people', defaultMessage: 'People' }, + nature: { id: 'emoji_button.nature', defaultMessage: 'Nature' }, + food: { id: 'emoji_button.food', defaultMessage: 'Food & Drink' }, + activity: { id: 'emoji_button.activity', defaultMessage: 'Activity' }, + travel: { id: 'emoji_button.travel', defaultMessage: 'Travel & Places' }, + objects: { id: 'emoji_button.objects', defaultMessage: 'Objects' }, + symbols: { id: 'emoji_button.symbols', defaultMessage: 'Symbols' }, + flags: { id: 'emoji_button.flags', defaultMessage: 'Flags' }, +}); + +let EmojiPicker, Emoji; // load asynchronously + +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +const backgroundImageFn = () => `${assetHost}/emoji/sheet_13.png`; + +const notFoundFn = () => ( +
+ + +
+ +
+
+); + +class ModifierPickerMenu extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + onSelect: PropTypes.func.isRequired, + onClose: PropTypes.func.isRequired, + }; + + handleClick = e => { + this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1); + }; + + componentWillReceiveProps (nextProps) { + if (nextProps.active) { + this.attachListeners(); + } else { + this.removeListeners(); + } + } + + componentWillUnmount () { + this.removeListeners(); + } + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + }; + + attachListeners () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + removeListeners () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + }; + + render () { + const { active } = this.props; + + return ( +
+ + + + + + +
+ ); + } + +} + +class ModifierPicker extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + modifier: PropTypes.number, + onChange: PropTypes.func, + onClose: PropTypes.func, + onOpen: PropTypes.func, + }; + + handleClick = () => { + if (this.props.active) { + this.props.onClose(); + } else { + this.props.onOpen(); + } + }; + + handleSelect = modifier => { + this.props.onChange(modifier); + this.props.onClose(); + }; + + render () { + const { active, modifier } = this.props; + + return ( +
+ + +
+ ); + } + +} + +@injectIntl +class EmojiPickerMenu extends React.PureComponent { + + static propTypes = { + custom_emojis: ImmutablePropTypes.list, + frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), + loading: PropTypes.bool, + onClose: PropTypes.func.isRequired, + onPick: PropTypes.func.isRequired, + style: PropTypes.object, + intl: PropTypes.object.isRequired, + skinTone: PropTypes.number.isRequired, + onSkinTone: PropTypes.func.isRequired, + }; + + static defaultProps = { + style: {}, + loading: true, + frequentlyUsedEmojis: [], + }; + + state = { + modifierOpen: false, + readyToFocus: false, + }; + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + }; + + componentDidMount () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + + // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need + // to wait for a frame before focusing + requestAnimationFrame(() => { + this.setState({ readyToFocus: true }); + if (this.node) { + const element = this.node.querySelector('input[type="search"]'); + if (element) element.focus(); + } + }); + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + }; + + getI18n = () => { + const { intl } = this.props; + + return { + search: intl.formatMessage(messages.emoji_search), + categories: { + search: intl.formatMessage(messages.search_results), + recent: intl.formatMessage(messages.recent), + people: intl.formatMessage(messages.people), + nature: intl.formatMessage(messages.nature), + foods: intl.formatMessage(messages.food), + activity: intl.formatMessage(messages.activity), + places: intl.formatMessage(messages.travel), + objects: intl.formatMessage(messages.objects), + symbols: intl.formatMessage(messages.symbols), + flags: intl.formatMessage(messages.flags), + custom: intl.formatMessage(messages.custom), + }, + }; + }; + + handleClick = (emoji, event) => { + if (!emoji.native) { + emoji.native = emoji.colons; + } + if (!(event.ctrlKey || event.metaKey)) { + this.props.onClose(); + } + this.props.onPick(emoji); + }; + + handleModifierOpen = () => { + this.setState({ modifierOpen: true }); + }; + + handleModifierClose = () => { + this.setState({ modifierOpen: false }); + }; + + handleModifierChange = modifier => { + this.props.onSkinTone(modifier); + }; + + render () { + const { loading, style, intl, custom_emojis, skinTone, frequentlyUsedEmojis } = this.props; + + if (loading) { + return
; + } + + const title = intl.formatMessage(messages.emoji); + + const { modifierOpen } = this.state; + + const categoriesSort = [ + 'recent', + 'people', + 'nature', + 'foods', + 'activity', + 'places', + 'objects', + 'symbols', + 'flags', + ]; + + categoriesSort.splice(1, 0, ...Array.from(categoriesFromEmojis(custom_emojis)).sort()); + + return ( +
+ + + +
+ ); + } + +} + +export default @injectIntl +class EmojiPickerDropdown extends React.PureComponent { + + static propTypes = { + custom_emojis: ImmutablePropTypes.list, + frequentlyUsedEmojis: PropTypes.arrayOf(PropTypes.string), + intl: PropTypes.object.isRequired, + onPickEmoji: PropTypes.func.isRequired, + onSkinTone: PropTypes.func.isRequired, + skinTone: PropTypes.number.isRequired, + button: PropTypes.node, + }; + + state = { + active: false, + loading: false, + }; + + setRef = (c) => { + this.dropdown = c; + }; + + onShowDropdown = () => { + this.setState({ active: true }); + + if (!EmojiPicker) { + this.setState({ loading: true }); + + EmojiPickerAsync().then(EmojiMart => { + EmojiPicker = EmojiMart.Picker; + Emoji = EmojiMart.Emoji; + + this.setState({ loading: false }); + }).catch(() => { + this.setState({ loading: false, active: false }); + }); + } + }; + + onHideDropdown = () => { + this.setState({ active: false }); + }; + + onToggle = (e) => { + if (!this.state.loading && (!e.key || e.key === 'Enter')) { + if (this.state.active) { + this.onHideDropdown(); + } else { + this.onShowDropdown(e); + } + } + }; + + handleKeyDown = e => { + if (e.key === 'Escape') { + this.onHideDropdown(); + } + }; + + setTargetRef = c => { + this.target = c; + }; + + findTarget = () => { + return this.target; + }; + + render () { + const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; + const title = intl.formatMessage(messages.emoji); + const { active, loading } = this.state; + + return ( +
+
+ {button || šŸ™‚} +
+ + + {({ props, placement })=> ( +
+
+ +
+
+ )} +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/header.js b/app/javascript/flavours/glitch/features/compose/components/header.js deleted file mode 100644 index dcbdafa57..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/header.js +++ /dev/null @@ -1,136 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { injectIntl, defineMessages } from 'react-intl'; -import { Link } from 'react-router-dom'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; -import { signOutLink } from 'flavours/glitch/utils/backend_links'; - -// Messages. -const messages = defineMessages({ - community: { - defaultMessage: 'Local timeline', - id: 'navigation_bar.community_timeline', - }, - home_timeline: { - defaultMessage: 'Home', - id: 'tabs_bar.home', - }, - logout: { - defaultMessage: 'Logout', - id: 'navigation_bar.logout', - }, - notifications: { - defaultMessage: 'Notifications', - id: 'tabs_bar.notifications', - }, - public: { - defaultMessage: 'Federated timeline', - id: 'navigation_bar.public_timeline', - }, - settings: { - defaultMessage: 'App settings', - id: 'navigation_bar.app_settings', - }, - start: { - defaultMessage: 'Getting started', - id: 'getting_started.heading', - }, -}); - -export default @injectIntl -class Header extends ImmutablePureComponent { - - static propTypes = { - columns: ImmutablePropTypes.list, - unreadNotifications: PropTypes.number, - showNotificationsBadge: PropTypes.bool, - intl: PropTypes.object, - onSettingsClick: PropTypes.func, - onLogout: PropTypes.func.isRequired, - }; - - handleLogoutClick = e => { - e.preventDefault(); - e.stopPropagation(); - - this.props.onLogout(); - - return false; - }; - - render () { - const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props; - - // Only renders the component if the column isn't being shown. - const renderForColumn = conditionalRender.bind(null, - columnId => !columns || !columns.some( - column => column.get('id') === columnId, - ), - ); - - // The result. - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/header.jsx b/app/javascript/flavours/glitch/features/compose/components/header.jsx new file mode 100644 index 000000000..dcbdafa57 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/header.jsx @@ -0,0 +1,136 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { injectIntl, defineMessages } from 'react-intl'; +import { Link } from 'react-router-dom'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// Components. +import Icon from 'flavours/glitch/components/icon'; + +// Utils. +import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; +import { signOutLink } from 'flavours/glitch/utils/backend_links'; + +// Messages. +const messages = defineMessages({ + community: { + defaultMessage: 'Local timeline', + id: 'navigation_bar.community_timeline', + }, + home_timeline: { + defaultMessage: 'Home', + id: 'tabs_bar.home', + }, + logout: { + defaultMessage: 'Logout', + id: 'navigation_bar.logout', + }, + notifications: { + defaultMessage: 'Notifications', + id: 'tabs_bar.notifications', + }, + public: { + defaultMessage: 'Federated timeline', + id: 'navigation_bar.public_timeline', + }, + settings: { + defaultMessage: 'App settings', + id: 'navigation_bar.app_settings', + }, + start: { + defaultMessage: 'Getting started', + id: 'getting_started.heading', + }, +}); + +export default @injectIntl +class Header extends ImmutablePureComponent { + + static propTypes = { + columns: ImmutablePropTypes.list, + unreadNotifications: PropTypes.number, + showNotificationsBadge: PropTypes.bool, + intl: PropTypes.object, + onSettingsClick: PropTypes.func, + onLogout: PropTypes.func.isRequired, + }; + + handleLogoutClick = e => { + e.preventDefault(); + e.stopPropagation(); + + this.props.onLogout(); + + return false; + }; + + render () { + const { intl, columns, unreadNotifications, showNotificationsBadge, onSettingsClick } = this.props; + + // Only renders the component if the column isn't being shown. + const renderForColumn = conditionalRender.bind(null, + columnId => !columns || !columns.some( + column => column.get('id') === columnId, + ), + ); + + // The result. + return ( + + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js deleted file mode 100644 index 3f8411ab1..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js +++ /dev/null @@ -1,327 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { injectIntl, defineMessages } from 'react-intl'; -import TextIconButton from './text_icon_button'; -import Overlay from 'react-overlays/Overlay'; -import { supportsPassiveEvents } from 'detect-passive-events'; -import classNames from 'classnames'; -import { languages as preloadedLanguages } from 'flavours/glitch/initial_state'; -import { loupeIcon, deleteIcon } from 'flavours/glitch/utils/icons'; -import fuzzysort from 'fuzzysort'; - -const messages = defineMessages({ - changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' }, - search: { id: 'compose.language.search', defaultMessage: 'Search languages...' }, - clear: { id: 'emoji_button.clear', defaultMessage: 'Clear' }, -}); - -const listenerOptions = supportsPassiveEvents ? { passive: true } : false; - -class LanguageDropdownMenu extends React.PureComponent { - - static propTypes = { - value: PropTypes.string.isRequired, - frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired, - onClose: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), - intl: PropTypes.object, - }; - - static defaultProps = { - languages: preloadedLanguages, - }; - - state = { - searchValue: '', - }; - - handleDocumentClick = e => { - if (this.node && !this.node.contains(e.target)) { - this.props.onClose(); - } - }; - - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, false); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - - // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need - // to wait for a frame before focusing - requestAnimationFrame(() => { - if (this.node) { - const element = this.node.querySelector('input[type="search"]'); - if (element) element.focus(); - } - }); - } - - componentWillUnmount () { - document.removeEventListener('click', this.handleDocumentClick, false); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - setRef = c => { - this.node = c; - }; - - setListRef = c => { - this.listNode = c; - }; - - handleSearchChange = ({ target }) => { - this.setState({ searchValue: target.value }); - }; - - search () { - const { languages, value, frequentlyUsedLanguages } = this.props; - const { searchValue } = this.state; - - if (searchValue === '') { - return [...languages].sort((a, b) => { - // Push current selection to the top of the list - - if (a[0] === value) { - return -1; - } else if (b[0] === value) { - return 1; - } else { - // Sort according to frequently used languages - - const indexOfA = frequentlyUsedLanguages.indexOf(a[0]); - const indexOfB = frequentlyUsedLanguages.indexOf(b[0]); - - return ((indexOfA > -1 ? indexOfA : Infinity) - (indexOfB > -1 ? indexOfB : Infinity)); - } - }); - } - - return fuzzysort.go(searchValue, languages, { - keys: ['0', '1', '2'], - limit: 5, - threshold: -10000, - }).map(result => result.obj); - } - - frequentlyUsed () { - const { languages, value } = this.props; - const current = languages.find(lang => lang[0] === value); - const results = []; - - if (current) { - results.push(current); - } - - return results; - } - - handleClick = e => { - const value = e.currentTarget.getAttribute('data-index'); - - e.preventDefault(); - - this.props.onClose(); - this.props.onChange(value); - }; - - handleKeyDown = e => { - const { onClose } = this.props; - const index = Array.from(this.listNode.childNodes).findIndex(node => node === e.currentTarget); - - let element = null; - - switch(e.key) { - case 'Escape': - onClose(); - break; - case 'Enter': - this.handleClick(e); - break; - case 'ArrowDown': - element = this.listNode.childNodes[index + 1] || this.listNode.firstChild; - break; - case 'ArrowUp': - element = this.listNode.childNodes[index - 1] || this.listNode.lastChild; - break; - case 'Tab': - if (e.shiftKey) { - element = this.listNode.childNodes[index - 1] || this.listNode.lastChild; - } else { - element = this.listNode.childNodes[index + 1] || this.listNode.firstChild; - } - break; - case 'Home': - element = this.listNode.firstChild; - break; - case 'End': - element = this.listNode.lastChild; - break; - } - - if (element) { - element.focus(); - e.preventDefault(); - e.stopPropagation(); - } - }; - - handleSearchKeyDown = e => { - const { onChange, onClose } = this.props; - const { searchValue } = this.state; - - let element = null; - - switch(e.key) { - case 'Tab': - case 'ArrowDown': - element = this.listNode.firstChild; - - if (element) { - element.focus(); - e.preventDefault(); - e.stopPropagation(); - } - - break; - case 'Enter': - element = this.listNode.firstChild; - - if (element) { - onChange(element.getAttribute('data-index')); - onClose(); - } - break; - case 'Escape': - if (searchValue !== '') { - e.preventDefault(); - this.handleClear(); - } - - break; - } - }; - - handleClear = () => { - this.setState({ searchValue: '' }); - }; - - renderItem = lang => { - const { value } = this.props; - - return ( -
- {lang[2]} ({lang[1]}) -
- ); - }; - - render () { - const { intl } = this.props; - const { searchValue } = this.state; - const isSearching = searchValue !== ''; - const results = this.search(); - - return ( -
-
- - -
- -
- {results.map(this.renderItem)} -
-
- ); - } - -} - -export default @injectIntl -class LanguageDropdown extends React.PureComponent { - - static propTypes = { - value: PropTypes.string, - frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string), - intl: PropTypes.object.isRequired, - onChange: PropTypes.func, - onClose: PropTypes.func, - }; - - state = { - open: false, - placement: 'bottom', - }; - - handleToggle = () => { - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - - this.setState({ open: !this.state.open }); - }; - - handleClose = () => { - const { value, onClose } = this.props; - - if (this.state.open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - - this.setState({ open: false }); - onClose(value); - }; - - handleChange = value => { - const { onChange } = this.props; - onChange(value); - }; - - setTargetRef = c => { - this.target = c; - }; - - findTarget = () => { - return this.target; - }; - - handleOverlayEnter = (state) => { - this.setState({ placement: state.placement }); - }; - - render () { - const { value, intl, frequentlyUsedLanguages } = this.props; - const { open, placement } = this.state; - - return ( -
-
- -
- - - {({ props, placement }) => ( -
-
- -
-
- )} -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx new file mode 100644 index 000000000..3f8411ab1 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.jsx @@ -0,0 +1,327 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import { injectIntl, defineMessages } from 'react-intl'; +import TextIconButton from './text_icon_button'; +import Overlay from 'react-overlays/Overlay'; +import { supportsPassiveEvents } from 'detect-passive-events'; +import classNames from 'classnames'; +import { languages as preloadedLanguages } from 'flavours/glitch/initial_state'; +import { loupeIcon, deleteIcon } from 'flavours/glitch/utils/icons'; +import fuzzysort from 'fuzzysort'; + +const messages = defineMessages({ + changeLanguage: { id: 'compose.language.change', defaultMessage: 'Change language' }, + search: { id: 'compose.language.search', defaultMessage: 'Search languages...' }, + clear: { id: 'emoji_button.clear', defaultMessage: 'Clear' }, +}); + +const listenerOptions = supportsPassiveEvents ? { passive: true } : false; + +class LanguageDropdownMenu extends React.PureComponent { + + static propTypes = { + value: PropTypes.string.isRequired, + frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired, + onClose: PropTypes.func.isRequired, + onChange: PropTypes.func.isRequired, + languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), + intl: PropTypes.object, + }; + + static defaultProps = { + languages: preloadedLanguages, + }; + + state = { + searchValue: '', + }; + + handleDocumentClick = e => { + if (this.node && !this.node.contains(e.target)) { + this.props.onClose(); + } + }; + + componentDidMount () { + document.addEventListener('click', this.handleDocumentClick, false); + document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); + + // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need + // to wait for a frame before focusing + requestAnimationFrame(() => { + if (this.node) { + const element = this.node.querySelector('input[type="search"]'); + if (element) element.focus(); + } + }); + } + + componentWillUnmount () { + document.removeEventListener('click', this.handleDocumentClick, false); + document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); + } + + setRef = c => { + this.node = c; + }; + + setListRef = c => { + this.listNode = c; + }; + + handleSearchChange = ({ target }) => { + this.setState({ searchValue: target.value }); + }; + + search () { + const { languages, value, frequentlyUsedLanguages } = this.props; + const { searchValue } = this.state; + + if (searchValue === '') { + return [...languages].sort((a, b) => { + // Push current selection to the top of the list + + if (a[0] === value) { + return -1; + } else if (b[0] === value) { + return 1; + } else { + // Sort according to frequently used languages + + const indexOfA = frequentlyUsedLanguages.indexOf(a[0]); + const indexOfB = frequentlyUsedLanguages.indexOf(b[0]); + + return ((indexOfA > -1 ? indexOfA : Infinity) - (indexOfB > -1 ? indexOfB : Infinity)); + } + }); + } + + return fuzzysort.go(searchValue, languages, { + keys: ['0', '1', '2'], + limit: 5, + threshold: -10000, + }).map(result => result.obj); + } + + frequentlyUsed () { + const { languages, value } = this.props; + const current = languages.find(lang => lang[0] === value); + const results = []; + + if (current) { + results.push(current); + } + + return results; + } + + handleClick = e => { + const value = e.currentTarget.getAttribute('data-index'); + + e.preventDefault(); + + this.props.onClose(); + this.props.onChange(value); + }; + + handleKeyDown = e => { + const { onClose } = this.props; + const index = Array.from(this.listNode.childNodes).findIndex(node => node === e.currentTarget); + + let element = null; + + switch(e.key) { + case 'Escape': + onClose(); + break; + case 'Enter': + this.handleClick(e); + break; + case 'ArrowDown': + element = this.listNode.childNodes[index + 1] || this.listNode.firstChild; + break; + case 'ArrowUp': + element = this.listNode.childNodes[index - 1] || this.listNode.lastChild; + break; + case 'Tab': + if (e.shiftKey) { + element = this.listNode.childNodes[index - 1] || this.listNode.lastChild; + } else { + element = this.listNode.childNodes[index + 1] || this.listNode.firstChild; + } + break; + case 'Home': + element = this.listNode.firstChild; + break; + case 'End': + element = this.listNode.lastChild; + break; + } + + if (element) { + element.focus(); + e.preventDefault(); + e.stopPropagation(); + } + }; + + handleSearchKeyDown = e => { + const { onChange, onClose } = this.props; + const { searchValue } = this.state; + + let element = null; + + switch(e.key) { + case 'Tab': + case 'ArrowDown': + element = this.listNode.firstChild; + + if (element) { + element.focus(); + e.preventDefault(); + e.stopPropagation(); + } + + break; + case 'Enter': + element = this.listNode.firstChild; + + if (element) { + onChange(element.getAttribute('data-index')); + onClose(); + } + break; + case 'Escape': + if (searchValue !== '') { + e.preventDefault(); + this.handleClear(); + } + + break; + } + }; + + handleClear = () => { + this.setState({ searchValue: '' }); + }; + + renderItem = lang => { + const { value } = this.props; + + return ( +
+ {lang[2]} ({lang[1]}) +
+ ); + }; + + render () { + const { intl } = this.props; + const { searchValue } = this.state; + const isSearching = searchValue !== ''; + const results = this.search(); + + return ( +
+
+ + +
+ +
+ {results.map(this.renderItem)} +
+
+ ); + } + +} + +export default @injectIntl +class LanguageDropdown extends React.PureComponent { + + static propTypes = { + value: PropTypes.string, + frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string), + intl: PropTypes.object.isRequired, + onChange: PropTypes.func, + onClose: PropTypes.func, + }; + + state = { + open: false, + placement: 'bottom', + }; + + handleToggle = () => { + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } + + this.setState({ open: !this.state.open }); + }; + + handleClose = () => { + const { value, onClose } = this.props; + + if (this.state.open && this.activeElement) { + this.activeElement.focus({ preventScroll: true }); + } + + this.setState({ open: false }); + onClose(value); + }; + + handleChange = value => { + const { onChange } = this.props; + onChange(value); + }; + + setTargetRef = c => { + this.target = c; + }; + + findTarget = () => { + return this.target; + }; + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + }; + + render () { + const { value, intl, frequentlyUsedLanguages } = this.props; + const { open, placement } = this.state; + + return ( +
+
+ +
+ + + {({ props, placement }) => ( +
+
+ +
+
+ )} +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js deleted file mode 100644 index 1a68f1e12..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.js +++ /dev/null @@ -1,46 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ActionBar from './action_bar'; -import Avatar from 'flavours/glitch/components/avatar'; -import Permalink from 'flavours/glitch/components/permalink'; -import { FormattedMessage } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { profileLink } from 'flavours/glitch/utils/backend_links'; - -export default class NavigationBar extends ImmutablePureComponent { - - static propTypes = { - account: ImmutablePropTypes.map.isRequired, - onLogout: PropTypes.func.isRequired, - }; - - render () { - return ( -
- - {this.props.account.get('acct')} - - - -
- - @{this.props.account.get('acct')} - - - { profileLink !== undefined && ( - - )} -
- -
- -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx new file mode 100644 index 000000000..1a68f1e12 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx @@ -0,0 +1,46 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import ActionBar from './action_bar'; +import Avatar from 'flavours/glitch/components/avatar'; +import Permalink from 'flavours/glitch/components/permalink'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { profileLink } from 'flavours/glitch/utils/backend_links'; + +export default class NavigationBar extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + onLogout: PropTypes.func.isRequired, + }; + + render () { + return ( +
+ + {this.props.account.get('acct')} + + + +
+ + @{this.props.account.get('acct')} + + + { profileLink !== undefined && ( + + )} +
+ +
+ +
+
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/options.js b/app/javascript/flavours/glitch/features/compose/components/options.js deleted file mode 100644 index e09e13bcb..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/options.js +++ /dev/null @@ -1,317 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl } from 'react-intl'; -import spring from 'react-motion/lib/spring'; -import Toggle from 'react-toggle'; -import { connect } from 'react-redux'; - -// Components. -import IconButton from 'flavours/glitch/components/icon_button'; -import TextIconButton from './text_icon_button'; -import DropdownContainer from '../containers/dropdown_container'; -import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; -import LanguageDropdown from '../containers/language_dropdown_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Utils. -import Motion from '../../ui/util/optional_motion'; -import { pollLimits } from 'flavours/glitch/initial_state'; - -// Messages. -const messages = defineMessages({ - advanced_options_icon_title: { - defaultMessage: 'Advanced options', - id: 'advanced_options.icon_title', - }, - attach: { - defaultMessage: 'Attach...', - id: 'compose.attach', - }, - content_type: { - defaultMessage: 'Content type', - id: 'content-type.change', - }, - doodle: { - defaultMessage: 'Draw something', - id: 'compose.attach.doodle', - }, - html: { - defaultMessage: 'HTML', - id: 'compose.content-type.html', - }, - 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', - }, - markdown: { - defaultMessage: 'Markdown', - id: 'compose.content-type.markdown', - }, - plain: { - defaultMessage: 'Plain text', - id: 'compose.content-type.plain', - }, - 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', - }, - 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', - }, -}); - -@connect((state, { name }) => ({ checked: state.getIn(['compose', 'advanced_options', name]) })) -class ToggleOption extends ImmutablePureComponent { - - static propTypes = { - name: PropTypes.string.isRequired, - checked: PropTypes.bool, - onChangeAdvancedOption: PropTypes.func.isRequired, - }; - - handleChange = () => { - this.props.onChangeAdvancedOption(this.props.name); - }; - - render() { - const { meta, text, checked } = this.props; - - return ( - - - -
- {text} - {meta} -
-
- ); - } - -} - -export default @injectIntl -class ComposerOptions extends ImmutablePureComponent { - - static 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, - onChangeContentType: PropTypes.func, - onTogglePoll: PropTypes.func, - onDoodleOpen: PropTypes.func, - onToggleSpoiler: PropTypes.func, - onUpload: PropTypes.func, - contentType: PropTypes.string, - resetFileKey: PropTypes.number, - spoiler: PropTypes.bool, - showContentTypeChoice: PropTypes.bool, - isEditing: PropTypes.bool, - }; - - // 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; - }; - - renderToggleItemContents = (item) => { - const { onChangeAdvancedOption } = this.props; - const { name, meta, text } = item; - - return ; - }; - - // Rendering. - render () { - const { - acceptContentTypes, - advancedOptions, - contentType, - disabled, - allowMedia, - hasMedia, - allowPoll, - hasPoll, - onChangeAdvancedOption, - onChangeContentType, - onTogglePoll, - onToggleSpoiler, - resetFileKey, - spoiler, - showContentTypeChoice, - isEditing, - intl: { formatMessage }, - } = this.props; - - const contentTypeItems = { - plain: { - icon: 'file-text', - name: 'text/plain', - text: formatMessage(messages.plain), - }, - html: { - icon: 'code', - name: 'text/html', - text: formatMessage(messages.html), - }, - markdown: { - icon: 'arrow-circle-down', - name: 'text/markdown', - text: formatMessage(messages.markdown), - }, - }; - - // The result. - return ( -
- - - {!!pollLimits && ( - - )} -
- - {showContentTypeChoice && ( - - )} - {onToggleSpoiler && ( - - )} - - -
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/options.jsx b/app/javascript/flavours/glitch/features/compose/components/options.jsx new file mode 100644 index 000000000..e09e13bcb --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/options.jsx @@ -0,0 +1,317 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl } from 'react-intl'; +import spring from 'react-motion/lib/spring'; +import Toggle from 'react-toggle'; +import { connect } from 'react-redux'; + +// Components. +import IconButton from 'flavours/glitch/components/icon_button'; +import TextIconButton from './text_icon_button'; +import DropdownContainer from '../containers/dropdown_container'; +import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; +import LanguageDropdown from '../containers/language_dropdown_container'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// Utils. +import Motion from '../../ui/util/optional_motion'; +import { pollLimits } from 'flavours/glitch/initial_state'; + +// Messages. +const messages = defineMessages({ + advanced_options_icon_title: { + defaultMessage: 'Advanced options', + id: 'advanced_options.icon_title', + }, + attach: { + defaultMessage: 'Attach...', + id: 'compose.attach', + }, + content_type: { + defaultMessage: 'Content type', + id: 'content-type.change', + }, + doodle: { + defaultMessage: 'Draw something', + id: 'compose.attach.doodle', + }, + html: { + defaultMessage: 'HTML', + id: 'compose.content-type.html', + }, + 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', + }, + markdown: { + defaultMessage: 'Markdown', + id: 'compose.content-type.markdown', + }, + plain: { + defaultMessage: 'Plain text', + id: 'compose.content-type.plain', + }, + 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', + }, + 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', + }, +}); + +@connect((state, { name }) => ({ checked: state.getIn(['compose', 'advanced_options', name]) })) +class ToggleOption extends ImmutablePureComponent { + + static propTypes = { + name: PropTypes.string.isRequired, + checked: PropTypes.bool, + onChangeAdvancedOption: PropTypes.func.isRequired, + }; + + handleChange = () => { + this.props.onChangeAdvancedOption(this.props.name); + }; + + render() { + const { meta, text, checked } = this.props; + + return ( + + + +
+ {text} + {meta} +
+
+ ); + } + +} + +export default @injectIntl +class ComposerOptions extends ImmutablePureComponent { + + static 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, + onChangeContentType: PropTypes.func, + onTogglePoll: PropTypes.func, + onDoodleOpen: PropTypes.func, + onToggleSpoiler: PropTypes.func, + onUpload: PropTypes.func, + contentType: PropTypes.string, + resetFileKey: PropTypes.number, + spoiler: PropTypes.bool, + showContentTypeChoice: PropTypes.bool, + isEditing: PropTypes.bool, + }; + + // 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; + }; + + renderToggleItemContents = (item) => { + const { onChangeAdvancedOption } = this.props; + const { name, meta, text } = item; + + return ; + }; + + // Rendering. + render () { + const { + acceptContentTypes, + advancedOptions, + contentType, + disabled, + allowMedia, + hasMedia, + allowPoll, + hasPoll, + onChangeAdvancedOption, + onChangeContentType, + onTogglePoll, + onToggleSpoiler, + resetFileKey, + spoiler, + showContentTypeChoice, + isEditing, + intl: { formatMessage }, + } = this.props; + + const contentTypeItems = { + plain: { + icon: 'file-text', + name: 'text/plain', + text: formatMessage(messages.plain), + }, + html: { + icon: 'code', + name: 'text/html', + text: formatMessage(messages.html), + }, + markdown: { + icon: 'arrow-circle-down', + name: 'text/markdown', + text: formatMessage(messages.markdown), + }, + }; + + // The result. + return ( +
+ + + {!!pollLimits && ( + + )} +
+ + {showContentTypeChoice && ( + + )} + {onToggleSpoiler && ( + + )} + + +
+ ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_form.js b/app/javascript/flavours/glitch/features/compose/components/poll_form.js deleted file mode 100644 index cb6b577bf..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/poll_form.js +++ /dev/null @@ -1,170 +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 AutosuggestInput from 'flavours/glitch/components/autosuggest_input'; -import classNames from 'classnames'; -import { pollLimits } from 'flavours/glitch/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, - lang: PropTypes.string, - index: PropTypes.number.isRequired, - isPollMultiple: PropTypes.bool, - autoFocus: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onRemove: PropTypes.func.isRequired, - suggestions: ImmutablePropTypes.list, - onClearSuggestions: PropTypes.func.isRequired, - onFetchSuggestions: PropTypes.func.isRequired, - onSuggestionSelected: 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); - }; - - onSuggestionsClearRequested = () => { - this.props.onClearSuggestions(); - }; - - onSuggestionsFetchRequested = (token) => { - this.props.onFetchSuggestions(token); - }; - - onSuggestionSelected = (tokenStart, token, value) => { - this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]); - }; - - render () { - const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props; - - return ( -
  • - - -
    - -
    -
  • - ); - } - -} - -export default -@injectIntl -class PollForm extends ImmutablePureComponent { - - static propTypes = { - options: ImmutablePropTypes.list, - lang: PropTypes.string, - expiresIn: PropTypes.number, - isMultiple: PropTypes.bool, - onChangeOption: PropTypes.func.isRequired, - onAddOption: PropTypes.func.isRequired, - onRemoveOption: PropTypes.func.isRequired, - onChangeSettings: PropTypes.func.isRequired, - suggestions: ImmutablePropTypes.list, - onClearSuggestions: PropTypes.func.isRequired, - onFetchSuggestions: PropTypes.func.isRequired, - onSuggestionSelected: 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, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props; - - if (!options) { - return null; - } - - const autoFocusIndex = options.indexOf(''); - - return ( -
    -
      - {options.map((title, i) =>
    - -
    - - - {/* eslint-disable-next-line jsx-a11y/no-onchange */} - -
    -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx b/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx new file mode 100644 index 000000000..cb6b577bf --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx @@ -0,0 +1,170 @@ +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 AutosuggestInput from 'flavours/glitch/components/autosuggest_input'; +import classNames from 'classnames'; +import { pollLimits } from 'flavours/glitch/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, + lang: PropTypes.string, + index: PropTypes.number.isRequired, + isPollMultiple: PropTypes.bool, + autoFocus: PropTypes.bool, + onChange: PropTypes.func.isRequired, + onRemove: PropTypes.func.isRequired, + suggestions: ImmutablePropTypes.list, + onClearSuggestions: PropTypes.func.isRequired, + onFetchSuggestions: PropTypes.func.isRequired, + onSuggestionSelected: 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); + }; + + onSuggestionsClearRequested = () => { + this.props.onClearSuggestions(); + }; + + onSuggestionsFetchRequested = (token) => { + this.props.onFetchSuggestions(token); + }; + + onSuggestionSelected = (tokenStart, token, value) => { + this.props.onSuggestionSelected(tokenStart, token, value, ['poll', 'options', this.props.index]); + }; + + render () { + const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props; + + return ( +
  • + + +
    + +
    +
  • + ); + } + +} + +export default +@injectIntl +class PollForm extends ImmutablePureComponent { + + static propTypes = { + options: ImmutablePropTypes.list, + lang: PropTypes.string, + expiresIn: PropTypes.number, + isMultiple: PropTypes.bool, + onChangeOption: PropTypes.func.isRequired, + onAddOption: PropTypes.func.isRequired, + onRemoveOption: PropTypes.func.isRequired, + onChangeSettings: PropTypes.func.isRequired, + suggestions: ImmutablePropTypes.list, + onClearSuggestions: PropTypes.func.isRequired, + onFetchSuggestions: PropTypes.func.isRequired, + onSuggestionSelected: 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, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props; + + if (!options) { + return null; + } + + const autoFocusIndex = options.indexOf(''); + + return ( +
    +
      + {options.map((title, i) =>
    + +
    + + + {/* eslint-disable-next-line jsx-a11y/no-onchange */} + +
    +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js deleted file mode 100644 index 02cf72289..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.js +++ /dev/null @@ -1,88 +0,0 @@ -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl } from 'react-intl'; -import Dropdown from './dropdown'; - -const messages = defineMessages({ - public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, - public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' }, - unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, - unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' }, - private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, - private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, - direct_short: { id: 'privacy.direct.short', defaultMessage: 'Only people I mention' }, - direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, - change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, -}); - -export default @injectIntl -class PrivacyDropdown extends React.PureComponent { - - static propTypes = { - isUserTouching: PropTypes.func, - onModalOpen: PropTypes.func, - onModalClose: PropTypes.func, - value: PropTypes.string.isRequired, - onChange: PropTypes.func.isRequired, - noDirect: PropTypes.bool, - container: PropTypes.func, - disabled: PropTypes.bool, - intl: PropTypes.object.isRequired, - }; - - render () { - const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, isUserTouching, intl: { formatMessage } } = this.props; - - // We predefine our privacy items so that we can easily pick the - // dropdown icon later. - const privacyItems = { - direct: { - icon: 'envelope', - meta: formatMessage(messages.direct_long), - name: 'direct', - text: formatMessage(messages.direct_short), - }, - private: { - icon: 'lock', - meta: formatMessage(messages.private_long), - name: 'private', - text: formatMessage(messages.private_short), - }, - public: { - icon: 'globe', - meta: formatMessage(messages.public_long), - name: 'public', - text: formatMessage(messages.public_short), - }, - unlisted: { - icon: 'unlock', - meta: formatMessage(messages.unlisted_long), - name: 'unlisted', - text: formatMessage(messages.unlisted_short), - }, - }; - - const items = [privacyItems.public, privacyItems.unlisted, privacyItems.private]; - - if (!noDirect) { - items.push(privacyItems.direct); - } - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx new file mode 100644 index 000000000..02cf72289 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/privacy_dropdown.jsx @@ -0,0 +1,88 @@ +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl } from 'react-intl'; +import Dropdown from './dropdown'; + +const messages = defineMessages({ + public_short: { id: 'privacy.public.short', defaultMessage: 'Public' }, + public_long: { id: 'privacy.public.long', defaultMessage: 'Visible for all' }, + unlisted_short: { id: 'privacy.unlisted.short', defaultMessage: 'Unlisted' }, + unlisted_long: { id: 'privacy.unlisted.long', defaultMessage: 'Visible for all, but opted-out of discovery features' }, + private_short: { id: 'privacy.private.short', defaultMessage: 'Followers only' }, + private_long: { id: 'privacy.private.long', defaultMessage: 'Visible for followers only' }, + direct_short: { id: 'privacy.direct.short', defaultMessage: 'Only people I mention' }, + direct_long: { id: 'privacy.direct.long', defaultMessage: 'Visible for mentioned users only' }, + change_privacy: { id: 'privacy.change', defaultMessage: 'Adjust status privacy' }, +}); + +export default @injectIntl +class PrivacyDropdown extends React.PureComponent { + + static propTypes = { + isUserTouching: PropTypes.func, + onModalOpen: PropTypes.func, + onModalClose: PropTypes.func, + value: PropTypes.string.isRequired, + onChange: PropTypes.func.isRequired, + noDirect: PropTypes.bool, + container: PropTypes.func, + disabled: PropTypes.bool, + intl: PropTypes.object.isRequired, + }; + + render () { + const { value, onChange, onModalOpen, onModalClose, disabled, noDirect, container, isUserTouching, intl: { formatMessage } } = this.props; + + // We predefine our privacy items so that we can easily pick the + // dropdown icon later. + const privacyItems = { + direct: { + icon: 'envelope', + meta: formatMessage(messages.direct_long), + name: 'direct', + text: formatMessage(messages.direct_short), + }, + private: { + icon: 'lock', + meta: formatMessage(messages.private_long), + name: 'private', + text: formatMessage(messages.private_short), + }, + public: { + icon: 'globe', + meta: formatMessage(messages.public_long), + name: 'public', + text: formatMessage(messages.public_short), + }, + unlisted: { + icon: 'unlock', + meta: formatMessage(messages.unlisted_long), + name: 'unlisted', + text: formatMessage(messages.unlisted_short), + }, + }; + + const items = [privacyItems.public, privacyItems.unlisted, privacyItems.private]; + + if (!noDirect) { + items.push(privacyItems.direct); + } + + return ( + + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.js b/app/javascript/flavours/glitch/features/compose/components/publisher.js deleted file mode 100644 index 59254990b..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/publisher.js +++ /dev/null @@ -1,99 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { defineMessages, injectIntl } from 'react-intl'; -import { length } from 'stringz'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// Components. -import Button from 'flavours/glitch/components/button'; -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import { maxChars } from 'flavours/glitch/initial_state'; - -// Messages. -const messages = defineMessages({ - publish: { - defaultMessage: 'Publish', - id: 'compose_form.publish', - }, - publishLoud: { - defaultMessage: '{publish}!', - id: 'compose_form.publish_loud', - }, - saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' }, -}); - -export default @injectIntl -class Publisher extends ImmutablePureComponent { - - static 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']), - isEditing: PropTypes.bool, - }; - - handleSubmit = () => { - this.props.onSubmit(); - }; - - render () { - const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props; - - const diff = maxChars - length(countText || ''); - const computedClass = classNames('compose-form__publish', { - disabled: disabled, - over: diff < 0, - }); - - const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' }; - - let publishText; - if (isEditing) { - publishText = intl.formatMessage(messages.saveChanges); - } else if (privacy === 'private' || privacy === 'direct') { - const iconId = privacyIcons[privacy]; - publishText = ( - - {intl.formatMessage(messages.publish)} - - ); - } else { - publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); - } - - return ( -
    - {sideArm && !isEditing && sideArm !== 'none' ? ( -
    -
    - ) : null} -
    -
    -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/publisher.jsx b/app/javascript/flavours/glitch/features/compose/components/publisher.jsx new file mode 100644 index 000000000..59254990b --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/publisher.jsx @@ -0,0 +1,99 @@ +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { defineMessages, injectIntl } from 'react-intl'; +import { length } from 'stringz'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// Components. +import Button from 'flavours/glitch/components/button'; +import Icon from 'flavours/glitch/components/icon'; + +// Utils. +import { maxChars } from 'flavours/glitch/initial_state'; + +// Messages. +const messages = defineMessages({ + publish: { + defaultMessage: 'Publish', + id: 'compose_form.publish', + }, + publishLoud: { + defaultMessage: '{publish}!', + id: 'compose_form.publish_loud', + }, + saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' }, +}); + +export default @injectIntl +class Publisher extends ImmutablePureComponent { + + static 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']), + isEditing: PropTypes.bool, + }; + + handleSubmit = () => { + this.props.onSubmit(); + }; + + render () { + const { countText, disabled, intl, onSecondarySubmit, privacy, sideArm, isEditing } = this.props; + + const diff = maxChars - length(countText || ''); + const computedClass = classNames('compose-form__publish', { + disabled: disabled, + over: diff < 0, + }); + + const privacyIcons = { direct: 'envelope', private: 'lock', public: 'globe', unlisted: 'unlock' }; + + let publishText; + if (isEditing) { + publishText = intl.formatMessage(messages.saveChanges); + } else if (privacy === 'private' || privacy === 'direct') { + const iconId = privacyIcons[privacy]; + publishText = ( + + {intl.formatMessage(messages.publish)} + + ); + } else { + publishText = privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); + } + + return ( +
    + {sideArm && !isEditing && sideArm !== 'none' ? ( +
    +
    + ) : null} +
    +
    +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js deleted file mode 100644 index ca167d114..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/reply_indicator.js +++ /dev/null @@ -1,82 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { defineMessages, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// 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'; - -// Messages. -const messages = defineMessages({ - cancel: { - defaultMessage: 'Cancel', - id: 'reply_indicator.cancel', - }, -}); - - -export default @injectIntl -class ReplyIndicator extends ImmutablePureComponent { - - static propTypes = { - status: ImmutablePropTypes.map, - intl: PropTypes.object.isRequired, - onCancel: PropTypes.func, - }; - - handleClick = () => { - const { onCancel } = this.props; - if (onCancel) { - onCancel(); - } - }; - - // Rendering. - render () { - const { status, intl } = this.props; - - if (!status) { - return null; - } - - const account = status.get('account'); - const content = status.get('content'); - const attachments = status.get('media_attachments'); - - // The result. - return ( -
    -
    - - {account && ( - - )} -
    -
    - {attachments.size > 0 && ( - - )} -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx new file mode 100644 index 000000000..ca167d114 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/reply_indicator.jsx @@ -0,0 +1,82 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// 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'; + +// Messages. +const messages = defineMessages({ + cancel: { + defaultMessage: 'Cancel', + id: 'reply_indicator.cancel', + }, +}); + + +export default @injectIntl +class ReplyIndicator extends ImmutablePureComponent { + + static propTypes = { + status: ImmutablePropTypes.map, + intl: PropTypes.object.isRequired, + onCancel: PropTypes.func, + }; + + handleClick = () => { + const { onCancel } = this.props; + if (onCancel) { + onCancel(); + } + }; + + // Rendering. + render () { + const { status, intl } = this.props; + + if (!status) { + return null; + } + + const account = status.get('account'); + const content = status.get('content'); + const attachments = status.get('media_attachments'); + + // The result. + return ( +
    +
    + + {account && ( + + )} +
    +
    + {attachments.size > 0 && ( + + )} +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js deleted file mode 100644 index 6241e2a0a..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ /dev/null @@ -1,168 +0,0 @@ -// Package imports. -import classNames from 'classnames'; -import PropTypes from 'prop-types'; -import React from 'react'; -import { connect } from 'react-redux'; -import { - injectIntl, - FormattedMessage, - defineMessages, -} from 'react-intl'; -import Overlay from 'react-overlays/Overlay'; - -// Components. -import Icon from 'flavours/glitch/components/icon'; - -// Utils. -import { focusRoot } from 'flavours/glitch/utils/dom_helpers'; -import { searchEnabled } from 'flavours/glitch/initial_state'; - -const messages = defineMessages({ - placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, - placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' }, -}); - -class SearchPopout extends React.PureComponent { - - render () { - const extraInformation = searchEnabled ? : ; - return ( -
    -

    - -
      -
    • #example
    • -
    • @username@domain
    • -
    • URL
    • -
    • URL
    • -
    - - {extraInformation} -
    - ); - } - -} - -// The component. -export default @injectIntl -class Search extends React.PureComponent { - - static contextTypes = { - router: PropTypes.object.isRequired, - identity: PropTypes.object.isRequired, - }; - - static propTypes = { - value: PropTypes.string.isRequired, - submitted: PropTypes.bool, - onChange: PropTypes.func.isRequired, - onSubmit: PropTypes.func.isRequired, - onClear: PropTypes.func.isRequired, - onShow: PropTypes.func.isRequired, - openInRoute: PropTypes.bool, - intl: PropTypes.object.isRequired, - singleColumn: PropTypes.bool, - }; - - state = { - expanded: false, - }; - - setRef = c => { - this.searchForm = c; - }; - - handleChange = (e) => { - const { onChange } = this.props; - if (onChange) { - onChange(e.target.value); - } - }; - - handleClear = (e) => { - const { - onClear, - submitted, - value, - } = this.props; - e.preventDefault(); // Prevents focus change ?? - if (onClear && (submitted || value && value.length)) { - onClear(); - } - }; - - handleBlur = () => { - this.setState({ expanded: false }); - }; - - handleFocus = () => { - this.setState({ expanded: true }); - this.props.onShow(); - - if (this.searchForm && !this.props.singleColumn) { - const { left, right } = this.searchForm.getBoundingClientRect(); - if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { - this.searchForm.scrollIntoView(); - } - } - }; - - handleKeyUp = (e) => { - const { onSubmit } = this.props; - switch (e.key) { - case 'Enter': - onSubmit(); - - if (this.props.openInRoute) { - this.context.router.history.push('/search'); - } - break; - case 'Escape': - focusRoot(); - } - }; - - findTarget = () => { - return this.searchForm; - }; - - render () { - const { intl, value, submitted } = this.props; - const { expanded } = this.state; - const { signedIn } = this.context.identity; - const hasValue = value.length > 0 || submitted; - - return ( -
    - - -
    - - -
    - - {({ props, placement }) => ( -
    -
    - -
    -
    - )} -
    -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/search.jsx b/app/javascript/flavours/glitch/features/compose/components/search.jsx new file mode 100644 index 000000000..6241e2a0a --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/search.jsx @@ -0,0 +1,168 @@ +// Package imports. +import classNames from 'classnames'; +import PropTypes from 'prop-types'; +import React from 'react'; +import { connect } from 'react-redux'; +import { + injectIntl, + FormattedMessage, + defineMessages, +} from 'react-intl'; +import Overlay from 'react-overlays/Overlay'; + +// Components. +import Icon from 'flavours/glitch/components/icon'; + +// Utils. +import { focusRoot } from 'flavours/glitch/utils/dom_helpers'; +import { searchEnabled } from 'flavours/glitch/initial_state'; + +const messages = defineMessages({ + placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, + placeholderSignedIn: { id: 'search.search_or_paste', defaultMessage: 'Search or paste URL' }, +}); + +class SearchPopout extends React.PureComponent { + + render () { + const extraInformation = searchEnabled ? : ; + return ( +
    +

    + +
      +
    • #example
    • +
    • @username@domain
    • +
    • URL
    • +
    • URL
    • +
    + + {extraInformation} +
    + ); + } + +} + +// The component. +export default @injectIntl +class Search extends React.PureComponent { + + static contextTypes = { + router: PropTypes.object.isRequired, + identity: PropTypes.object.isRequired, + }; + + static propTypes = { + value: PropTypes.string.isRequired, + submitted: PropTypes.bool, + onChange: PropTypes.func.isRequired, + onSubmit: PropTypes.func.isRequired, + onClear: PropTypes.func.isRequired, + onShow: PropTypes.func.isRequired, + openInRoute: PropTypes.bool, + intl: PropTypes.object.isRequired, + singleColumn: PropTypes.bool, + }; + + state = { + expanded: false, + }; + + setRef = c => { + this.searchForm = c; + }; + + handleChange = (e) => { + const { onChange } = this.props; + if (onChange) { + onChange(e.target.value); + } + }; + + handleClear = (e) => { + const { + onClear, + submitted, + value, + } = this.props; + e.preventDefault(); // Prevents focus change ?? + if (onClear && (submitted || value && value.length)) { + onClear(); + } + }; + + handleBlur = () => { + this.setState({ expanded: false }); + }; + + handleFocus = () => { + this.setState({ expanded: true }); + this.props.onShow(); + + if (this.searchForm && !this.props.singleColumn) { + const { left, right } = this.searchForm.getBoundingClientRect(); + if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) { + this.searchForm.scrollIntoView(); + } + } + }; + + handleKeyUp = (e) => { + const { onSubmit } = this.props; + switch (e.key) { + case 'Enter': + onSubmit(); + + if (this.props.openInRoute) { + this.context.router.history.push('/search'); + } + break; + case 'Escape': + focusRoot(); + } + }; + + findTarget = () => { + return this.searchForm; + }; + + render () { + const { intl, value, submitted } = this.props; + const { expanded } = this.state; + const { signedIn } = this.context.identity; + const hasValue = value.length > 0 || submitted; + + return ( +
    + + +
    + + +
    + + {({ props, placement }) => ( +
    +
    + +
    +
    + )} +
    +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.js b/app/javascript/flavours/glitch/features/compose/components/search_results.js deleted file mode 100644 index 23ff60936..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/search_results.js +++ /dev/null @@ -1,141 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; -import AccountContainer from 'flavours/glitch/containers/account_container'; -import StatusContainer from 'flavours/glitch/containers/status_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; -import Icon from 'flavours/glitch/components/icon'; -import { searchEnabled } from 'flavours/glitch/initial_state'; -import LoadMore from 'flavours/glitch/components/load_more'; - -const messages = defineMessages({ - dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, -}); - -export default @injectIntl -class SearchResults extends ImmutablePureComponent { - - static propTypes = { - results: ImmutablePropTypes.map.isRequired, - suggestions: ImmutablePropTypes.list.isRequired, - fetchSuggestions: PropTypes.func.isRequired, - expandSearch: PropTypes.func.isRequired, - dismissSuggestion: PropTypes.func.isRequired, - searchTerm: PropTypes.string, - intl: PropTypes.object.isRequired, - }; - - componentDidMount () { - if (this.props.searchTerm === '') { - this.props.fetchSuggestions(); - } - } - - componentDidUpdate () { - if (this.props.searchTerm === '') { - this.props.fetchSuggestions(); - } - } - - handleLoadMoreAccounts = () => this.props.expandSearch('accounts'); - - handleLoadMoreStatuses = () => this.props.expandSearch('statuses'); - - handleLoadMoreHashtags = () => this.props.expandSearch('hashtags'); - - render () { - const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props; - - let accounts, statuses, hashtags; - let count = 0; - - if (searchTerm === '' && !suggestions.isEmpty()) { - return ( -
    -
    -
    - - -
    - - {suggestions && suggestions.map(suggestion => ( - - ))} -
    -
    - ); - } else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) { - statuses = ( -
    -
    - -
    - -
    -
    - ); - } - - if (results.get('accounts') && results.get('accounts').size > 0) { - count += results.get('accounts').size; - accounts = ( -
    -
    - - {results.get('accounts').map(accountId => )} - - {results.get('accounts').size >= 5 && } -
    - ); - } - - if (results.get('statuses') && results.get('statuses').size > 0) { - count += results.get('statuses').size; - statuses = ( -
    -
    - - {results.get('statuses').map(statusId => )} - - {results.get('statuses').size >= 5 && } -
    - ); - } - - if (results.get('hashtags') && results.get('hashtags').size > 0) { - count += results.get('hashtags').size; - hashtags = ( -
    -
    - - {results.get('hashtags').map(hashtag => )} - - {results.get('hashtags').size >= 5 && } -
    - ); - } - - // The result. - return ( -
    -
    - - -
    - - {accounts} - {statuses} - {hashtags} -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/search_results.jsx b/app/javascript/flavours/glitch/features/compose/components/search_results.jsx new file mode 100644 index 000000000..23ff60936 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/search_results.jsx @@ -0,0 +1,141 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage, defineMessages, injectIntl } from 'react-intl'; +import AccountContainer from 'flavours/glitch/containers/account_container'; +import StatusContainer from 'flavours/glitch/containers/status_container'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { ImmutableHashtag as Hashtag } from 'flavours/glitch/components/hashtag'; +import Icon from 'flavours/glitch/components/icon'; +import { searchEnabled } from 'flavours/glitch/initial_state'; +import LoadMore from 'flavours/glitch/components/load_more'; + +const messages = defineMessages({ + dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' }, +}); + +export default @injectIntl +class SearchResults extends ImmutablePureComponent { + + static propTypes = { + results: ImmutablePropTypes.map.isRequired, + suggestions: ImmutablePropTypes.list.isRequired, + fetchSuggestions: PropTypes.func.isRequired, + expandSearch: PropTypes.func.isRequired, + dismissSuggestion: PropTypes.func.isRequired, + searchTerm: PropTypes.string, + intl: PropTypes.object.isRequired, + }; + + componentDidMount () { + if (this.props.searchTerm === '') { + this.props.fetchSuggestions(); + } + } + + componentDidUpdate () { + if (this.props.searchTerm === '') { + this.props.fetchSuggestions(); + } + } + + handleLoadMoreAccounts = () => this.props.expandSearch('accounts'); + + handleLoadMoreStatuses = () => this.props.expandSearch('statuses'); + + handleLoadMoreHashtags = () => this.props.expandSearch('hashtags'); + + render () { + const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props; + + let accounts, statuses, hashtags; + let count = 0; + + if (searchTerm === '' && !suggestions.isEmpty()) { + return ( +
    +
    +
    + + +
    + + {suggestions && suggestions.map(suggestion => ( + + ))} +
    +
    + ); + } else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) { + statuses = ( +
    +
    + +
    + +
    +
    + ); + } + + if (results.get('accounts') && results.get('accounts').size > 0) { + count += results.get('accounts').size; + accounts = ( +
    +
    + + {results.get('accounts').map(accountId => )} + + {results.get('accounts').size >= 5 && } +
    + ); + } + + if (results.get('statuses') && results.get('statuses').size > 0) { + count += results.get('statuses').size; + statuses = ( +
    +
    + + {results.get('statuses').map(statusId => )} + + {results.get('statuses').size >= 5 && } +
    + ); + } + + if (results.get('hashtags') && results.get('hashtags').size > 0) { + count += results.get('hashtags').size; + hashtags = ( +
    +
    + + {results.get('hashtags').map(hashtag => )} + + {results.get('hashtags').size >= 5 && } +
    + ); + } + + // The result. + return ( +
    +
    + + +
    + + {accounts} + {statuses} + {hashtags} +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js deleted file mode 100644 index a35bd4ff5..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/text_icon_button.js +++ /dev/null @@ -1,38 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; - -const iconStyle = { - height: null, - lineHeight: '27px', - width: `${18 * 1.28571429}px`, -}; - -export default class TextIconButton extends React.PureComponent { - - static propTypes = { - label: PropTypes.string.isRequired, - title: PropTypes.string, - active: PropTypes.bool, - onClick: PropTypes.func.isRequired, - ariaControls: PropTypes.string, - }; - - render () { - const { label, title, active, ariaControls } = this.props; - - return ( - - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/text_icon_button.jsx b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.jsx new file mode 100644 index 000000000..a35bd4ff5 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/text_icon_button.jsx @@ -0,0 +1,38 @@ +import React from 'react'; +import PropTypes from 'prop-types'; + +const iconStyle = { + height: null, + lineHeight: '27px', + width: `${18 * 1.28571429}px`, +}; + +export default class TextIconButton extends React.PureComponent { + + static propTypes = { + label: PropTypes.string.isRequired, + title: PropTypes.string, + active: PropTypes.bool, + onClick: PropTypes.func.isRequired, + ariaControls: PropTypes.string, + }; + + render () { + const { label, title, active, ariaControls } = this.props; + + return ( + + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js b/app/javascript/flavours/glitch/features/compose/components/textarea_icons.js deleted file mode 100644 index d8ee5c81b..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/textarea_icons.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, injectIntl } from 'react-intl'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -// 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], -]; - -export default @injectIntl -class TextareaIcons extends ImmutablePureComponent { - - static propTypes = { - advancedOptions: ImmutablePropTypes.map, - intl: PropTypes.object.isRequired, - }; - - render () { - const { advancedOptions, intl } = this.props; - return ( -
    - {advancedOptions ? iconMap.map( - ([key, icon, message]) => advancedOptions.get(key) ? ( - - - - ) : null, - ) : null} -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/textarea_icons.jsx b/app/javascript/flavours/glitch/features/compose/components/textarea_icons.jsx new file mode 100644 index 000000000..d8ee5c81b --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/textarea_icons.jsx @@ -0,0 +1,60 @@ +// Package imports. +import PropTypes from 'prop-types'; +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { defineMessages, injectIntl } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; + +// 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], +]; + +export default @injectIntl +class TextareaIcons extends ImmutablePureComponent { + + static propTypes = { + advancedOptions: ImmutablePropTypes.map, + intl: PropTypes.object.isRequired, + }; + + render () { + const { advancedOptions, intl } = this.props; + return ( +
    + {advancedOptions ? iconMap.map( + ([key, icon, message]) => advancedOptions.get(key) ? ( + + + + ) : null, + ) : null} +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.js b/app/javascript/flavours/glitch/features/compose/components/upload.js deleted file mode 100644 index 63582c636..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/upload.js +++ /dev/null @@ -1,67 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import PropTypes from 'prop-types'; -import Motion from '../../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { FormattedMessage } from 'react-intl'; -import Icon from 'flavours/glitch/components/icon'; -import { isUserTouching } from 'flavours/glitch/is_mobile'; - -export default class Upload extends ImmutablePureComponent { - - static contextTypes = { - router: PropTypes.object, - }; - - static propTypes = { - media: ImmutablePropTypes.map.isRequired, - onUndo: PropTypes.func.isRequired, - onOpenFocalPoint: PropTypes.func.isRequired, - }; - - handleUndoClick = e => { - e.stopPropagation(); - this.props.onUndo(this.props.media.get('id')); - }; - - handleFocalPointClick = e => { - e.stopPropagation(); - this.props.onOpenFocalPoint(this.props.media.get('id')); - }; - - render () { - const { media } = this.props; - - if (!media) { - return null; - } - - const focusX = media.getIn(['meta', 'focus', 'x']); - const focusY = media.getIn(['meta', 'focus', 'y']); - const x = ((focusX / 2) + .5) * 100; - const y = ((focusY / -2) + .5) * 100; - - return ( -
    - - {({ scale }) => ( -
    -
    - - -
    - - {(media.get('description') || '').length === 0 && ( -
    - -
    - )} -
    - )} -
    -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload.jsx b/app/javascript/flavours/glitch/features/compose/components/upload.jsx new file mode 100644 index 000000000..63582c636 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/upload.jsx @@ -0,0 +1,67 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import PropTypes from 'prop-types'; +import Motion from '../../ui/util/optional_motion'; +import spring from 'react-motion/lib/spring'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import { FormattedMessage } from 'react-intl'; +import Icon from 'flavours/glitch/components/icon'; +import { isUserTouching } from 'flavours/glitch/is_mobile'; + +export default class Upload extends ImmutablePureComponent { + + static contextTypes = { + router: PropTypes.object, + }; + + static propTypes = { + media: ImmutablePropTypes.map.isRequired, + onUndo: PropTypes.func.isRequired, + onOpenFocalPoint: PropTypes.func.isRequired, + }; + + handleUndoClick = e => { + e.stopPropagation(); + this.props.onUndo(this.props.media.get('id')); + }; + + handleFocalPointClick = e => { + e.stopPropagation(); + this.props.onOpenFocalPoint(this.props.media.get('id')); + }; + + render () { + const { media } = this.props; + + if (!media) { + return null; + } + + const focusX = media.getIn(['meta', 'focus', 'x']); + const focusY = media.getIn(['meta', 'focus', 'y']); + const x = ((focusX / 2) + .5) * 100; + const y = ((focusY / -2) + .5) * 100; + + return ( +
    + + {({ scale }) => ( +
    +
    + + +
    + + {(media.get('description') || '').length === 0 && ( +
    + +
    + )} +
    + )} +
    +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.js b/app/javascript/flavours/glitch/features/compose/components/upload_form.js deleted file mode 100644 index f2e7fe7a2..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/upload_form.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import UploadProgressContainer from '../containers/upload_progress_container'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import UploadContainer from '../containers/upload_container'; -import SensitiveButtonContainer from '../containers/sensitive_button_container'; - -export default class UploadForm extends ImmutablePureComponent { - - static propTypes = { - mediaIds: ImmutablePropTypes.list.isRequired, - }; - - render () { - const { mediaIds } = this.props; - - return ( -
    - - - {mediaIds.size > 0 && ( -
    - {mediaIds.map(id => ( - - ))} -
    - )} - - {!mediaIds.isEmpty() && } -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx b/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx new file mode 100644 index 000000000..f2e7fe7a2 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/upload_form.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import UploadProgressContainer from '../containers/upload_progress_container'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import UploadContainer from '../containers/upload_container'; +import SensitiveButtonContainer from '../containers/sensitive_button_container'; + +export default class UploadForm extends ImmutablePureComponent { + + static propTypes = { + mediaIds: ImmutablePropTypes.list.isRequired, + }; + + render () { + const { mediaIds } = this.props; + + return ( +
    + + + {mediaIds.size > 0 && ( +
    + {mediaIds.map(id => ( + + ))} +
    + )} + + {!mediaIds.isEmpty() && } +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js b/app/javascript/flavours/glitch/features/compose/components/upload_progress.js deleted file mode 100644 index 39ac31053..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/upload_progress.js +++ /dev/null @@ -1,52 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Motion from '../../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import Icon from 'flavours/glitch/components/icon'; -import { FormattedMessage } from 'react-intl'; - -export default class UploadProgress extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - progress: PropTypes.number, - isProcessing: PropTypes.bool, - }; - - render () { - const { active, progress, isProcessing } = this.props; - - if (!active) { - return null; - } - - let message; - - if (isProcessing) { - message = ; - } else { - message = ; - } - - return ( -
    -
    - -
    - -
    - {message} - -
    - - {({ width }) => -
    - } - -
    -
    -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx b/app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx new file mode 100644 index 000000000..39ac31053 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/upload_progress.jsx @@ -0,0 +1,52 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Motion from '../../ui/util/optional_motion'; +import spring from 'react-motion/lib/spring'; +import Icon from 'flavours/glitch/components/icon'; +import { FormattedMessage } from 'react-intl'; + +export default class UploadProgress extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + progress: PropTypes.number, + isProcessing: PropTypes.bool, + }; + + render () { + const { active, progress, isProcessing } = this.props; + + if (!active) { + return null; + } + + let message; + + if (isProcessing) { + message = ; + } else { + message = ; + } + + return ( +
    +
    + +
    + +
    + {message} + +
    + + {({ width }) => +
    + } + +
    +
    +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/components/warning.js b/app/javascript/flavours/glitch/features/compose/components/warning.js deleted file mode 100644 index 803b7f86a..000000000 --- a/app/javascript/flavours/glitch/features/compose/components/warning.js +++ /dev/null @@ -1,26 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import Motion from '../../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; - -export default class Warning extends React.PureComponent { - - static propTypes = { - message: PropTypes.node.isRequired, - }; - - render () { - const { message } = this.props; - - return ( - - {({ opacity, scaleX, scaleY }) => ( -
    - {message} -
    - )} -
    - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/warning.jsx b/app/javascript/flavours/glitch/features/compose/components/warning.jsx new file mode 100644 index 000000000..803b7f86a --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/warning.jsx @@ -0,0 +1,26 @@ +import React from 'react'; +import PropTypes from 'prop-types'; +import Motion from '../../ui/util/optional_motion'; +import spring from 'react-motion/lib/spring'; + +export default class Warning extends React.PureComponent { + + static propTypes = { + message: PropTypes.node.isRequired, + }; + + render () { + const { message } = this.props; + + return ( + + {({ opacity, scaleX, scaleY }) => ( +
    + {message} +
    + )} +
    + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js deleted file mode 100644 index 9c23d3f47..000000000 --- a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.js +++ /dev/null @@ -1,75 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import PropTypes from 'prop-types'; -import classNames from 'classnames'; -import { changeComposeSensitivity } from 'flavours/glitch/actions/compose'; -import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; - -const messages = defineMessages({ - marked: { - id: 'compose_form.sensitive.marked', - defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}', - }, - unmarked: { - id: 'compose_form.sensitive.unmarked', - defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}', - }, -}); - -const mapStateToProps = state => { - const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']); - const spoilerText = state.getIn(['compose', 'spoiler_text']); - return { - active: state.getIn(['compose', 'sensitive']) || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0), - disabled: state.getIn(['compose', 'spoiler']), - mediaCount: state.getIn(['compose', 'media_attachments']).size, - }; -}; - -const mapDispatchToProps = dispatch => ({ - - onClick () { - dispatch(changeComposeSensitivity()); - }, - -}); - -class SensitiveButton extends React.PureComponent { - - static propTypes = { - active: PropTypes.bool, - disabled: PropTypes.bool, - mediaCount: PropTypes.number, - onClick: PropTypes.func.isRequired, - intl: PropTypes.object.isRequired, - }; - - render () { - const { active, disabled, mediaCount, onClick, intl } = this.props; - - return ( -
    - -
    - ); - } - -} - -export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); diff --git a/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx new file mode 100644 index 000000000..9c23d3f47 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/sensitive_button_container.jsx @@ -0,0 +1,75 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import PropTypes from 'prop-types'; +import classNames from 'classnames'; +import { changeComposeSensitivity } from 'flavours/glitch/actions/compose'; +import { injectIntl, defineMessages, FormattedMessage } from 'react-intl'; + +const messages = defineMessages({ + marked: { + id: 'compose_form.sensitive.marked', + defaultMessage: '{count, plural, one {Media is marked as sensitive} other {Media is marked as sensitive}}', + }, + unmarked: { + id: 'compose_form.sensitive.unmarked', + defaultMessage: '{count, plural, one {Media is not marked as sensitive} other {Media is not marked as sensitive}}', + }, +}); + +const mapStateToProps = state => { + const spoilersAlwaysOn = state.getIn(['local_settings', 'always_show_spoilers_field']); + const spoilerText = state.getIn(['compose', 'spoiler_text']); + return { + active: state.getIn(['compose', 'sensitive']) || (spoilersAlwaysOn && spoilerText && spoilerText.length > 0), + disabled: state.getIn(['compose', 'spoiler']), + mediaCount: state.getIn(['compose', 'media_attachments']).size, + }; +}; + +const mapDispatchToProps = dispatch => ({ + + onClick () { + dispatch(changeComposeSensitivity()); + }, + +}); + +class SensitiveButton extends React.PureComponent { + + static propTypes = { + active: PropTypes.bool, + disabled: PropTypes.bool, + mediaCount: PropTypes.number, + onClick: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + render () { + const { active, disabled, mediaCount, onClick, intl } = this.props; + + return ( +
    + +
    + ); + } + +} + +export default connect(mapStateToProps, mapDispatchToProps)(injectIntl(SensitiveButton)); diff --git a/app/javascript/flavours/glitch/features/compose/containers/warning_container.js b/app/javascript/flavours/glitch/features/compose/containers/warning_container.js deleted file mode 100644 index 5b48c45e4..000000000 --- a/app/javascript/flavours/glitch/features/compose/containers/warning_container.js +++ /dev/null @@ -1,68 +0,0 @@ -import React from 'react'; -import { connect } from 'react-redux'; -import Warning from '../components/warning'; -import PropTypes from 'prop-types'; -import { FormattedMessage } from 'react-intl'; -import { me } from 'flavours/glitch/initial_state'; -import { profileLink, termsLink } from 'flavours/glitch/utils/backend_links'; - -const buildHashtagRE = () => { - try { - const HASHTAG_SEPARATORS = '_\\u00b7\\u200c'; - const ALPHA = '\\p{L}\\p{M}'; - const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}'; - return new RegExp( - '(?:^|[^\\/\\)\\w])#((' + - '[' + WORD + '_]' + - '[' + WORD + HASHTAG_SEPARATORS + ']*' + - '[' + ALPHA + HASHTAG_SEPARATORS + ']' + - '[' + WORD + HASHTAG_SEPARATORS +']*' + - '[' + WORD + '_]' + - ')|(' + - '[' + WORD + '_]*' + - '[' + ALPHA + ']' + - '[' + WORD + '_]*' + - '))', 'iu', - ); - } catch { - return /(?:^|[^/)\w])#(\w*[a-zA-ZĀ·]\w*)/i; - } -}; - -const APPROX_HASHTAG_RE = buildHashtagRE(); - -const mapStateToProps = state => ({ - needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), - hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])), - directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct', -}); - -const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => { - if (needsLockWarning) { - return }} />} />; - } - - if (hashtagWarning) { - return } />; - } - - if (directMessageWarning) { - const message = ( - - {!!termsLink && } - - ); - - return ; - } - - return null; -}; - -WarningWrapper.propTypes = { - needsLockWarning: PropTypes.bool, - hashtagWarning: PropTypes.bool, - directMessageWarning: PropTypes.bool, -}; - -export default connect(mapStateToProps)(WarningWrapper); diff --git a/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx b/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx new file mode 100644 index 000000000..5b48c45e4 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/containers/warning_container.jsx @@ -0,0 +1,68 @@ +import React from 'react'; +import { connect } from 'react-redux'; +import Warning from '../components/warning'; +import PropTypes from 'prop-types'; +import { FormattedMessage } from 'react-intl'; +import { me } from 'flavours/glitch/initial_state'; +import { profileLink, termsLink } from 'flavours/glitch/utils/backend_links'; + +const buildHashtagRE = () => { + try { + const HASHTAG_SEPARATORS = '_\\u00b7\\u200c'; + const ALPHA = '\\p{L}\\p{M}'; + const WORD = '\\p{L}\\p{M}\\p{N}\\p{Pc}'; + return new RegExp( + '(?:^|[^\\/\\)\\w])#((' + + '[' + WORD + '_]' + + '[' + WORD + HASHTAG_SEPARATORS + ']*' + + '[' + ALPHA + HASHTAG_SEPARATORS + ']' + + '[' + WORD + HASHTAG_SEPARATORS +']*' + + '[' + WORD + '_]' + + ')|(' + + '[' + WORD + '_]*' + + '[' + ALPHA + ']' + + '[' + WORD + '_]*' + + '))', 'iu', + ); + } catch { + return /(?:^|[^/)\w])#(\w*[a-zA-ZĀ·]\w*)/i; + } +}; + +const APPROX_HASHTAG_RE = buildHashtagRE(); + +const mapStateToProps = state => ({ + needsLockWarning: state.getIn(['compose', 'privacy']) === 'private' && !state.getIn(['accounts', me, 'locked']), + hashtagWarning: state.getIn(['compose', 'privacy']) !== 'public' && APPROX_HASHTAG_RE.test(state.getIn(['compose', 'text'])), + directMessageWarning: state.getIn(['compose', 'privacy']) === 'direct', +}); + +const WarningWrapper = ({ needsLockWarning, hashtagWarning, directMessageWarning }) => { + if (needsLockWarning) { + return }} />} />; + } + + if (hashtagWarning) { + return } />; + } + + if (directMessageWarning) { + const message = ( + + {!!termsLink && } + + ); + + return ; + } + + return null; +}; + +WarningWrapper.propTypes = { + needsLockWarning: PropTypes.bool, + hashtagWarning: PropTypes.bool, + directMessageWarning: PropTypes.bool, +}; + +export default connect(mapStateToProps)(WarningWrapper); diff --git a/app/javascript/flavours/glitch/features/compose/index.js b/app/javascript/flavours/glitch/features/compose/index.js deleted file mode 100644 index 241caa03b..000000000 --- a/app/javascript/flavours/glitch/features/compose/index.js +++ /dev/null @@ -1,116 +0,0 @@ -import React from 'react'; -import ComposeFormContainer from './containers/compose_form_container'; -import NavigationContainer from './containers/navigation_container'; -import PropTypes from 'prop-types'; -import ImmutablePropTypes from 'react-immutable-proptypes'; -import { connect } from 'react-redux'; -import { mountCompose, unmountCompose, cycleElefriendCompose } from 'flavours/glitch/actions/compose'; -import { injectIntl, defineMessages } from 'react-intl'; -import classNames from 'classnames'; -import SearchContainer from './containers/search_container'; -import Motion from '../ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; -import SearchResultsContainer from './containers/search_results_container'; -import { me, mascot } from 'flavours/glitch/initial_state'; -import HeaderContainer from './containers/header_container'; -import Column from 'flavours/glitch/components/column'; -import { Helmet } from 'react-helmet'; - -const messages = defineMessages({ - compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, -}); - -const mapStateToProps = (state, ownProps) => ({ - elefriend: state.getIn(['compose', 'elefriend']), - showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false, -}); - -const mapDispatchToProps = (dispatch, { intl }) => ({ - onClickElefriend () { - dispatch(cycleElefriendCompose()); - }, - - onMount () { - dispatch(mountCompose()); - }, - - onUnmount () { - dispatch(unmountCompose()); - }, -}); - -export default @connect(mapStateToProps, mapDispatchToProps) -@injectIntl -class Compose extends React.PureComponent { - - static propTypes = { - multiColumn: PropTypes.bool, - showSearch: PropTypes.bool, - elefriend: PropTypes.number, - onClickElefriend: PropTypes.func, - onMount: PropTypes.func, - onUnmount: PropTypes.func, - intl: PropTypes.object.isRequired, - }; - - componentDidMount () { - this.props.onMount(); - } - - componentWillUnmount () { - this.props.onUnmount(); - } - - render () { - const { - elefriend, - intl, - multiColumn, - onClickElefriend, - showSearch, - } = this.props; - const computedClass = classNames('drawer', `mbstobon-${elefriend}`); - - if (multiColumn) { - return ( -
    - - - {multiColumn && } - -
    -
    - - - - -
    - {mascot ? :
    -
    - - - {({ x }) => ( -
    - -
    - )} -
    -
    -
    - ); - } - - return ( - - - - - - - - - ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/index.jsx b/app/javascript/flavours/glitch/features/compose/index.jsx new file mode 100644 index 000000000..241caa03b --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/index.jsx @@ -0,0 +1,116 @@ +import React from 'react'; +import ComposeFormContainer from './containers/compose_form_container'; +import NavigationContainer from './containers/navigation_container'; +import PropTypes from 'prop-types'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { connect } from 'react-redux'; +import { mountCompose, unmountCompose, cycleElefriendCompose } from 'flavours/glitch/actions/compose'; +import { injectIntl, defineMessages } from 'react-intl'; +import classNames from 'classnames'; +import SearchContainer from './containers/search_container'; +import Motion from '../ui/util/optional_motion'; +import spring from 'react-motion/lib/spring'; +import SearchResultsContainer from './containers/search_results_container'; +import { me, mascot } from 'flavours/glitch/initial_state'; +import HeaderContainer from './containers/header_container'; +import Column from 'flavours/glitch/components/column'; +import { Helmet } from 'react-helmet'; + +const messages = defineMessages({ + compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new post' }, +}); + +const mapStateToProps = (state, ownProps) => ({ + elefriend: state.getIn(['compose', 'elefriend']), + showSearch: ownProps.multiColumn ? state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']) : false, +}); + +const mapDispatchToProps = (dispatch, { intl }) => ({ + onClickElefriend () { + dispatch(cycleElefriendCompose()); + }, + + onMount () { + dispatch(mountCompose()); + }, + + onUnmount () { + dispatch(unmountCompose()); + }, +}); + +export default @connect(mapStateToProps, mapDispatchToProps) +@injectIntl +class Compose extends React.PureComponent { + + static propTypes = { + multiColumn: PropTypes.bool, + showSearch: PropTypes.bool, + elefriend: PropTypes.number, + onClickElefriend: PropTypes.func, + onMount: PropTypes.func, + onUnmount: PropTypes.func, + intl: PropTypes.object.isRequired, + }; + + componentDidMount () { + this.props.onMount(); + } + + componentWillUnmount () { + this.props.onUnmount(); + } + + render () { + const { + elefriend, + intl, + multiColumn, + onClickElefriend, + showSearch, + } = this.props; + const computedClass = classNames('drawer', `mbstobon-${elefriend}`); + + if (multiColumn) { + return ( +
    + + + {multiColumn && } + +
    +
    + + + + +
    + {mascot ? :
    +
    + + + {({ x }) => ( +
    + +
    + )} +
    +
    +
    + ); + } + + return ( + + + + + + + + + ); + } + +} -- cgit