Merge tag 'v2.6.0rc1' into instance_only_statuses

This commit is contained in:
Renato "Lond" Cerqueira
2018-10-23 08:32:55 +02:00
570 changed files with 11506 additions and 5693 deletions

View File

@ -0,0 +1,27 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`<AutosuggestEmoji /> renders emoji with custom url 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="foobar"
className="emojione"
src="http://example.com/emoji.png"
/>
:foobar:
</div>
`;
exports[`<AutosuggestEmoji /> renders native emoji 1`] = `
<div
className="autosuggest-emoji"
>
<img
alt="💙"
className="emojione"
src="/emoji/1f499.svg"
/>
:foobar:
</div>
`;

View File

@ -3,7 +3,6 @@
exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] = `
<button
className="button button-secondary"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -18,7 +17,6 @@ exports[`<Button /> adds class "button-secondary" if props.secondary given 1`] =
exports[`<Button /> renders a button element 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -48,7 +46,6 @@ exports[`<Button /> renders a disabled attribute if props.disabled given 1`] = `
exports[`<Button /> renders class="button--block" if props.block given 1`] = `
<button
className="button button--block"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -63,7 +60,6 @@ exports[`<Button /> renders class="button--block" if props.block given 1`] = `
exports[`<Button /> renders the children 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -82,7 +78,6 @@ exports[`<Button /> renders the children 1`] = `
exports[`<Button /> renders the given text 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {
@ -99,7 +94,6 @@ exports[`<Button /> renders the given text 1`] = `
exports[`<Button /> renders the props.text instead of children 1`] = `
<button
className="button"
disabled={undefined}
onClick={[Function]}
style={
Object {

View File

@ -0,0 +1,29 @@
import React from 'react';
import renderer from 'react-test-renderer';
import AutosuggestEmoji from '../autosuggest_emoji';
describe('<AutosuggestEmoji />', () => {
it('renders native emoji', () => {
const emoji = {
native: '💙',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
it('renders emoji with custom url', () => {
const emoji = {
custom: true,
imageUrl: 'http://example.com/emoji.png',
native: 'foobar',
colons: ':foobar:',
};
const component = renderer.create(<AutosuggestEmoji emoji={emoji} />);
const tree = component.toJSON();
expect(tree).toMatchSnapshot();
});
});

View File

@ -19,8 +19,8 @@ const messages = defineMessages({
unmute_notifications: { id: 'account.unmute_notifications', defaultMessage: 'Unmute notifications from @{name}' },
});
@injectIntl
export default class Account extends ImmutablePureComponent {
export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,

View File

@ -0,0 +1,96 @@
import React from 'react';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { autoPlayGif } from '../initial_state';
export default class AvatarComposite extends React.PureComponent {
static propTypes = {
accounts: ImmutablePropTypes.list.isRequired,
animate: PropTypes.bool,
size: PropTypes.number.isRequired,
};
static defaultProps = {
animate: autoPlayGif,
};
renderItem (account, size, index) {
const { animate } = this.props;
let width = 50;
let height = 100;
let top = 'auto';
let left = 'auto';
let bottom = 'auto';
let right = 'auto';
if (size === 1) {
width = 100;
}
if (size === 4 || (size === 3 && index > 0)) {
height = 50;
}
if (size === 2) {
if (index === 0) {
right = '2px';
} else {
left = '2px';
}
} else if (size === 3) {
if (index === 0) {
right = '2px';
} else if (index > 0) {
left = '2px';
}
if (index === 1) {
bottom = '2px';
} else if (index > 1) {
top = '2px';
}
} else if (size === 4) {
if (index === 0 || index === 2) {
right = '2px';
}
if (index === 1 || index === 3) {
left = '2px';
}
if (index < 2) {
bottom = '2px';
} else {
top = '2px';
}
}
const style = {
left: left,
top: top,
right: right,
bottom: bottom,
width: `${width}%`,
height: `${height}%`,
backgroundSize: 'cover',
backgroundImage: `url(${account.get(animate ? 'avatar' : 'avatar_static')})`,
};
return (
<div key={account.get('id')} style={style} />
);
}
render() {
const { accounts, size } = this.props;
return (
<div className='account__avatar-composite' style={{ width: `${size}px`, height: `${size}px` }}>
{accounts.take(4).map((account, i) => this.renderItem(account, accounts.size, i))}
</div>
);
}
}

View File

@ -10,8 +10,8 @@ const messages = defineMessages({
moveRight: { id: 'column_header.moveRight_settings', defaultMessage: 'Move column to the right' },
});
@injectIntl
export default class ColumnHeader extends React.PureComponent {
export default @injectIntl
class ColumnHeader extends React.PureComponent {
static contextTypes = {
router: PropTypes.object,

View File

@ -5,14 +5,24 @@ export default class DisplayName extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
others: ImmutablePropTypes.list,
};
render () {
const displayNameHtml = { __html: this.props.account.get('display_name_html') };
const { account, others } = this.props;
const displayNameHtml = { __html: account.get('display_name_html') };
let suffix;
if (others && others.size > 1) {
suffix = `+${others.size}`;
} else {
suffix = <span className='display-name__account'>@{account.get('acct')}</span>;
}
return (
<span className='display-name'>
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> <span className='display-name__account'>@{this.props.account.get('acct')}</span>
<bdi><strong className='display-name__html' dangerouslySetInnerHTML={displayNameHtml} /></bdi> {suffix}
</span>
);
}

View File

@ -8,8 +8,8 @@ const messages = defineMessages({
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unhide {domain}' },
});
@injectIntl
export default class Account extends ImmutablePureComponent {
export default @injectIntl
class Account extends ImmutablePureComponent {
static propTypes = {
domain: PropTypes.string,

View File

@ -23,6 +23,7 @@ class DropdownMenu extends React.PureComponent {
placement: PropTypes.string,
arrowOffsetLeft: PropTypes.string,
arrowOffsetTop: PropTypes.string,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
@ -42,13 +43,15 @@ class DropdownMenu extends React.PureComponent {
componentDidMount () {
document.addEventListener('click', this.handleDocumentClick, false);
document.addEventListener('keydown', this.handleKeyDown, false);
document.addEventListener('touchend', this.handleDocumentClick, listenerOptions);
if (this.focusedItem) this.focusedItem.focus();
if (this.focusedItem && this.props.openedViaKeyboard) this.focusedItem.focus();
this.setState({ mounted: true });
}
componentWillUnmount () {
document.removeEventListener('click', this.handleDocumentClick, false);
document.removeEventListener('keydown', this.handleKeyDown, false);
document.removeEventListener('touchend', this.handleDocumentClick, listenerOptions);
}
@ -62,13 +65,10 @@ class DropdownMenu extends React.PureComponent {
handleKeyDown = e => {
const items = Array.from(this.node.getElementsByTagName('a'));
const index = items.indexOf(e.currentTarget);
const index = items.indexOf(document.activeElement);
let element;
switch(e.key) {
case 'Enter':
this.handleClick(e);
break;
case 'ArrowDown':
element = items[index+1];
if (element) {
@ -96,6 +96,12 @@ class DropdownMenu extends React.PureComponent {
}
}
handleItemKeyDown = e => {
if (e.key === 'Enter') {
this.handleClick(e);
}
}
handleClick = e => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const { action, to } = this.props.items[i];
@ -120,7 +126,7 @@ class DropdownMenu extends React.PureComponent {
return (
<li className='dropdown-menu__item' key={`${text}-${i}`}>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleKeyDown} data-index={i}>
<a href={href} target='_blank' rel='noopener' role='button' tabIndex='0' ref={i === 0 ? this.setFocusRef : null} onClick={this.handleClick} onKeyDown={this.handleItemKeyDown} data-index={i}>
{text}
</a>
</li>
@ -170,6 +176,7 @@ export default class Dropdown extends React.PureComponent {
onClose: PropTypes.func.isRequired,
dropdownPlacement: PropTypes.string,
openDropdownId: PropTypes.number,
openedViaKeyboard: PropTypes.bool,
};
static defaultProps = {
@ -180,14 +187,14 @@ export default class Dropdown extends React.PureComponent {
id: id++,
};
handleClick = ({ target }) => {
handleClick = ({ target, type }) => {
if (this.state.id === this.props.openDropdownId) {
this.handleClose();
} else {
const { top } = target.getBoundingClientRect();
const placement = top * 2 < innerHeight ? 'bottom' : 'top';
this.props.onOpen(this.state.id, this.handleItemClick, placement);
this.props.onOpen(this.state.id, this.handleItemClick, placement, type !== 'click');
}
}
@ -197,6 +204,11 @@ export default class Dropdown extends React.PureComponent {
handleKeyDown = e => {
switch(e.key) {
case ' ':
case 'Enter':
this.handleClick(e);
e.preventDefault();
break;
case 'Escape':
this.handleClose();
break;
@ -233,7 +245,7 @@ export default class Dropdown extends React.PureComponent {
}
render () {
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId } = this.props;
const { icon, items, size, title, disabled, dropdownPlacement, openDropdownId, openedViaKeyboard } = this.props;
const open = this.state.id === openDropdownId;
return (
@ -249,7 +261,7 @@ export default class Dropdown extends React.PureComponent {
/>
<Overlay show={open} placement={dropdownPlacement} target={this.findTarget}>
<DropdownMenu items={items} onClose={this.handleClose} />
<DropdownMenu items={items} onClose={this.handleClose} openedViaKeyboard={openedViaKeyboard} />
</Overlay>
</div>
);

View File

@ -6,8 +6,8 @@ const messages = defineMessages({
load_more: { id: 'status.load_more', defaultMessage: 'Load more' },
});
@injectIntl
export default class LoadGap extends React.PureComponent {
export default @injectIntl
class LoadGap extends React.PureComponent {
static propTypes = {
disabled: PropTypes.bool,

View File

@ -6,7 +6,7 @@ import IconButton from './icon_button';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { isIOS } from '../is_mobile';
import classNames from 'classnames';
import { autoPlayGif, displaySensitiveMedia } from '../initial_state';
import { autoPlayGif, displayMedia } from '../initial_state';
const messages = defineMessages({
toggle_visible: { id: 'media_gallery.toggle_visible', defaultMessage: 'Toggle visibility' },
@ -179,8 +179,8 @@ class Item extends React.PureComponent {
}
@injectIntl
export default class MediaGallery extends React.PureComponent {
export default @injectIntl
class MediaGallery extends React.PureComponent {
static propTypes = {
sensitive: PropTypes.bool,
@ -197,7 +197,7 @@ export default class MediaGallery extends React.PureComponent {
};
state = {
visible: !this.props.sensitive || displaySensitiveMedia,
visible: displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all',
};
componentWillReceiveProps (nextProps) {

View File

@ -86,8 +86,8 @@ export const timeAgoString = (intl, date, now, year) => {
return relativeTime;
};
@injectIntl
export default class RelativeTimestamp extends React.Component {
export default @injectIntl
class RelativeTimestamp extends React.Component {
static propTypes = {
intl: PropTypes.object.isRequired,

View File

@ -3,6 +3,7 @@ import ImmutablePropTypes from 'react-immutable-proptypes';
import PropTypes from 'prop-types';
import Avatar from './avatar';
import AvatarOverlay from './avatar_overlay';
import AvatarComposite from './avatar_composite';
import RelativeTimestamp from './relative_timestamp';
import DisplayName from './display_name';
import StatusContent from './status_content';
@ -35,8 +36,8 @@ export const textForScreenReader = (intl, status, rebloggedByText = false) => {
return values.join(', ');
};
@injectIntl
export default class Status extends ImmutablePureComponent {
export default @injectIntl
class Status extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,
@ -45,6 +46,8 @@ export default class Status extends ImmutablePureComponent {
static propTypes = {
status: ImmutablePropTypes.map,
account: ImmutablePropTypes.map,
otherAccounts: ImmutablePropTypes.list,
onClick: PropTypes.func,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
@ -60,6 +63,7 @@ export default class Status extends ImmutablePureComponent {
onToggleHidden: PropTypes.func,
muted: PropTypes.bool,
hidden: PropTypes.bool,
unread: PropTypes.bool,
onMoveUp: PropTypes.func,
onMoveDown: PropTypes.func,
};
@ -74,6 +78,11 @@ export default class Status extends ImmutablePureComponent {
]
handleClick = () => {
if (this.props.onClick) {
this.props.onClick();
return;
}
if (!this.context.router) {
return;
}
@ -158,7 +167,7 @@ export default class Status extends ImmutablePureComponent {
let media = null;
let statusAvatar, prepend, rebloggedByText;
const { intl, hidden, featured } = this.props;
const { intl, hidden, featured, otherAccounts, unread } = this.props;
let { status, account, ...other } = this.props;
@ -249,9 +258,11 @@ export default class Status extends ImmutablePureComponent {
}
}
if (account === undefined || account === null) {
if (otherAccounts) {
statusAvatar = <AvatarComposite accounts={otherAccounts} size={48} />;
} else if (account === undefined || account === null) {
statusAvatar = <Avatar account={status.get('account')} size={48} />;
}else{
} else {
statusAvatar = <AvatarOverlay account={status.get('account')} friend={account} />;
}
@ -269,10 +280,10 @@ export default class Status extends ImmutablePureComponent {
return (
<HotKeys handlers={handlers}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}>
<div className={classNames('status__wrapper', `status__wrapper-${status.get('visibility')}`, { 'status__wrapper-reply': !!status.get('in_reply_to_id'), read: unread === false, focusable: !this.props.muted })} tabIndex={this.props.muted ? null : 0} data-featured={featured ? 'true' : null} aria-label={textForScreenReader(intl, status, rebloggedByText, !status.get('hidden'))}>
{prepend}
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted })} data-id={status.get('id')}>
<div className={classNames('status', `status-${status.get('visibility')}`, { 'status-reply': !!status.get('in_reply_to_id'), muted: this.props.muted, read: unread === false })} data-id={status.get('id')}>
<div className='status__info'>
<a href={status.get('url')} className='status__relative-time' target='_blank' rel='noopener'><RelativeTimestamp timestamp={status.get('created_at')} /></a>
@ -281,11 +292,11 @@ export default class Status extends ImmutablePureComponent {
{statusAvatar}
</div>
<DisplayName account={status.get('account')} />
<DisplayName account={status.get('account')} others={otherAccounts} />
</a>
</div>
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} />
<StatusContent status={status} onClick={this.handleClick} expanded={!status.get('hidden')} onExpandedToggle={this.handleExpandedToggle} collapsable />
{media}

View File

@ -43,8 +43,8 @@ const obfuscatedCount = count => {
}
};
@injectIntl
export default class StatusActionBar extends ImmutablePureComponent {
export default @injectIntl
class StatusActionBar extends ImmutablePureComponent {
static contextTypes = {
router: PropTypes.object,

View File

@ -6,6 +6,8 @@ import { FormattedMessage } from 'react-intl';
import Permalink from './permalink';
import classnames from 'classnames';
const MAX_HEIGHT = 642; // 20px * 32 (+ 2px padding at the top)
export default class StatusContent extends React.PureComponent {
static contextTypes = {
@ -17,10 +19,12 @@ export default class StatusContent extends React.PureComponent {
expanded: PropTypes.bool,
onExpandedToggle: PropTypes.func,
onClick: PropTypes.func,
collapsable: PropTypes.bool,
};
state = {
hidden: true,
collapsed: null, // `collapsed: null` indicates that an element doesn't need collapsing, while `true` or `false` indicates that it does (and is/isn't).
};
_updateStatusLinks () {
@ -53,6 +57,16 @@ export default class StatusContent extends React.PureComponent {
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener');
}
if (
this.props.collapsable
&& this.props.onClick
&& this.state.collapsed === null
&& node.clientHeight > MAX_HEIGHT
&& this.props.status.get('spoiler_text').length === 0
) {
this.setState({ collapsed: true });
}
}
componentDidMount () {
@ -113,6 +127,11 @@ export default class StatusContent extends React.PureComponent {
}
}
handleCollapsedClick = (e) => {
e.preventDefault();
this.setState({ collapsed: !this.state.collapsed });
}
setRef = (c) => {
this.node = c;
}
@ -132,12 +151,19 @@ export default class StatusContent extends React.PureComponent {
const classNames = classnames('status__content', {
'status__content--with-action': this.props.onClick && this.context.router,
'status__content--with-spoiler': status.get('spoiler_text').length > 0,
'status__content--collapsed': this.state.collapsed === true,
});
if (isRtl(status.get('search_index'))) {
directionStyle.direction = 'rtl';
}
const readMoreButton = (
<button className='status__content__read-more-button' onClick={this.props.onClick}>
<FormattedMessage id='status.read_more' defaultMessage='Read more' /><i className='fa fa-fw fa-angle-right' />
</button>
);
if (status.get('spoiler_text').length > 0) {
let mentionsPlaceholder = '';
@ -167,17 +193,23 @@ export default class StatusContent extends React.PureComponent {
</div>
);
} else if (this.props.onClick) {
return (
const output = [
<div
ref={this.setRef}
tabIndex='0'
className={classNames}
style={directionStyle}
dangerouslySetInnerHTML={content}
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
dangerouslySetInnerHTML={content}
/>
);
/>,
];
if (this.state.collapsed) {
output.push(readMoreButton);
}
return output;
} else {
return (
<div