diff options
Diffstat (limited to 'app/javascript/flavours/glitch/features')
24 files changed, 278 insertions, 227 deletions
diff --git a/app/javascript/flavours/glitch/features/account/components/follow_request_note.js b/app/javascript/flavours/glitch/features/account/components/follow_request_note.js new file mode 100644 index 000000000..73c1737a6 --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/components/follow_request_note.js @@ -0,0 +1,37 @@ +import React from 'react'; +import ImmutablePropTypes from 'react-immutable-proptypes'; +import { FormattedMessage } from 'react-intl'; +import ImmutablePureComponent from 'react-immutable-pure-component'; +import Icon from 'flavours/glitch/components/icon'; + +export default class FollowRequestNote extends ImmutablePureComponent { + + static propTypes = { + account: ImmutablePropTypes.map.isRequired, + }; + + render () { + const { account, onAuthorize, onReject } = this.props; + + return ( + <div className='follow-request-banner'> + <div className='follow-request-banner__message'> + <FormattedMessage id='account.requested_follow' defaultMessage='{name} has requested to follow you' values={{ name: <bdi><strong dangerouslySetInnerHTML={{ __html: account.get('display_name_html') }} /></bdi> }} /> + </div> + + <div className='follow-request-banner__action'> + <button type='button' className='button button-tertiary button--confirmation' onClick={onAuthorize}> + <Icon id='check' fixedWidth /> + <FormattedMessage id='follow_request.authorize' defaultMessage='Authorize' /> + </button> + + <button type='button' className='button button-tertiary button--destructive' onClick={onReject}> + <Icon id='times' fixedWidth /> + <FormattedMessage id='follow_request.reject' defaultMessage='Reject' /> + </button> + </div> + </div> + ); + } + +} diff --git a/app/javascript/flavours/glitch/features/account/components/header.js b/app/javascript/flavours/glitch/features/account/components/header.js index 9a5f2fd62..ec4a192bc 100644 --- a/app/javascript/flavours/glitch/features/account/components/header.js +++ b/app/javascript/flavours/glitch/features/account/components/header.js @@ -13,7 +13,8 @@ import Button from 'flavours/glitch/components/button'; import { NavLink } from 'react-router-dom'; import DropdownMenuContainer from 'flavours/glitch/containers/dropdown_menu_container'; import AccountNoteContainer from '../containers/account_note_container'; -import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; +import FollowRequestNoteContainer from '../containers/follow_request_note_container'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; import { Helmet } from 'react-helmet'; const messages = defineMessages({ @@ -51,6 +52,7 @@ const messages = defineMessages({ unendorse: { id: 'account.unendorse', defaultMessage: 'Don\'t feature on profile' }, add_or_remove_from_list: { id: 'account.add_or_remove_from_list', defaultMessage: 'Add or Remove from lists' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, + admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, add_account_note: { id: 'account.add_account_note', defaultMessage: 'Add note for @{name}' }, languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, @@ -154,7 +156,7 @@ class Header extends ImmutablePureComponent { render () { const { account, hidden, intl, domain } = this.props; - const { signedIn } = this.context.identity; + const { signedIn, permissions } = this.context.identity; if (!account) { return null; @@ -290,9 +292,14 @@ class Header extends ImmutablePureComponent { } } - if (account.get('id') !== me && (this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) { + if (account.get('id') !== me && ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { menu.push(null); - menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) }); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && accountAdminLink) { + menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: accountAdminLink(account.get('id')) }); + } + if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { + menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: remoteDomain }), href: `/admin/instances/${remoteDomain}` }); + } } const content = { __html: account.get('note_emojified') }; @@ -314,6 +321,8 @@ class Header extends ImmutablePureComponent { return ( <div className={classNames('account__header', { inactive: !!account.get('moved') })} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + {!(suspended || hidden || account.get('moved')) && account.getIn(['relationship', 'requested_by']) && <FollowRequestNoteContainer account={account} />} + <div className='account__header__image'> <div className='account__header__info'> {info} @@ -345,7 +354,9 @@ class Header extends ImmutablePureComponent { <div className='account__header__tabs__name'> <h1> <span dangerouslySetInnerHTML={displayNameHtml} /> {badge} - <small>@{acct} {lockedIcon}</small> + <small> + <span>@{acct}</span> {lockedIcon} + </small> </h1> </div> diff --git a/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js b/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js new file mode 100644 index 000000000..c6a3afb7e --- /dev/null +++ b/app/javascript/flavours/glitch/features/account/containers/follow_request_note_container.js @@ -0,0 +1,15 @@ +import { connect } from 'react-redux'; +import FollowRequestNote from '../components/follow_request_note'; +import { authorizeFollowRequest, rejectFollowRequest } from 'flavours/glitch/actions/accounts'; + +const mapDispatchToProps = (dispatch, { account }) => ({ + onAuthorize () { + dispatch(authorizeFollowRequest(account.get('id'))); + }, + + onReject () { + dispatch(rejectFollowRequest(account.get('id'))); + }, +}); + +export default connect(null, mapDispatchToProps)(FollowRequestNote); diff --git a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js index f7a7fd467..f20ee685e 100644 --- a/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js +++ b/app/javascript/flavours/glitch/features/account_gallery/components/media_item.js @@ -104,6 +104,7 @@ export default class MediaItem extends ImmutablePureComponent { <video className='media-gallery__item-gifv-thumbnail' aria-label={attachment.get('description')} + title={attachment.get('description')} role='application' src={attachment.get('url')} onMouseEnter={this.handleMouseEnter} diff --git a/app/javascript/flavours/glitch/features/compose/components/compose_form.js b/app/javascript/flavours/glitch/features/compose/components/compose_form.js index abdd247a0..0462c7c4b 100644 --- a/app/javascript/flavours/glitch/features/compose/components/compose_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/compose_form.js @@ -310,7 +310,7 @@ class ComposeForm extends ImmutablePureComponent { <ReplyIndicatorContainer /> - <div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}> + <div className={`spoiler-input ${spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}> <AutosuggestInput placeholder={intl.formatMessage(messages.spoiler_placeholder)} value={spoilerText} diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown.js b/app/javascript/flavours/glitch/features/compose/components/dropdown.js index 3de198c45..d98b311d9 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown.js @@ -2,7 +2,7 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; -import Overlay from 'react-overlays/lib/Overlay'; +import Overlay from 'react-overlays/Overlay'; // Components. import IconButton from 'flavours/glitch/components/icon_button'; @@ -45,7 +45,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { }; // Toggles opening and closing the dropdown. - handleToggle = ({ target, type }) => { + handleToggle = ({ type }) => { const { onModalOpen } = this.props; const { open } = this.state; @@ -59,11 +59,9 @@ export default class ComposerOptionsDropdown extends React.PureComponent { } } } else { - const { top } = target.getBoundingClientRect(); if (this.state.open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open, openedViaKeyboard: type !== 'click' }); } } @@ -158,6 +156,18 @@ export default class ComposerOptionsDropdown extends React.PureComponent { }; } + setTargetRef = c => { + this.target = c; + } + + findTarget = () => { + return this.target; + } + + handleOverlayEnter = (state) => { + this.setState({ placement: state.placement }); + } + // Rendering. render () { const { @@ -179,6 +189,7 @@ export default class ComposerOptionsDropdown extends React.PureComponent { <div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown} + ref={this.setTargetRef} > <div className={classNames('privacy-dropdown__value', { active })}> <IconButton @@ -204,18 +215,26 @@ export default class ComposerOptionsDropdown extends React.PureComponent { containerPadding={20} placement={placement} show={open} - target={this} + flip + target={this.findTarget} container={container} + popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }} > - <DropdownMenu - items={items} - renderItemContents={renderItemContents} - onChange={onChange} - onClose={this.handleClose} - value={value} - openedViaKeyboard={this.state.openedViaKeyboard} - closeOnChange={closeOnChange} - /> + {({ props, placement }) => ( + <div {...props}> + <div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}> + <DropdownMenu + items={items} + renderItemContents={renderItemContents} + onChange={onChange} + onClose={this.handleClose} + value={value} + openedViaKeyboard={this.state.openedViaKeyboard} + closeOnChange={closeOnChange} + /> + </div> + </div> + )} </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js index 09e8fc35a..c4895dfd0 100644 --- a/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js +++ b/app/javascript/flavours/glitch/features/compose/components/dropdown_menu.js @@ -1,7 +1,6 @@ // Package imports. import PropTypes from 'prop-types'; import React from 'react'; -import spring from 'react-motion/lib/spring'; import ImmutablePureComponent from 'react-immutable-pure-component'; import classNames from 'classnames'; @@ -10,15 +9,8 @@ import Icon from 'flavours/glitch/components/icon'; // Utils. import { withPassive } from 'flavours/glitch/utils/dom_helpers'; -import Motion from '../../ui/util/optional_motion'; import { assignHandlers } from 'flavours/glitch/utils/react_helpers'; -// The spring to use with our motion. -const springMotion = spring(1, { - damping: 35, - stiffness: 400, -}); - // The component. export default class ComposerOptionsDropdownContent extends React.PureComponent { @@ -44,7 +36,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent }; state = { - mounted: false, value: this.props.openedViaKeyboard ? this.props.items[0].name : undefined, }; @@ -56,7 +47,7 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent } // Stores our node in `this.node`. - handleRef = (node) => { + setRef = (node) => { this.node = node; } @@ -69,7 +60,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent } else { this.node.firstChild.focus({ preventScroll: true }); } - this.setState({ mounted: true }); } // On unmounting, we remove our listeners. @@ -191,7 +181,6 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // Rendering. render () { - const { mounted } = this.state; const { items, onChange, @@ -201,36 +190,9 @@ export default class ComposerOptionsDropdownContent extends React.PureComponent // The result. return ( - <Motion - defaultStyle={{ - opacity: 0, - scaleX: 0.85, - scaleY: 0.75, - }} - style={{ - opacity: springMotion, - scaleX: springMotion, - scaleY: springMotion, - }} - > - {({ opacity, scaleX, scaleY }) => ( - // It should not be transformed when mounting because the resulting - // size will be used to determine the coordinate of the menu by - // react-overlays - <div - className='privacy-dropdown__dropdown' - ref={this.handleRef} - role='listbox' - style={{ - ...style, - opacity: opacity, - transform: mounted ? `scale(${scaleX}, ${scaleY})` : null, - }} - > - {!!items && items.map((item, i) => this.renderItem(item, i))} - </div> - )} - </Motion> + <div style={{ ...style }} role='listbox' ref={this.setRef}> + {!!items && items.map((item, i) => this.renderItem(item, i))} + </div> ); } 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 index 546d398a0..38c735551 100644 --- a/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/emoji_picker_dropdown.js @@ -2,7 +2,7 @@ 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/lib/Overlay'; +import Overlay from 'react-overlays/Overlay'; import classNames from 'classnames'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { supportsPassiveEvents } from 'detect-passive-events'; @@ -155,9 +155,6 @@ class EmojiPickerMenu extends React.PureComponent { onClose: PropTypes.func.isRequired, onPick: PropTypes.func.isRequired, style: PropTypes.object, - placement: PropTypes.string, - arrowOffsetLeft: PropTypes.string, - arrowOffsetTop: PropTypes.string, intl: PropTypes.object.isRequired, skinTone: PropTypes.number.isRequired, onSkinTone: PropTypes.func.isRequired, @@ -326,14 +323,13 @@ class EmojiPickerDropdown extends React.PureComponent { state = { active: false, loading: false, - placement: null, }; setRef = (c) => { this.dropdown = c; } - onShowDropdown = ({ target }) => { + onShowDropdown = () => { this.setState({ active: true }); if (!EmojiPicker) { @@ -348,9 +344,6 @@ class EmojiPickerDropdown extends React.PureComponent { this.setState({ loading: false, active: false }); }); } - - const { top } = target.getBoundingClientRect(); - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); } onHideDropdown = () => { @@ -384,7 +377,7 @@ class EmojiPickerDropdown extends React.PureComponent { render () { const { intl, onPickEmoji, onSkinTone, skinTone, frequentlyUsedEmojis, button } = this.props; const title = intl.formatMessage(messages.emoji); - const { active, loading, placement } = this.state; + const { active, loading } = this.state; return ( <div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}> @@ -396,16 +389,22 @@ class EmojiPickerDropdown extends React.PureComponent { />} </div> - <Overlay show={active} placement={placement} target={this.findTarget}> - <EmojiPickerMenu - custom_emojis={this.props.custom_emojis} - loading={loading} - onClose={this.onHideDropdown} - onPick={onPickEmoji} - onSkinTone={onSkinTone} - skinTone={skinTone} - frequentlyUsedEmojis={frequentlyUsedEmojis} - /> + <Overlay show={active} placement={'bottom'} target={this.findTarget} popperConfig={{ strategy: 'fixed' }}> + {({ props, placement })=> ( + <div {...props} style={{ ...props.style, width: 299 }}> + <div className={`dropdown-animation ${placement}`}> + <EmojiPickerMenu + custom_emojis={this.props.custom_emojis} + loading={loading} + onClose={this.onHideDropdown} + onPick={onPickEmoji} + onSkinTone={onSkinTone} + skinTone={skinTone} + frequentlyUsedEmojis={frequentlyUsedEmojis} + /> + </div> + </div> + )} </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js index a3256aa9b..3a1fa0226 100644 --- a/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js +++ b/app/javascript/flavours/glitch/features/compose/components/language_dropdown.js @@ -2,9 +2,7 @@ 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/lib/Overlay'; -import Motion from 'flavours/glitch/features/ui/util/optional_motion'; -import spring from 'react-motion/lib/spring'; +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'; @@ -22,10 +20,8 @@ const listenerOptions = supportsPassiveEvents ? { passive: true } : false; class LanguageDropdownMenu extends React.PureComponent { static propTypes = { - style: PropTypes.object, value: PropTypes.string.isRequired, frequentlyUsedLanguages: PropTypes.arrayOf(PropTypes.string).isRequired, - placement: PropTypes.string.isRequired, onClose: PropTypes.func.isRequired, onChange: PropTypes.func.isRequired, languages: PropTypes.arrayOf(PropTypes.arrayOf(PropTypes.string)), @@ -37,7 +33,6 @@ class LanguageDropdownMenu extends React.PureComponent { }; state = { - mounted: false, searchValue: '', }; @@ -50,7 +45,6 @@ class LanguageDropdownMenu extends React.PureComponent { componentDidMount () { document.addEventListener('click', this.handleDocumentClick, false); document.addEventListener('touchend', this.handleDocumentClick, listenerOptions); - this.setState({ mounted: true }); // Because of https://github.com/react-bootstrap/react-bootstrap/issues/2614 we need // to wait for a frame before focusing @@ -222,29 +216,22 @@ class LanguageDropdownMenu extends React.PureComponent { } render () { - const { style, placement, intl } = this.props; - const { mounted, searchValue } = this.state; + const { intl } = this.props; + const { searchValue } = this.state; const isSearching = searchValue !== ''; const results = this.search(); return ( - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - // It should not be transformed when mounting because the resulting - // size will be used to determine the coordinate of the menu by - // react-overlays - <div className={`language-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} ref={this.setRef}> - <div className='emoji-mart-search'> - <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} /> - <button className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button> - </div> + <div ref={this.setRef}> + <div className='emoji-mart-search'> + <input type='search' value={searchValue} onChange={this.handleSearchChange} onKeyDown={this.handleSearchKeyDown} placeholder={intl.formatMessage(messages.search)} /> + <button type='button' className='emoji-mart-search-icon' disabled={!isSearching} aria-label={intl.formatMessage(messages.clear)} onClick={this.handleClear}>{!isSearching ? loupeIcon : deleteIcon}</button> + </div> - <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> - {results.map(this.renderItem)} - </div> - </div> - )} - </Motion> + <div className='language-dropdown__dropdown__results emoji-mart-scroll' role='listbox' ref={this.setListRef}> + {results.map(this.renderItem)} + </div> + </div> ); } @@ -266,14 +253,11 @@ class LanguageDropdown extends React.PureComponent { placement: 'bottom', }; - handleToggle = ({ target }) => { - const { top } = target.getBoundingClientRect(); - + handleToggle = () => { if (this.state.open && this.activeElement) { this.activeElement.focus({ preventScroll: true }); } - this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' }); this.setState({ open: !this.state.open }); } @@ -293,13 +277,25 @@ class LanguageDropdown extends React.PureComponent { 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 ( - <div className={classNames('privacy-dropdown', { active: open })}> - <div className='privacy-dropdown__value'> + <div className={classNames('privacy-dropdown', placement, { active: open })}> + <div className='privacy-dropdown__value' ref={this.setTargetRef} > <TextIconButton className='privacy-dropdown__value-icon' label={value && value.toUpperCase()} @@ -309,15 +305,20 @@ class LanguageDropdown extends React.PureComponent { /> </div> - <Overlay show={open} placement={placement} target={this}> - <LanguageDropdownMenu - value={value} - frequentlyUsedLanguages={frequentlyUsedLanguages} - onClose={this.handleClose} - onChange={this.handleChange} - placement={placement} - intl={intl} - /> + <Overlay show={open} placement={'bottom'} flip target={this.findTarget} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}> + {({ props, placement }) => ( + <div {...props}> + <div className={`dropdown-animation language-dropdown__dropdown ${placement}`} > + <LanguageDropdownMenu + value={value} + frequentlyUsedLanguages={frequentlyUsedLanguages} + onClose={this.handleClose} + onChange={this.handleChange} + intl={intl} + /> + </div> + </div> + )} </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/components/poll_form.js b/app/javascript/flavours/glitch/features/compose/components/poll_form.js index d5edccff3..afb5da097 100644 --- a/app/javascript/flavours/glitch/features/compose/components/poll_form.js +++ b/app/javascript/flavours/glitch/features/compose/components/poll_form.js @@ -153,6 +153,7 @@ class PollForm extends ImmutablePureComponent { <option value={1800}>{intl.formatMessage(messages.minutes, { number: 30 })}</option> <option value={3600}>{intl.formatMessage(messages.hours, { number: 1 })}</option> <option value={21600}>{intl.formatMessage(messages.hours, { number: 6 })}</option> + <option value={43200}>{intl.formatMessage(messages.hours, { number: 12 })}</option> <option value={86400}>{intl.formatMessage(messages.days, { number: 1 })}</option> <option value={259200}>{intl.formatMessage(messages.days, { number: 3 })}</option> <option value={604800}>{intl.formatMessage(messages.days, { number: 7 })}</option> diff --git a/app/javascript/flavours/glitch/features/compose/components/search.js b/app/javascript/flavours/glitch/features/compose/components/search.js index 326fe5b70..e5874de75 100644 --- a/app/javascript/flavours/glitch/features/compose/components/search.js +++ b/app/javascript/flavours/glitch/features/compose/components/search.js @@ -3,13 +3,12 @@ import classNames from 'classnames'; import PropTypes from 'prop-types'; import React from 'react'; import { connect } from 'react-redux'; -import spring from 'react-motion/lib/spring'; import { injectIntl, FormattedMessage, defineMessages, } from 'react-intl'; -import Overlay from 'react-overlays/lib/Overlay'; +import Overlay from 'react-overlays/Overlay'; // Components. import Icon from 'flavours/glitch/components/icon'; @@ -17,7 +16,6 @@ import Icon from 'flavours/glitch/components/icon'; // Utils. import { focusRoot } from 'flavours/glitch/utils/dom_helpers'; import { searchEnabled } from 'flavours/glitch/initial_state'; -import Motion from '../../ui/util/optional_motion'; const messages = defineMessages({ placeholder: { id: 'search.placeholder', defaultMessage: 'Search' }, @@ -26,31 +24,20 @@ const messages = defineMessages({ class SearchPopout extends React.PureComponent { - static propTypes = { - style: PropTypes.object, - }; - render () { - const { style } = this.props; const extraInformation = searchEnabled ? <FormattedMessage id='search_popout.tips.full_text' defaultMessage='Simple text returns statuses you have written, favourited, boosted, or have been mentioned in, as well as matching usernames, display names, and hashtags.' /> : <FormattedMessage id='search_popout.tips.text' defaultMessage='Simple text returns matching display names, usernames and hashtags' />; return ( - <div style={{ ...style, position: 'absolute', width: 285, zIndex: 2 }}> - <Motion defaultStyle={{ opacity: 0, scaleX: 0.85, scaleY: 0.75 }} style={{ opacity: spring(1, { damping: 35, stiffness: 400 }), scaleX: spring(1, { damping: 35, stiffness: 400 }), scaleY: spring(1, { damping: 35, stiffness: 400 }) }}> - {({ opacity, scaleX, scaleY }) => ( - <div className='search-popout' style={{ opacity: opacity, transform: `scale(${scaleX}, ${scaleY})` }}> - <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> - - <ul> - <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li> - <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> - <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> - </ul> - - {extraInformation} - </div> - )} - </Motion> + <div className='search-popout'> + <h4><FormattedMessage id='search_popout.search_format' defaultMessage='Advanced search format' /></h4> + + <ul> + <li><em>#example</em> <FormattedMessage id='search_popout.tips.hashtag' defaultMessage='hashtag' /></li> + <li><em>@username@domain</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> + <li><em>URL</em> <FormattedMessage id='search_popout.tips.user' defaultMessage='user' /></li> + <li><em>URL</em> <FormattedMessage id='search_popout.tips.status' defaultMessage='status' /></li> + </ul> + + {extraInformation} </div> ); } @@ -136,6 +123,10 @@ class Search extends React.PureComponent { } } + findTarget = () => { + return this.searchForm; + } + render () { const { intl, value, submitted } = this.props; const { expanded } = this.state; @@ -144,34 +135,31 @@ class Search extends React.PureComponent { return ( <div className='search'> - <label> - <span style={{ display: 'none' }}>{intl.formatMessage(messages.placeholder)}</span> - <input - ref={this.setRef} - className='search__input' - type='text' - placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)} - value={value || ''} - onChange={this.handleChange} - onKeyUp={this.handleKeyUp} - onFocus={this.handleFocus} - onBlur={this.handleBlur} - /> - </label> - - <div - aria-label={intl.formatMessage(messages.placeholder)} - className='search__icon' - onClick={this.handleClear} - role='button' - tabIndex='0' - > + <input + ref={this.setRef} + className='search__input' + type='text' + placeholder={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)} + aria-label={intl.formatMessage(signedIn ? messages.placeholderSignedIn : messages.placeholder)} + value={value || ''} + onChange={this.handleChange} + onKeyUp={this.handleKeyUp} + onFocus={this.handleFocus} + onBlur={this.handleBlur} + /> + + <div role='button' tabIndex='0' className='search__icon' onClick={this.handleClear}> <Icon id='search' className={hasValue ? '' : 'active'} /> <Icon id='times-circle' className={hasValue ? 'active' : ''} /> </div> - - <Overlay show={expanded && !hasValue} placement='bottom' target={this}> - <SearchPopout /> + <Overlay show={expanded && !hasValue} placement='bottom' target={this.findTarget} popperConfig={{ strategy: 'fixed' }}> + {({ props, placement }) => ( + <div {...props} style={{ ...props.style, width: 285, zIndex: 2 }}> + <div className={`dropdown-animation ${placement}`}> + <SearchPopout /> + </div> + </div> + )} </Overlay> </div> ); diff --git a/app/javascript/flavours/glitch/features/compose/containers/options_container.js b/app/javascript/flavours/glitch/features/compose/containers/options_container.js index 6c3db8173..5de9f5419 100644 --- a/app/javascript/flavours/glitch/features/compose/containers/options_container.js +++ b/app/javascript/flavours/glitch/features/compose/containers/options_container.js @@ -46,7 +46,7 @@ const mapDispatchToProps = (dispatch) => ({ }, onDoodleOpen() { - dispatch(openModal('DOODLE', { noEsc: true })); + dispatch(openModal('DOODLE', { noEsc: true, noClose: true })); }, }); diff --git a/app/javascript/flavours/glitch/features/explore/index.js b/app/javascript/flavours/glitch/features/explore/index.js index ba435d7e3..da0dc7f7c 100644 --- a/app/javascript/flavours/glitch/features/explore/index.js +++ b/app/javascript/flavours/glitch/features/explore/index.js @@ -24,16 +24,6 @@ const mapStateToProps = state => ({ isSearching: state.getIn(['search', 'submitted']) || !showTrends, }); -// Fix strange bug on Safari where <span> (rendered by FormattedMessage) disappears -// after clicking around Explore top bar (issue #20885). -// Removing width=100% from <a> also fixes it, as well as replacing <span> with <div> -// We're choosing to wrap span with div to keep the changes local only to this tool bar. -const WrapFormattedMessage = ({ children, ...props }) => <div><FormattedMessage {...props}>{children}</FormattedMessage></div>; -WrapFormattedMessage.propTypes = { - children: PropTypes.any, -}; - - export default @connect(mapStateToProps) @injectIntl class Explore extends React.PureComponent { @@ -78,12 +68,22 @@ class Explore extends React.PureComponent { {isSearching ? ( <SearchResults /> ) : ( - <React.Fragment> + <> <div className='account__section-headline'> - <NavLink exact to='/explore'><WrapFormattedMessage id='explore.trending_statuses' defaultMessage='Posts' /></NavLink> - <NavLink exact to='/explore/tags'><WrapFormattedMessage id='explore.trending_tags' defaultMessage='Hashtags' /></NavLink> - <NavLink exact to='/explore/links'><WrapFormattedMessage id='explore.trending_links' defaultMessage='News' /></NavLink> - {signedIn && <NavLink exact to='/explore/suggestions'><WrapFormattedMessage id='explore.suggested_follows' defaultMessage='For you' /></NavLink>} + <NavLink exact to='/explore'> + <FormattedMessage tagName='div' id='explore.trending_statuses' defaultMessage='Posts' /> + </NavLink> + <NavLink exact to='/explore/tags'> + <FormattedMessage tagName='div' id='explore.trending_tags' defaultMessage='Hashtags' /> + </NavLink> + <NavLink exact to='/explore/links'> + <FormattedMessage tagName='div' id='explore.trending_links' defaultMessage='News' /> + </NavLink> + {signedIn && ( + <NavLink exact to='/explore/suggestions'> + <FormattedMessage tagName='div' id='explore.suggested_follows' defaultMessage='For you' /> + </NavLink> + )} </div> <Switch> @@ -97,7 +97,7 @@ class Explore extends React.PureComponent { <title>{intl.formatMessage(messages.title)}</title> <meta name='robots' content={isSearching ? 'noindex' : 'all'} /> </Helmet> - </React.Fragment> + </> )} </div> </Column> diff --git a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js index 4428fdecf..219dc0ec6 100644 --- a/app/javascript/flavours/glitch/features/hashtag_timeline/index.js +++ b/app/javascript/flavours/glitch/features/hashtag_timeline/index.js @@ -194,7 +194,7 @@ class HashtagTimeline extends React.PureComponent { const following = tag.get('following'); followButton = ( - <button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}> + <button className={classNames('column-header__button')} onClick={this.handleFollow} disabled={!signedIn} active={following} title={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)} aria-label={intl.formatMessage(following ? messages.unfollowHashtag : messages.followHashtag)}> <Icon id={following ? 'user-times' : 'user-plus'} fixedWidth className='column-header__icon' /> </button> ); diff --git a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js index e618a981e..98dda182f 100644 --- a/app/javascript/flavours/glitch/features/local_settings/navigation/index.js +++ b/app/javascript/flavours/glitch/features/local_settings/navigation/index.js @@ -13,7 +13,6 @@ const messages = defineMessages({ general: { id: 'settings.general', defaultMessage: 'General' }, compose: { id: 'settings.compose_box_opts', defaultMessage: 'Compose box' }, content_warnings: { id: 'settings.content_warnings', defaultMessage: 'Content Warnings' }, - filters: { id: 'settings.filters', defaultMessage: 'Filters' }, collapsed: { id: 'settings.collapsed_statuses', defaultMessage: 'Collapsed toots' }, media: { id: 'settings.media', defaultMessage: 'Media' }, preferences: { id: 'settings.preferences', defaultMessage: 'Preferences' }, diff --git a/app/javascript/flavours/glitch/features/status/components/action_bar.js b/app/javascript/flavours/glitch/features/status/components/action_bar.js index b6f8a9877..73913dd49 100644 --- a/app/javascript/flavours/glitch/features/status/components/action_bar.js +++ b/app/javascript/flavours/glitch/features/status/components/action_bar.js @@ -7,7 +7,7 @@ import { defineMessages, injectIntl } from 'react-intl'; import { me } from 'flavours/glitch/initial_state'; import { accountAdminLink, statusAdminLink } from 'flavours/glitch/utils/backend_links'; import classNames from 'classnames'; -import { PERMISSION_MANAGE_USERS } from 'flavours/glitch/permissions'; +import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'flavours/glitch/permissions'; const messages = defineMessages({ delete: { id: 'status.delete', defaultMessage: 'Delete' }, @@ -34,6 +34,7 @@ const messages = defineMessages({ embed: { id: 'status.embed', defaultMessage: 'Embed' }, admin_account: { id: 'status.admin_account', defaultMessage: 'Open moderation interface for @{name}' }, admin_status: { id: 'status.admin_status', defaultMessage: 'Open this status in the moderation interface' }, + admin_domain: { id: 'status.admin_domain', defaultMessage: 'Open moderation interface for {domain}' }, copy: { id: 'status.copy', defaultMessage: 'Copy link to status' }, openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' }, }); @@ -177,19 +178,19 @@ class ActionBar extends React.PureComponent { menu.push({ text: intl.formatMessage(messages.mute, { name: status.getIn(['account', 'username']) }), action: this.handleMuteClick }); menu.push({ text: intl.formatMessage(messages.block, { name: status.getIn(['account', 'username']) }), action: this.handleBlockClick }); menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport }); - if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) { + if (((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS && (accountAdminLink || statusAdminLink)) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) { menu.push(null); - if (accountAdminLink !== undefined) { - menu.push({ - text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), - href: accountAdminLink(status.getIn(['account', 'id'])), - }); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + if (accountAdminLink !== undefined) { + menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: accountAdminLink(status.getIn(['account', 'id'])) }); + } + if (statusAdminLink !== undefined) { + menu.push({ text: intl.formatMessage(messages.admin_status), href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')) }); + } } - if (statusAdminLink !== undefined) { - menu.push({ - text: intl.formatMessage(messages.admin_status), - href: statusAdminLink(status.getIn(['account', 'id']), status.get('id')), - }); + if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) { + const domain = status.getIn(['account', 'acct']).split('@')[1]; + menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` }); } } } diff --git a/app/javascript/flavours/glitch/features/status/components/detailed_status.js b/app/javascript/flavours/glitch/features/status/components/detailed_status.js index 7d2c2aace..907fc3f1c 100644 --- a/app/javascript/flavours/glitch/features/status/components/detailed_status.js +++ b/app/javascript/flavours/glitch/features/status/components/detailed_status.js @@ -41,7 +41,10 @@ class DetailedStatus extends ImmutablePureComponent { domain: PropTypes.string.isRequired, compact: PropTypes.bool, showMedia: PropTypes.bool, - usingPiP: PropTypes.bool, + pictureInPicture: ImmutablePropTypes.contains({ + inUse: PropTypes.bool, + available: PropTypes.bool, + }), onToggleMediaVisibility: PropTypes.func, intl: PropTypes.object.isRequired, }; @@ -120,7 +123,7 @@ class DetailedStatus extends ImmutablePureComponent { render () { const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status; - const { expanded, onToggleHidden, settings, usingPiP, intl } = this.props; + const { expanded, onToggleHidden, settings, pictureInPicture, intl } = this.props; const outerStyle = { boxSizing: 'border-box' }; const { compact } = this.props; @@ -153,7 +156,7 @@ class DetailedStatus extends ImmutablePureComponent { outerStyle.height = `${this.state.height}px`; } - if (usingPiP) { + if (pictureInPicture.get('inUse')) { media.push(<PictureInPicturePlaceholder />); mediaIcons.push('video-camera'); } else if (status.get('media_attachments').size > 0) { diff --git a/app/javascript/flavours/glitch/features/status/index.js b/app/javascript/flavours/glitch/features/status/index.js index e190652b0..c22e7f0bd 100644 --- a/app/javascript/flavours/glitch/features/status/index.js +++ b/app/javascript/flavours/glitch/features/status/index.js @@ -41,7 +41,7 @@ import { initMuteModal } from 'flavours/glitch/actions/mutes'; import { initBlockModal } from 'flavours/glitch/actions/blocks'; import { initReport } from 'flavours/glitch/actions/reports'; import { initBoostModal } from 'flavours/glitch/actions/boosts'; -import { makeGetStatus } from 'flavours/glitch/selectors'; +import { makeGetStatus, makeGetPictureInPicture } from 'flavours/glitch/selectors'; import ScrollContainer from 'flavours/glitch/containers/scroll_container'; import ColumnBackButton from 'flavours/glitch/components/column_back_button'; import ColumnHeader from '../../components/column_header'; @@ -67,11 +67,12 @@ const messages = defineMessages({ detailedStatus: { id: 'status.detailed_status', defaultMessage: 'Detailed conversation view' }, replyConfirm: { id: 'confirmations.reply.confirm', defaultMessage: 'Reply' }, replyMessage: { id: 'confirmations.reply.message', defaultMessage: 'Replying now will overwrite the message you are currently composing. Are you sure you want to proceed?' }, - tootHeading: { id: 'column.toot', defaultMessage: 'Toots and replies' }, + tootHeading: { id: 'account.posts_with_replies', defaultMessage: 'Posts and replies' }, }); const makeMapStateToProps = () => { const getStatus = makeGetStatus(); + const getPictureInPicture = makeGetPictureInPicture(); const getAncestorsIds = createSelector([ (_, { id }) => id, @@ -129,11 +130,12 @@ const makeMapStateToProps = () => { const mapStateToProps = (state, props) => { const status = getStatus(state, { id: props.params.statusId }); - let ancestorsIds = Immutable.List(); + + let ancestorsIds = Immutable.List(); let descendantsIds = Immutable.List(); if (status) { - ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); + ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') }); descendantsIds = getDescendantsIds(state, { id: status.get('id') }); } @@ -145,7 +147,7 @@ const makeMapStateToProps = () => { settings: state.get('local_settings'), askReplyConfirmation: state.getIn(['local_settings', 'confirm_before_clearing_draft']) && state.getIn(['compose', 'text']).trim().length !== 0, domain: state.getIn(['meta', 'domain']), - usingPiP: state.get('picture_in_picture').statusId === props.params.statusId, + pictureInPicture: getPictureInPicture(state, { id: props.params.statusId }), }; }; @@ -190,7 +192,10 @@ class Status extends ImmutablePureComponent { askReplyConfirmation: PropTypes.bool, multiColumn: PropTypes.bool, domain: PropTypes.string.isRequired, - usingPiP: PropTypes.bool, + pictureInPicture: ImmutablePropTypes.contains({ + inUse: PropTypes.bool, + available: PropTypes.bool, + }), }; state = { @@ -604,7 +609,7 @@ class Status extends ImmutablePureComponent { render () { let ancestors, descendants; - const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, usingPiP } = this.props; + const { isLoading, status, settings, ancestorsIds, descendantsIds, intl, domain, multiColumn, pictureInPicture } = this.props; const { fullscreen } = this.state; if (isLoading) { @@ -682,7 +687,7 @@ class Status extends ImmutablePureComponent { domain={domain} showMedia={this.state.showMedia} onToggleMediaVisibility={this.handleToggleMediaVisibility} - usingPiP={usingPiP} + pictureInPicture={pictureInPicture} /> <ActionBar diff --git a/app/javascript/flavours/glitch/features/ui/components/columns_area.js b/app/javascript/flavours/glitch/features/ui/components/columns_area.js index bf3e79c24..993a50796 100644 --- a/app/javascript/flavours/glitch/features/ui/components/columns_area.js +++ b/app/javascript/flavours/glitch/features/ui/components/columns_area.js @@ -99,7 +99,7 @@ export default class ColumnsArea extends ImmutablePureComponent { if (this.mediaQuery.removeEventListener) { this.mediaQuery.removeEventListener('change', this.handleLayoutChange); } else { - this.mediaQuery.removeListener(this.handleLayouteChange); + this.mediaQuery.removeListener(this.handleLayoutChange); } } } diff --git a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js index de330b3a1..0dd07fb76 100644 --- a/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/focal_point_modal.js @@ -291,11 +291,11 @@ class FocalPointModal extends ImmutablePureComponent { let descriptionLabel = null; if (media.get('type') === 'audio') { - descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people with hearing loss' />; + descriptionLabel = <FormattedMessage id='upload_form.audio_description' defaultMessage='Describe for people who are hard of hearing' />; } else if (media.get('type') === 'video') { - descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people with hearing loss or visual impairment' />; + descriptionLabel = <FormattedMessage id='upload_form.video_description' defaultMessage='Describe for people who are deaf, hard of hearing, blind or have low vision' />; } else { - descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for the visually impaired' />; + descriptionLabel = <FormattedMessage id='upload_form.description' defaultMessage='Describe for people who are blind or have low vision' />; } let ocrMessage = ''; diff --git a/app/javascript/flavours/glitch/features/ui/components/link_footer.js b/app/javascript/flavours/glitch/features/ui/components/link_footer.js index 28adf5e06..ac0c78674 100644 --- a/app/javascript/flavours/glitch/features/ui/components/link_footer.js +++ b/app/javascript/flavours/glitch/features/ui/components/link_footer.js @@ -52,6 +52,8 @@ class LinkFooter extends React.PureComponent { const canInvite = signedIn && ((permissions & PERMISSION_INVITE_USERS) === PERMISSION_INVITE_USERS); const canProfileDirectory = profileDirectory; + const DividingCircle = <span aria-hidden>{' · '}</span>; + return ( <div className='link-footer'> <p> @@ -60,17 +62,17 @@ class LinkFooter extends React.PureComponent { <Link key='about' to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link> {canInvite && ( <> - {' · '} + {DividingCircle} <a key='invites' href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a> </> )} {canProfileDirectory && ( <> - {' · '} + {DividingCircle} <Link key='directory' to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link> </> )} - {' · '} + {DividingCircle} <Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link> </p> @@ -78,11 +80,13 @@ class LinkFooter extends React.PureComponent { <strong>Mastodon</strong>: {' '} <a href='https://joinmastodon.org' target='_blank'><FormattedMessage id='footer.about' defaultMessage='About' /></a> - {' · '} + {DividingCircle} + <a href='https://joinmastodon.org/apps' target='_blank'><FormattedMessage id='footer.get_app' defaultMessage='Get the app' /></a> + {DividingCircle} <Link to='/keyboard-shortcuts'><FormattedMessage id='footer.keyboard_shortcuts' defaultMessage='Keyboard shortcuts' /></Link> - {' · '} + {DividingCircle} <a href={source_url} rel='noopener noreferrer' target='_blank'><FormattedMessage id='footer.source_code' defaultMessage='View source code' /></a> - {' · '} + {DividingCircle} v{version} </p> </div> diff --git a/app/javascript/flavours/glitch/features/ui/components/modal_root.js b/app/javascript/flavours/glitch/features/ui/components/modal_root.js index d2ee28948..379f57cbb 100644 --- a/app/javascript/flavours/glitch/features/ui/components/modal_root.js +++ b/app/javascript/flavours/glitch/features/ui/components/modal_root.js @@ -116,13 +116,16 @@ export default class ModalRoot extends React.PureComponent { this._modal = c; } + // prevent closing of modal when clicking the overlay + noop = () => {} + render () { const { type, props, ignoreFocus } = this.props; const { backgroundColor } = this.state; const visible = !!type; return ( - <Base backgroundColor={backgroundColor} onClose={this.handleClose} noEsc={props ? props.noEsc : false} ignoreFocus={ignoreFocus}> + <Base backgroundColor={backgroundColor} onClose={props && props.noClose ? this.noop : this.handleClose} noEsc={props ? props.noEsc : false} ignoreFocus={ignoreFocus}> {visible && ( <> <BundleContainer fetchComponent={MODAL_COMPONENTS[type]} loading={this.renderLoading(type)} error={this.renderError} renderDelay={200}> diff --git a/app/javascript/flavours/glitch/features/ui/components/video_modal.js b/app/javascript/flavours/glitch/features/ui/components/video_modal.js index 6b6e615a6..90be11e4b 100644 --- a/app/javascript/flavours/glitch/features/ui/components/video_modal.js +++ b/app/javascript/flavours/glitch/features/ui/components/video_modal.js @@ -50,6 +50,7 @@ export default class VideoModal extends ImmutablePureComponent { autoPlay={options.autoPlay} volume={options.defaultVolume} onCloseVideo={onClose} + autoFocus detailed alt={media.get('description')} /> diff --git a/app/javascript/flavours/glitch/features/video/index.js b/app/javascript/flavours/glitch/features/video/index.js index cb4655f7f..0daab747b 100644 --- a/app/javascript/flavours/glitch/features/video/index.js +++ b/app/javascript/flavours/glitch/features/video/index.js @@ -124,6 +124,7 @@ class Video extends React.PureComponent { volume: PropTypes.number, muted: PropTypes.bool, componentIndex: PropTypes.number, + autoFocus: PropTypes.bool, }; static defaultProps = { @@ -537,7 +538,7 @@ class Video extends React.PureComponent { } render () { - const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, editable, blurhash } = this.props; + const { preview, src, inline, onOpenVideo, onCloseVideo, intl, alt, letterbox, fullwidth, detailed, sensitive, editable, blurhash, autoFocus } = this.props; const { containerWidth, currentTime, duration, volume, buffer, dragging, paused, fullscreen, hovered, muted, revealed } = this.state; const progress = Math.min((currentTime / duration) * 100, 100); const playerStyle = {}; @@ -635,7 +636,7 @@ class Video extends React.PureComponent { <div className='video-player__buttons-bar'> <div className='video-player__buttons left'> - <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={detailed}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> + <button type='button' title={intl.formatMessage(paused ? messages.play : messages.pause)} aria-label={intl.formatMessage(paused ? messages.play : messages.pause)} className='player-button' onClick={this.togglePlay} autoFocus={autoFocus}><Icon id={paused ? 'play' : 'pause'} fixedWidth /></button> <button type='button' title={intl.formatMessage(muted ? messages.unmute : messages.mute)} aria-label={intl.formatMessage(muted ? messages.unmute : messages.mute)} className='player-button' onClick={this.toggleMute}><Icon id={muted ? 'volume-off' : 'volume-up'} fixedWidth /></button> <div className={classNames('video-player__volume', { active: this.state.hovered })} onMouseDown={this.handleVolumeMouseDown} ref={this.setVolumeRef}> |