Merge tag 'v4.1.8' into chinwag-4.1

This commit is contained in:
Mike Barnes 2023-09-20 13:37:47 +10:00
commit d9dfd09ac8
995 changed files with 53450 additions and 19456 deletions

View file

@ -11,6 +11,7 @@ const messages = defineMessages({
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' },
@ -30,7 +31,7 @@ class ActionBar extends React.PureComponent {
handleLogout = () => {
this.props.onLogout();
}
};
render () {
const { intl } = this.props;
@ -45,6 +46,7 @@ class ActionBar extends React.PureComponent {
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' });

View file

@ -16,7 +16,6 @@ import PollFormContainer from '../containers/poll_form_container';
import UploadFormContainer from '../containers/upload_form_container';
import WarningContainer from '../containers/warning_container';
import LanguageDropdown from '../containers/language_dropdown_container';
import { isMobile } from '../../../is_mobile';
import ImmutablePureComponent from 'react-immutable-pure-component';
import { length } from 'stringz';
import { countableText } from '../util/counter';
@ -61,29 +60,30 @@ 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,
};
static defaultProps = {
showSearch: false,
autoFocus: false,
};
handleChange = (e) => {
this.props.onChange(e.target.value);
}
};
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.handleSubmit();
}
}
};
getFulltextForCharacterCounting = () => {
return [this.props.spoiler? this.props.spoilerText: '', countableText(this.props.text)].join('');
}
};
canSubmit = () => {
const { isSubmitting, isChangingUpload, isUploading, anyMedia } = this.props;
@ -91,7 +91,7 @@ class ComposeForm extends ImmutablePureComponent {
const isOnlyWhitespace = fulltext.length !== 0 && fulltext.trim().length === 0;
return !(isSubmitting || isUploading || isChangingUpload || length(fulltext) > 640 || (isOnlyWhitespace && !anyMedia));
}
};
handleSubmit = (e) => {
if (this.props.text !== this.autosuggestTextarea.textarea.value) {
@ -109,27 +109,27 @@ class ComposeForm extends ImmutablePureComponent {
if (e) {
e.preventDefault();
}
}
};
onSuggestionsClearRequested = () => {
this.props.onClearSuggestions();
}
};
onSuggestionsFetchRequested = (token) => {
this.props.onFetchSuggestions(token);
}
};
onSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['text']);
}
};
onSpoilerSuggestionSelected = (tokenStart, token, value) => {
this.props.onSuggestionSelected(tokenStart, token, value, ['spoiler_text']);
}
};
handleChangeSpoilerText = (e) => {
this.props.onChangeSpoilerText(e.target.value);
}
};
handleFocus = () => {
if (this.composeForm && !this.props.singleColumn) {
@ -138,7 +138,7 @@ class ComposeForm extends ImmutablePureComponent {
this.composeForm.scrollIntoView();
}
}
}
};
componentDidMount () {
this._updateFocusAndSelection({ });
@ -154,7 +154,7 @@ class ComposeForm extends ImmutablePureComponent {
// - Replying to zero or one users, places the cursor at the end of the textbox.
// - Replying to more than one user, selects any usernames past the first;
// this provides a convenient shortcut to drop everyone else from the conversation.
if (this.props.focusDate !== prevProps.focusDate) {
if (this.props.focusDate && this.props.focusDate !== prevProps.focusDate) {
let selectionEnd, selectionStart;
if (this.props.preselectDate !== prevProps.preselectDate && this.props.isInReply) {
@ -180,19 +180,19 @@ class ComposeForm extends ImmutablePureComponent {
} else if (this.props.spoiler !== prevProps.spoiler) {
if (this.props.spoiler) {
this.spoilerText.input.focus();
} else {
} else if (prevProps.spoiler) {
this.autosuggestTextarea.textarea.focus();
}
}
}
};
setAutosuggestTextarea = (c) => {
this.autosuggestTextarea = c;
}
};
setSpoilerText = (c) => {
this.spoilerText = c;
}
};
setRef = c => {
this.composeForm = c;
@ -204,10 +204,10 @@ class ComposeForm extends ImmutablePureComponent {
const needsSpace = data.custom && position > 0 && !allowedAroundShortCode.includes(text[position - 1]);
this.props.onPickEmoji(position, data, needsSpace);
}
};
render () {
const { intl, onPaste, showSearch } = this.props;
const { intl, onPaste, autoFocus } = this.props;
const disabled = this.props.isSubmitting;
let publishText = '';
@ -226,7 +226,7 @@ class ComposeForm extends ImmutablePureComponent {
<ReplyIndicatorContainer />
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}>
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef} aria-hidden={!this.props.spoiler}>
<AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}
@ -241,6 +241,8 @@ class ComposeForm extends ImmutablePureComponent {
searchTokens={[':']}
id='cw-spoiler-input'
className='spoiler-input__input'
lang={this.props.lang}
spellCheck
/>
</div>
@ -257,7 +259,8 @@ class ComposeForm extends ImmutablePureComponent {
onSuggestionsClearRequested={this.onSuggestionsClearRequested}
onSuggestionSelected={this.onSuggestionSelected}
onPaste={onPaste}
autoFocus={!showSearch && !isMobile(window.innerWidth)}
autoFocus={autoFocus}
lang={this.props.lang}
>
<EmojiPickerDropdown onPickEmoji={this.handleEmojiPick} />

View file

@ -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';
@ -57,7 +57,7 @@ class ModifierPickerMenu extends React.PureComponent {
handleClick = e => {
this.props.onSelect(e.currentTarget.getAttribute('data-index') * 1);
}
};
componentWillReceiveProps (nextProps) {
if (nextProps.active) {
@ -75,7 +75,7 @@ class ModifierPickerMenu extends React.PureComponent {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
};
attachListeners () {
document.addEventListener('click', this.handleDocumentClick, false);
@ -89,7 +89,7 @@ class ModifierPickerMenu extends React.PureComponent {
setRef = c => {
this.node = c;
}
};
render () {
const { active } = this.props;
@ -124,12 +124,12 @@ class ModifierPicker extends React.PureComponent {
} else {
this.props.onOpen();
}
}
};
handleSelect = modifier => {
this.props.onChange(modifier);
this.props.onClose();
}
};
render () {
const { active, modifier } = this.props;
@ -154,9 +154,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,
@ -177,7 +174,7 @@ class EmojiPickerMenu extends React.PureComponent {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
};
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
@ -201,7 +198,7 @@ class EmojiPickerMenu extends React.PureComponent {
setRef = c => {
this.node = c;
}
};
getI18n = () => {
const { intl } = this.props;
@ -222,7 +219,7 @@ class EmojiPickerMenu extends React.PureComponent {
custom: intl.formatMessage(messages.custom),
},
};
}
};
handleClick = (emoji, event) => {
if (!emoji.native) {
@ -232,19 +229,19 @@ class EmojiPickerMenu extends React.PureComponent {
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;
@ -324,14 +321,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) {
@ -346,14 +342,11 @@ class EmojiPickerDropdown extends React.PureComponent {
this.setState({ loading: false, active: false });
});
}
const { top } = target.getBoundingClientRect();
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
}
};
onHideDropdown = () => {
this.setState({ active: false });
}
};
onToggle = (e) => {
if (!this.state.loading && (!e.key || e.key === 'Enter')) {
@ -363,26 +356,26 @@ class EmojiPickerDropdown extends React.PureComponent {
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, placement } = this.state;
const { active, loading } = this.state;
return (
<div className='emoji-picker-dropdown' onKeyDown={this.handleKeyDown}>
@ -394,16 +387,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>
);

View file

@ -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 'mastodon/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 'mastodon/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: '',
};
@ -45,12 +40,11 @@ class LanguageDropdownMenu extends React.PureComponent {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
};
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
@ -69,15 +63,15 @@ class LanguageDropdownMenu extends React.PureComponent {
setRef = c => {
this.node = c;
}
};
setListRef = c => {
this.listNode = c;
}
};
handleSearchChange = ({ target }) => {
this.setState({ searchValue: target.value });
}
};
search () {
const { languages, value, frequentlyUsedLanguages } = this.props;
@ -128,7 +122,7 @@ class LanguageDropdownMenu extends React.PureComponent {
this.props.onClose();
this.props.onChange(value);
}
};
handleKeyDown = e => {
const { onClose } = this.props;
@ -169,7 +163,7 @@ class LanguageDropdownMenu extends React.PureComponent {
e.preventDefault();
e.stopPropagation();
}
}
};
handleSearchKeyDown = e => {
const { onChange, onClose } = this.props;
@ -205,11 +199,11 @@ class LanguageDropdownMenu extends React.PureComponent {
break;
}
}
};
handleClear = () => {
this.setState({ searchValue: '' });
}
};
renderItem = lang => {
const { value } = this.props;
@ -219,32 +213,25 @@ class LanguageDropdownMenu extends React.PureComponent {
<span className='language-dropdown__dropdown__results__item__native-name' lang={lang[0]}>{lang[2]}</span> <span className='language-dropdown__dropdown__results__item__common-name'>({lang[1]})</span>
</div>
);
}
};
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 type='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,16 +253,13 @@ 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 });
}
};
handleClose = () => {
const { value, onClose } = this.props;
@ -286,20 +270,32 @@ class LanguageDropdown extends React.PureComponent {
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 (
<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>
);

View file

@ -27,7 +27,7 @@ class PollButton extends React.PureComponent {
handleClick = () => {
this.props.onClick();
}
};
render () {
const { intl, active, unavailable, disabled } = this.props;

View file

@ -25,6 +25,7 @@ class Option extends React.PureComponent {
static propTypes = {
title: PropTypes.string.isRequired,
lang: PropTypes.string,
index: PropTypes.number.isRequired,
isPollMultiple: PropTypes.bool,
autoFocus: PropTypes.bool,
@ -57,22 +58,22 @@ class Option extends React.PureComponent {
if (e.key === 'Enter' || e.key === ' ') {
this.handleToggleMultiple(e);
}
}
};
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, index, autoFocus, intl } = this.props;
const { isPollMultiple, title, lang, index, autoFocus, intl } = this.props;
return (
<li>
@ -91,6 +92,8 @@ class Option extends React.PureComponent {
placeholder={intl.formatMessage(messages.option_placeholder, { number: index + 1 })}
maxLength={50}
value={title}
lang={lang}
spellCheck
onChange={this.handleOptionTitleChange}
suggestions={this.props.suggestions}
onSuggestionsFetchRequested={this.onSuggestionsFetchRequested}
@ -116,6 +119,7 @@ class PollForm extends ImmutablePureComponent {
static propTypes = {
options: ImmutablePropTypes.list,
lang: PropTypes.string,
expiresIn: PropTypes.number,
isMultiple: PropTypes.bool,
onChangeOption: PropTypes.func.isRequired,
@ -142,7 +146,7 @@ class PollForm extends ImmutablePureComponent {
};
render () {
const { options, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
const { options, lang, expiresIn, isMultiple, onChangeOption, onRemoveOption, intl, ...other } = this.props;
if (!options) {
return null;
@ -153,7 +157,7 @@ class PollForm extends ImmutablePureComponent {
return (
<div className='compose-form__poll-wrapper'>
<ul>
{options.map((title, i) => <Option title={title} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
{options.map((title, i) => <Option title={title} lang={lang} key={i} index={i} onChange={onChangeOption} onRemove={onRemoveOption} isPollMultiple={isMultiple} onToggleMultiple={this.handleToggleMultiple} autoFocus={i === autoFocusIndex} {...other} />)}
</ul>
<div className='poll__footer'>
@ -165,6 +169,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>

View file

@ -2,9 +2,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { injectIntl, defineMessages } from 'react-intl';
import IconButton from '../../../components/icon_button';
import Overlay from 'react-overlays/lib/Overlay';
import Motion from '../../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 Icon from 'mastodon/components/icon';
@ -29,20 +27,15 @@ class PrivacyDropdownMenu extends React.PureComponent {
style: PropTypes.object,
items: PropTypes.array.isRequired,
value: PropTypes.string.isRequired,
placement: PropTypes.string.isRequired,
onClose: PropTypes.func.isRequired,
onChange: PropTypes.func.isRequired,
};
state = {
mounted: false,
};
handleDocumentClick = e => {
if (this.node && !this.node.contains(e.target)) {
this.props.onClose();
}
}
};
handleKeyDown = e => {
const { items } = this.props;
@ -86,7 +79,7 @@ class PrivacyDropdownMenu extends React.PureComponent {
e.preventDefault();
e.stopPropagation();
}
}
};
handleClick = e => {
const value = e.currentTarget.getAttribute('data-index');
@ -95,13 +88,12 @@ class PrivacyDropdownMenu extends React.PureComponent {
this.props.onClose();
this.props.onChange(value);
}
};
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem) this.focusedItem.focus({ preventScroll: true });
this.setState({ mounted: true });
}
componentWillUnmount () {
@ -111,38 +103,30 @@ class PrivacyDropdownMenu extends React.PureComponent {
setRef = c => {
this.node = c;
}
};
setFocusRef = c => {
this.focusedItem = c;
}
};
render () {
const { mounted } = this.state;
const { style, items, placement, value } = this.props;
const { style, items, value } = this.props;
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={`privacy-dropdown__dropdown ${placement}`} style={{ ...style, opacity: opacity, transform: mounted ? `scale(${scaleX}, ${scaleY})` : null }} role='listbox' ref={this.setRef}>
{items.map(item => (
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
<div className='privacy-dropdown__option__icon'>
<Icon id={item.icon} fixedWidth />
</div>
<div style={{ ...style }} role='listbox' ref={this.setRef}>
{items.map(item => (
<div role='option' tabIndex='0' key={item.value} data-index={item.value} onKeyDown={this.handleKeyDown} onClick={this.handleClick} className={classNames('privacy-dropdown__option', { active: item.value === value })} aria-selected={item.value === value} ref={item.value === value ? this.setFocusRef : null}>
<div className='privacy-dropdown__option__icon'>
<Icon id={item.icon} fixedWidth />
</div>
<div className='privacy-dropdown__option__content'>
<strong>{item.text}</strong>
{item.meta}
</div>
</div>
))}
<div className='privacy-dropdown__option__content'>
<strong>{item.text}</strong>
{item.meta}
</div>
</div>
)}
</Motion>
))}
</div>
);
}
@ -168,7 +152,7 @@ class PrivacyDropdown extends React.PureComponent {
placement: 'bottom',
};
handleToggle = ({ target }) => {
handleToggle = () => {
if (this.props.isUserTouching && this.props.isUserTouching()) {
if (this.state.open) {
this.props.onModalClose();
@ -179,14 +163,12 @@ class PrivacyDropdown 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 });
}
}
};
handleModalActionClick = (e) => {
e.preventDefault();
@ -195,7 +177,7 @@ class PrivacyDropdown extends React.PureComponent {
this.props.onModalClose();
this.props.onChange(value);
}
};
handleKeyDown = e => {
switch(e.key) {
@ -203,13 +185,13 @@ class PrivacyDropdown extends React.PureComponent {
this.handleClose();
break;
}
}
};
handleMouseDown = () => {
if (!this.state.open) {
this.activeElement = document.activeElement;
}
}
};
handleButtonKeyDown = (e) => {
switch(e.key) {
@ -218,18 +200,18 @@ class PrivacyDropdown extends React.PureComponent {
this.handleMouseDown();
break;
}
}
};
handleClose = () => {
if (this.state.open && this.activeElement) {
this.activeElement.focus({ preventScroll: true });
}
this.setState({ open: false });
}
};
handleChange = value => {
this.props.onChange(value);
}
};
componentWillMount () {
const { intl: { formatMessage } } = this.props;
@ -247,6 +229,18 @@ class PrivacyDropdown extends React.PureComponent {
}
}
setTargetRef = c => {
this.target = c;
};
findTarget = () => {
return this.target;
};
handleOverlayEnter = (state) => {
this.setState({ placement: state.placement });
};
render () {
const { value, container, disabled, intl } = this.props;
const { open, placement } = this.state;
@ -255,7 +249,7 @@ class PrivacyDropdown extends React.PureComponent {
return (
<div className={classNames('privacy-dropdown', placement, { active: open })} onKeyDown={this.handleKeyDown}>
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === (placement === 'bottom' ? 0 : (this.options.length - 1)) })}>
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === (placement === 'bottom' ? 0 : (this.options.length - 1)) })} ref={this.setTargetRef}>
<IconButton
className='privacy-dropdown__value-icon'
icon={valueOption.icon}
@ -272,14 +266,19 @@ class PrivacyDropdown extends React.PureComponent {
/>
</div>
<Overlay show={open} placement={placement} target={this} container={container}>
<PrivacyDropdownMenu
items={this.options}
value={value}
onClose={this.handleClose}
onChange={this.handleChange}
placement={placement}
/>
<Overlay show={open} placement={'bottom'} flip target={this.findTarget} container={container} popperConfig={{ strategy: 'fixed', onFirstUpdate: this.handleOverlayEnter }}>
{({ props, placement }) => (
<div {...props}>
<div className={`dropdown-animation privacy-dropdown__dropdown ${placement}`}>
<PrivacyDropdownMenu
items={this.options}
value={value}
onClose={this.handleClose}
onChange={this.handleChange}
/>
</div>
</div>
)}
</Overlay>
</div>
);

View file

@ -27,14 +27,14 @@ class ReplyIndicator extends ImmutablePureComponent {
handleClick = () => {
this.props.onCancel();
}
};
handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
}
}
};
render () {
const { status, intl } = this.props;

View file

@ -1,9 +1,7 @@
import React from 'react';
import PropTypes from 'prop-types';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import Overlay from 'react-overlays/lib/Overlay';
import Motion from '../../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import Overlay from 'react-overlays/Overlay';
import { searchEnabled } from '../../../initial_state';
import Icon from 'mastodon/components/icon';
@ -14,31 +12,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>
<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>
<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>
{extraInformation}
</div>
);
}
@ -71,11 +58,11 @@ class Search extends React.PureComponent {
setRef = c => {
this.searchForm = c;
}
};
handleChange = (e) => {
this.props.onChange(e.target.value);
}
};
handleClear = (e) => {
e.preventDefault();
@ -83,7 +70,7 @@ class Search extends React.PureComponent {
if (this.props.value.length > 0 || this.props.submitted) {
this.props.onClear();
}
}
};
handleKeyUp = (e) => {
if (e.key === 'Enter') {
@ -97,7 +84,7 @@ class Search extends React.PureComponent {
} else if (e.key === 'Escape') {
document.querySelector('.ui').parentElement.focus();
}
}
};
handleFocus = () => {
this.setState({ expanded: true });
@ -109,11 +96,15 @@ class Search extends React.PureComponent {
this.searchForm.scrollIntoView();
}
}
}
};
handleBlur = () => {
this.setState({ expanded: false });
}
};
findTarget = () => {
return this.searchForm;
};
render () {
const { intl, value, submitted } = this.props;
@ -123,28 +114,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>
<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' : ''} aria-label={intl.formatMessage(messages.placeholder)} />
</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>
);

View file

@ -22,15 +22,20 @@ export default class Upload extends ImmutablePureComponent {
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;
@ -43,10 +48,10 @@ export default class Upload extends ImmutablePureComponent {
<div className='compose-form__upload-thumbnail' style={{ transform: `scale(${scale})`, backgroundImage: `url(${media.get('preview_url')})`, backgroundPosition: `${x}% ${y}%` }}>
<div className='compose-form__upload__actions'>
<button type='button' className='icon-button' onClick={this.handleUndoClick}><Icon id='times' /> <FormattedMessage id='upload_form.undo' defaultMessage='Delete' /></button>
{!!media.get('unattached') && (<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>)}
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='pencil' /> <FormattedMessage id='upload_form.edit' defaultMessage='Edit' /></button>
</div>
{(media.get('description') || '').length === 0 && !!media.get('unattached') && (
{(media.get('description') || '').length === 0 && (
<div className='compose-form__upload__warning'>
<button type='button' className='icon-button' onClick={this.handleFocalPointClick}><Icon id='info-circle' /> <FormattedMessage id='upload_form.description_missing' defaultMessage='No description added' /></button>
</div>

View file

@ -41,15 +41,15 @@ class UploadButton extends ImmutablePureComponent {
if (e.target.files.length > 0) {
this.props.onSelectFile(e.target.files);
}
}
};
handleClick = () => {
this.fileElement.click();
}
};
setRef = (c) => {
this.fileElement = c;
}
};
render () {
const { intl, resetFileKey, unavailable, disabled, acceptContentTypes } = this.props;

View file

@ -24,9 +24,9 @@ const mapStateToProps = state => ({
isEditing: state.getIn(['compose', 'id']) !== null,
isChangingUpload: state.getIn(['compose', 'is_changing_upload']),
isUploading: state.getIn(['compose', 'is_uploading']),
showSearch: state.getIn(['search', 'submitted']) && !state.getIn(['search', 'hidden']),
anyMedia: state.getIn(['compose', 'media_attachments']).size > 0,
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),
});
const mapDispatchToProps = (dispatch) => ({

View file

@ -10,6 +10,7 @@ import {
const mapStateToProps = state => ({
suggestions: state.getIn(['compose', 'suggestions']),
options: state.getIn(['compose', 'poll', 'options']),
lang: state.getIn(['compose', 'language']),
expiresIn: state.getIn(['compose', 'poll', 'expires_in']),
isMultiple: state.getIn(['compose', 'poll', 'multiple']),
});

View file

@ -18,6 +18,7 @@ import Icon from 'mastodon/components/icon';
import { logOut } from 'mastodon/utils/log_out';
import Column from 'mastodon/components/column';
import { Helmet } from 'react-helmet';
import { isMobile } from '../../is_mobile';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@ -73,15 +74,15 @@ class Compose extends React.PureComponent {
}));
return false;
}
};
onFocus = () => {
this.props.dispatch(changeComposing(true));
}
};
onBlur = () => {
this.props.dispatch(changeComposing(false));
}
};
render () {
const { multiColumn, showSearch, intl } = this.props;
@ -115,7 +116,7 @@ class Compose extends React.PureComponent {
<div className='drawer__inner' onFocus={this.onFocus}>
<NavigationContainer onClose={this.onBlur} />
<ComposeFormContainer />
<ComposeFormContainer autoFocus={!isMobile(window.innerWidth)} />
<div className='drawer__inner__mastodon'>
<img alt='' draggable='false' src={mascot || elephantUIPlane} />

View file

@ -6,4 +6,4 @@ export function countableText(inputText) {
return inputText
.replace(urlRegex, urlPlaceholder)
.replace(/(^|[^\/\w])@(([a-z0-9_]+)@[a-z0-9\.\-]+[a-z0-9]+)/ig, '$1@$3');
};
}