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

@ -59,7 +59,7 @@ class Section extends React.PureComponent {
const { collapsed } = this.state;
this.setState({ collapsed: !collapsed }, () => onOpen && onOpen());
}
};
render () {
const { title, children } = this.props;
@ -106,7 +106,7 @@ class About extends React.PureComponent {
handleDomainBlocksOpen = () => {
const { dispatch } = this.props;
dispatch(fetchDomainBlocks());
}
};
render () {
const { multiColumn, intl, server, extendedDescription, domainBlocks } = this.props;

View file

@ -90,7 +90,7 @@ class AccountNote extends ImmutablePureComponent {
setTextareaRef = c => {
this.textarea = c;
}
};
handleChange = e => {
this.setState({ value: e.target.value, saving: false });
@ -114,13 +114,13 @@ class AccountNote extends ImmutablePureComponent {
}
});
}
}
};
handleBlur = () => {
if (this._isDirty()) {
this._save();
}
}
};
_save (showMessage = true) {
this.setState({ saving: true }, () => this.props.onSave(this.state.value));

View file

@ -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 'mastodon/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>
);
}
}

View file

@ -14,7 +14,8 @@ import ShortNumber from 'mastodon/components/short_number';
import { NavLink } from 'react-router-dom';
import DropdownMenuContainer from 'mastodon/containers/dropdown_menu_container';
import AccountNoteContainer from '../containers/account_note_container';
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
import FollowRequestNoteContainer from '../containers/follow_request_note_container';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/permissions';
import { Helmet } from 'react-helmet';
const messages = defineMessages({
@ -45,6 +46,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: 'Blocked domains' },
mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' },
@ -52,6 +54,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}' },
languages: { id: 'account.languages', defaultMessage: 'Change subscribed languages' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
});
@ -106,7 +109,7 @@ class Header extends ImmutablePureComponent {
openEditProfile = () => {
window.open('/settings/profile', '_blank');
}
};
isStatusesPageActive = (match, location) => {
if (!match) {
@ -114,7 +117,7 @@ class Header extends ImmutablePureComponent {
}
return !location.pathname.match(/\/(followers|following)\/?$/);
}
};
handleMouseEnter = ({ currentTarget }) => {
if (autoPlayGif) {
@ -127,7 +130,7 @@ class Header extends ImmutablePureComponent {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
}
};
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
@ -140,18 +143,29 @@ class Header extends ImmutablePureComponent {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
}
};
handleAvatarClick = e => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.props.onOpenAvatar();
}
}
};
handleShare = () => {
const { account } = this.props;
navigator.share({
text: `${titleFromAccount(account)}\n${account.get('note_plain')}`,
url: account.get('url'),
}).catch((e) => {
if (e.name !== 'AbortError') console.error(e);
});
};
render () {
const { account, hidden, intl, domain } = this.props;
const { signedIn } = this.context.identity;
const { signedIn, permissions } = this.context.identity;
if (!account) {
return null;
@ -180,7 +194,7 @@ class Header extends ImmutablePureComponent {
}
if (account.getIn(['relationship', 'requested']) || account.getIn(['relationship', 'following'])) {
bellBtn = <IconButton icon='bell-o' size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
bellBtn = <IconButton icon={account.getIn(['relationship', 'notifying']) ? 'bell' : 'bell-o'} size={24} active={account.getIn(['relationship', 'notifying'])} title={intl.formatMessage(account.getIn(['relationship', 'notifying']) ? messages.disableNotifications : messages.enableNotifications, { name: account.get('username') })} onClick={this.props.onNotifyToggle} />;
}
if (me !== account.get('id')) {
@ -229,6 +243,7 @@ class Header extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.follow_requests), to: '/follow_requests' });
menu.push({ text: intl.formatMessage(messages.favourites), to: '/favourites' });
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' });
@ -276,9 +291,14 @@ class Header extends ImmutablePureComponent {
}
}
if (account.get('id') !== me && (this.context.identity.permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
if ((account.get('id') !== me && (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${account.get('id')}` });
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${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') };
@ -300,6 +320,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'>
{!suspended && info}
@ -331,7 +353,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>

View file

@ -0,0 +1,15 @@
import { connect } from 'react-redux';
import FollowRequestNote from '../components/follow_request_note';
import { authorizeFollowRequest, rejectFollowRequest } from 'mastodon/actions/accounts';
const mapDispatchToProps = (dispatch, { account }) => ({
onAuthorize () {
dispatch(authorizeFollowRequest(account.get('id')));
},
onReject () {
dispatch(rejectFollowRequest(account.get('id')));
},
});
export default connect(null, mapDispatchToProps)(FollowRequestNote);

View file

@ -2,7 +2,6 @@ import Blurhash from 'mastodon/components/blurhash';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
import { autoPlayGif, displayMedia, useBlurhash } from 'mastodon/initial_state';
import { isIOS } from 'mastodon/is_mobile';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';
@ -23,20 +22,20 @@ export default class MediaItem extends ImmutablePureComponent {
handleImageLoad = () => {
this.setState({ loaded: true });
}
};
handleMouseEnter = e => {
if (this.hoverToPlay()) {
e.target.play();
}
}
};
handleMouseLeave = e => {
if (this.hoverToPlay()) {
e.target.pause();
e.target.currentTime = 0;
}
}
};
hoverToPlay () {
return !autoPlayGif && ['gifv', 'video'].indexOf(this.props.attachment.get('type')) !== -1;
@ -52,7 +51,7 @@ export default class MediaItem extends ImmutablePureComponent {
this.setState({ visible: true });
}
}
}
};
render () {
const { attachment, displayWidth } = this.props;
@ -105,11 +104,13 @@ 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}
onMouseLeave={this.handleMouseLeave}
autoPlay={!isIOS() && autoPlayGif}
autoPlay={autoPlayGif}
playsInline
loop
muted
/>

View file

@ -47,7 +47,7 @@ class LoadMoreMedia extends ImmutablePureComponent {
handleLoadMore = () => {
this.props.onLoadMore(this.props.maxId);
}
};
render () {
return (
@ -114,7 +114,7 @@ class AccountGallery extends ImmutablePureComponent {
if (this.props.hasMore) {
this.handleLoadMore(this.props.attachments.size > 0 ? this.props.attachments.last().getIn(['status', 'id']) : undefined);
}
}
};
handleScroll = e => {
const { scrollTop, scrollHeight, clientHeight } = e.target;
@ -123,7 +123,7 @@ class AccountGallery extends ImmutablePureComponent {
if (150 > offset && !this.props.isLoading) {
this.handleScrollToBottom();
}
}
};
handleLoadMore = maxId => {
this.props.dispatch(expandAccountMediaTimeline(this.props.accountId, { maxId }));
@ -132,7 +132,7 @@ class AccountGallery extends ImmutablePureComponent {
handleLoadOlder = e => {
e.preventDefault();
this.handleScrollToBottom();
}
};
handleOpenMedia = attachment => {
const { dispatch } = this.props;
@ -148,13 +148,13 @@ class AccountGallery extends ImmutablePureComponent {
dispatch(openModal('MEDIA', { media, index, statusId }));
}
}
};
handleRef = c => {
if (c) {
this.setState({ width: c.offsetWidth });
}
}
};
render () {
const { attachments, isLoading, hasMore, isAccount, multiColumn, blockedBy, suspended } = this.props;

View file

@ -36,35 +36,35 @@ export default class Header extends ImmutablePureComponent {
handleFollow = () => {
this.props.onFollow(this.props.account);
}
};
handleBlock = () => {
this.props.onBlock(this.props.account);
}
};
handleMention = () => {
this.props.onMention(this.props.account, this.context.router.history);
}
};
handleDirect = () => {
this.props.onDirect(this.props.account, this.context.router.history);
}
};
handleReport = () => {
this.props.onReport(this.props.account);
}
};
handleReblogToggle = () => {
this.props.onReblogToggle(this.props.account);
}
};
handleNotifyToggle = () => {
this.props.onNotifyToggle(this.props.account);
}
};
handleMute = () => {
this.props.onMute(this.props.account);
}
};
handleBlockDomain = () => {
const domain = this.props.account.get('acct').split('@')[1];
@ -72,7 +72,7 @@ export default class Header extends ImmutablePureComponent {
if (!domain) return;
this.props.onBlockDomain(domain);
}
};
handleUnblockDomain = () => {
const domain = this.props.account.get('acct').split('@')[1];
@ -80,31 +80,31 @@ export default class Header extends ImmutablePureComponent {
if (!domain) return;
this.props.onUnblockDomain(domain);
}
};
handleEndorseToggle = () => {
this.props.onEndorseToggle(this.props.account);
}
};
handleAddToList = () => {
this.props.onAddToList(this.props.account);
}
};
handleEditAccountNote = () => {
this.props.onEditAccountNote(this.props.account);
}
};
handleChangeLanguages = () => {
this.props.onChangeLanguages(this.props.account);
}
};
handleInteractionModal = () => {
this.props.onInteractionModal(this.props.account);
}
};
handleOpenAvatar = () => {
this.props.onOpenAvatar(this.props.account);
}
};
render () {
const { account, hidden, hideTabs } = this.props;

View file

@ -20,7 +20,7 @@ class LimitedAccountHint extends React.PureComponent {
static propTypes = {
accountId: PropTypes.string.isRequired,
reveal: PropTypes.func,
}
};
render () {
const { reveal } = this.props;

View file

@ -26,7 +26,13 @@ const emptyList = ImmutableList();
const mapStateToProps = (state, { params: { acct, id, tagged }, withReplies = false }) => {
const accountId = id || state.getIn(['accounts_map', normalizeForLookup(acct)]);
if (!accountId) {
if (accountId === null) {
return {
isLoading: false,
isAccount: false,
statusIds: emptyList,
};
} else if (!accountId) {
return {
isLoading: true,
statusIds: emptyList,
@ -139,7 +145,7 @@ class AccountTimeline extends ImmutablePureComponent {
handleLoadMore = maxId => {
this.props.dispatch(expandAccountTimeline(this.props.accountId, { maxId, withReplies: this.props.withReplies, tagged: this.props.params.tagged }));
}
};
render () {
const { accountId, statusIds, featuredStatusIds, isLoading, hasMore, blockedBy, suspended, isAccount, hidden, multiColumn, remote, remoteUrl } = this.props;

View file

@ -59,7 +59,7 @@ class Audio extends React.PureComponent {
duration: null,
paused: true,
muted: false,
volume: 0.5,
volume: 1,
dragging: false,
revealed: this.props.visible !== undefined ? this.props.visible : (displayMedia !== 'hide_all' && !this.props.sensitive || displayMedia === 'show_all'),
};
@ -75,13 +75,13 @@ class Audio extends React.PureComponent {
if (this.player) {
this._setDimensions();
}
}
};
_pack() {
return {
src: this.props.src,
volume: this.audio.volume,
muted: this.audio.muted,
volume: this.state.volume,
muted: this.state.muted,
currentTime: this.audio.currentTime,
poster: this.props.poster,
backgroundColor: this.props.backgroundColor,
@ -105,25 +105,26 @@ class Audio extends React.PureComponent {
setSeekRef = c => {
this.seek = c;
}
};
setVolumeRef = c => {
this.volume = c;
}
};
setAudioRef = c => {
this.audio = c;
if (this.audio) {
this.setState({ volume: this.audio.volume, muted: this.audio.muted });
this.audio.volume = 1;
this.audio.muted = false;
}
}
};
setCanvasRef = c => {
this.canvas = c;
this.visualizer.setCanvas(c);
}
};
componentDidMount () {
window.addEventListener('scroll', this.handleScroll);
@ -162,7 +163,7 @@ class Audio extends React.PureComponent {
} else {
this.setState({ paused: true }, () => this.audio.pause());
}
}
};
handleResize = debounce(() => {
if (this.player) {
@ -180,7 +181,7 @@ class Audio extends React.PureComponent {
}
this._renderCanvas();
}
};
handlePause = () => {
this.setState({ paused: true });
@ -188,7 +189,7 @@ class Audio extends React.PureComponent {
if (this.audioContext) {
this.audioContext.suspend();
}
}
};
handleProgress = () => {
const lastTimeRange = this.audio.buffered.length - 1;
@ -196,15 +197,17 @@ class Audio extends React.PureComponent {
if (lastTimeRange > -1) {
this.setState({ buffer: Math.ceil(this.audio.buffered.end(lastTimeRange) / this.audio.duration * 100) });
}
}
};
toggleMute = () => {
const muted = !this.state.muted;
this.setState({ muted }, () => {
this.audio.muted = muted;
if (this.gainNode) {
this.gainNode.gain.value = muted ? 0 : this.state.volume;
}
});
}
};
toggleReveal = () => {
if (this.props.onToggleVisibility) {
@ -212,7 +215,7 @@ class Audio extends React.PureComponent {
} else {
this.setState({ revealed: !this.state.revealed });
}
}
};
handleVolumeMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseVolSlide, true);
@ -224,14 +227,14 @@ class Audio extends React.PureComponent {
e.preventDefault();
e.stopPropagation();
}
};
handleVolumeMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseVolSlide, true);
document.removeEventListener('mouseup', this.handleVolumeMouseUp, true);
document.removeEventListener('touchmove', this.handleMouseVolSlide, true);
document.removeEventListener('touchend', this.handleVolumeMouseUp, true);
}
};
handleMouseDown = e => {
document.addEventListener('mousemove', this.handleMouseMove, true);
@ -245,7 +248,7 @@ class Audio extends React.PureComponent {
e.preventDefault();
e.stopPropagation();
}
};
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove, true);
@ -255,7 +258,7 @@ class Audio extends React.PureComponent {
this.setState({ dragging: false });
this.audio.play();
}
};
handleMouseMove = throttle(e => {
const { x } = getPointerPosition(this.seek, e);
@ -273,14 +276,16 @@ class Audio extends React.PureComponent {
currentTime: this.audio.currentTime,
duration: this.audio.duration,
});
}
};
handleMouseVolSlide = throttle(e => {
const { x } = getPointerPosition(this.volume, e);
if(!isNaN(x)) {
this.setState({ volume: x }, () => {
this.audio.volume = x;
if (this.gainNode) {
this.gainNode.gain.value = this.state.muted ? 0 : x;
}
});
}
}, 15);
@ -306,41 +311,38 @@ class Audio extends React.PureComponent {
handleMouseEnter = () => {
this.setState({ hovered: true });
}
};
handleMouseLeave = () => {
this.setState({ hovered: false });
}
};
handleLoadedData = () => {
const { autoPlay, currentTime, volume, muted } = this.props;
const { autoPlay, currentTime } = this.props;
if (currentTime) {
this.audio.currentTime = currentTime;
}
if (volume !== undefined) {
this.audio.volume = volume;
}
if (muted !== undefined) {
this.audio.muted = muted;
}
if (autoPlay) {
this.togglePlay();
}
}
};
_initAudioContext () {
const AudioContext = window.AudioContext || window.webkitAudioContext;
const context = new AudioContext();
const source = context.createMediaElementSource(this.audio);
const gainNode = context.createGain();
gainNode.gain.value = this.state.muted ? 0 : this.state.volume;
this.visualizer.setAudioContext(context, source);
source.connect(context.destination);
source.connect(gainNode);
gainNode.connect(context.destination);
this.audioContext = context;
this.gainNode = gainNode;
}
handleDownload = () => {
@ -359,7 +361,7 @@ class Audio extends React.PureComponent {
}).catch(err => {
console.error(err);
});
}
};
_renderCanvas () {
requestAnimationFrame(() => {
@ -430,7 +432,7 @@ class Audio extends React.PureComponent {
e.stopPropagation();
this.togglePlay();
}
}
};
handleKeyDown = e => {
switch(e.key) {
@ -455,7 +457,7 @@ class Audio extends React.PureComponent {
this.seekBy(10);
break;
}
}
};
render () {
const { src, intl, alt, editable, autoPlay, sensitive, blurhash } = this.props;

View file

@ -48,24 +48,24 @@ class Bookmarks extends ImmutablePureComponent {
} else {
dispatch(addColumn('BOOKMARKS', {}));
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
setRef = c => {
this.column = c;
}
};
handleLoadMore = debounce(() => {
this.props.dispatch(expandBookmarkedStatuses());
}, 300, { leading: true })
}, 300, { leading: true });
render () {
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;

View file

@ -72,4 +72,4 @@ class ClosedRegistrationsModal extends ImmutablePureComponent {
);
}
};
}

View file

@ -60,16 +60,16 @@ class CommunityTimeline extends React.PureComponent {
} else {
dispatch(addColumn('COMMUNITY', { other: { onlyMedia } }));
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
componentDidMount () {
const { dispatch, onlyMedia } = this.props;
@ -109,13 +109,13 @@ class CommunityTimeline extends React.PureComponent {
setRef = c => {
this.column = c;
}
};
handleLoadMore = maxId => {
const { dispatch, onlyMedia } = this.props;
dispatch(expandCommunityTimeline({ maxId, onlyMedia }));
}
};
render () {
const { intl, hasUnread, columnId, multiColumn, onlyMedia } = this.props;

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');
};
}

View file

@ -55,7 +55,7 @@ class Conversation extends ImmutablePureComponent {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
}
};
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
@ -68,7 +68,7 @@ class Conversation extends ImmutablePureComponent {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
}
};
handleClick = () => {
if (!this.context.router) {
@ -82,35 +82,35 @@ class Conversation extends ImmutablePureComponent {
}
this.context.router.history.push(`/@${lastStatus.getIn(['account', 'acct'])}/${lastStatus.get('id')}`);
}
};
handleMarkAsRead = () => {
this.props.markRead();
}
};
handleReply = () => {
this.props.reply(this.props.lastStatus, this.context.router.history);
}
};
handleDelete = () => {
this.props.delete();
}
};
handleHotkeyMoveUp = () => {
this.props.onMoveUp(this.props.conversationId);
}
};
handleHotkeyMoveDown = () => {
this.props.onMoveDown(this.props.conversationId);
}
};
handleConversationMute = () => {
this.props.onMute(this.props.lastStatus);
}
};
handleShowMore = () => {
this.props.onToggleHidden(this.props.lastStatus);
}
};
render () {
const { accounts, lastStatus, unread, scrollKey, intl } = this.props;

View file

@ -16,17 +16,17 @@ export default class ConversationsList extends ImmutablePureComponent {
onLoadMore: PropTypes.func,
};
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id)
getCurrentIndex = id => this.props.conversations.findIndex(x => x.get('id') === id);
handleMoveUp = id => {
const elementIndex = this.getCurrentIndex(id) - 1;
this._selectChild(elementIndex, true);
}
};
handleMoveDown = id => {
const elementIndex = this.getCurrentIndex(id) + 1;
this._selectChild(elementIndex, false);
}
};
_selectChild (index, align_top) {
const container = this.node.node;
@ -44,7 +44,7 @@ export default class ConversationsList extends ImmutablePureComponent {
setRef = c => {
this.node = c;
}
};
handleLoadOlder = debounce(() => {
const last = this.props.conversations.last();
@ -52,7 +52,7 @@ export default class ConversationsList extends ImmutablePureComponent {
if (last && last.get('last_status')) {
this.props.onLoadMore(last.get('last_status'));
}
}, 300, { leading: true })
}, 300, { leading: true });
render () {
const { conversations, onLoadMore, ...other } = this.props;

View file

@ -34,16 +34,16 @@ class DirectTimeline extends React.PureComponent {
} else {
dispatch(addColumn('DIRECT', {}));
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
componentDidMount () {
const { dispatch } = this.props;
@ -64,11 +64,11 @@ class DirectTimeline extends React.PureComponent {
setRef = c => {
this.column = c;
}
};
handleLoadMore = maxId => {
this.props.dispatch(expandConversations({ maxId }));
}
};
render () {
const { intl, hasUnread, columnId, multiColumn } = this.props;

View file

@ -115,7 +115,7 @@ class AccountCard extends ImmutablePureComponent {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
}
};
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
@ -128,7 +128,7 @@ class AccountCard extends ImmutablePureComponent {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
}
};
handleFollow = () => {
this.props.onFollow(this.props.account);
@ -140,11 +140,11 @@ class AccountCard extends ImmutablePureComponent {
handleMute = () => {
this.props.onMute(this.props.account);
}
};
handleEditProfile = () => {
window.open('/settings/profile', '_blank');
}
};
render() {
const { account, intl } = this.props;

View file

@ -64,7 +64,7 @@ class Directory extends React.PureComponent {
} else {
dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state)));
}
}
};
getParams = (props, state) => ({
order: state.order === null ? (props.params.order || 'active') : state.order,
@ -74,11 +74,11 @@ class Directory extends React.PureComponent {
handleMove = dir => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
componentDidMount () {
const { dispatch } = this.props;
@ -97,7 +97,7 @@ class Directory extends React.PureComponent {
setRef = c => {
this.column = c;
}
};
handleChangeOrder = e => {
const { dispatch, columnId } = this.props;
@ -107,7 +107,7 @@ class Directory extends React.PureComponent {
} else {
this.setState({ order: e.target.value });
}
}
};
handleChangeLocal = e => {
const { dispatch, columnId } = this.props;
@ -117,12 +117,12 @@ class Directory extends React.PureComponent {
} else {
this.setState({ local: e.target.value === '1' });
}
}
};
handleLoadMore = () => {
const { dispatch } = this.props;
dispatch(expandDirectory(this.getParams(this.props, this.state)));
}
};
render () {
const { isLoading, accountIds, intl, columnId, multiColumn, domain } = this.props;

View file

@ -88,5 +88,10 @@ describe('emoji', () => {
expect(emojify('💂‍♀️💂‍♂️'))
.toEqual('<img draggable="false" class="emojione" alt="💂\u200D♀" title=":female-guard:" src="/emoji/1f482-200d-2640-fe0f_border.svg"><img draggable="false" class="emojione" alt="💂\u200D♂" title=":male-guard:" src="/emoji/1f482-200d-2642-fe0f_border.svg">');
});
it('keeps ordering as expected (issue fixed by PR 20677)', () => {
expect(emojify('<p>💕 <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>'))
.toEqual('<p><img draggable="false" class="emojione" alt="💕" title=":two_hearts:" src="/emoji/1f495.svg"> <a class="hashtag" href="https://example.com/tags/foo" rel="nofollow noopener noreferrer" target="_blank">#<span>foo</span></a> test: foo.</p>');
});
});
});

View file

@ -19,8 +19,6 @@ const emojiFilename = (filename) => {
return borderedEmoji.includes(filename) ? (filename + '_border') : filename;
};
const domParser = new DOMParser();
const emojifyTextNode = (node, customEmojis) => {
let str = node.textContent;
@ -39,7 +37,7 @@ const emojifyTextNode = (node, customEmojis) => {
}
}
let rend, replacement = '';
let rend, replacement = null;
if (i === str.length) {
break;
} else if (str[i] === ':') {
@ -51,7 +49,14 @@ const emojifyTextNode = (node, 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 custom-emoji" alt="${shortname}" title="${shortname}" src="${filename}" data-original="${customEmojis[shortname].url}" data-static="${customEmojis[shortname].static_url}" />`;
replacement = document.createElement('img');
replacement.setAttribute('draggable', false);
replacement.setAttribute('class', 'emojione custom-emoji');
replacement.setAttribute('alt', shortname);
replacement.setAttribute('title', shortname);
replacement.setAttribute('src', filename);
replacement.setAttribute('data-original', customEmojis[shortname].url);
replacement.setAttribute('data-static', customEmojis[shortname].static_url);
return true;
}
return false;
@ -59,7 +64,12 @@ const emojifyTextNode = (node, customEmojis) => {
} else { // matched to unicode emoji
const { filename, shortCode } = unicodeMapping[match];
const title = shortCode ? `:${shortCode}:` : '';
replacement = `<img draggable="false" class="emojione" alt="${match}" title="${title}" src="${assetHost}/emoji/${emojiFilename(filename)}.svg" />`;
replacement = document.createElement('img');
replacement.setAttribute('draggable', false);
replacement.setAttribute('class', 'emojione');
replacement.setAttribute('alt', match);
replacement.setAttribute('title', title);
replacement.setAttribute('src', `${assetHost}/emoji/${emojiFilename(filename)}.svg`);
rend = i + match.length;
// If the matched character was followed by VS15 (for selecting text presentation), skip it.
if (str.codePointAt(rend) === 65038) {
@ -69,9 +79,8 @@ const emojifyTextNode = (node, customEmojis) => {
fragment.append(document.createTextNode(str.slice(0, i)));
if (replacement) {
fragment.append(domParser.parseFromString(replacement, 'text/html').documentElement.getElementsByTagName('img')[0]);
fragment.append(replacement);
}
node.textContent = str.slice(0, i);
str = str.slice(rend);
}

View file

@ -135,19 +135,19 @@ function getData(emoji, skin, set) {
}
}
if (data.short_names.hasOwnProperty(emoji)) {
if (Object.prototype.hasOwnProperty.call(data.short_names, emoji)) {
emoji = data.short_names[emoji];
}
if (data.emojis.hasOwnProperty(emoji)) {
if (Object.prototype.hasOwnProperty.call(data.emojis, emoji)) {
emojiData = data.emojis[emoji];
}
} else if (emoji.id) {
if (data.short_names.hasOwnProperty(emoji.id)) {
if (Object.prototype.hasOwnProperty.call(data.short_names, emoji.id)) {
emoji.id = data.short_names[emoji.id];
}
if (data.emojis.hasOwnProperty(emoji.id)) {
if (Object.prototype.hasOwnProperty.call(data.emojis, emoji.id)) {
emojiData = data.emojis[emoji.id];
skin = skin || emoji.skin;
}
@ -216,7 +216,7 @@ function deepMerge(a, b) {
let originalValue = a[key],
value = originalValue;
if (b.hasOwnProperty(key)) {
if (Object.prototype.hasOwnProperty.call(b, key)) {
value = b[key];
}

View file

@ -41,13 +41,13 @@ class Explore extends React.PureComponent {
handleHeaderClick = () => {
this.column.scrollTop();
}
};
setRef = c => {
this.column = c;
}
};
render () {
render() {
const { intl, multiColumn, isSearching } = this.props;
const { signedIn } = this.context.identity;
@ -68,12 +68,22 @@ class Explore extends React.PureComponent {
{isSearching ? (
<SearchResults />
) : (
<React.Fragment>
<>
<div className='account__section-headline'>
<NavLink exact to='/explore'><FormattedMessage id='explore.trending_statuses' defaultMessage='Posts' /></NavLink>
<NavLink exact to='/explore/tags'><FormattedMessage id='explore.trending_tags' defaultMessage='Hashtags' /></NavLink>
<NavLink exact to='/explore/links'><FormattedMessage id='explore.trending_links' defaultMessage='News' /></NavLink>
{signedIn && <NavLink exact to='/explore/suggestions'><FormattedMessage 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>
@ -87,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>

View file

@ -33,7 +33,7 @@ class Statuses extends React.PureComponent {
handleLoadMore = debounce(() => {
const { dispatch } = this.props;
dispatch(expandTrendingStatuses());
}, 300, { leading: true })
}, 300, { leading: true });
render () {
const { isLoading, hasMore, statusIds, multiColumn } = this.props;

View file

@ -48,24 +48,24 @@ class Favourites extends ImmutablePureComponent {
} else {
dispatch(addColumn('FAVOURITES', {}));
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
setRef = c => {
this.column = c;
}
};
handleLoadMore = debounce(() => {
this.props.dispatch(expandFavouritedStatuses());
}, 300, { leading: true })
}, 300, { leading: true });
render () {
const { intl, statusIds, columnId, multiColumn, hasMore, isLoading } = this.props;

View file

@ -47,7 +47,7 @@ class Favourites extends ImmutablePureComponent {
handleRefresh = () => {
this.props.dispatch(fetchFavourites(this.props.params.statusId));
}
};
render () {
const { intl, accountIds, multiColumn } = this.props;

View file

@ -71,7 +71,7 @@ class SelectFilter extends React.PureComponent {
<span className='language-dropdown__dropdown__results__item__native-name'>{filter[1]}</span> {warning}
</div>
);
}
};
renderCreateNew (name) {
return (
@ -83,11 +83,11 @@ class SelectFilter extends React.PureComponent {
handleSearchChange = ({ target }) => {
this.setState({ searchValue: target.value });
}
};
setListRef = c => {
this.listNode = c;
}
};
handleKeyDown = e => {
const index = Array.from(this.listNode.childNodes).findIndex(node => node === e.currentTarget);
@ -125,7 +125,7 @@ class SelectFilter extends React.PureComponent {
e.preventDefault();
e.stopPropagation();
}
}
};
handleSearchKeyDown = e => {
let element = null;
@ -143,11 +143,11 @@ class SelectFilter extends React.PureComponent {
break;
}
}
};
handleClear = () => {
this.setState({ searchValue: '' });
}
};
handleItemClick = e => {
const value = e.currentTarget.getAttribute('data-index');
@ -155,7 +155,7 @@ class SelectFilter extends React.PureComponent {
e.preventDefault();
this.props.onSelectFilter(value);
}
};
handleNewFilterClick = e => {
e.preventDefault();

View file

@ -50,7 +50,7 @@ class Account extends ImmutablePureComponent {
} else {
dispatch(followAccount(account.get('id')));
}
}
};
render () {
const { account, intl } = this.props;

View file

@ -69,7 +69,7 @@ class FollowRecommendations extends ImmutablePureComponent {
}));
router.history.push('/home');
}
};
render () {
const { suggestions, isLoading } = this.props;

View file

@ -5,7 +5,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { debounce } from 'lodash';
import LoadingIndicator from '../../components/loading_indicator';
import Column from '../ui/components/column';
import ColumnBackButtonSlim from '../../components/column_back_button_slim';
import AccountAuthorizeContainer from './containers/account_authorize_container';
@ -53,16 +52,8 @@ class FollowRequests extends ImmutablePureComponent {
render () {
const { intl, accountIds, hasMore, multiColumn, locked, domain, isLoading } = this.props;
if (!accountIds) {
return (
<Column>
<LoadingIndicator />
</Column>
);
}
const emptyMessage = <FormattedMessage id='empty_column.follow_requests' defaultMessage="You don't have any follow requests yet. When you receive one, it will show up here." />;
const unlockedPrependMessage = locked ? null : (
const unlockedPrependMessage = !locked && accountIds.size > 0 && (
<div className='follow_requests-unlocked_explanation'>
<FormattedMessage
id='follow_requests.unlocked_explanation'
@ -80,6 +71,7 @@ class FollowRequests extends ImmutablePureComponent {
onLoadMore={this.handleLoadMore}
hasMore={hasMore}
isLoading={isLoading}
showLoading={isLoading && accountIds.size === 0}
emptyMessage={emptyMessage}
bindToDocument={!multiColumn}
prepend={unlockedPrependMessage}

View file

@ -0,0 +1,89 @@
import { debounce } from 'lodash';
import PropTypes from 'prop-types';
import React from 'react';
import ImmutablePureComponent from 'react-immutable-pure-component';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { defineMessages, injectIntl, FormattedMessage } from 'react-intl';
import { connect } from 'react-redux';
import ColumnHeader from 'mastodon/components/column_header';
import ScrollableList from 'mastodon/components/scrollable_list';
import Column from 'mastodon/features/ui/components/column';
import { Helmet } from 'react-helmet';
import Hashtag from 'mastodon/components/hashtag';
import { expandFollowedHashtags, fetchFollowedHashtags } from 'mastodon/actions/tags';
const messages = defineMessages({
heading: { id: 'followed_tags', defaultMessage: 'Followed hashtags' },
});
const mapStateToProps = state => ({
hashtags: state.getIn(['followed_tags', 'items']),
isLoading: state.getIn(['followed_tags', 'isLoading'], true),
hasMore: !!state.getIn(['followed_tags', 'next']),
});
export default @connect(mapStateToProps)
@injectIntl
class FollowedTags extends ImmutablePureComponent {
static propTypes = {
params: PropTypes.object.isRequired,
dispatch: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
hashtags: ImmutablePropTypes.list,
isLoading: PropTypes.bool,
hasMore: PropTypes.bool,
multiColumn: PropTypes.bool,
};
componentDidMount() {
this.props.dispatch(fetchFollowedHashtags());
}
handleLoadMore = debounce(() => {
this.props.dispatch(expandFollowedHashtags());
}, 300, { leading: true });
render () {
const { intl, hashtags, isLoading, hasMore, multiColumn } = this.props;
const emptyMessage = <FormattedMessage id='empty_column.followed_tags' defaultMessage='You have not followed any hashtags yet. When you do, they will show up here.' />;
return (
<Column bindToDocument={!multiColumn}>
<ColumnHeader
icon='hashtag'
title={intl.formatMessage(messages.heading)}
showBackButton
multiColumn={multiColumn}
/>
<ScrollableList
scrollKey='followed_tags'
emptyMessage={emptyMessage}
hasMore={hasMore}
isLoading={isLoading}
onLoadMore={this.handleLoadMore}
bindToDocument={!multiColumn}
>
{hashtags.map((hashtag) => (
<Hashtag
key={hashtag.get('name')}
name={hashtag.get('name')}
to={`/tags/${hashtag.get('name')}`}
withGraph={false}
// Taken from ImmutableHashtag. Should maybe refactor ImmutableHashtag to accept more options?
people={hashtag.getIn(['history', 0, 'accounts']) * 1 + hashtag.getIn(['history', 1, 'accounts']) * 1}
history={hashtag.get('history').reverse().map((day) => day.get('uses')).toArray()}
/>
))}
</ScrollableList>
<Helmet>
<meta name='robots' content='noindex' />
</Helmet>
</Column>
);
}
}

View file

@ -35,7 +35,7 @@ class Content extends ImmutablePureComponent {
setRef = c => {
this.node = c;
}
};
componentDidMount () {
this._updateLinks();
@ -89,7 +89,7 @@ class Content extends ImmutablePureComponent {
e.preventDefault();
this.context.router.history.push(`/@${mention.get('acct')}`);
}
}
};
onHashtagClick = (hashtag, e) => {
hashtag = hashtag.replace(/^#/, '');
@ -98,14 +98,14 @@ class Content extends ImmutablePureComponent {
e.preventDefault();
this.context.router.history.push(`/tags/${hashtag}`);
}
}
};
onStatusClick = (status, e) => {
if (this.context.router && e.button === 0 && !(e.ctrlKey || e.metaKey)) {
e.preventDefault();
this.context.router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
}
}
};
handleMouseEnter = ({ currentTarget }) => {
if (autoPlayGif) {
@ -118,7 +118,7 @@ class Content extends ImmutablePureComponent {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-original');
}
}
};
handleMouseLeave = ({ currentTarget }) => {
if (autoPlayGif) {
@ -131,7 +131,7 @@ class Content extends ImmutablePureComponent {
let emoji = emojis[i];
emoji.src = emoji.getAttribute('data-static');
}
}
};
render () {
const { announcement } = this.props;
@ -216,11 +216,11 @@ class Reaction extends ImmutablePureComponent {
} else {
addReaction(announcementId, reaction.get('name'));
}
}
};
handleMouseEnter = () => this.setState({ hovered: true })
handleMouseEnter = () => this.setState({ hovered: true });
handleMouseLeave = () => this.setState({ hovered: false })
handleMouseLeave = () => this.setState({ hovered: false });
render () {
const { reaction } = this.props;
@ -254,7 +254,7 @@ class ReactionsBar extends ImmutablePureComponent {
handleEmojiPick = data => {
const { addReaction, announcementId } = this.props;
addReaction(announcementId, data.native.replace(/:/g, ''));
}
};
willEnter () {
return { scale: reduceMotion ? 1 : 0 };
@ -397,15 +397,15 @@ class Announcements extends ImmutablePureComponent {
handleChangeIndex = index => {
this.setState({ index: index % this.props.announcements.size });
}
};
handleNextClick = () => {
this.setState({ index: (this.state.index + 1) % this.props.announcements.size });
}
};
handlePrevClick = () => {
this.setState({ index: (this.props.announcements.size + this.state.index - 1) % this.props.announcements.size });
}
};
render () {
const { announcements, intl } = this.props;

View file

@ -4,6 +4,7 @@ import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import { ImmutableHashtag as Hashtag } from 'mastodon/components/hashtag';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom';
export default class Trends extends ImmutablePureComponent {
@ -36,7 +37,11 @@ export default class Trends extends ImmutablePureComponent {
return (
<div className='getting-started__trends'>
<h4><FormattedMessage id='trends.trending_now' defaultMessage='Trending now' /></h4>
<h4>
<Link to={'/explore/tags'}>
<FormattedMessage id='trends.trending_now' defaultMessage='Trending now' />
</Link>
</h4>
{trends.take(3).map(hashtag => <Hashtag key={hashtag.get('name')} hashtag={hashtag} />)}
</div>

View file

@ -38,7 +38,7 @@ class ColumnSettings extends React.PureComponent {
} else {
return tags;
}
};
}
onSelect = mode => value => {
const oldValue = this.tags(mode);
@ -98,7 +98,7 @@ class ColumnSettings extends React.PureComponent {
default:
return '';
}
};
}
render () {
const { settings, onChange } = this.props;

View file

@ -54,7 +54,7 @@ class HashtagTimeline extends React.PureComponent {
} else {
dispatch(addColumn('HASHTAG', { id: this.props.params.id }));
}
}
};
title = () => {
const { id } = this.props.params;
@ -73,7 +73,7 @@ class HashtagTimeline extends React.PureComponent {
}
return title;
}
};
additionalFor = (mode) => {
const { tags } = this.props.params;
@ -83,16 +83,16 @@ class HashtagTimeline extends React.PureComponent {
} else {
return '';
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
_subscribe (dispatch, id, tags = {}, local) {
const { signedIn } = this.context.identity;
@ -157,14 +157,14 @@ class HashtagTimeline extends React.PureComponent {
setRef = c => {
this.column = c;
}
};
handleLoadMore = maxId => {
const { dispatch, params } = this.props;
const { id, tags, local } = params;
dispatch(expandHashtagTimeline(id, { maxId, tags, local }));
}
};
handleFollow = () => {
const { dispatch, params, tag } = this.props;
@ -180,7 +180,7 @@ class HashtagTimeline extends React.PureComponent {
} else {
dispatch(followHashtag(id));
}
}
};
render () {
const { hasUnread, columnId, multiColumn, tag, intl } = this.props;
@ -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>
);

View file

@ -58,24 +58,24 @@ class HomeTimeline extends React.PureComponent {
} else {
dispatch(addColumn('HOME', {}));
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
setRef = c => {
this.column = c;
}
};
handleLoadMore = maxId => {
this.props.dispatch(expandHomeTimeline({ maxId }));
}
};
componentDidMount () {
setTimeout(() => this.props.dispatch(fetchAnnouncements()), 700);
@ -114,7 +114,7 @@ class HomeTimeline extends React.PureComponent {
handleToggleAnnouncementsClick = (e) => {
e.stopPropagation();
this.props.dispatch(toggleShowAnnouncements());
}
};
render () {
const { intl, hasUnread, columnId, multiColumn, hasAnnouncements, unreadAnnouncements, showAnnouncements } = this.props;

View file

@ -30,14 +30,14 @@ class Copypaste extends React.PureComponent {
setRef = c => {
this.input = c;
}
};
handleInputClick = () => {
this.setState({ copied: false });
this.input.focus();
this.input.select();
this.input.setSelectionRange(0, this.input.value.length);
}
};
handleButtonClick = () => {
const { value } = this.props;
@ -45,7 +45,7 @@ class Copypaste extends React.PureComponent {
this.input.blur();
this.setState({ copied: true });
this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
}
};
componentWillUnmount () {
if (this.timeout) clearTimeout(this.timeout);
@ -86,7 +86,7 @@ class InteractionModal extends React.PureComponent {
handleSignupClick = () => {
this.props.onSignupClick();
}
};
render () {
const { url, type, displayNameHtml } = this.props;

View file

@ -33,16 +33,16 @@ class ListForm extends React.PureComponent {
handleChange = e => {
this.props.onChange(e.target.value);
}
};
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit();
}
};
handleClick = () => {
this.props.onSubmit();
}
};
render () {
const { value, disabled, intl } = this.props;

View file

@ -34,17 +34,17 @@ class Search extends React.PureComponent {
handleChange = e => {
this.props.onChange(e.target.value);
}
};
handleKeyUp = e => {
if (e.keyCode === 13) {
this.props.onSubmit(this.props.value);
}
}
};
handleClear = () => {
this.props.onClear();
}
};
render () {
const { value, intl } = this.props;

View file

@ -58,16 +58,16 @@ class ListTimeline extends React.PureComponent {
dispatch(addColumn('LIST', { id: this.props.params.id }));
this.context.router.history.push('/');
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
componentDidMount () {
const { dispatch } = this.props;
@ -105,16 +105,16 @@ class ListTimeline extends React.PureComponent {
setRef = c => {
this.column = c;
}
};
handleLoadMore = maxId => {
const { id } = this.props.params;
this.props.dispatch(expandListTimeline(id, { maxId }));
}
};
handleEditClick = () => {
this.props.dispatch(openModal('LIST_EDITOR', { listId: this.props.params.id }));
}
};
handleDeleteClick = () => {
const { dispatch, columnId, intl } = this.props;
@ -126,20 +126,20 @@ class ListTimeline extends React.PureComponent {
onConfirm: () => {
dispatch(deleteList(id));
if (!!columnId) {
if (columnId) {
dispatch(removeColumn(columnId));
} else {
this.context.router.history.push('/lists');
}
},
}));
}
};
handleRepliesPolicyChange = ({ target }) => {
const { dispatch } = this.props;
const { id } = this.props.params;
dispatch(updateList(id, undefined, false, target.value));
}
};
render () {
const { hasUnread, columnId, multiColumn, list, intl } = this.props;

View file

@ -34,16 +34,16 @@ class NewListForm extends React.PureComponent {
handleChange = e => {
this.props.onChange(e.target.value);
}
};
handleSubmit = e => {
e.preventDefault();
this.props.onSubmit();
}
};
handleClick = () => {
this.props.onSubmit();
}
};
render () {
const { value, disabled, intl } = this.props;

View file

@ -26,7 +26,7 @@ export default class ColumnSettings extends React.PureComponent {
onPushChange = (path, checked) => {
this.props.onChange(['push', ...path], checked);
}
};
render () {
const { settings, pushSettings, onChange, onClear, alertsEnabled, browserSupport, browserPermission, onRequestNotificationPermission } = this.props;

View file

@ -61,12 +61,12 @@ class Notification extends ImmutablePureComponent {
handleMoveUp = () => {
const { notification, onMoveUp } = this.props;
onMoveUp(notification.get('id'));
}
};
handleMoveDown = () => {
const { notification, onMoveDown } = this.props;
onMoveDown(notification.get('id'));
}
};
handleOpen = () => {
const { notification } = this.props;
@ -76,34 +76,34 @@ class Notification extends ImmutablePureComponent {
} else {
this.handleOpenProfile();
}
}
};
handleOpenProfile = () => {
const { notification } = this.props;
this.context.router.history.push(`/@${notification.getIn(['account', 'acct'])}`);
}
};
handleMention = e => {
e.preventDefault();
const { notification, onMention } = this.props;
onMention(notification.get('account'), this.context.router.history);
}
};
handleHotkeyFavourite = () => {
const { status } = this.props;
if (status) this.props.onFavourite(status);
}
};
handleHotkeyBoost = e => {
const { status } = this.props;
if (status) this.props.onReblog(status, e);
}
};
handleHotkeyToggleHidden = () => {
const { status } = this.props;
if (status) this.props.onToggleHidden(status);
}
};
getHandlers () {
return {
@ -246,7 +246,11 @@ class Notification extends ImmutablePureComponent {
}
renderStatus (notification, link) {
const { intl, unread } = this.props;
const { intl, unread, status } = this.props;
if (!status) {
return null;
}
return (
<HotKeys handlers={this.getHandlers()}>
@ -264,6 +268,7 @@ class Notification extends ImmutablePureComponent {
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
contextType='notifications'
muted
withDismiss
hidden={this.props.hidden}
@ -278,7 +283,11 @@ class Notification extends ImmutablePureComponent {
}
renderUpdate (notification, link) {
const { intl, unread } = this.props;
const { intl, unread, status } = this.props;
if (!status) {
return null;
}
return (
<HotKeys handlers={this.getHandlers()}>
@ -296,6 +305,7 @@ class Notification extends ImmutablePureComponent {
<StatusContainer
id={notification.get('status')}
account={notification.get('account')}
contextType='notifications'
muted
withDismiss
hidden={this.props.hidden}
@ -310,10 +320,14 @@ class Notification extends ImmutablePureComponent {
}
renderPoll (notification, account) {
const { intl, unread } = this.props;
const { intl, unread, status } = this.props;
const ownPoll = me === account.get('id');
const message = ownPoll ? intl.formatMessage(messages.ownPoll) : intl.formatMessage(messages.poll);
if (!status) {
return null;
}
return (
<HotKeys handlers={this.getHandlers()}>
<div className={classNames('notification notification-poll focusable', { unread })} tabIndex='0' aria-label={notificationForScreenReader(intl, message, notification.get('created_at'))}>
@ -334,6 +348,7 @@ class Notification extends ImmutablePureComponent {
<StatusContainer
id={notification.get('status')}
account={account}
contextType='notifications'
muted
withDismiss
hidden={this.props.hidden}

View file

@ -23,11 +23,11 @@ class NotificationsPermissionBanner extends React.PureComponent {
handleClick = () => {
this.props.dispatch(requestBrowserPermission());
}
};
handleClose = () => {
this.props.dispatch(changeSetting(['notifications', 'dismissPermissionBanner'], true));
}
};
render () {
const { intl } = this.props;

View file

@ -13,11 +13,11 @@ export default class SettingToggle extends React.PureComponent {
onChange: PropTypes.func.isRequired,
defaultValue: PropTypes.bool,
disabled: PropTypes.bool,
}
};
onChange = ({ target }) => {
this.props.onChange(this.props.settingPath, target.checked);
}
};
render () {
const { prefix, settings, settingPath, label, defaultValue, disabled } = this.props;

View file

@ -24,7 +24,7 @@ const makeMapStateToProps = () => {
const notification = getNotification(state, props.notification, props.accountId);
return {
notification: notification,
status: notification.get('status') ? getStatus(state, { id: notification.get('status') }) : null,
status: notification.get('status') ? getStatus(state, { id: notification.get('status'), contextType: 'notifications' }) : null,
report: notification.get('report') ? getReport(state, notification.get('report'), notification.getIn(['report', 'target_account', 'id'])) : null,
};
};

View file

@ -136,30 +136,30 @@ class Notifications extends React.PureComponent {
} else {
dispatch(addColumn('NOTIFICATIONS', {}));
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
setColumnRef = c => {
this.column = c;
}
};
handleMoveUp = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) - 1;
this._selectChild(elementIndex, true);
}
};
handleMoveDown = id => {
const elementIndex = this.props.notifications.findIndex(item => item !== null && item.get('id') === id) + 1;
this._selectChild(elementIndex, false);
}
};
_selectChild (index, align_top) {
const container = this.column.node;

View file

@ -112,7 +112,7 @@ class Footer extends ImmutablePureComponent {
_performReblog = (status, privacy) => {
const { dispatch } = this.props;
dispatch(reblog(status, privacy));
}
};
handleReblogClick = e => {
const { dispatch, status } = this.props;
@ -149,7 +149,7 @@ class Footer extends ImmutablePureComponent {
}
router.history.push(`/@${status.getIn(['account', 'acct'])}/${status.get('id')}`);
}
};
render () {
const { status, intl, withOpenButton } = this.props;

View file

@ -32,7 +32,7 @@ class PictureInPicture extends React.Component {
handleClose = () => {
const { dispatch } = this.props;
dispatch(removePictureInPicture());
}
};
render () {
const { type, src, currentTime, accountId, statusId } = this.props;

View file

@ -37,11 +37,11 @@ class PinnedStatuses extends ImmutablePureComponent {
handleHeaderClick = () => {
this.column.scrollTop();
}
};
setRef = c => {
this.column = c;
}
};
render () {
const { intl, statusIds, hasMore, multiColumn } = this.props;

View file

@ -62,16 +62,16 @@ class PublicTimeline extends React.PureComponent {
} else {
dispatch(addColumn(onlyRemote ? 'REMOTE' : 'PUBLIC', { other: { onlyMedia, onlyRemote } }));
}
}
};
handleMove = (dir) => {
const { columnId, dispatch } = this.props;
dispatch(moveColumn(columnId, dir));
}
};
handleHeaderClick = () => {
this.column.scrollTop();
}
};
componentDidMount () {
const { dispatch, onlyMedia, onlyRemote } = this.props;
@ -111,13 +111,13 @@ class PublicTimeline extends React.PureComponent {
setRef = c => {
this.column = c;
}
};
handleLoadMore = maxId => {
const { dispatch, onlyMedia, onlyRemote } = this.props;
dispatch(expandPublicTimeline({ maxId, onlyMedia, onlyRemote }));
}
};
render () {
const { intl, columnId, hasUnread, multiColumn, onlyMedia, onlyRemote } = this.props;

View file

@ -47,7 +47,7 @@ class Reblogs extends ImmutablePureComponent {
handleRefresh = () => {
this.props.dispatch(fetchReblogs(this.props.params.statusId));
}
};
render () {
const { intl, accountIds, multiColumn } = this.props;

View file

@ -24,12 +24,12 @@ export default class Option extends React.PureComponent {
e.preventDefault();
onToggle(value, !checked);
}
}
};
handleChange = e => {
const { value, onToggle } = this.props;
onToggle(value, e.target.checked);
}
};
render () {
const { name, value, checked, label, labelComponent, description, multiple } = this.props;

View file

@ -9,7 +9,7 @@ export default class Compose extends React.PureComponent {
render () {
return (
<div>
<ComposeFormContainer />
<ComposeFormContainer autoFocus />
<NotificationsContainer />
<ModalContainer />
<LoadingBarContainer className='loading-bar' />

View file

@ -7,7 +7,7 @@ import DropdownMenuContainer from '../../../containers/dropdown_menu_container';
import { defineMessages, injectIntl } from 'react-intl';
import { me } from '../../../initial_state';
import classNames from 'classnames';
import { PERMISSION_MANAGE_USERS } from 'mastodon/permissions';
import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/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' },
blockDomain: { id: 'account.block_domain', defaultMessage: 'Block domain {domain}' },
unblockDomain: { id: 'account.unblock_domain', defaultMessage: 'Unblock domain {domain}' },
@ -81,39 +82,39 @@ class ActionBar extends React.PureComponent {
handleReplyClick = () => {
this.props.onReply(this.props.status);
}
};
handleReblogClick = (e) => {
this.props.onReblog(this.props.status, e);
}
};
handleFavouriteClick = () => {
this.props.onFavourite(this.props.status);
}
};
handleBookmarkClick = (e) => {
this.props.onBookmark(this.props.status, e);
}
};
handleDeleteClick = () => {
this.props.onDelete(this.props.status, this.context.router.history);
}
};
handleRedraftClick = () => {
this.props.onDelete(this.props.status, this.context.router.history, true);
}
};
handleEditClick = () => {
this.props.onEdit(this.props.status, this.context.router.history);
}
};
handleDirectClick = () => {
this.props.onDirect(this.props.status.get('account'), this.context.router.history);
}
};
handleMentionClick = () => {
this.props.onMention(this.props.status.get('account'), this.context.router.history);
}
};
handleMuteClick = () => {
const { status, relationship, onMute, onUnmute } = this.props;
@ -124,7 +125,7 @@ class ActionBar extends React.PureComponent {
} else {
onMute(account);
}
}
};
handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props;
@ -135,49 +136,49 @@ class ActionBar extends React.PureComponent {
} else {
onBlock(status);
}
}
};
handleBlockDomain = () => {
const { status, onBlockDomain } = this.props;
const account = status.get('account');
onBlockDomain(account.get('acct').split('@')[1]);
}
};
handleUnblockDomain = () => {
const { status, onUnblockDomain } = this.props;
const account = status.get('account');
onUnblockDomain(account.get('acct').split('@')[1]);
}
};
handleConversationMuteClick = () => {
this.props.onMuteConversation(this.props.status);
}
};
handleReport = () => {
this.props.onReport(this.props.status);
}
};
handlePinClick = () => {
this.props.onPin(this.props.status);
}
};
handleShare = () => {
navigator.share({
text: this.props.status.get('search_index'),
url: this.props.status.get('url'),
});
}
};
handleEmbed = () => {
this.props.onEmbed(this.props.status);
}
};
handleCopy = () => {
const url = this.props.status.get('url');
navigator.clipboard.writeText(url);
}
};
render () {
const { status, relationship, intl } = this.props;
@ -243,10 +244,16 @@ class ActionBar extends React.PureComponent {
}
}
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
menu.push(null);
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
}
if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
const domain = account.get('acct').split('@')[1];
menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
}
}
}

View file

@ -146,7 +146,7 @@ export default class Card extends React.PureComponent {
} else {
this.setState({ embedded: true });
}
}
};
setRef = c => {
this.node = c;
@ -154,17 +154,17 @@ export default class Card extends React.PureComponent {
if (this.node) {
this._setDimensions();
}
}
};
handleImageLoad = () => {
this.setState({ previewLoaded: true });
}
};
handleReveal = e => {
e.preventDefault();
e.stopPropagation();
this.setState({ revealed: true });
}
};
renderVideo () {
const { card } = this.props;

View file

@ -61,15 +61,15 @@ class DetailedStatus extends ImmutablePureComponent {
}
e.stopPropagation();
}
};
handleOpenVideo = (options) => {
this.props.onOpenVideo(this.props.status.getIn(['media_attachments', 0]), options);
}
};
handleExpandedToggle = () => {
this.props.onToggleHidden(this.props.status);
}
};
_measureHeight (heightJustChanged) {
if (this.props.measureHeight && this.node) {
@ -84,7 +84,7 @@ class DetailedStatus extends ImmutablePureComponent {
setRef = c => {
this.node = c;
this._measureHeight();
}
};
componentDidUpdate (prevProps, prevState) {
this._measureHeight(prevState.height !== this.state.height);
@ -102,12 +102,12 @@ class DetailedStatus extends ImmutablePureComponent {
}
window.open(href, 'mastodon-intent', 'width=445,height=600,resizable=no,menubar=no,status=no,scrollbars=yes');
}
};
handleTranslate = () => {
const { onTranslate, status } = this.props;
onTranslate(status);
}
};
render () {
const status = (this.props.status && this.props.status.get('reblog')) ? this.props.status.get('reblog') : this.props.status;

View file

@ -222,6 +222,10 @@ class Status extends ImmutablePureComponent {
this.props.dispatch(fetchStatus(nextProps.params.statusId));
}
if (nextProps.params.statusId && nextProps.ancestorsIds.size > this.props.ancestorsIds.size) {
this._scrolledIntoView = false;
}
if (nextProps.status && nextProps.status.get('id') !== this.state.loadedStatusId) {
this.setState({ showMedia: defaultMediaVisibility(nextProps.status), loadedStatusId: nextProps.status.get('id') });
}
@ -229,7 +233,7 @@ class Status extends ImmutablePureComponent {
handleToggleMediaVisibility = () => {
this.setState({ showMedia: !this.state.showMedia });
}
};
handleFavouriteClick = (status) => {
const { dispatch } = this.props;
@ -248,7 +252,7 @@ class Status extends ImmutablePureComponent {
url: status.get('url'),
}));
}
}
};
handlePin = (status) => {
if (status.get('pinned')) {
@ -256,7 +260,7 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(pin(status));
}
}
};
handleReplyClick = (status) => {
const { askReplyConfirmation, dispatch, intl } = this.props;
@ -279,11 +283,11 @@ class Status extends ImmutablePureComponent {
url: status.get('url'),
}));
}
}
};
handleModalReblog = (status, privacy) => {
this.props.dispatch(reblog(status, privacy));
}
};
handleReblogClick = (status, e) => {
const { dispatch } = this.props;
@ -306,7 +310,7 @@ class Status extends ImmutablePureComponent {
url: status.get('url'),
}));
}
}
};
handleBookmarkClick = (status) => {
if (status.get('bookmarked')) {
@ -314,7 +318,7 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(bookmark(status));
}
}
};
handleDeleteClick = (status, history, withRedraft = false) => {
const { dispatch, intl } = this.props;
@ -328,27 +332,27 @@ class Status extends ImmutablePureComponent {
onConfirm: () => dispatch(deleteStatus(status.get('id'), history, withRedraft)),
}));
}
}
};
handleEditClick = (status, history) => {
this.props.dispatch(editStatus(status.get('id'), history));
}
};
handleDirectClick = (account, router) => {
this.props.dispatch(directCompose(account, router));
}
};
handleMentionClick = (account, router) => {
this.props.dispatch(mentionCompose(account, router));
}
};
handleOpenMedia = (media, index) => {
this.props.dispatch(openModal('MEDIA', { statusId: this.props.status.get('id'), media, index }));
}
};
handleOpenVideo = (media, options) => {
this.props.dispatch(openModal('VIDEO', { statusId: this.props.status.get('id'), media, options }));
}
};
handleHotkeyOpenMedia = e => {
const { status } = this.props;
@ -362,11 +366,11 @@ class Status extends ImmutablePureComponent {
this.handleOpenMedia(status.get('media_attachments'), 0);
}
}
}
};
handleMuteClick = (account) => {
this.props.dispatch(initMuteModal(account));
}
};
handleConversationMuteClick = (status) => {
if (status.get('muted')) {
@ -374,7 +378,7 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(muteStatus(status.get('id')));
}
}
};
handleToggleHidden = (status) => {
if (status.get('hidden')) {
@ -382,7 +386,7 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(hideStatus(status.get('id')));
}
}
};
handleToggleAll = () => {
const { status, ancestorsIds, descendantsIds } = this.props;
@ -393,7 +397,7 @@ class Status extends ImmutablePureComponent {
} else {
this.props.dispatch(hideStatus(statusIds));
}
}
};
handleTranslate = status => {
const { dispatch } = this.props;
@ -403,29 +407,29 @@ class Status extends ImmutablePureComponent {
} else {
dispatch(translateStatus(status.get('id')));
}
}
};
handleBlockClick = (status) => {
const { dispatch } = this.props;
const account = status.get('account');
dispatch(initBlockModal(account));
}
};
handleReport = (status) => {
this.props.dispatch(initReport(status.get('account'), status));
}
};
handleEmbed = (status) => {
this.props.dispatch(openModal('EMBED', { url: status.get('url') }));
}
};
handleUnmuteClick = account => {
this.props.dispatch(unmuteAccount(account.get('id')));
}
};
handleUnblockClick = account => {
this.props.dispatch(unblockAccount(account.get('id')));
}
};
handleBlockDomainClick = domain => {
this.props.dispatch(openModal('CONFIRM', {
@ -433,50 +437,50 @@ class Status extends ImmutablePureComponent {
confirm: this.props.intl.formatMessage(messages.blockDomainConfirm),
onConfirm: () => this.props.dispatch(blockDomain(domain)),
}));
}
};
handleUnblockDomainClick = domain => {
this.props.dispatch(unblockDomain(domain));
}
};
handleHotkeyMoveUp = () => {
this.handleMoveUp(this.props.status.get('id'));
}
};
handleHotkeyMoveDown = () => {
this.handleMoveDown(this.props.status.get('id'));
}
};
handleHotkeyReply = e => {
e.preventDefault();
this.handleReplyClick(this.props.status);
}
};
handleHotkeyFavourite = () => {
this.handleFavouriteClick(this.props.status);
}
};
handleHotkeyBoost = () => {
this.handleReblogClick(this.props.status);
}
};
handleHotkeyMention = e => {
e.preventDefault();
this.handleMentionClick(this.props.status.get('account'));
}
};
handleHotkeyOpenProfile = () => {
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
}
};
handleHotkeyToggleHidden = () => {
this.handleToggleHidden(this.props.status);
}
};
handleHotkeyToggleSensitive = () => {
this.handleToggleMediaVisibility();
}
};
handleMoveUp = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
@ -493,7 +497,7 @@ class Status extends ImmutablePureComponent {
this._selectChild(index - 1, true);
}
}
}
};
handleMoveDown = id => {
const { status, ancestorsIds, descendantsIds } = this.props;
@ -510,7 +514,7 @@ class Status extends ImmutablePureComponent {
this._selectChild(index + 1, false);
}
}
}
};
_selectChild (index, align_top) {
const container = this.node;
@ -540,7 +544,7 @@ class Status extends ImmutablePureComponent {
setRef = c => {
this.node = c;
}
};
componentDidUpdate () {
if (this._scrolledIntoView) {
@ -565,7 +569,7 @@ class Status extends ImmutablePureComponent {
onFullScreenChange = () => {
this.setState({ fullscreen: isFullscreen() });
}
};
render () {
let ancestors, descendants;

View file

@ -72,7 +72,7 @@ class SubscribedLanguagesModal extends ImmutablePureComponent {
handleSubmit = () => {
this.props.onSubmit(this.state.selectedLanguages.toArray());
this.props.onClose();
}
};
renderItem (value) {
const language = this.props.languages.find(language => language[0] === value);

View file

@ -31,7 +31,7 @@ export default class ActionsModal extends ImmutablePureComponent {
</a>
</li>
);
}
};
render () {
return (

View file

@ -55,20 +55,20 @@ class BlockModal extends React.PureComponent {
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account);
}
};
handleSecondary = () => {
this.props.onClose();
this.props.onBlockAndReport(this.props.account);
}
};
handleCancel = () => {
this.props.onClose();
}
};
setRef = (c) => {
this.button = c;
}
};
render () {
const { account } = this.props;

View file

@ -62,7 +62,7 @@ class BoostModal extends ImmutablePureComponent {
handleReblog = () => {
this.props.onReblog(this.props.status, this.props.privacy);
this.props.onClose();
}
};
handleAccountClick = (e) => {
if (e.button === 0 && !(e.ctrlKey || e.metaKey)) {
@ -70,7 +70,7 @@ class BoostModal extends ImmutablePureComponent {
this.props.onClose();
this.context.router.history.push(`/@${this.props.status.getIn(['account', 'acct'])}`);
}
}
};
_findContainer = () => {
return document.getElementsByClassName('modal-root__container')[0];
@ -78,7 +78,7 @@ class BoostModal extends ImmutablePureComponent {
setRef = (c) => {
this.button = c;
}
};
render () {
const { status, privacy, intl } = this.props;

View file

@ -15,7 +15,7 @@ class Bundle extends React.PureComponent {
onFetch: PropTypes.func,
onFetchSuccess: PropTypes.func,
onFetchFail: PropTypes.func,
}
};
static defaultProps = {
loading: emptyComponent,
@ -24,14 +24,14 @@ class Bundle extends React.PureComponent {
onFetch: noop,
onFetchSuccess: noop,
onFetchFail: noop,
}
};
static cache = new Map
static cache = new Map;
state = {
mod: undefined,
forceRender: false,
}
};
componentWillMount() {
this.load(this.props);
@ -83,7 +83,7 @@ class Bundle extends React.PureComponent {
this.setState({ mod: null });
onFetchFail(error);
});
}
};
render() {
const { loading: Loading, error: Error, children, renderDelay } = this.props;

View file

@ -31,7 +31,7 @@ class GIF extends React.PureComponent {
if (!animate) {
this.setState({ hovering: true });
}
}
};
handleMouseLeave = () => {
const { animate } = this.props;
@ -39,7 +39,7 @@ class GIF extends React.PureComponent {
if (!animate) {
this.setState({ hovering: false });
}
}
};
render () {
const { src, staticSrc, className, animate } = this.props;
@ -75,7 +75,7 @@ class CopyButton extends React.PureComponent {
navigator.clipboard.writeText(value);
this.setState({ copied: true });
this.timeout = setTimeout(() => this.setState({ copied: false }), 700);
}
};
componentWillUnmount () {
if (this.timeout) clearTimeout(this.timeout);
@ -113,7 +113,7 @@ class BundleColumnError extends React.PureComponent {
if (onRetry) {
onRetry();
}
}
};
render () {
const { errorType, multiColumn, stacktrace } = this.props;

View file

@ -16,11 +16,11 @@ class BundleModalError extends React.PureComponent {
onRetry: PropTypes.func.isRequired,
onClose: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}
};
handleRetry = () => {
this.props.onRetry();
}
};
render () {
const { onClose, intl: { formatMessage } } = this.props;

View file

@ -23,7 +23,7 @@ export default class Column extends React.PureComponent {
}
this._interruptScrollAnimation = scrollTop(scrollable);
}
};
scrollTop () {
const scrollable = this.node.querySelector('.scrollable');
@ -40,11 +40,11 @@ export default class Column extends React.PureComponent {
if (typeof this._interruptScrollAnimation !== 'undefined') {
this._interruptScrollAnimation();
}
}, 200)
}, 200);
setRef = (c) => {
this.node = c;
}
};
render () {
const { heading, icon, children, active, hideHeadingOnMobile } = this.props;

View file

@ -15,7 +15,7 @@ export default class ColumnHeader extends React.PureComponent {
handleClick = () => {
this.props.onClick();
}
};
render () {
const { icon, type, active, columnHeaderId } = this.props;

View file

@ -57,7 +57,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
state = {
renderComposePanel: !(this.mediaQuery && this.mediaQuery.matches),
}
};
componentDidMount() {
if (!this.props.singleColumn) {
@ -97,7 +97,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);
}
}
}
@ -111,7 +111,7 @@ export default class ColumnsArea extends ImmutablePureComponent {
handleLayoutChange = (e) => {
this.setState({ renderComposePanel: !e.matches });
}
};
handleWheel = () => {
if (typeof this._interruptScrollAnimation !== 'function') {
@ -119,19 +119,19 @@ export default class ColumnsArea extends ImmutablePureComponent {
}
this._interruptScrollAnimation();
}
};
setRef = (node) => {
this.node = node;
}
};
renderLoading = columnId => () => {
return columnId === 'COMPOSE' ? <DrawerLoading /> : <ColumnLoading multiColumn />;
}
};
renderError = (props) => {
return <BundleColumnError multiColumn errorType='network' {...props} />;
}
};
render () {
const { columns, children, singleColumn, isModalOpen } = this.props;

View file

@ -22,12 +22,12 @@ class ComposePanel extends React.PureComponent {
onFocus = () => {
const { dispatch } = this.props;
dispatch(changeComposing(true));
}
};
onBlur = () => {
const { dispatch } = this.props;
dispatch(changeComposing(false));
}
};
componentDidMount () {
const { dispatch } = this.props;

View file

@ -30,20 +30,20 @@ class ConfirmationModal extends React.PureComponent {
this.props.onClose();
}
this.props.onConfirm();
}
};
handleSecondary = () => {
this.props.onClose();
this.props.onSecondary();
}
};
handleCancel = () => {
this.props.onClose();
}
};
setRef = (c) => {
this.button = c;
}
};
render () {
const { message, confirm, secondary } = this.props;

View file

@ -46,7 +46,7 @@ class DisabledAccountBanner extends React.PureComponent {
this.props.onLogout();
return false;
}
};
render () {
const { disabledAcct, movedToAcct } = this.props;
@ -89,4 +89,4 @@ class DisabledAccountBanner extends React.PureComponent {
);
}
};
}

View file

@ -17,7 +17,7 @@ class EmbedModal extends ImmutablePureComponent {
onClose: PropTypes.func.isRequired,
onError: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
}
};
state = {
loading: false,
@ -48,11 +48,11 @@ class EmbedModal extends ImmutablePureComponent {
setIframeRef = c => {
this.iframe = c;
}
};
handleTextareaClick = (e) => {
e.target.select();
}
};
render () {
const { intl, onClose } = this.props;

View file

@ -39,6 +39,7 @@ const mapStateToProps = (state, { id }) => ({
account: state.getIn(['accounts', me]),
isUploadingThumbnail: state.getIn(['compose', 'isUploadingThumbnail']),
description: state.getIn(['compose', 'media_modal', 'description']),
lang: state.getIn(['compose', 'language']),
focusX: state.getIn(['compose', 'media_modal', 'focusX']),
focusY: state.getIn(['compose', 'media_modal', 'focusY']),
dirty: state.getIn(['compose', 'media_modal', 'dirty']),
@ -134,7 +135,7 @@ class FocalPointModal extends ImmutablePureComponent {
this.updatePosition(e);
this.setState({ dragging: true });
}
};
handleTouchStart = e => {
document.addEventListener('touchmove', this.handleMouseMove);
@ -142,25 +143,25 @@ class FocalPointModal extends ImmutablePureComponent {
this.updatePosition(e);
this.setState({ dragging: true });
}
};
handleMouseMove = e => {
this.updatePosition(e);
}
};
handleMouseUp = () => {
document.removeEventListener('mousemove', this.handleMouseMove);
document.removeEventListener('mouseup', this.handleMouseUp);
this.setState({ dragging: false });
}
};
handleTouchEnd = () => {
document.removeEventListener('touchmove', this.handleMouseMove);
document.removeEventListener('touchend', this.handleTouchEnd);
this.setState({ dragging: false });
}
};
updatePosition = e => {
const { x, y } = getPointerPosition(this.node, e);
@ -168,24 +169,24 @@ class FocalPointModal extends ImmutablePureComponent {
const focusY = (y - .5) * -2;
this.props.onChangeFocus(focusX, focusY);
}
};
handleChange = e => {
this.props.onChangeDescription(e.target.value);
}
};
handleKeyDown = (e) => {
if (e.keyCode === 13 && (e.ctrlKey || e.metaKey)) {
this.props.onChangeDescription(e.target.value);
this.handleSubmit(e);
}
}
};
handleSubmit = (e) => {
e.preventDefault();
e.stopPropagation();
this.props.onSave(this.props.description, this.props.focusX, this.props.focusY);
}
};
getCloseConfirmationMessage = () => {
const { intl, dirty } = this.props;
@ -198,15 +199,15 @@ class FocalPointModal extends ImmutablePureComponent {
} else {
return null;
}
}
};
setRef = c => {
this.node = c;
}
};
handleTextDetection = () => {
this._detectText();
}
};
_detectText = (refreshCache = false) => {
const { media } = this.props;
@ -257,24 +258,24 @@ class FocalPointModal extends ImmutablePureComponent {
console.error(e);
this.setState({ detecting: false });
});
}
};
handleThumbnailChange = e => {
if (e.target.files.length > 0) {
this.props.onSelectThumbnail(e.target.files);
}
}
};
setFileInputRef = c => {
this.fileInput = c;
}
};
handleFileInputClick = () => {
this.fileInput.click();
}
};
render () {
const { media, intl, account, onClose, isUploadingThumbnail, description, focusX, focusY, dirty, is_changing_upload } = this.props;
const { media, intl, account, onClose, isUploadingThumbnail, description, lang, focusX, focusY, dirty, is_changing_upload } = this.props;
const { dragging, detecting, progress, ocrStatus } = this.state;
const x = (focusX / 2) + .5;
const y = (focusY / -2) + .5;
@ -291,11 +292,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 = '';
@ -320,7 +321,7 @@ class FocalPointModal extends ImmutablePureComponent {
<React.Fragment>
<label className='setting-text-label' htmlFor='upload-modal__thumbnail'><FormattedMessage id='upload_form.thumbnail' defaultMessage='Change thumbnail' /></label>
<Button disabled={isUploadingThumbnail} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
<Button disabled={isUploadingThumbnail || !media.get('unattached')} text={intl.formatMessage(messages.chooseImage)} onClick={this.handleFileInputClick} />
<label>
<span style={{ display: 'none' }}>{intl.formatMessage(messages.chooseImage)}</span>
@ -349,6 +350,7 @@ class FocalPointModal extends ImmutablePureComponent {
id='upload-modal__description'
className='setting-text light'
value={detecting ? '…' : description}
lang={lang}
onChange={this.handleChange}
onKeyDown={this.handleKeyDown}
disabled={detecting || is_changing_upload}

View file

@ -6,6 +6,7 @@ import { registrationsOpen, me } from 'mastodon/initial_state';
import Avatar from 'mastodon/components/avatar';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { openModal } from 'mastodon/actions/modal';
const Account = connect(state => ({
account: state.getIn(['accounts', me]),
@ -15,7 +16,14 @@ const Account = connect(state => ({
</Link>
));
const mapDispatchToProps = (dispatch) => ({
openClosedRegistrationsModal() {
dispatch(openModal('CLOSED_REGISTRATIONS'));
},
});
export default @withRouter
@connect(null, mapDispatchToProps)
class Header extends React.PureComponent {
static contextTypes = {
@ -23,27 +31,44 @@ class Header extends React.PureComponent {
};
static propTypes = {
openClosedRegistrationsModal: PropTypes.func,
location: PropTypes.object,
};
render () {
const { signedIn } = this.context.identity;
const { location } = this.props;
const { location, openClosedRegistrationsModal } = this.props;
let content;
if (signedIn) {
content = (
<>
{location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish' defaultMessage='Publish' /></Link>}
{location.pathname !== '/publish' && <Link to='/publish' className='button'><FormattedMessage id='compose_form.publish_form' defaultMessage='Publish' /></Link>}
<Account />
</>
);
} else {
let signupButton;
if (registrationsOpen) {
signupButton = (
<a href='/auth/sign_up' className='button button-tertiary'>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</a>
);
} else {
signupButton = (
<button className='button button-tertiary' onClick={openClosedRegistrationsModal}>
<FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' />
</button>
);
}
content = (
<>
<a href='/auth/sign_in' className='button'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
<a href={registrationsOpen ? '/auth/sign_up' : 'https://joinmastodon.org/servers'} className='button button-tertiary'><FormattedMessage id='sign_in_banner.create_account' defaultMessage='Create account' /></a>
{signupButton}
</>
);
}

View file

@ -14,7 +14,7 @@ export default class ImageLoader extends PureComponent {
height: PropTypes.number,
onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool,
}
};
static defaultProps = {
alt: '',
@ -26,7 +26,7 @@ export default class ImageLoader extends PureComponent {
loading: true,
error: false,
width: null,
}
};
removers = [];
canvas = null;
@ -86,7 +86,7 @@ export default class ImageLoader extends PureComponent {
image.addEventListener('load', handleLoad);
image.src = previewSrc;
this.removers.push(removeEventListeners);
})
});
clearPreviewCanvas () {
const { width, height } = this.canvas;
@ -126,7 +126,7 @@ export default class ImageLoader extends PureComponent {
setCanvasRef = c => {
this.canvas = c;
if (c) this.setState({ width: c.offsetWidth });
}
};
render () {
const { alt, src, width, height, onClick } = this.props;

View file

@ -3,7 +3,7 @@ import React from 'react';
import PropTypes from 'prop-types';
import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { domain, version, source_url, profile_directory as profileDirectory } from 'mastodon/initial_state';
import { domain, version, source_url, statusPageUrl, profile_directory as profileDirectory } from 'mastodon/initial_state';
import { logOut } from 'mastodon/utils/log_out';
import { openModal } from 'mastodon/actions/modal';
import { PERMISSION_INVITE_USERS } from 'mastodon/permissions';
@ -44,7 +44,7 @@ class LinkFooter extends React.PureComponent {
this.props.onLogout();
return false;
}
};
render () {
const { signedIn, permissions } = this.context.identity;
@ -52,43 +52,51 @@ 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>
<strong>{domain}</strong>:
{' '}
<Link key='about' to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
<Link to='/about'><FormattedMessage id='footer.about' defaultMessage='About' /></Link>
{statusPageUrl && (
<>
{DividingCircle}
<a href={statusPageUrl} target='_blank' rel='noopener'><FormattedMessage id='footer.status' defaultMessage='Status' /></a>
</>
)}
{canInvite && (
<>
{' · '}
<a key='invites' href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
{DividingCircle}
<a href='/invites' target='_blank'><FormattedMessage id='footer.invite' defaultMessage='Invite people' /></a>
</>
)}
{canProfileDirectory && (
<>
{' · '}
<Link key='directory' to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
{DividingCircle}
<Link to='/directory'><FormattedMessage id='footer.directory' defaultMessage='Profiles directory' /></Link>
</>
)}
{' · '}
<Link key='privacy-policy' to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
{DividingCircle}
<Link to='/privacy-policy'><FormattedMessage id='footer.privacy_policy' defaultMessage='Privacy policy' /></Link>
</p>
<p>
<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>
);
}
};
}

View file

@ -43,27 +43,27 @@ class MediaModal extends ImmutablePureComponent {
handleSwipe = (index) => {
this.setState({ index: index % this.props.media.size });
}
};
handleTransitionEnd = () => {
this.setState({
zoomButtonHidden: false,
});
}
};
handleNextClick = () => {
this.setState({
index: (this.getIndex() + 1) % this.props.media.size,
zoomButtonHidden: true,
});
}
};
handlePrevClick = () => {
this.setState({
index: (this.props.media.size + this.getIndex() - 1) % this.props.media.size,
zoomButtonHidden: true,
});
}
};
handleChangeIndex = (e) => {
const index = Number(e.currentTarget.getAttribute('data-index'));
@ -72,7 +72,7 @@ class MediaModal extends ImmutablePureComponent {
index: index % this.props.media.size,
zoomButtonHidden: true,
});
}
};
handleKeyDown = (e) => {
switch(e.key) {
@ -87,7 +87,7 @@ class MediaModal extends ImmutablePureComponent {
e.stopPropagation();
break;
}
}
};
componentDidMount () {
window.addEventListener('keydown', this.handleKeyDown, false);

View file

@ -79,17 +79,17 @@ export default class ModalRoot extends React.PureComponent {
setBackgroundColor = color => {
this.setState({ backgroundColor: color });
}
};
renderLoading = modalId => () => {
return ['MEDIA', 'VIDEO', 'BOOST', 'CONFIRM', 'ACTIONS'].indexOf(modalId) === -1 ? <ModalLoading /> : null;
}
};
renderError = (props) => {
const { onClose } = this.props;
return <BundleModalError {...props} onClose={onClose} />;
}
};
handleClose = (ignoreFocus = false) => {
const { onClose } = this.props;
@ -102,11 +102,11 @@ export default class ModalRoot extends React.PureComponent {
// This would be much smoother with react-intl 3+ and `forwardRef`.
}
onClose(message, ignoreFocus);
}
};
setModalRef = (c) => {
this._modal = c;
}
};
render () {
const { type, props, ignoreFocus } = this.props;

View file

@ -65,23 +65,23 @@ class MuteModal extends React.PureComponent {
handleClick = () => {
this.props.onClose();
this.props.onConfirm(this.props.account, this.props.notifications, this.props.muteDuration);
}
};
handleCancel = () => {
this.props.onClose();
}
};
setRef = (c) => {
this.button = c;
}
};
toggleNotifications = () => {
this.props.onToggleNotifications();
}
};
changeMuteDuration = (e) => {
this.props.onChangeMuteDuration(e);
}
};
render () {
const { account, notifications, muteDuration, intl } = this.props;

View file

@ -82,8 +82,8 @@ class NavigationPanel extends React.Component {
{signedIn && (
<React.Fragment>
<ColumnLink transparent to='/conversations' icon='at' text={intl.formatMessage(messages.direct)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/bookmarks' icon='bookmark' text={intl.formatMessage(messages.bookmarks)} />
<ColumnLink transparent to='/favourites' icon='star' text={intl.formatMessage(messages.favourites)} />
<ColumnLink transparent to='/lists' icon='list-ul' text={intl.formatMessage(messages.lists)} />
<ListPanel />

View file

@ -95,7 +95,7 @@ class ReportModal extends ImmutablePureComponent {
} else {
this.setState({ selectedRuleIds: selectedRuleIds.remove(ruleId) });
}
}
};
handleChangeCategory = category => {
this.setState({ category });

View file

@ -30,7 +30,7 @@ const SignInBanner = () => {
return (
<div className='sign-in-banner'>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts, or interact from your account on a different server.' /></p>
<p><FormattedMessage id='sign_in_banner.text' defaultMessage='Sign in to follow profiles or hashtags, favourite, share and reply to posts. You can also interact from your account on a different server.' /></p>
<a href='/auth/sign_in' className='button button--block'><FormattedMessage id='sign_in_banner.sign_in' defaultMessage='Sign in' /></a>
{signupButton}
</div>

View file

@ -22,7 +22,7 @@ export default class UploadArea extends React.PureComponent {
break;
}
}
}
};
componentDidMount () {
window.addEventListener('keyup', this.handleKeyUp, false);

View file

@ -46,6 +46,7 @@ export default class VideoModal extends ImmutablePureComponent {
autoPlay={options.autoPlay}
volume={options.defaultVolume}
onCloseVideo={onClose}
autoFocus
detailed
alt={media.get('description')}
/>

View file

@ -102,7 +102,7 @@ class ZoomableImage extends React.PureComponent {
onClick: PropTypes.func,
zoomButtonHidden: PropTypes.bool,
intl: PropTypes.object.isRequired,
}
};
static defaultProps = {
alt: '',
@ -132,7 +132,7 @@ class ZoomableImage extends React.PureComponent {
dragged: false,
lockScroll: { x: 0, y: 0 },
lockTranslate: { x: 0, y: 0 },
}
};
removers = [];
container = null;
@ -212,7 +212,7 @@ class ZoomableImage extends React.PureComponent {
// lock horizontal scroll
this.container.scrollLeft = Math.max(this.container.scrollLeft + event.pixelX, this.state.lockScroll.x);
}
};
mouseDownHandler = e => {
this.container.style.cursor = 'grabbing';
@ -228,7 +228,7 @@ class ZoomableImage extends React.PureComponent {
this.image.addEventListener('mousemove', this.mouseMoveHandler);
this.image.addEventListener('mouseup', this.mouseUpHandler);
}
};
mouseMoveHandler = e => {
const dx = e.clientX - this.state.dragPosition.x;
@ -238,7 +238,7 @@ class ZoomableImage extends React.PureComponent {
this.container.scrollTop = Math.max(this.state.dragPosition.top - dy, this.state.lockScroll.y);
this.setState({ dragged: true });
}
};
mouseUpHandler = () => {
this.container.style.cursor = 'grab';
@ -246,13 +246,13 @@ class ZoomableImage extends React.PureComponent {
this.image.removeEventListener('mousemove', this.mouseMoveHandler);
this.image.removeEventListener('mouseup', this.mouseUpHandler);
}
};
handleTouchStart = e => {
if (e.touches.length !== 2) return;
this.lastDistance = getDistance(...e.touches);
}
};
handleTouchMove = e => {
const { scrollTop, scrollHeight, clientHeight } = this.container;
@ -275,7 +275,7 @@ class ZoomableImage extends React.PureComponent {
this.lastMidpoint = midpoint;
this.lastDistance = distance;
}
};
zoom(nextScale, midpoint) {
const { scale, zoomMatrix } = this.state;
@ -314,11 +314,11 @@ class ZoomableImage extends React.PureComponent {
const handler = this.props.onClick;
if (handler) handler();
this.setState({ navigationHidden: !this.state.navigationHidden });
}
};
handleMouseDown = e => {
e.preventDefault();
}
};
initZoomMatrix = () => {
const { width, height } = this.props;
@ -350,7 +350,7 @@ class ZoomableImage extends React.PureComponent {
translateY: translateY,
},
});
}
};
handleZoomClick = e => {
e.preventDefault();
@ -392,15 +392,15 @@ class ZoomableImage extends React.PureComponent {
this.container.style.cursor = 'grab';
this.container.style.removeProperty('user-select');
}
};
setContainerRef = c => {
this.container = c;
}
};
setImageRef = c => {
this.image = c;
}
};
render () {
const { alt, src, width, height, intl } = this.props;

Some files were not shown because too many files have changed in this diff Show more