diff --git a/app/javascript/flavours/glitch/actions/compose.js b/app/javascript/flavours/glitch/actions/compose.js index 07f60b9270..d2c049f341 100644 --- a/app/javascript/flavours/glitch/actions/compose.js +++ b/app/javascript/flavours/glitch/actions/compose.js @@ -59,7 +59,7 @@ export const COMPOSE_SENSITIVITY_CHANGE = 'COMPOSE_SENSITIVITY_CHANGE'; export const COMPOSE_SPOILERNESS_CHANGE = 'COMPOSE_SPOILERNESS_CHANGE'; export const COMPOSE_SPOILER_TEXT_CHANGE = 'COMPOSE_SPOILER_TEXT_CHANGE'; export const COMPOSE_VISIBILITY_CHANGE = 'COMPOSE_VISIBILITY_CHANGE'; -export const COMPOSE_LISTABILITY_CHANGE = 'COMPOSE_LISTABILITY_CHANGE'; +export const COMPOSE_COMPOSING_CHANGE = 'COMPOSE_COMPOSING_CHANGE'; export const COMPOSE_CONTENT_TYPE_CHANGE = 'COMPOSE_CONTENT_TYPE_CHANGE'; export const COMPOSE_LANGUAGE_CHANGE = 'COMPOSE_LANGUAGE_CHANGE'; @@ -148,13 +148,13 @@ export function resetCompose() { }; } -export const focusCompose = (routerHistory, defaultText) => dispatch => { +export const focusCompose = (routerHistory, defaultText) => (dispatch, getState) => { dispatch({ type: COMPOSE_FOCUS, defaultText, }); - ensureComposeIsVisible(routerHistory); + ensureComposeIsVisible(getState, routerHistory); }; export function mentionCompose(account, routerHistory) { @@ -246,11 +246,6 @@ export function submitCompose(routerHistory) { dispatch(insertIntoTagHistory(response.data.tags, status)); dispatch(submitComposeSuccess({ ...response.data })); - // If the response has no data then we can't do anything else. - if (!response.data) { - return; - } - // To make the app more responsive, immediately push the status // into the columns const insertIfOnline = timelineId => { @@ -660,15 +655,19 @@ export const readyComposeSuggestionsTags = (token, tags) => ({ export function selectComposeSuggestion(position, token, suggestion, path) { return (dispatch, getState) => { - let completion; + let completion, startPosition; + if (suggestion.type === 'emoji') { - completion = suggestion.native || suggestion.colons; + completion = suggestion.native || suggestion.colons; + startPosition = position - 1; dispatch(useEmoji(suggestion)); } else if (suggestion.type === 'hashtag') { - completion = `#${suggestion.name}`; + completion = `#${suggestion.name}`; + startPosition = position - 1; } else if (suggestion.type === 'account') { - completion = '@' + getState().getIn(['accounts', suggestion.id, 'acct']); + completion = getState().getIn(['accounts', suggestion.id, 'acct']); + startPosition = position; } // We don't want to replace hashtags that vary only in case due to accessibility, but we need to fire off an event so that @@ -676,7 +675,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) { if (suggestion.type !== 'hashtag' || token.slice(1).localeCompare(suggestion.name, undefined, { sensitivity: 'accent' }) !== 0) { dispatch({ type: COMPOSE_SUGGESTION_SELECT, - position, + position: startPosition, token, completion, path, @@ -684,7 +683,7 @@ export function selectComposeSuggestion(position, token, suggestion, path) { } else { dispatch({ type: COMPOSE_SUGGESTION_IGNORE, - position, + position: startPosition, token, completion, path, @@ -786,18 +785,26 @@ export function changeComposeVisibility(value) { }; } -export function changeComposeContentType(value) { - return { - type: COMPOSE_CONTENT_TYPE_CHANGE, - value, - }; -} - -export function insertEmojiCompose(position, emoji) { +export function insertEmojiCompose(position, emoji, needsSpace) { return { type: COMPOSE_EMOJI_INSERT, position, emoji, + needsSpace, + }; +} + +export function changeComposing(value) { + return { + type: COMPOSE_COMPOSING_CHANGE, + value, + }; +} + +export function changeComposeContentType(value) { + return { + type: COMPOSE_CONTENT_TYPE_CHANGE, + value, }; } diff --git a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx index de5d2369c3..411ad77c79 100644 --- a/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/action_bar.jsx @@ -6,8 +6,6 @@ import { defineMessages, injectIntl } from 'react-intl'; import ImmutablePropTypes from 'react-immutable-proptypes'; import MenuIcon from '@/material-icons/400-24px/menu.svg?react'; -import { preferencesLink, profileLink } from 'flavours/glitch/utils/backend_links'; - import DropdownMenuContainer from '../../../containers/dropdown_menu_container'; @@ -44,8 +42,8 @@ class ActionBar extends PureComponent { 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.edit_profile), href: '/settings/profile' }); + menu.push({ text: intl.formatMessage(messages.preferences), href: '/settings/preferences' }); menu.push({ text: intl.formatMessage(messages.pins), to: '/pinned' }); menu.push(null); menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' }); diff --git a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx index 6c3ee3846b..0a73bc1020 100644 --- a/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/autosuggest_account.jsx @@ -16,7 +16,7 @@ export default class AutosuggestAccount extends ImmutablePureComponent { return (
- +
); } diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx index 12f4f2948b..1f24828c66 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.jsx @@ -10,35 +10,36 @@ import ImmutablePureComponent from 'react-immutable-pure-component'; import { length } from 'stringz'; -import { maxChars } from 'flavours/glitch/initial_state'; -import { isMobile } from 'flavours/glitch/is_mobile'; +import LockIcon from '@/material-icons/400-24px/lock.svg?react'; +import { Icon } from 'flavours/glitch/components/icon'; import { WithOptionalRouterPropTypes, withOptionalRouter } from 'flavours/glitch/utils/react_router'; import AutosuggestInput from '../../../components/autosuggest_input'; import AutosuggestTextarea from '../../../components/autosuggest_textarea'; +import { Button } from '../../../components/button'; +import { maxChars } from '../../../initial_state'; import EmojiPickerDropdown from '../containers/emoji_picker_dropdown_container'; -import OptionsContainer from '../containers/options_container'; +import LanguageDropdown from '../containers/language_dropdown_container'; +import PollButtonContainer from '../containers/poll_button_container'; import PollFormContainer from '../containers/poll_form_container'; +import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; import ReplyIndicatorContainer from '../containers/reply_indicator_container'; +import SpoilerButtonContainer from '../containers/spoiler_button_container'; +import UploadButtonContainer from '../containers/upload_button_container'; import UploadFormContainer from '../containers/upload_form_container'; import WarningContainer from '../containers/warning_container'; import { countableText } from '../util/counter'; import CharacterCounter from './character_counter'; -import Publisher from './publisher'; -import TextareaIcons from './textarea_icons'; + +const allowedAroundShortCode = '><\u0085\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u202f\u205f\u3000\u2028\u2029\u0009\u000a\u000b\u000c\u000d'; 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' }, + publish: { id: 'compose_form.publish', defaultMessage: 'Publish' }, + publishLoud: { id: 'compose_form.publish_loud', defaultMessage: '{publish}!' }, + saveChanges: { id: 'compose_form.save_changes', defaultMessage: 'Save changes' }, }); class ComposeForm extends ImmutablePureComponent { @@ -64,26 +65,16 @@ class ComposeForm extends ImmutablePureComponent { onChangeSpoilerText: PropTypes.func.isRequired, onPaste: PropTypes.func.isRequired, onPickEmoji: PropTypes.func.isRequired, - showSearch: PropTypes.bool, + autoFocus: PropTypes.bool, anyMedia: PropTypes.bool, isInReply: PropTypes.bool, singleColumn: PropTypes.bool, lang: PropTypes.string, - advancedOptions: ImmutablePropTypes.map, - media: ImmutablePropTypes.list, - sideArm: PropTypes.string, - sensitive: PropTypes.bool, - spoilersAlwaysOn: PropTypes.bool, - mediaDescriptionConfirmation: PropTypes.bool, - preselectOnReply: PropTypes.bool, - onChangeSpoilerness: PropTypes.func.isRequired, - onChangeVisibility: PropTypes.func.isRequired, - onMediaDescriptionConfirm: PropTypes.func.isRequired, ...WithOptionalRouterPropTypes }; static defaultProps = { - showSearch: false, + autoFocus: false, }; state = { @@ -103,28 +94,21 @@ class ComposeForm extends ImmutablePureComponent { if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) { this.handleSubmit(); } - - if (e.keyCode === 13 && e.altKey) { - this.handleSecondarySubmit(); - } }; getFulltextForCharacterCounting = () => { - return [ - this.props.spoiler? this.props.spoilerText: '', - countableText(this.props.text), - this.props.advancedOptions && this.props.advancedOptions.get('do_not_federate') ? ' 👁️' : '', - ].join(''); + return [this.props.spoiler? this.props.spoilerText: '', countableText(this.props.text)].join(''); }; canSubmit = () => { const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props; const fulltext = this.getFulltextForCharacterCounting(); + const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0; - return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (!fulltext.trim().length && !anyMedia)); + return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > maxChars || (isOnlyWhitespace && !anyMedia)); }; - handleSubmit = (e, overriddenVisibility = null) => { + handleSubmit = (e) => { if (this.props.text !== this.textareaRef.current.value) { // Something changed the text inside the textarea (e.g. browser extensions like Grammarly) // Update the state to match the current text @@ -135,26 +119,11 @@ class ComposeForm extends ImmutablePureComponent { return; } + this.props.onSubmit(this.props.history || null); + if (e) { e.preventDefault(); } - - // Submit unless there are media with missing descriptions - if (this.props.mediaDescriptionConfirmation && this.props.media && this.props.media.some(item => !item.get('description'))) { - const firstWithoutDescription = this.props.media.find(item => !item.get('description')); - this.props.onMediaDescriptionConfirm(this.props.history || null, firstWithoutDescription.get('id'), overriddenVisibility); - } else { - if (overriddenVisibility) { - this.props.onChangeVisibility(overriddenVisibility); - } - this.props.onSubmit(this.props.history || null); - } - }; - - // Handles the secondary submit button. - handleSecondarySubmit = () => { - const { sideArm } = this.props; - this.handleSubmit(null, sideArm === 'none' ? null : sideArm); }; onSuggestionsClearRequested = () => { @@ -207,7 +176,7 @@ class ComposeForm extends ImmutablePureComponent { if (this.props.focusDate && this.props.focusDate !== prevProps.focusDate) { let selectionEnd, selectionStart; - if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply && this.props.preselectOnReply) { + if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply) { selectionEnd = this.props.text.length; selectionStart = this.props.text.search(/\s/) + 1; } else if (typeof this.props.caretPosition === 'number') { @@ -224,7 +193,6 @@ class ComposeForm extends ImmutablePureComponent { Promise.resolve().then(() => { this.textareaRef.current.setSelectionRange(selectionStart, selectionEnd); this.textareaRef.current.focus(); - if (!this.props.singleColumn) this.textareaRef.current.scrollIntoView(); this.setState({ highlighted: true }); this.timeout = setTimeout(() => this.setState({ highlighted: false }), 700); }).catch(console.error); @@ -248,28 +216,28 @@ class ComposeForm extends ImmutablePureComponent { }; handleEmojiPick = (data) => { - const position = this.textareaRef.current.selectionStart; + const { text } = this.props; + const position = this.textareaRef.current.selectionStart; + const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]); - this.props.onPickEmoji(position, data); + this.props.onPickEmoji(position, data, needsSpace); }; render () { - const { - intl, - advancedOptions, - isSubmitting, - onChangeSpoilerness, - onPaste, - privacy, - sensitive, - showSearch, - sideArm, - spoilersAlwaysOn, - isEditing, - } = this.props; + const { intl, onPaste, autoFocus } = this.props; const { highlighted } = this.state; const disabled = this.props.isSubmitting; + let publishText = ''; + + if (this.props.isEditing) { + publishText = intl.formatMessage(messages.saveChanges); + } else if (this.props.privacy === 'private' || this.props.privacy === 'direct') { + publishText = <> {intl.formatMessage(messages.publish)}; + } else { + publishText = this.props.privacy !== 'unlisted' ? intl.formatMessage(messages.publishLoud, { publish: intl.formatMessage(messages.publish) }) : intl.formatMessage(messages.publish); + } + return (
@@ -292,7 +260,6 @@ class ComposeForm extends ImmutablePureComponent { id='cw-spoiler-input' className='spoiler-input__input' lang={this.props.lang} - autoFocus={false} spellCheck /> @@ -311,10 +278,9 @@ class ComposeForm extends ImmutablePureComponent { onSuggestionsClearRequested={this.onSuggestionsClearRequested} onSuggestionSelected={this.onSuggestionSelected} onPaste={onPaste} - autoFocus={!showSearch && !isMobile(window.innerWidth)} + autoFocus={autoFocus} lang={this.props.lang} > -
@@ -323,28 +289,30 @@ class ComposeForm extends ImmutablePureComponent {
- 0)} - spoiler={spoilersAlwaysOn ? (this.props.spoilerText && this.props.spoilerText.length > 0) : this.props.spoiler} - /> +
+ + + + + +
+
- +
+
+
+
); } diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx deleted file mode 100644 index 4b9098835f..0000000000 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.jsx +++ /dev/null @@ -1,243 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import classNames from 'classnames'; - -import Overlay from 'react-overlays/Overlay'; - -// Components. -import { IconButton } from 'flavours/glitch/components/icon_button'; - -import DropdownMenu from './dropdown_menu'; - -// The component. -export default class ComposerOptionsDropdown extends PureComponent { - - static propTypes = { - isUserTouching: PropTypes.func, - disabled: PropTypes.bool, - icon: PropTypes.string, - iconComponent: PropTypes.func, - items: PropTypes.arrayOf(PropTypes.shape({ - icon: PropTypes.string, - iconComponent: PropTypes.func, - 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 (open) { - this.props.onModalClose(); - } else { - const modal = this.handleMakeModal(); - if (modal && onModalOpen) { - onModalOpen(modal); - } - } - } else { - if (open && this.activeElement) { - this.activeElement.focus({ preventScroll: true }); - } - this.setState({ open: !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, - iconComponent, - items, - onChange, - value, - container, - renderItemContents, - closeOnChange, - } = this.props; - const { open, placement } = this.state; - - return ( -
- - - - {({ props, placement }) => ( -
-
- -
-
- )} -
-
- ); - } - -} diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx deleted file mode 100644 index e5f24b51f9..0000000000 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.jsx +++ /dev/null @@ -1,205 +0,0 @@ -// Package imports. -import PropTypes from 'prop-types'; -import { PureComponent } from 'react'; - -import classNames from 'classnames'; - -import { supportsPassiveEvents } from 'detect-passive-events'; - -// Components. -import { Icon } from 'flavours/glitch/components/icon'; - -const listenerOptions = supportsPassiveEvents ? { passive: true, capture: true } : true; - -// The component. -export default class ComposerOptionsDropdownContent extends PureComponent { - - static propTypes = { - items: PropTypes.arrayOf(PropTypes.shape({ - icon: PropTypes.string, - iconComponent: PropTypes.func, - 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(); - e.stopPropagation(); - } - }; - - // Stores our node in `this.node`. - setRef = (node) => { - this.node = node; - }; - - // On mounting, we add our listeners. - componentDidMount () { - document.addEventListener('click', this.handleDocumentClick, { capture: true }); - document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - 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, { capture: true }); - document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions); - } - - handleClick = (e) => { - const i = Number(e.currentTarget.getAttribute('data-index')); - - const { - onChange, - onClose, - closeOnChange, - items, - } = this.props; - - const { name } = 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(items[Number(element.getAttribute('data-index'))].name); - e.preventDefault(); - e.stopPropagation(); - } - }; - - setFocusRef = c => { - this.focusedItem = c; - }; - - renderItem = (item, i) => { - const { name, icon, iconComponent, 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, - 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/header.jsx b/app/javascript/flavours/glitch/features/compose/components/header.jsx deleted file mode 100644 index bdcb51cd92..0000000000 --- a/app/javascript/flavours/glitch/features/compose/components/header.jsx +++ /dev/null @@ -1,149 +0,0 @@ -import PropTypes from 'prop-types'; - -import { injectIntl, defineMessages } from 'react-intl'; - -import { Link } from 'react-router-dom'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; - -import PeopleIcon from '@/material-icons/400-24px/group.svg?react'; -import HomeIcon from '@/material-icons/400-24px/home-fill.svg?react'; -import LogoutIcon from '@/material-icons/400-24px/logout.svg?react'; -import ManufacturingIcon from '@/material-icons/400-24px/manufacturing.svg?react'; -import MenuIcon from '@/material-icons/400-24px/menu.svg?react'; -import NotificationsIcon from '@/material-icons/400-24px/notifications-fill.svg?react'; -import PublicIcon from '@/material-icons/400-24px/public.svg?react'; -import { Icon } from 'flavours/glitch/components/icon'; -import { signOutLink } from 'flavours/glitch/utils/backend_links'; -import { conditionalRender } from 'flavours/glitch/utils/react_helpers'; - - -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', - }, -}); - -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 ( - - ); - } - -} - -export default injectIntl(Header); diff --git a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx index fed84359fa..2f0bb79f89 100644 --- a/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/navigation_bar.jsx @@ -2,12 +2,11 @@ import PropTypes from 'prop-types'; import { FormattedMessage } from 'react-intl'; +import { Link } from 'react-router-dom'; + import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; -import { Permalink } from 'flavours/glitch/components/permalink'; -import { profileLink } from 'flavours/glitch/utils/backend_links'; - import { Avatar } from '../../../components/avatar'; import ActionBar from './action_bar'; @@ -24,24 +23,21 @@ export default class NavigationBar extends ImmutablePureComponent { const username = this.props.account.get('acct'); return (
- + {username} - +
- + @{username} - + - { profileLink !== undefined && ( - - )} + + +
diff --git a/app/javascript/flavours/glitch/features/compose/components/options.jsx b/app/javascript/flavours/glitch/features/compose/components/options.jsx deleted file mode 100644 index 5460960364..0000000000 --- a/app/javascript/flavours/glitch/features/compose/components/options.jsx +++ /dev/null @@ -1,330 +0,0 @@ -import PropTypes from 'prop-types'; - -import { defineMessages, injectIntl } from 'react-intl'; - -import ImmutablePropTypes from 'react-immutable-proptypes'; -import ImmutablePureComponent from 'react-immutable-pure-component'; -import { connect } from 'react-redux'; - -import Toggle from 'react-toggle'; - -import AttachFileIcon from '@/material-icons/400-24px/attach_file.svg?react'; -import BrushIcon from '@/material-icons/400-24px/brush.svg?react'; -import CodeIcon from '@/material-icons/400-24px/code.svg?react'; -import DescriptionIcon from '@/material-icons/400-24px/description.svg?react'; -import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; -import MarkdownIcon from '@/material-icons/400-24px/markdown.svg?react'; -import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; -import UploadFileIcon from '@/material-icons/400-24px/upload_file.svg?react'; -import { IconButton } from 'flavours/glitch/components/icon_button'; -import { pollLimits } from 'flavours/glitch/initial_state'; - - -import DropdownContainer from '../containers/dropdown_container'; -import LanguageDropdown from '../containers/language_dropdown_container'; -import PrivacyDropdownContainer from '../containers/privacy_dropdown_container'; - -import TextIconButton from './text_icon_button'; - -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', - }, -}); - -const mapStateToProps = (state, { name }) => ({ - checked: state.getIn(['compose', 'advanced_options', name]), -}); - -class ToggleOptionImpl 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} -
- - ); - } - -} - -const ToggleOption = connect(mapStateToProps)(ToggleOptionImpl); - -class ComposerOptions extends ImmutablePureComponent { - - static propTypes = { - acceptContentTypes: PropTypes.string, - advancedOptions: ImmutablePropTypes.map, - disabled: PropTypes.bool, - allowMedia: PropTypes.bool, - allowPoll: PropTypes.bool, - hasPoll: PropTypes.bool, - intl: PropTypes.object.isRequired, - onChangeAdvancedOption: PropTypes.func.isRequired, - onChangeContentType: PropTypes.func.isRequired, - onTogglePoll: PropTypes.func.isRequired, - onDoodleOpen: PropTypes.func.isRequired, - onToggleSpoiler: PropTypes.func, - onUpload: PropTypes.func.isRequired, - contentType: PropTypes.string, - resetFileKey: PropTypes.number, - spoiler: PropTypes.bool, - showContentTypeChoice: PropTypes.bool, - isEditing: PropTypes.bool, - }; - - handleChangeFiles = ({ target: { files } }) => { - const { onUpload } = this.props; - if (files.length) { - onUpload(files); - } - }; - - handleClickAttach = (name) => { - const { fileElement } = this; - const { onDoodleOpen } = this.props; - - switch (name) { - case 'upload': - if (fileElement) { - fileElement.click(); - } - return; - case 'doodle': - onDoodleOpen(); - return; - } - }; - - handleRefFileElement = (fileElement) => { - this.fileElement = fileElement; - }; - - renderToggleItemContents = (item) => { - const { onChangeAdvancedOption } = this.props; - const { name, meta, text } = item; - - return ; - }; - - render () { - const { - acceptContentTypes, - advancedOptions, - contentType, - disabled, - allowMedia, - allowPoll, - hasPoll, - onChangeAdvancedOption, - onChangeContentType, - onTogglePoll, - onToggleSpoiler, - resetFileKey, - spoiler, - showContentTypeChoice, - isEditing, - intl: { formatMessage }, - } = this.props; - - const contentTypeItems = { - plain: { - icon: 'file-text', - iconComponent: DescriptionIcon, - name: 'text/plain', - text: formatMessage(messages.plain), - }, - html: { - icon: 'code', - iconComponent: CodeIcon, - name: 'text/html', - text: formatMessage(messages.html), - }, - markdown: { - icon: 'arrow-circle-down', - iconComponent: MarkdownIcon, - name: 'text/markdown', - text: formatMessage(messages.markdown), - }, - }; - - // The result. - return ( -
- - - {!!pollLimits && ( - - )} - - {showContentTypeChoice && ( - - )} - {onToggleSpoiler && ( - - )} - - -
- ); - } - -} - -export default injectIntl(ComposerOptions); diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_button.jsx b/app/javascript/flavours/glitch/features/compose/components/poll_button.jsx new file mode 100644 index 0000000000..4900d38119 --- /dev/null +++ b/app/javascript/flavours/glitch/features/compose/components/poll_button.jsx @@ -0,0 +1,61 @@ +import PropTypes from 'prop-types'; +import { PureComponent } from 'react'; + +import { defineMessages, injectIntl } from 'react-intl'; + +import InsertChartIcon from '@/material-icons/400-24px/insert_chart.svg?react'; + +import { IconButton } from '../../../components/icon_button'; + + +const messages = defineMessages({ + add_poll: { id: 'poll_button.add_poll', defaultMessage: 'Add a poll' }, + remove_poll: { id: 'poll_button.remove_poll', defaultMessage: 'Remove poll' }, +}); + +const iconStyle = { + height: null, + lineHeight: '27px', +}; + +class PollButton extends PureComponent { + + static propTypes = { + disabled: PropTypes.bool, + unavailable: PropTypes.bool, + active: PropTypes.bool, + onClick: PropTypes.func.isRequired, + intl: PropTypes.object.isRequired, + }; + + handleClick = () => { + this.props.onClick(); + }; + + render () { + const { intl, active, unavailable, disabled } = this.props; + + if (unavailable) { + return null; + } + + return ( +
+ +
+ ); + } + +} + +export default injectIntl(PollButton); diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx b/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx index 053b02673d..1ee0a06c62 100644 --- a/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx +++ b/app/javascript/flavours/glitch/features/compose/components/poll_form.jsx @@ -8,21 +8,19 @@ import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import ImmutablePureComponent from 'react-immutable-pure-component'; - import AddIcon from '@/material-icons/400-24px/add.svg?react'; import CloseIcon from '@/material-icons/400-24px/close.svg?react'; import AutosuggestInput from 'flavours/glitch/components/autosuggest_input'; import { Icon } from 'flavours/glitch/components/icon'; import { IconButton } from 'flavours/glitch/components/icon_button'; -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' }, + switchToMultiple: { id: 'compose_form.poll.switch_to_multiple', defaultMessage: 'Change poll to allow multiple choices' }, + switchToSingle: { id: 'compose_form.poll.switch_to_single', defaultMessage: 'Change poll to allow for a single choice' }, 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}}' }, @@ -38,6 +36,7 @@ class OptionIntl extends PureComponent { autoFocus: PropTypes.bool, onChange: PropTypes.func.isRequired, onRemove: PropTypes.func.isRequired, + onToggleMultiple: PropTypes.func.isRequired, suggestions: ImmutablePropTypes.list, onClearSuggestions: PropTypes.func.isRequired, onFetchSuggestions: PropTypes.func.isRequired, @@ -53,6 +52,19 @@ class OptionIntl extends PureComponent { this.props.onRemove(this.props.index); }; + + handleToggleMultiple = e => { + this.props.onToggleMultiple(); + e.preventDefault(); + e.stopPropagation(); + }; + + handleCheckboxKeypress = e => { + if (e.key === 'Enter' || e.key === ' ') { + this.handleToggleMultiple(e); + } + }; + onSuggestionsClearRequested = () => { this.props.onClearSuggestions(); }; @@ -71,11 +83,19 @@ class OptionIntl extends PureComponent { return (