Merge tag 'v2.9.3' into hometown-2.9.3

This commit is contained in:
Darius Kazemi
2019-08-19 14:28:19 -07:00
98 changed files with 852 additions and 270 deletions

View File

@ -15,6 +15,7 @@ import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
const messages = defineMessages({
unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
follow: { id: 'account.follow', defaultMessage: 'Follow' },
cancel_follow_request: { id: 'account.cancel_follow_request', defaultMessage: 'Cancel follow request' },
requested: { id: 'account.requested', defaultMessage: 'Awaiting approval. Click to cancel follow request' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' },
@ -79,6 +80,47 @@ class Header extends ImmutablePureComponent {
return !location.pathname.match(/\/(followers|following)\/?$/);
}
_updateEmojis () {
const node = this.node;
if (!node || autoPlayGif) {
return;
}
const emojis = node.querySelectorAll('.custom-emoji');
for (var i = 0; i < emojis.length; i++) {
let emoji = emojis[i];
if (emoji.classList.contains('status-emoji')) {
continue;
}
emoji.classList.add('status-emoji');
emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
}
}
componentDidMount () {
this._updateEmojis();
}
componentDidUpdate () {
this._updateEmojis();
}
handleEmojiMouseEnter = ({ target }) => {
target.src = target.getAttribute('data-original');
}
handleEmojiMouseLeave = ({ target }) => {
target.src = target.getAttribute('data-static');
}
setRef = (c) => {
this.node = c;
}
render () {
const { account, intl, domain, identity_proofs } = this.props;
@ -107,7 +149,7 @@ class Header extends ImmutablePureComponent {
if (!account.get('relationship')) { // Wait until the relationship is loaded
actionBtn = '';
} else if (account.getIn(['relationship', 'requested'])) {
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
actionBtn = <Button className='logo-button' text={intl.formatMessage(messages.cancel_follow_request)} title={intl.formatMessage(messages.requested)} onClick={this.props.onFollow} />;
} else if (!account.getIn(['relationship', 'blocking'])) {
actionBtn = <Button disabled={account.getIn(['relationship', 'blocked_by'])} className={classNames('logo-button', { 'button--destructive': account.getIn(['relationship', 'following']) })} text={intl.formatMessage(account.getIn(['relationship', 'following']) ? messages.unfollow : messages.follow)} onClick={this.props.onFollow} />;
} else if (account.getIn(['relationship', 'blocking'])) {
@ -200,7 +242,7 @@ class Header extends ImmutablePureComponent {
const acct = account.get('acct').indexOf('@') === -1 && domain ? `${account.get('acct')}@${domain}` : account.get('acct');
return (
<div className={classNames('account__header', { inactive: !!account.get('moved') })}>
<div className={classNames('account__header', { inactive: !!account.get('moved') })} ref={this.setRef}>
<div className='account__header__image'>
<div className='account__header__info'>
{info}

View File

@ -15,6 +15,7 @@ const messages = defineMessages({
domain_blocks: { id: 'navigation_bar.domain_blocks', defaultMessage: 'Hidden domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
filters: { id: 'navigation_bar.filters', defaultMessage: 'Muted words' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
});
export default @injectIntl
@ -42,6 +43,8 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.blocks), to: '/blocks' });
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
menu.push(null);
menu.push({ text: intl.formatMessage(messages.logout), href: '/auth/sign_out', target: null, method: 'delete' });
return (
<div className='compose__action-bar'>

View File

@ -119,7 +119,10 @@ class ComposeForm extends ImmutablePureComponent {
handleFocus = () => {
if (this.composeForm && !this.props.singleColumn) {
this.composeForm.scrollIntoView();
const { left, right } = this.composeForm.getBoundingClientRect();
if (left < 0 || right > (window.innerWidth || document.documentElement.clientWidth)) {
this.composeForm.scrollIntoView();
}
}
}
@ -190,12 +193,12 @@ class ComposeForm extends ImmutablePureComponent {
}
return (
<div className='compose-form' ref={this.setRef}>
<div className='compose-form'>
<WarningContainer />
<ReplyIndicatorContainer />
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`}>
<div className={`spoiler-input ${this.props.spoiler ? 'spoiler-input--visible' : ''}`} ref={this.setRef}>
<AutosuggestInput
placeholder={intl.formatMessage(messages.spoiler_placeholder)}
value={this.props.spoilerText}

View File

@ -73,6 +73,19 @@ class PrivacyDropdownMenu extends React.PureComponent {
this.props.onChange(element.getAttribute('data-index'));
}
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;
}
if (element) {
element.focus();
this.props.onChange(element.getAttribute('data-index'));
e.preventDefault();
e.stopPropagation();
}
break;
case 'Home':
element = this.node.firstChild;
if (element) {
@ -180,6 +193,9 @@ class PrivacyDropdown extends React.PureComponent {
}
} else {
const { top } = target.getBoundingClientRect();
if (this.state.open && this.activeElement) {
this.activeElement.focus();
}
this.setState({ placement: top * 2 < innerHeight ? 'bottom' : 'top' });
this.setState({ open: !this.state.open });
}
@ -202,7 +218,25 @@ class PrivacyDropdown extends React.PureComponent {
}
}
handleMouseDown = () => {
if (!this.state.open) {
this.activeElement = document.activeElement;
}
}
handleButtonKeyDown = (e) => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleMouseDown();
break;
}
}
handleClose = () => {
if (this.state.open && this.activeElement) {
this.activeElement.focus();
}
this.setState({ open: false });
}
@ -229,7 +263,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) === 0 })}>
<div className={classNames('privacy-dropdown__value', { active: this.options.indexOf(valueOption) === (placement === 'bottom' ? 0 : (this.options.length - 1)) })}>
<IconButton
className='privacy-dropdown__value-icon'
icon={valueOption.icon}
@ -239,6 +273,8 @@ class PrivacyDropdown extends React.PureComponent {
active={open}
inverted
onClick={this.handleToggle}
onMouseDown={this.handleMouseDown}
onKeyDown={this.handleButtonKeyDown}
style={{ height: null, lineHeight: '27px' }}
/>
</div>

View File

@ -7,6 +7,7 @@ import StatusContainer from '../../../containers/status_container';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Hashtag from '../../../components/hashtag';
import Icon from 'mastodon/components/icon';
import { searchEnabled } from '../../../initial_state';
const messages = defineMessages({
dismissSuggestion: { id: 'suggestions.dismiss', defaultMessage: 'Dismiss suggestion' },
@ -20,6 +21,7 @@ class SearchResults extends ImmutablePureComponent {
suggestions: ImmutablePropTypes.list.isRequired,
fetchSuggestions: PropTypes.func.isRequired,
dismissSuggestion: PropTypes.func.isRequired,
searchTerm: PropTypes.string,
intl: PropTypes.object.isRequired,
};
@ -28,7 +30,7 @@ class SearchResults extends ImmutablePureComponent {
}
render () {
const { intl, results, suggestions, dismissSuggestion } = this.props;
const { intl, results, suggestions, dismissSuggestion, searchTerm } = this.props;
if (results.isEmpty() && !suggestions.isEmpty()) {
return (
@ -76,6 +78,16 @@ class SearchResults extends ImmutablePureComponent {
{results.get('statuses').map(statusId => <StatusContainer key={statusId} id={statusId} />)}
</div>
);
} else if(results.get('statuses') && results.get('statuses').size === 0 && !searchEnabled && !(searchTerm.startsWith('@') || searchTerm.startsWith('#') || searchTerm.includes(' '))) {
statuses = (
<div className='search-results__section'>
<h5><Icon id='quote-right' fixedWidth /><FormattedMessage id='search_results.statuses' defaultMessage='Toots' /></h5>
<div className='search-results__info'>
<FormattedMessage id='search_results.statuses_fts_disabled' defaultMessage='Searching toots by their content is not enabled on this Mastodon server.' />
</div>
</div>
);
}
if (results.get('hashtags') && results.get('hashtags').size > 0) {

View File

@ -5,6 +5,7 @@ import { fetchSuggestions, dismissSuggestion } from '../../../actions/suggestion
const mapStateToProps = state => ({
results: state.getIn(['search', 'results']),
suggestions: state.getIn(['suggestions', 'items']),
searchTerm: state.getIn(['search', 'searchTerm']),
});
const mapDispatchToProps = dispatch => ({

View File

@ -29,7 +29,7 @@ const emojify = (str, customEmojis = {}) => {
// if you want additional emoji handler, add statements below which set replacement and return true.
if (shortname in customEmojis) {
const filename = autoPlayGif ? customEmojis[shortname].url : customEmojis[shortname].static_url;
replacement = `<img draggable="false" class="emojione" alt="${shortname}" title="${shortname}" src="${filename}" />`;
replacement = `<img draggable="false" class="emojione custom-emoji" alt="${shortname}" title="${shortname}" src="${filename}" data-original="${customEmojis[shortname].url}" data-static="${customEmojis[shortname].static_url}" />`;
return true;
}
return false;

View File

@ -12,7 +12,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({
value: state.getIn(['listEditor', 'title']),
disabled: !state.getIn(['listEditor', 'isChanged']),
disabled: !state.getIn(['listEditor', 'isChanged']) || !state.getIn(['listEditor', 'title']),
});
const mapDispatchToProps = dispatch => ({

View File

@ -66,7 +66,7 @@ class NewListForm extends React.PureComponent {
</label>
<IconButton
disabled={disabled}
disabled={disabled || !value}
icon='plus'
title={title}
onClick={this.handleClick}

View File

@ -4,6 +4,7 @@ import { connect } from 'react-redux';
import PropTypes from 'prop-types';
import classNames from 'classnames';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { createSelector } from 'reselect';
import { fetchStatus } from '../../actions/statuses';
import MissingIndicator from '../../components/missing_indicator';
import DetailedStatus from './components/detailed_status';
@ -63,39 +64,58 @@ const messages = defineMessages({
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getAncestorsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'inReplyTos']),
], (statusId, inReplyTos) => {
let ancestorsIds = Immutable.List();
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = statusId;
while (id) {
mutable.unshift(id);
id = inReplyTos.get(id);
}
});
return ancestorsIds;
});
const getDescendantsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
], (statusId, contextReplies) => {
let descendantsIds = Immutable.List();
descendantsIds = descendantsIds.withMutations(mutable => {
const ids = [statusId];
while (ids.length > 0) {
let id = ids.shift();
const replies = contextReplies.get(id);
if (statusId !== id) {
mutable.push(id);
}
if (replies) {
replies.reverse().forEach(reply => {
ids.unshift(reply);
});
}
}
});
return descendantsIds;
});
const mapStateToProps = (state, props) => {
const status = getStatus(state, { id: props.params.statusId });
let ancestorsIds = Immutable.List();
let descendantsIds = Immutable.List();
if (status) {
ancestorsIds = ancestorsIds.withMutations(mutable => {
let id = status.get('in_reply_to_id');
while (id) {
mutable.unshift(id);
id = state.getIn(['contexts', 'inReplyTos', id]);
}
});
descendantsIds = descendantsIds.withMutations(mutable => {
const ids = [status.get('id')];
while (ids.length > 0) {
let id = ids.shift();
const replies = state.getIn(['contexts', 'replies', id]);
if (status.get('id') !== id) {
mutable.push(id);
}
if (replies) {
replies.reverse().forEach(reply => {
ids.unshift(reply);
});
}
}
});
ancestorsIds = getAncestorsIds(state, { id: status.get('in_reply_to_id') });
descendantsIds = getDescendantsIds(state, { id: status.get('id') });
}
return {

View File

@ -110,6 +110,11 @@ class ColumnsArea extends ImmutablePureComponent {
// React-router does this for us, but too late, feeling laggy.
document.querySelector(currentLinkSelector).classList.remove('active');
document.querySelector(nextLinkSelector).classList.add('active');
if (!this.state.shouldAnimate && typeof this.pendingIndex === 'number') {
this.context.router.history.push(getLink(this.pendingIndex));
this.pendingIndex = null;
}
}
handleAnimationEnd = () => {
@ -160,7 +165,6 @@ class ColumnsArea extends ImmutablePureComponent {
const { shouldAnimate } = this.state;
const columnIndex = getIndex(this.context.router.history.location.pathname);
this.pendingIndex = null;
if (singleColumn) {
const floatingActionButton = shouldHideFAB(this.context.router.history.location.pathname) ? null : <Link key='floating-action-button' to='/statuses/new' className='floating-action-button' aria-label={intl.formatMessage(messages.publish)}><Icon id='pencil' /></Link>;