diff --git a/app/javascript/mastodon/components/status_content.js b/app/javascript/mastodon/components/status_content.js
index 6aa0bfcc2..c171e7a66 100644
--- a/app/javascript/mastodon/components/status_content.js
+++ b/app/javascript/mastodon/components/status_content.js
@@ -230,7 +230,7 @@ export default class StatusContent extends React.PureComponent {
);
} else if (this.props.onClick) {
const output = [
-
+
{!!status.get('poll') &&
}
diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js
index 8fddb6f54..db340032a 100644
--- a/app/javascript/mastodon/containers/media_container.js
+++ b/app/javascript/mastodon/containers/media_container.js
@@ -8,6 +8,7 @@ import Video from '../features/video';
import Card from '../features/status/components/card';
import Poll from 'mastodon/components/poll';
import Hashtag from 'mastodon/components/hashtag';
+import Audio from 'mastodon/features/audio';
import ModalRoot from '../components/modal_root';
import { getScrollbarWidth } from '../features/ui/components/modal_root';
import MediaModal from '../features/ui/components/media_modal';
@@ -16,7 +17,7 @@ import { List as ImmutableList, fromJS } from 'immutable';
const { localeData, messages } = getLocale();
addLocaleData(localeData);
-const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag };
+const MEDIA_COMPONENTS = { MediaGallery, Video, Card, Poll, Hashtag, Audio };
export default class MediaContainer extends PureComponent {
diff --git a/app/javascript/mastodon/features/account_gallery/components/media_item.js b/app/javascript/mastodon/features/account_gallery/components/media_item.js
index 2609b96ff..d1b3c3bd4 100644
--- a/app/javascript/mastodon/features/account_gallery/components/media_item.js
+++ b/app/javascript/mastodon/features/account_gallery/components/media_item.js
@@ -96,6 +96,12 @@ export default class MediaItem extends ImmutablePureComponent {
if (attachment.get('type') === 'unknown') {
// Skip
+ } else if (attachment.get('type') === 'audio') {
+ thumbnail = (
+
+
+
+ );
} else if (attachment.get('type') === 'image') {
const focusX = attachment.getIn(['meta', 'focus', 'x']) || 0;
const focusY = attachment.getIn(['meta', 'focus', 'y']) || 0;
diff --git a/app/javascript/mastodon/features/account_gallery/index.js b/app/javascript/mastodon/features/account_gallery/index.js
index f1a665d8f..f3bf7a2bd 100644
--- a/app/javascript/mastodon/features/account_gallery/index.js
+++ b/app/javascript/mastodon/features/account_gallery/index.js
@@ -100,7 +100,7 @@ class AccountGallery extends ImmutablePureComponent {
}
handleOpenMedia = attachment => {
- if (attachment.get('type') === 'video') {
+ if (['video', 'audio'].includes(attachment.get('type'))) {
this.props.dispatch(openModal('VIDEO', { media: attachment, status: attachment.get('status') }));
} else {
const media = attachment.getIn(['status', 'media_attachments']);
diff --git a/app/javascript/mastodon/features/audio/index.js b/app/javascript/mastodon/features/audio/index.js
new file mode 100644
index 000000000..95e5675f3
--- /dev/null
+++ b/app/javascript/mastodon/features/audio/index.js
@@ -0,0 +1,226 @@
+import React from 'react';
+import PropTypes from 'prop-types';
+import WaveSurfer from 'wavesurfer.js';
+import { defineMessages, injectIntl } from 'react-intl';
+import { formatTime } from 'mastodon/features/video';
+import Icon from 'mastodon/components/icon';
+import classNames from 'classnames';
+import { throttle } from 'lodash';
+
+const messages = defineMessages({
+ play: { id: 'video.play', defaultMessage: 'Play' },
+ pause: { id: 'video.pause', defaultMessage: 'Pause' },
+ mute: { id: 'video.mute', defaultMessage: 'Mute sound' },
+ unmute: { id: 'video.unmute', defaultMessage: 'Unmute sound' },
+});
+
+export default @injectIntl
+class Audio extends React.PureComponent {
+
+ static propTypes = {
+ src: PropTypes.string.isRequired,
+ alt: PropTypes.string,
+ duration: PropTypes.number,
+ peaks: PropTypes.arrayOf(PropTypes.number),
+ height: PropTypes.number,
+ preload: PropTypes.bool,
+ editable: PropTypes.bool,
+ intl: PropTypes.object.isRequired,
+ };
+
+ state = {
+ currentTime: 0,
+ duration: null,
+ paused: true,
+ muted: false,
+ volume: 0.5,
+ };
+
+ // hard coded in components.scss
+ // any way to get ::before values programatically?
+
+ volWidth = 50;
+
+ volOffset = 70;
+
+ volHandleOffset = v => {
+ const offset = v * this.volWidth + this.volOffset;
+ return (offset > 110) ? 110 : offset;
+ }
+
+ setVolumeRef = c => {
+ this.volume = c;
+ }
+
+ setWaveformRef = c => {
+ this.waveform = c;
+ }
+
+ componentDidMount () {
+ if (this.waveform) {
+ this._updateWaveform();
+ }
+ }
+
+ componentDidUpdate (prevProps) {
+ if (this.waveform && prevProps.src !== this.props.src) {
+ this._updateWaveform();
+ }
+ }
+
+ componentWillUnmount () {
+ if (this.wavesurfer) {
+ this.wavesurfer.destroy();
+ this.wavesurfer = null;
+ }
+ }
+
+ _updateWaveform () {
+ const { src, height, duration, peaks, preload } = this.props;
+
+ const progressColor = window.getComputedStyle(document.querySelector('.audio-player__progress-placeholder')).getPropertyValue('background-color');
+ const waveColor = window.getComputedStyle(document.querySelector('.audio-player__wave-placeholder')).getPropertyValue('background-color');
+
+ if (this.wavesurfer) {
+ this.wavesurfer.destroy();
+ this.loaded = false;
+ }
+
+ const wavesurfer = WaveSurfer.create({
+ container: this.waveform,
+ height,
+ barWidth: 3,
+ cursorWidth: 0,
+ progressColor,
+ waveColor,
+ backend: 'MediaElement',
+ interact: preload,
+ });
+
+ wavesurfer.setVolume(this.state.volume);
+
+ if (preload) {
+ wavesurfer.load(src);
+ this.loaded = true;
+ } else {
+ wavesurfer.load(src, peaks, 'none', duration);
+ this.loaded = false;
+ }
+
+ wavesurfer.on('ready', () => this.setState({ duration: Math.floor(wavesurfer.getDuration()) }));
+ wavesurfer.on('audioprocess', () => this.setState({ currentTime: Math.floor(wavesurfer.getCurrentTime()) }));
+ wavesurfer.on('pause', () => this.setState({ paused: true }));
+ wavesurfer.on('play', () => this.setState({ paused: false }));
+ wavesurfer.on('volume', volume => this.setState({ volume }));
+ wavesurfer.on('mute', muted => this.setState({ muted }));
+
+ this.wavesurfer = wavesurfer;
+ }
+
+ togglePlay = () => {
+ if (this.state.paused) {
+ if (!this.props.preload && !this.loaded) {
+ this.wavesurfer.createBackend();
+ this.wavesurfer.createPeakCache();
+ this.wavesurfer.load(this.props.src);
+ this.wavesurfer.toggleInteraction();
+ this.loaded = true;
+ }
+
+ this.wavesurfer.play();
+ this.setState({ paused: false });
+ } else {
+ this.wavesurfer.pause();
+ this.setState({ paused: true });
+ }
+ }
+
+ toggleMute = () => {
+ this.wavesurfer.setMute(!this.state.muted);
+ }
+
+ handleVolumeMouseDown = e => {
+ document.addEventListener('mousemove', this.handleMouseVolSlide, true);
+ document.addEventListener('mouseup', this.handleVolumeMouseUp, true);
+ document.addEventListener('touchmove', this.handleMouseVolSlide, true);
+ document.addEventListener('touchend', this.handleVolumeMouseUp, true);
+
+ this.handleMouseVolSlide(e);
+
+ 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);
+ }
+
+ handleMouseVolSlide = throttle(e => {
+ const rect = this.volume.getBoundingClientRect();
+ const x = (e.clientX - rect.left) / this.volWidth; // x position within the element.
+
+ if(!isNaN(x)) {
+ let slideamt = x;
+
+ if (x > 1) {
+ slideamt = 1;
+ } else if(x < 0) {
+ slideamt = 0;
+ }
+
+ this.wavesurfer.setVolume(slideamt);
+ }
+ }, 60);
+
+ render () {
+ const { height, intl, alt, editable } = this.props;
+ const { paused, muted, volume, currentTime } = this.state;
+
+ const volumeWidth = muted ? 0 : volume * this.volWidth;
+ const volumeHandleLoc = muted ? this.volHandleOffset(0) : this.volHandleOffset(volume);
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {formatTime(currentTime)}
+ /
+ {formatTime(this.state.duration || Math.floor(this.props.duration))}
+
+
+
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/compose/components/action_bar.js b/app/javascript/mastodon/features/compose/components/action_bar.js
index d0303dbfb..dd2632796 100644
--- a/app/javascript/mastodon/features/compose/components/action_bar.js
+++ b/app/javascript/mastodon/features/compose/components/action_bar.js
@@ -23,9 +23,14 @@ class ActionBar extends React.PureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
+ onLogout: PropTypes.func.isRequired,
intl: PropTypes.object.isRequired,
};
+ handleLogout = () => {
+ this.props.onLogout();
+ }
+
render () {
const { intl } = this.props;
@@ -44,7 +49,7 @@ class ActionBar extends React.PureComponent {
menu.push({ text: intl.formatMessage(messages.domain_blocks), to: '/domain_blocks' });
menu.push({ text: intl.formatMessage(messages.filters), href: '/filters' });
menu.push(null);
- menu.push({ text: intl.formatMessage(messages.logout), href: '/auth/sign_out', target: null, method: 'delete' });
+ menu.push({ text: intl.formatMessage(messages.logout), action: this.handleLogout });
return (
diff --git a/app/javascript/mastodon/features/compose/components/navigation_bar.js b/app/javascript/mastodon/features/compose/components/navigation_bar.js
index d8d49cb95..840d0a3da 100644
--- a/app/javascript/mastodon/features/compose/components/navigation_bar.js
+++ b/app/javascript/mastodon/features/compose/components/navigation_bar.js
@@ -12,6 +12,7 @@ export default class NavigationBar extends ImmutablePureComponent {
static propTypes = {
account: ImmutablePropTypes.map.isRequired,
+ onLogout: PropTypes.func.isRequired,
onClose: PropTypes.func,
};
@@ -33,7 +34,7 @@ export default class NavigationBar extends ImmutablePureComponent {
);
diff --git a/app/javascript/mastodon/features/compose/containers/navigation_container.js b/app/javascript/mastodon/features/compose/containers/navigation_container.js
index eb9f3ea45..8606a642e 100644
--- a/app/javascript/mastodon/features/compose/containers/navigation_container.js
+++ b/app/javascript/mastodon/features/compose/containers/navigation_container.js
@@ -1,11 +1,29 @@
import { connect } from 'react-redux';
+import { defineMessages, injectIntl } from 'react-intl';
import NavigationBar from '../components/navigation_bar';
+import { logOut } from 'mastodon/utils/log_out';
+import { openModal } from 'mastodon/actions/modal';
import { me } from '../../../initial_state';
+const messages = defineMessages({
+ logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+ logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
+});
+
const mapStateToProps = state => {
return {
account: state.getIn(['accounts', me]),
};
};
-export default connect(mapStateToProps)(NavigationBar);
+const mapDispatchToProps = (dispatch, { intl }) => ({
+ onLogout () {
+ dispatch(openModal('CONFIRM', {
+ message: intl.formatMessage(messages.logoutMessage),
+ confirm: intl.formatMessage(messages.logoutConfirm),
+ onConfirm: () => logOut(),
+ }));
+ },
+});
+
+export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NavigationBar));
diff --git a/app/javascript/mastodon/features/compose/index.js b/app/javascript/mastodon/features/compose/index.js
index b19b1b2e4..474527a4b 100644
--- a/app/javascript/mastodon/features/compose/index.js
+++ b/app/javascript/mastodon/features/compose/index.js
@@ -12,9 +12,11 @@ import Motion from '../ui/util/optional_motion';
import spring from 'react-motion/lib/spring';
import SearchResultsContainer from './containers/search_results_container';
import { changeComposing } from '../../actions/compose';
+import { openModal } from 'mastodon/actions/modal';
import elephantUIPlane from '../../../images/elephant_ui_plane.svg';
import { mascot } from '../../initial_state';
import Icon from 'mastodon/components/icon';
+import { logOut } from 'mastodon/utils/log_out';
import AnnouncementsContainer from './containers/announcements_container';
const messages = defineMessages({
@@ -26,6 +28,8 @@ const messages = defineMessages({
preferences: { id: 'navigation_bar.preferences', defaultMessage: 'Preferences' },
logout: { id: 'navigation_bar.logout', defaultMessage: 'Logout' },
compose: { id: 'navigation_bar.compose', defaultMessage: 'Compose new toot' },
+ logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+ logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
});
const mapStateToProps = (state, ownProps) => ({
@@ -62,6 +66,21 @@ class Compose extends React.PureComponent {
}
}
+ handleLogoutClick = e => {
+ const { dispatch, intl } = this.props;
+
+ e.preventDefault();
+ e.stopPropagation();
+
+ dispatch(openModal('CONFIRM', {
+ message: intl.formatMessage(messages.logoutMessage),
+ confirm: intl.formatMessage(messages.logoutConfirm),
+ onConfirm: () => logOut(),
+ }));
+
+ return false;
+ }
+
onFocus = () => {
this.props.dispatch(changeComposing(true));
}
@@ -93,7 +112,7 @@ class Compose extends React.PureComponent {
)}
-
+
);
}
diff --git a/app/javascript/mastodon/features/directory/components/account_card.js b/app/javascript/mastodon/features/directory/components/account_card.js
new file mode 100644
index 000000000..50ad74450
--- /dev/null
+++ b/app/javascript/mastodon/features/directory/components/account_card.js
@@ -0,0 +1,190 @@
+import React from 'react';
+import ImmutablePureComponent from 'react-immutable-pure-component';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import PropTypes from 'prop-types';
+import { connect } from 'react-redux';
+import { makeGetAccount } from 'mastodon/selectors';
+import Avatar from 'mastodon/components/avatar';
+import DisplayName from 'mastodon/components/display_name';
+import Permalink from 'mastodon/components/permalink';
+import RelativeTimestamp from 'mastodon/components/relative_timestamp';
+import IconButton from 'mastodon/components/icon_button';
+import { FormattedMessage, injectIntl, defineMessages } from 'react-intl';
+import { autoPlayGif, me, unfollowModal } from 'mastodon/initial_state';
+import { shortNumberFormat } from 'mastodon/utils/numbers';
+import { followAccount, unfollowAccount, blockAccount, unblockAccount, unmuteAccount } from 'mastodon/actions/accounts';
+import { openModal } from 'mastodon/actions/modal';
+import { initMuteModal } from 'mastodon/actions/mutes';
+
+const messages = defineMessages({
+ follow: { id: 'account.follow', defaultMessage: 'Follow' },
+ unfollow: { id: 'account.unfollow', defaultMessage: 'Unfollow' },
+ requested: { id: 'account.requested', defaultMessage: 'Awaiting approval' },
+ unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
+ unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
+});
+
+const makeMapStateToProps = () => {
+ const getAccount = makeGetAccount();
+
+ const mapStateToProps = (state, { id }) => ({
+ account: getAccount(state, id),
+ });
+
+ return mapStateToProps;
+};
+
+const mapDispatchToProps = (dispatch, { intl }) => ({
+
+ onFollow (account) {
+ if (account.getIn(['relationship', 'following']) || account.getIn(['relationship', 'requested'])) {
+ if (unfollowModal) {
+ dispatch(openModal('CONFIRM', {
+ message:
@{account.get('acct')} }} />,
+ confirm: intl.formatMessage(messages.unfollowConfirm),
+ onConfirm: () => dispatch(unfollowAccount(account.get('id'))),
+ }));
+ } else {
+ dispatch(unfollowAccount(account.get('id')));
+ }
+ } else {
+ dispatch(followAccount(account.get('id')));
+ }
+ },
+
+ onBlock (account) {
+ if (account.getIn(['relationship', 'blocking'])) {
+ dispatch(unblockAccount(account.get('id')));
+ } else {
+ dispatch(blockAccount(account.get('id')));
+ }
+ },
+
+ onMute (account) {
+ if (account.getIn(['relationship', 'muting'])) {
+ dispatch(unmuteAccount(account.get('id')));
+ } else {
+ dispatch(initMuteModal(account));
+ }
+ },
+
+});
+
+export default @injectIntl
+@connect(makeMapStateToProps, mapDispatchToProps)
+class AccountCard extends ImmutablePureComponent {
+
+ static propTypes = {
+ account: ImmutablePropTypes.map.isRequired,
+ intl: PropTypes.object.isRequired,
+ onFollow: PropTypes.func.isRequired,
+ onBlock: PropTypes.func.isRequired,
+ onMute: PropTypes.func.isRequired,
+ };
+
+ _updateEmojis () {
+ const node = this.node;
+
+ if (!node || autoPlayGif) {
+ return;
+ }
+
+ const emojis = node.querySelectorAll('.custom-emoji');
+
+ for (var i = 0; i < emojis.length; i++) {
+ let emoji = emojis[i];
+ if (emoji.classList.contains('status-emoji')) {
+ continue;
+ }
+ emoji.classList.add('status-emoji');
+
+ emoji.addEventListener('mouseenter', this.handleEmojiMouseEnter, false);
+ emoji.addEventListener('mouseleave', this.handleEmojiMouseLeave, false);
+ }
+ }
+
+ componentDidMount () {
+ this._updateEmojis();
+ }
+
+ componentDidUpdate () {
+ this._updateEmojis();
+ }
+
+ handleEmojiMouseEnter = ({ target }) => {
+ target.src = target.getAttribute('data-original');
+ }
+
+ handleEmojiMouseLeave = ({ target }) => {
+ target.src = target.getAttribute('data-static');
+ }
+
+ handleFollow = () => {
+ this.props.onFollow(this.props.account);
+ }
+
+ handleBlock = () => {
+ this.props.onBlock(this.props.account);
+ }
+
+ handleMute = () => {
+ this.props.onMute(this.props.account);
+ }
+
+ setRef = (c) => {
+ this.node = c;
+ }
+
+ render () {
+ const { account, intl } = this.props;
+
+ let buttons;
+
+ if (account.get('id') !== me && account.get('relationship', null) !== null) {
+ const following = account.getIn(['relationship', 'following']);
+ const requested = account.getIn(['relationship', 'requested']);
+ const blocking = account.getIn(['relationship', 'blocking']);
+ const muting = account.getIn(['relationship', 'muting']);
+
+ if (requested) {
+ buttons = ;
+ } else if (blocking) {
+ buttons = ;
+ } else if (muting) {
+ buttons = ;
+ } else if (!account.get('moved') || following) {
+ buttons = ;
+ }
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+ {buttons}
+
+
+
+
+
+
+
{shortNumberFormat(account.get('statuses_count'))}
+
{shortNumberFormat(account.get('followers_count'))}
+
{account.get('last_status_at') === null ? : }
+
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/directory/index.js b/app/javascript/mastodon/features/directory/index.js
new file mode 100644
index 000000000..2f91e759b
--- /dev/null
+++ b/app/javascript/mastodon/features/directory/index.js
@@ -0,0 +1,171 @@
+import React from 'react';
+import { connect } from 'react-redux';
+import { defineMessages, injectIntl } from 'react-intl';
+import PropTypes from 'prop-types';
+import ImmutablePropTypes from 'react-immutable-proptypes';
+import Column from 'mastodon/components/column';
+import ColumnHeader from 'mastodon/components/column_header';
+import { addColumn, removeColumn, moveColumn, changeColumnParams } from 'mastodon/actions/columns';
+import { fetchDirectory, expandDirectory } from 'mastodon/actions/directory';
+import { List as ImmutableList } from 'immutable';
+import AccountCard from './components/account_card';
+import RadioButton from 'mastodon/components/radio_button';
+import classNames from 'classnames';
+import LoadMore from 'mastodon/components/load_more';
+import { ScrollContainer } from 'react-router-scroll-4';
+
+const messages = defineMessages({
+ title: { id: 'column.directory', defaultMessage: 'Browse profiles' },
+ recentlyActive: { id: 'directory.recently_active', defaultMessage: 'Recently active' },
+ newArrivals: { id: 'directory.new_arrivals', defaultMessage: 'New arrivals' },
+ local: { id: 'directory.local', defaultMessage: 'From {domain} only' },
+ federated: { id: 'directory.federated', defaultMessage: 'From known fediverse' },
+});
+
+const mapStateToProps = state => ({
+ accountIds: state.getIn(['user_lists', 'directory', 'items'], ImmutableList()),
+ isLoading: state.getIn(['user_lists', 'directory', 'isLoading'], true),
+ domain: state.getIn(['meta', 'domain']),
+});
+
+export default @connect(mapStateToProps)
+@injectIntl
+class Directory extends React.PureComponent {
+
+ static contextTypes = {
+ router: PropTypes.object,
+ };
+
+ static propTypes = {
+ isLoading: PropTypes.bool,
+ accountIds: ImmutablePropTypes.list.isRequired,
+ dispatch: PropTypes.func.isRequired,
+ shouldUpdateScroll: PropTypes.func,
+ columnId: PropTypes.string,
+ intl: PropTypes.object.isRequired,
+ multiColumn: PropTypes.bool,
+ domain: PropTypes.string.isRequired,
+ params: PropTypes.shape({
+ order: PropTypes.string,
+ local: PropTypes.bool,
+ }),
+ };
+
+ state = {
+ order: null,
+ local: null,
+ };
+
+ handlePin = () => {
+ const { columnId, dispatch } = this.props;
+
+ if (columnId) {
+ dispatch(removeColumn(columnId));
+ } else {
+ dispatch(addColumn('DIRECTORY', this.getParams(this.props, this.state)));
+ }
+ }
+
+ getParams = (props, state) => ({
+ order: state.order === null ? (props.params.order || 'active') : state.order,
+ local: state.local === null ? (props.params.local || false) : state.local,
+ });
+
+ handleMove = dir => {
+ const { columnId, dispatch } = this.props;
+ dispatch(moveColumn(columnId, dir));
+ }
+
+ handleHeaderClick = () => {
+ this.column.scrollTop();
+ }
+
+ componentDidMount () {
+ const { dispatch } = this.props;
+ dispatch(fetchDirectory(this.getParams(this.props, this.state)));
+ }
+
+ componentDidUpdate (prevProps, prevState) {
+ const { dispatch } = this.props;
+ const paramsOld = this.getParams(prevProps, prevState);
+ const paramsNew = this.getParams(this.props, this.state);
+
+ if (paramsOld.order !== paramsNew.order || paramsOld.local !== paramsNew.local) {
+ dispatch(fetchDirectory(paramsNew));
+ }
+ }
+
+ setRef = c => {
+ this.column = c;
+ }
+
+ handleChangeOrder = e => {
+ const { dispatch, columnId } = this.props;
+
+ if (columnId) {
+ dispatch(changeColumnParams(columnId, ['order'], e.target.value));
+ } else {
+ this.setState({ order: e.target.value });
+ }
+ }
+
+ handleChangeLocal = e => {
+ const { dispatch, columnId } = this.props;
+
+ if (columnId) {
+ dispatch(changeColumnParams(columnId, ['local'], e.target.value === '1'));
+ } 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, shouldUpdateScroll } = this.props;
+ const { order, local } = this.getParams(this.props, this.state);
+ const pinned = !!columnId;
+
+ const scrollableArea = (
+
+
+
+
+ {accountIds.map(accountId =>
)}
+
+
+
+
+ );
+
+ return (
+
+
+
+ {multiColumn && !pinned ? {scrollableArea} : scrollableArea}
+
+ );
+ }
+
+}
diff --git a/app/javascript/mastodon/features/getting_started/components/trends.js b/app/javascript/mastodon/features/getting_started/components/trends.js
index 1dcacc8b3..3b9a3075f 100644
--- a/app/javascript/mastodon/features/getting_started/components/trends.js
+++ b/app/javascript/mastodon/features/getting_started/components/trends.js
@@ -3,6 +3,7 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import PropTypes from 'prop-types';
import ImmutablePropTypes from 'react-immutable-proptypes';
import Hashtag from 'mastodon/components/hashtag';
+import { FormattedMessage } from 'react-intl';
export default class Trends extends ImmutablePureComponent {
@@ -17,7 +18,7 @@ export default class Trends extends ImmutablePureComponent {
componentDidMount () {
this.props.fetchTrends();
- this.refreshInterval = setInterval(() => this.props.fetchTrends(), 36000);
+ this.refreshInterval = setInterval(() => this.props.fetchTrends(), 900 * 1000);
}
componentWillUnmount () {
@@ -35,6 +36,8 @@ export default class Trends extends ImmutablePureComponent {
return (
+
+
{trends.take(3).map(hashtag => )}
);
diff --git a/app/javascript/mastodon/features/getting_started/index.js b/app/javascript/mastodon/features/getting_started/index.js
index 3c6dc7194..f20a752fc 100644
--- a/app/javascript/mastodon/features/getting_started/index.js
+++ b/app/javascript/mastodon/features/getting_started/index.js
@@ -109,7 +109,7 @@ class GettingStarted extends ImmutablePureComponent {
if (profile_directory) {
navItems.push(
- ,
+ ,
,
);
@@ -123,7 +123,7 @@ class GettingStarted extends ImmutablePureComponent {
height += 34;
} else if (profile_directory) {
navItems.push(
- ,
+ ,
,
);
diff --git a/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js b/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js
index c5098052c..5914bbeaf 100644
--- a/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js
+++ b/app/javascript/mastodon/features/hashtag_timeline/containers/column_settings_container.js
@@ -20,7 +20,7 @@ const mapDispatchToProps = (dispatch, { columnId }) => ({
},
onLoad (value) {
- return api().get('/api/v2/search', { params: { q: value } }).then(response => {
+ return api().get('/api/v2/search', { params: { q: value, type: 'hashtags' } }).then(response => {
return (response.data.hashtags || []).map((tag) => {
return { value: tag.name, label: `#${tag.name}` };
});
diff --git a/app/javascript/mastodon/features/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 4af157af1..e97f18f08 100644
--- a/app/javascript/mastodon/features/status/components/detailed_status.js
+++ b/app/javascript/mastodon/features/status/components/detailed_status.js
@@ -10,6 +10,7 @@ import { FormattedDate, FormattedNumber } from 'react-intl';
import Card from './card';
import ImmutablePureComponent from 'react-immutable-pure-component';
import Video from '../../video';
+import Audio from '../../audio';
import scheduleIdleTask from '../../ui/util/schedule_idle_task';
import classNames from 'classnames';
import Icon from 'mastodon/components/icon';
@@ -107,7 +108,19 @@ export default class DetailedStatus extends ImmutablePureComponent {
}
if (status.get('media_attachments').size > 0) {
- if (['video', 'audio'].includes(status.getIn(['media_attachments', 0, 'type']))) {
+ if (status.getIn(['media_attachments', 0, 'type']) === 'audio') {
+ const attachment = status.getIn(['media_attachments', 0]);
+
+ media = (
+
+ );
+ } else if (status.getIn(['media_attachments', 0, 'type']) === 'video') {
const attachment = status.getIn(['media_attachments', 0]);
media = (
diff --git a/app/javascript/mastodon/features/status/index.js b/app/javascript/mastodon/features/status/index.js
index 780b3ec59..bbb49b045 100644
--- a/app/javascript/mastodon/features/status/index.js
+++ b/app/javascript/mastodon/features/status/index.js
@@ -86,28 +86,38 @@ const makeMapStateToProps = () => {
const getDescendantsIds = createSelector([
(_, { id }) => id,
state => state.getIn(['contexts', 'replies']),
- ], (statusId, contextReplies) => {
- let descendantsIds = Immutable.List();
- descendantsIds = descendantsIds.withMutations(mutable => {
- const ids = [statusId];
+ state => state.get('statuses'),
+ ], (statusId, contextReplies, statuses) => {
+ let descendantsIds = [];
+ const ids = [statusId];
- while (ids.length > 0) {
- let id = ids.shift();
- const replies = contextReplies.get(id);
+ while (ids.length > 0) {
+ let id = ids.shift();
+ const replies = contextReplies.get(id);
- if (statusId !== id) {
- mutable.push(id);
- }
-
- if (replies) {
- replies.reverse().forEach(reply => {
- ids.unshift(reply);
- });
- }
+ if (statusId !== id) {
+ descendantsIds.push(id);
}
- });
- return descendantsIds;
+ if (replies) {
+ replies.reverse().forEach(reply => {
+ ids.unshift(reply);
+ });
+ }
+ }
+
+ let insertAt = descendantsIds.findIndex((id) => statuses.get(id).get('in_reply_to_account_id') !== statuses.get(id).get('account'));
+ if (insertAt !== -1) {
+ descendantsIds.forEach((id, idx) => {
+ if (idx > insertAt && statuses.get(id).get('in_reply_to_account_id') === statuses.get(id).get('account')) {
+ descendantsIds.splice(idx, 1);
+ descendantsIds.splice(insertAt, 0, id);
+ insertAt += 1;
+ }
+ });
+ }
+
+ return Immutable.List(descendantsIds);
});
const mapStateToProps = (state, props) => {
diff --git a/app/javascript/mastodon/features/ui/components/columns_area.js b/app/javascript/mastodon/features/ui/components/columns_area.js
index 518611cce..16b660c51 100644
--- a/app/javascript/mastodon/features/ui/components/columns_area.js
+++ b/app/javascript/mastodon/features/ui/components/columns_area.js
@@ -12,7 +12,19 @@ import BundleContainer from '../containers/bundle_container';
import ColumnLoading from './column_loading';
import DrawerLoading from './drawer_loading';
import BundleColumnError from './bundle_column_error';
-import { Compose, Notifications, HomeTimeline, CommunityTimeline, PublicTimeline, HashtagTimeline, DirectTimeline, FavouritedStatuses, BookmarkedStatuses, ListTimeline } from '../../ui/util/async-components';
+import {
+ Compose,
+ Notifications,
+ HomeTimeline,
+ CommunityTimeline,
+ PublicTimeline,
+ HashtagTimeline,
+ DirectTimeline,
+ FavouritedStatuses,
+ BookmarkedStatuses,
+ ListTimeline,
+ Directory,
+} from '../../ui/util/async-components';
import Icon from 'mastodon/components/icon';
import ComposePanel from './compose_panel';
import NavigationPanel from './navigation_panel';
@@ -31,6 +43,7 @@ const componentMap = {
'FAVOURITES': FavouritedStatuses,
'BOOKMARKS': BookmarkedStatuses,
'LIST': ListTimeline,
+ 'DIRECTORY': Directory,
};
const messages = defineMessages({
diff --git a/app/javascript/mastodon/features/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
index e0ef1a066..735e445e8 100644
--- a/app/javascript/mastodon/features/ui/components/focal_point_modal.js
+++ b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
@@ -10,6 +10,7 @@ import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import IconButton from 'mastodon/components/icon_button';
import Button from 'mastodon/components/button';
import Video from 'mastodon/features/video';
+import Audio from 'mastodon/features/audio';
import Textarea from 'react-textarea-autosize';
import UploadProgress from 'mastodon/features/compose/components/upload_progress';
import CharacterCounter from 'mastodon/features/compose/components/character_counter';
@@ -244,12 +245,23 @@ class FocalPointModal extends ImmutablePureComponent {
)}
- {['audio', 'video'].includes(media.get('type')) && (
+ {media.get('type') === 'video' && (
+ )}
+
+ {media.get('type') === 'audio' && (
+
)}
diff --git a/app/javascript/mastodon/features/ui/components/link_footer.js b/app/javascript/mastodon/features/ui/components/link_footer.js
index b481983dc..2b9bd3875 100644
--- a/app/javascript/mastodon/features/ui/components/link_footer.js
+++ b/app/javascript/mastodon/features/ui/components/link_footer.js
@@ -1,35 +1,72 @@
+import { connect } from 'react-redux';
import React from 'react';
import PropTypes from 'prop-types';
-import { FormattedMessage } from 'react-intl';
+import { FormattedMessage, defineMessages, injectIntl } from 'react-intl';
import { Link } from 'react-router-dom';
import { invitesEnabled, version, repository, source_url } from 'mastodon/initial_state';
+import { logOut } from 'mastodon/utils/log_out';
+import { openModal } from 'mastodon/actions/modal';
-const LinkFooter = ({ withHotkeys }) => (
-
-
- {invitesEnabled && · }
- {withHotkeys && · }
- ·
- ·
- ·
- ·
- ·
- ·
-
-
+const messages = defineMessages({
+ logoutMessage: { id: 'confirmations.logout.message', defaultMessage: 'Are you sure you want to log out?' },
+ logoutConfirm: { id: 'confirmations.logout.confirm', defaultMessage: 'Log out' },
+});
-
- {repository} (v{version}) }}
- />
-
-
-);
+const mapDispatchToProps = (dispatch, { intl }) => ({
+ onLogout () {
+ dispatch(openModal('CONFIRM', {
+ message: intl.formatMessage(messages.logoutMessage),
+ confirm: intl.formatMessage(messages.logoutConfirm),
+ onConfirm: () => logOut(),
+ }));
+ },
+});
+
+export default @injectIntl
+@connect(null, mapDispatchToProps)
+class LinkFooter extends React.PureComponent {
+
+ static propTypes = {
+ withHotkeys: PropTypes.bool,
+ onLogout: PropTypes.func.isRequired,
+ intl: PropTypes.object.isRequired,
+ };
+
+ handleLogoutClick = e => {
+ e.preventDefault();
+ e.stopPropagation();
+
+ this.props.onLogout();
+
+ return false;
+ }
+
+ render () {
+ const { withHotkeys } = this.props;
+
+ return (
+
+
+ {invitesEnabled && · }
+ {withHotkeys && · }
+ ·
+ ·
+ ·
+ ·
+ ·
+ ·
+
+
+
+
+ {repository} (v{version}) }}
+ />
+
+
+ );
+ }
-LinkFooter.propTypes = {
- withHotkeys: PropTypes.bool,
};
-
-export default LinkFooter;
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.js b/app/javascript/mastodon/features/ui/components/navigation_panel.js
index 05e823c92..5a851ef50 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.js
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.js
@@ -20,6 +20,7 @@ const NavigationPanel = () => (
+ {profile_directory &&
}
@@ -27,7 +28,6 @@ const NavigationPanel = () => (
- {!!profile_directory &&
}
{showTrends &&
}
{showTrends &&
}
diff --git a/app/javascript/mastodon/features/ui/containers/notifications_container.js b/app/javascript/mastodon/features/ui/containers/notifications_container.js
index b60a0216f..3819da3d8 100644
--- a/app/javascript/mastodon/features/ui/containers/notifications_container.js
+++ b/app/javascript/mastodon/features/ui/containers/notifications_container.js
@@ -11,7 +11,7 @@ const mapStateToProps = (state, { intl }) => {
const value = notification[key];
if (typeof value === 'object') {
- notification[key] = intl.formatMessage(value);
+ notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
}
}));
diff --git a/app/javascript/mastodon/features/ui/index.js b/app/javascript/mastodon/features/ui/index.js
index 8b9b2283d..480002d28 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -48,6 +48,7 @@ import {
PinnedStatuses,
Lists,
Search,
+ Directory,
} from './util/async-components';
import { me, forceSingleColumn } from '../../initial_state';
import { previewState as previewMediaState } from './components/media_modal';
@@ -142,14 +143,24 @@ class SwitchingColumnsArea extends React.PureComponent {
return location.state !== previewMediaState && location.state !== previewVideoState;
}
- handleResize = debounce(() => {
+ handleLayoutChange = debounce(() => {
// The cached heights are no longer accurate, invalidate
this.props.onLayoutChange();
-
- this.setState({ mobile: isMobile(window.innerWidth) });
}, 500, {
trailing: true,
- });
+ })
+
+ handleResize = () => {
+ const mobile = isMobile(window.innerWidth);
+
+ if (mobile !== this.state.mobile) {
+ this.handleLayoutChange.cancel();
+ this.props.onLayoutChange();
+ this.setState({ mobile });
+ } else {
+ this.handleLayoutChange();
+ }
+ }
setRef = c => {
this.node = c.getWrappedInstance();
@@ -180,6 +191,7 @@ class SwitchingColumnsArea extends React.PureComponent {
+
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 9eef5fe01..3651a5d5c 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -141,3 +141,11 @@ export function Search () {
export function Tesseract () {
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
}
+
+export function Audio () {
+ return import(/* webpackChunkName: "features/audio" */'../../audio');
+}
+
+export function Directory () {
+ return import(/* webpackChunkName: "features/directory" */'../../directory');
+}
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index da48c165e..5fe4e956f 100644
--- a/app/javascript/mastodon/features/video/index.js
+++ b/app/javascript/mastodon/features/video/index.js
@@ -21,7 +21,7 @@ const messages = defineMessages({
exit_fullscreen: { id: 'video.exit_fullscreen', defaultMessage: 'Exit full screen' },
});
-const formatTime = secondsNum => {
+export const formatTime = secondsNum => {
let hours = Math.floor(secondsNum / 3600);
let minutes = Math.floor((secondsNum - (hours * 3600)) / 60);
let seconds = secondsNum - (hours * 3600) - (minutes * 60);
diff --git a/app/javascript/mastodon/locales/co.json b/app/javascript/mastodon/locales/co.json
index a4726148e..428c993a6 100644
--- a/app/javascript/mastodon/locales/co.json
+++ b/app/javascript/mastodon/locales/co.json
@@ -4,7 +4,7 @@
"account.block": "Bluccà @{name}",
"account.block_domain": "Piattà tuttu da {domain}",
"account.blocked": "Bluccatu",
- "account.cancel_follow_request": "Cancel follow request",
+ "account.cancel_follow_request": "Annullà a dumanda d'abbunamentu",
"account.direct": "Missaghju direttu @{name}",
"account.domain_blocked": "Duminiu piattatu",
"account.edit_profile": "Mudificà u prufile",
@@ -38,7 +38,7 @@
"account.unmute_notifications": "Ùn piattà più nutificazione da @{name}",
"alert.unexpected.message": "Un prublemu inaspettatu hè accadutu.",
"alert.unexpected.title": "Uups!",
- "autosuggest_hashtag.per_week": "{count} per week",
+ "autosuggest_hashtag.per_week": "{count} per settimana",
"boost_modal.combo": "Pudete appughjà nant'à {combo} per saltà quessa a prussima volta",
"bundle_column_error.body": "C'hè statu un prublemu caricandu st'elementu.",
"bundle_column_error.retry": "Pruvà torna",
@@ -253,7 +253,7 @@
"navigation_bar.profile_directory": "Annuariu di i prufili",
"navigation_bar.public_timeline": "Linea pubblica glubale",
"navigation_bar.security": "Sicurità",
- "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+ "notification.and_n_others": "è {count, plural, one {# altru} other {# altri}}",
"notification.favourite": "{name} hà aghjuntu u vostru statutu à i so favuriti",
"notification.follow": "{name} v'hà seguitatu",
"notification.mention": "{name} v'hà mintuvatu",
@@ -373,22 +373,22 @@
"time_remaining.moments": "Ci fermanu qualchi mumentu",
"time_remaining.seconds": "{number, plural, one {# siconda ferma} other {# siconde fermanu}}",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} parlanu",
- "trends.refresh": "Refresh",
+ "trends.refresh": "Attualizà",
"ui.beforeunload": "A bruttacopia sarà persa s'ellu hè chjosu Mastodon.",
"upload_area.title": "Drag & drop per caricà un fugliale",
"upload_button.label": "Aghjunghje un media (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Limita di caricamentu di fugliali trapassata.",
"upload_error.poll": "Ùn si pò micca caricà fugliali cù i scandagli.",
"upload_form.description": "Discrive per i malvistosi",
- "upload_form.edit": "Edit",
+ "upload_form.edit": "Mudificà",
"upload_form.undo": "Sguassà",
- "upload_modal.analyzing_picture": "Analyzing picture…",
- "upload_modal.apply": "Apply",
+ "upload_modal.analyzing_picture": "Analisi di u ritrattu…",
+ "upload_modal.apply": "Affettà",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
- "upload_modal.detect_text": "Detect text from picture",
- "upload_modal.edit_media": "Edit media",
- "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
- "upload_modal.preview_label": "Preview ({ratio})",
+ "upload_modal.detect_text": "Ditettà testu da u ritrattu",
+ "upload_modal.edit_media": "Cambià media",
+ "upload_modal.hint": "Cliccate o sguillate u chjerchju nant'à a vista per sceglie u puntu fucale chì sarà sempre in vista indè tutte e miniature.",
+ "upload_modal.preview_label": "Vista ({ratio})",
"upload_progress.label": "Caricamentu...",
"video.close": "Chjudà a video",
"video.exit_fullscreen": "Caccià u pienu screnu",
diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json
index 2d68fe1c8..28b41baf7 100644
--- a/app/javascript/mastodon/locales/de.json
+++ b/app/javascript/mastodon/locales/de.json
@@ -4,7 +4,7 @@
"account.block": "@{name} blockieren",
"account.block_domain": "Alles von {domain} verstecken",
"account.blocked": "Blockiert",
- "account.cancel_follow_request": "Cancel follow request",
+ "account.cancel_follow_request": "Folgeanfrage abbrechen",
"account.direct": "Direktnachricht an @{name}",
"account.domain_blocked": "Domain versteckt",
"account.edit_profile": "Profil bearbeiten",
@@ -38,7 +38,7 @@
"account.unmute_notifications": "Benachrichtigungen von @{name} einschalten",
"alert.unexpected.message": "Ein unerwarteter Fehler ist aufgetreten.",
"alert.unexpected.title": "Hoppla!",
- "autosuggest_hashtag.per_week": "{count} per week",
+ "autosuggest_hashtag.per_week": "{count} pro Woche",
"boost_modal.combo": "Drücke {combo}, um dieses Fenster zu überspringen",
"bundle_column_error.body": "Etwas ist beim Laden schiefgelaufen.",
"bundle_column_error.retry": "Erneut versuchen",
@@ -253,7 +253,7 @@
"navigation_bar.profile_directory": "Profilverzeichnis",
"navigation_bar.public_timeline": "Föderierte Zeitleiste",
"navigation_bar.security": "Sicherheit",
- "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+ "notification.and_n_others": "und {count, plural, one {# andere Person} other {# andere Personen}}",
"notification.favourite": "{name} hat deinen Beitrag favorisiert",
"notification.follow": "{name} folgt dir",
"notification.mention": "{name} hat dich erwähnt",
@@ -373,22 +373,22 @@
"time_remaining.moments": "Schließt in Kürze",
"time_remaining.seconds": "{number, plural, one {# Sekunde} other {# Sekunden}} verbleibend",
"trends.count_by_accounts": "{count} {rawCount, plural, eine {Person} other {Personen}} reden darüber",
- "trends.refresh": "Refresh",
+ "trends.refresh": "Aktualisieren",
"ui.beforeunload": "Dein Entwurf geht verloren, wenn du Mastodon verlässt.",
"upload_area.title": "Zum Hochladen hereinziehen",
"upload_button.label": "Mediendatei hinzufügen ({formats})",
"upload_error.limit": "Dateiupload-Limit erreicht.",
"upload_error.poll": "Dateiuploads sind in Kombination mit Umfragen nicht erlaubt.",
"upload_form.description": "Für Menschen mit Sehbehinderung beschreiben",
- "upload_form.edit": "Edit",
+ "upload_form.edit": "Bearbeiten",
"upload_form.undo": "Löschen",
- "upload_modal.analyzing_picture": "Analyzing picture…",
- "upload_modal.apply": "Apply",
- "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
- "upload_modal.detect_text": "Detect text from picture",
- "upload_modal.edit_media": "Edit media",
- "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
- "upload_modal.preview_label": "Preview ({ratio})",
+ "upload_modal.analyzing_picture": "Analysiere Bild…",
+ "upload_modal.apply": "Übernehmen",
+ "upload_modal.description_placeholder": "Franz jagt im komplett verwahrlosten Taxi quer durch Bayern",
+ "upload_modal.detect_text": "Text aus Bild erkennen",
+ "upload_modal.edit_media": "Medien bearbeiten",
+ "upload_modal.hint": "Klicke oder ziehe den Kreis auf die Vorschau, um den Brennpunkt auszuwählen, der immer auf allen Vorschaubilder angezeigt wird.",
+ "upload_modal.preview_label": "Vorschau ({ratio})",
"upload_progress.label": "Wird hochgeladen …",
"video.close": "Video schließen",
"video.exit_fullscreen": "Vollbild verlassen",
diff --git a/app/javascript/mastodon/locales/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index fa20be26c..9118527db 100644
--- a/app/javascript/mastodon/locales/defaultMessages.json
+++ b/app/javascript/mastodon/locales/defaultMessages.json
@@ -741,6 +741,27 @@
],
"path": "app/javascript/mastodon/features/account/components/header.json"
},
+ {
+ "descriptors": [
+ {
+ "defaultMessage": "Play",
+ "id": "video.play"
+ },
+ {
+ "defaultMessage": "Pause",
+ "id": "video.pause"
+ },
+ {
+ "defaultMessage": "Mute sound",
+ "id": "video.mute"
+ },
+ {
+ "defaultMessage": "Unmute sound",
+ "id": "video.unmute"
+ }
+ ],
+ "path": "app/javascript/mastodon/features/audio/index.json"
+ },
{
"descriptors": [
{
@@ -1096,15 +1117,6 @@
],
"path": "app/javascript/mastodon/features/compose/components/upload_form.json"
},
- {
- "descriptors": [
- {
- "defaultMessage": "Uploading...",
- "id": "upload_progress.label"
- }
- ],
- "path": "app/javascript/mastodon/features/compose/components/upload_progress.json"
- },
{
"descriptors": [
{
@@ -1317,8 +1329,8 @@
{
"descriptors": [
{
- "defaultMessage": "Refresh",
- "id": "trends.refresh"
+ "defaultMessage": "Trending now",
+ "id": "trends.trending_now"
}
],
"path": "app/javascript/mastodon/features/getting_started/components/trends.json"
@@ -1456,6 +1468,10 @@
},
{
"descriptors": [
+ {
+ "defaultMessage": "Basic",
+ "id": "home.column_settings.basic"
+ },
{
"defaultMessage": "Show boosts",
"id": "home.column_settings.show_reblogs"
@@ -1837,14 +1853,6 @@
"defaultMessage": "Push notifications",
"id": "notifications.column_settings.push"
},
- {
- "defaultMessage": "Basic",
- "id": "home.column_settings.basic"
- },
- {
- "defaultMessage": "Update in real-time",
- "id": "home.column_settings.update_live"
- },
{
"defaultMessage": "Quick filter bar",
"id": "notifications.column_settings.filter_bar.category"
@@ -1903,10 +1911,6 @@
},
{
"descriptors": [
- {
- "defaultMessage": "and {count, plural, one {# other} other {# others}}",
- "id": "notification.and_n_others"
- },
{
"defaultMessage": "{name} followed you",
"id": "notification.follow"
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 0abacbb63..6c0c5cbb8 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -158,7 +158,6 @@
"home.column_settings.basic": "Basic",
"home.column_settings.show_reblogs": "Show boosts",
"home.column_settings.show_replies": "Show replies",
- "home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# day} other {# days}}",
"intervals.full.hours": "{number, plural, one {# hour} other {# hours}}",
"intervals.full.minutes": "{number, plural, one {# minute} other {# minutes}}",
@@ -253,7 +252,6 @@
"navigation_bar.profile_directory": "Profile directory",
"navigation_bar.public_timeline": "Federated timeline",
"navigation_bar.security": "Security",
- "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} favourited your status",
"notification.follow": "{name} followed you",
"notification.mention": "{name} mentioned you",
@@ -373,7 +371,7 @@
"time_remaining.moments": "Moments remaining",
"time_remaining.seconds": "{number, plural, one {# second} other {# seconds}} left",
"trends.count_by_accounts": "{count} {rawCount, plural, one {person} other {people}} talking",
- "trends.refresh": "Refresh",
+ "trends.trending_now": "Trending now",
"ui.beforeunload": "Your draft will be lost if you leave Mastodon.",
"upload_area.title": "Drag & drop to upload",
"upload_button.label": "Add media ({formats})",
diff --git a/app/javascript/mastodon/locales/ja.json b/app/javascript/mastodon/locales/ja.json
index 9640c1fab..d11893e2e 100644
--- a/app/javascript/mastodon/locales/ja.json
+++ b/app/javascript/mastodon/locales/ja.json
@@ -4,7 +4,7 @@
"account.block": "@{name}さんをブロック",
"account.block_domain": "{domain}全体を非表示",
"account.blocked": "ブロック済み",
- "account.cancel_follow_request": "Cancel follow request",
+ "account.cancel_follow_request": "フォローリクエストを取り消す",
"account.direct": "@{name}さんにダイレクトメッセージ",
"account.domain_blocked": "ドメイン非表示中",
"account.edit_profile": "プロフィール編集",
@@ -38,7 +38,7 @@
"account.unmute_notifications": "@{name}さんからの通知を受け取るようにする",
"alert.unexpected.message": "不明なエラーが発生しました。",
"alert.unexpected.title": "エラー!",
- "autosuggest_hashtag.per_week": "{count} per week",
+ "autosuggest_hashtag.per_week": "{count} 回 / 週",
"boost_modal.combo": "次からは{combo}を押せばスキップできます",
"bundle_column_error.body": "コンポーネントの読み込み中に問題が発生しました。",
"bundle_column_error.retry": "再試行",
@@ -381,22 +381,22 @@
"time_remaining.moments": "まもなく終了",
"time_remaining.seconds": "残り{number}秒",
"trends.count_by_accounts": "{count}人がトゥート",
- "trends.refresh": "Refresh",
+ "trends.refresh": "更新",
"ui.beforeunload": "Mastodonから離れると送信前の投稿は失われます。",
"upload_area.title": "ドラッグ&ドロップでアップロード",
"upload_button.label": "メディアを追加 ({formats})",
"upload_error.limit": "アップロードできる上限を超えています。",
"upload_error.poll": "アンケートではファイルをアップロードできません。",
"upload_form.description": "視覚障害者のための説明",
- "upload_form.edit": "Edit",
+ "upload_form.edit": "編集",
"upload_form.undo": "削除",
- "upload_modal.analyzing_picture": "Analyzing picture…",
- "upload_modal.apply": "Apply",
+ "upload_modal.analyzing_picture": "画像を解析中…",
+ "upload_modal.apply": "適用",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
- "upload_modal.detect_text": "Detect text from picture",
- "upload_modal.edit_media": "Edit media",
- "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
- "upload_modal.preview_label": "Preview ({ratio})",
+ "upload_modal.detect_text": "画像からテキストを検出",
+ "upload_modal.edit_media": "メディアを編集",
+ "upload_modal.hint": "画像をクリックするか円をドラッグすると全てのサムネイルで注目する場所を選ぶことができます",
+ "upload_modal.preview_label": "プレビュー ({ratio})",
"upload_progress.label": "アップロード中...",
"video.close": "動画を閉じる",
"video.exit_fullscreen": "全画面を終了する",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index 15f20688f..ac6a3ca91 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -4,7 +4,7 @@
"account.block": "@{name}을 차단",
"account.block_domain": "{domain} 전체를 숨김",
"account.blocked": "차단 됨",
- "account.cancel_follow_request": "Cancel follow request",
+ "account.cancel_follow_request": "팔로우 요청 취소",
"account.direct": "@{name}으로부터의 다이렉트 메시지",
"account.domain_blocked": "도메인 숨겨짐",
"account.edit_profile": "프로필 편집",
@@ -38,7 +38,7 @@
"account.unmute_notifications": "@{name}의 알림 뮤트 해제",
"alert.unexpected.message": "예측하지 못한 에러가 발생했습니다.",
"alert.unexpected.title": "앗!",
- "autosuggest_hashtag.per_week": "{count} per week",
+ "autosuggest_hashtag.per_week": "주간 {count}회",
"boost_modal.combo": "{combo}를 누르면 다음부터 이 과정을 건너뛸 수 있습니다",
"bundle_column_error.body": "컴포넌트를 불러오는 과정에서 문제가 발생했습니다.",
"bundle_column_error.retry": "다시 시도",
@@ -253,7 +253,7 @@
"navigation_bar.profile_directory": "프로필 디렉토리",
"navigation_bar.public_timeline": "연합 타임라인",
"navigation_bar.security": "보안",
- "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+ "notification.and_n_others": "그리고 {count}개의 기타 항목",
"notification.favourite": "{name}님이 즐겨찾기 했습니다",
"notification.follow": "{name}님이 나를 팔로우 했습니다",
"notification.mention": "{name}님이 답글을 보냈습니다",
@@ -373,22 +373,22 @@
"time_remaining.moments": "남은 시간",
"time_remaining.seconds": "{number} 초 남음",
"trends.count_by_accounts": "{count} 명의 사람들이 말하고 있습니다",
- "trends.refresh": "Refresh",
+ "trends.refresh": "새로고침",
"ui.beforeunload": "지금 나가면 저장되지 않은 항목을 잃게 됩니다.",
"upload_area.title": "드래그 & 드롭으로 업로드",
"upload_button.label": "미디어 추가 (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "파일 업로드 제한에 도달했습니다.",
"upload_error.poll": "파일 업로드는 투표와 함께 첨부할 수 없습니다.",
"upload_form.description": "시각장애인을 위한 설명",
- "upload_form.edit": "Edit",
+ "upload_form.edit": "편집",
"upload_form.undo": "삭제",
- "upload_modal.analyzing_picture": "Analyzing picture…",
- "upload_modal.apply": "Apply",
- "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
- "upload_modal.detect_text": "Detect text from picture",
- "upload_modal.edit_media": "Edit media",
- "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
- "upload_modal.preview_label": "Preview ({ratio})",
+ "upload_modal.analyzing_picture": "이미지 분석 중…",
+ "upload_modal.apply": "적용",
+ "upload_modal.description_placeholder": "다람쥐 헌 쳇바퀴 타고파",
+ "upload_modal.detect_text": "이미지에서 텍스트 추출",
+ "upload_modal.edit_media": "미디어 편집",
+ "upload_modal.hint": "미리보기를 클릭하거나 드래그 해서 포컬 포인트를 맞추세요. 이 점은 썸네일에 항상 보여질 부분을 나타냅니다.",
+ "upload_modal.preview_label": "미리보기 ({ratio})",
"upload_progress.label": "업로드 중...",
"video.close": "동영상 닫기",
"video.exit_fullscreen": "전체화면 나가기",
diff --git a/app/javascript/mastodon/locales/ru.json b/app/javascript/mastodon/locales/ru.json
index 8559c81da..afc064a6b 100644
--- a/app/javascript/mastodon/locales/ru.json
+++ b/app/javascript/mastodon/locales/ru.json
@@ -4,7 +4,7 @@
"account.block": "Блокировать",
"account.block_domain": "Блокировать все с {domain}",
"account.blocked": "Заблокирован(а)",
- "account.cancel_follow_request": "Cancel follow request",
+ "account.cancel_follow_request": "Отменить запрос",
"account.direct": "Написать @{name}",
"account.domain_blocked": "Домен скрыт",
"account.edit_profile": "Изменить профиль",
@@ -38,7 +38,7 @@
"account.unmute_notifications": "Показывать уведомления от @{name}",
"alert.unexpected.message": "Что-то пошло не так.",
"alert.unexpected.title": "Ой!",
- "autosuggest_hashtag.per_week": "{count} per week",
+ "autosuggest_hashtag.per_week": "{count} / неделю",
"boost_modal.combo": "Нажмите {combo}, чтобы пропустить это в следующий раз",
"bundle_column_error.body": "Что-то пошло не так при загрузке этого компонента.",
"bundle_column_error.retry": "Попробовать снова",
@@ -255,8 +255,8 @@
"navigation_bar.security": "Безопасность",
"notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
"notification.favourite": "{name} понравился Ваш статус",
- "notification.follow": "{name} подписался(-лась) на Вас",
- "notification.mention": "{name} упомянул(а) Вас",
+ "notification.follow": "{name} подписался (-лась) на вас",
+ "notification.mention": "{name} упомянул(а) вас",
"notification.poll": "Опрос, в котором вы приняли участие, завершился",
"notification.reblog": "{name} продвинул(а) Ваш статус",
"notifications.clear": "Очистить уведомления",
@@ -373,22 +373,22 @@
"time_remaining.moments": "остались считанные мгновения",
"time_remaining.seconds": "{number, plural, one {осталась # секунду} few {осталось # секунды} many {осталось # секунд} other {осталось # секунд}}",
"trends.count_by_accounts": "Популярно у {count} {rawCount, plural, one {человека} few {человек} many {человек} other {человек}}",
- "trends.refresh": "Refresh",
+ "trends.refresh": "Обновить",
"ui.beforeunload": "Ваш черновик будет утерян, если вы покинете Mastodon.",
"upload_area.title": "Перетащите сюда, чтобы загрузить",
"upload_button.label": "Добавить медиаконтент",
"upload_error.limit": "Достигнут лимит загруженных файлов.",
"upload_error.poll": "К опросам нельзя прикреплять файлы.",
- "upload_form.description": "Описать для людей с нарушениями зрения",
- "upload_form.edit": "Edit",
+ "upload_form.description": "Добавьте описание для людей с нарушениями зрения:",
+ "upload_form.edit": "Изменить",
"upload_form.undo": "Отменить",
- "upload_modal.analyzing_picture": "Analyzing picture…",
- "upload_modal.apply": "Apply",
- "upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
- "upload_modal.detect_text": "Detect text from picture",
- "upload_modal.edit_media": "Edit media",
- "upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
- "upload_modal.preview_label": "Preview ({ratio})",
+ "upload_modal.analyzing_picture": "Обработка изображения…",
+ "upload_modal.apply": "Применить",
+ "upload_modal.description_placeholder": "На дворе трава, на траве дрова",
+ "upload_modal.detect_text": "Найти текст на картинке",
+ "upload_modal.edit_media": "Изменение медиа",
+ "upload_modal.hint": "Нажмите и перетащите круг в предпросмотре в точку фокуса, которая всегда будет видна на эскизах.",
+ "upload_modal.preview_label": "Предпросмотр ({ratio})",
"upload_progress.label": "Загрузка...",
"video.close": "Закрыть видео",
"video.exit_fullscreen": "Покинуть полноэкранный режим",
diff --git a/app/javascript/mastodon/locales/sk.json b/app/javascript/mastodon/locales/sk.json
index edf950a2c..4a7625aae 100644
--- a/app/javascript/mastodon/locales/sk.json
+++ b/app/javascript/mastodon/locales/sk.json
@@ -4,7 +4,7 @@
"account.block": "Blokuj @{name}",
"account.block_domain": "Ukry všetko z {domain}",
"account.blocked": "Blokovaný/á",
- "account.cancel_follow_request": "Cancel follow request",
+ "account.cancel_follow_request": "Zruš požiadanie o sledovanie",
"account.direct": "Súkromná správa pre @{name}",
"account.domain_blocked": "Doména ukrytá",
"account.edit_profile": "Uprav profil",
@@ -24,7 +24,7 @@
"account.mute": "Ignorovať @{name}",
"account.mute_notifications": "Stĺm oboznámenia od @{name}",
"account.muted": "Utíšený/á",
- "account.posts": "Príspevky",
+ "account.posts": "Príspevkov",
"account.posts_with_replies": "Príspevky aj s odpoveďami",
"account.report": "Nahlás @{name}",
"account.requested": "Čaká na schválenie. Klikni pre zrušenie žiadosti",
@@ -38,7 +38,7 @@
"account.unmute_notifications": "Zruš stĺmenie oboznámení od @{name}",
"alert.unexpected.message": "Vyskytla sa nečakaná chyba.",
"alert.unexpected.title": "Ups!",
- "autosuggest_hashtag.per_week": "{count} per week",
+ "autosuggest_hashtag.per_week": "{count} týždenne",
"boost_modal.combo": "Nabudúce môžeš kliknúť {combo} pre preskočenie",
"bundle_column_error.body": "Pri načítaní tohto prvku nastala nejaká chyba.",
"bundle_column_error.retry": "Skús to znova",
@@ -142,7 +142,7 @@
"getting_started.directory": "Zoznam profilov",
"getting_started.documentation": "Dokumentácia",
"getting_started.heading": "Začni tu",
- "getting_started.invite": "Pozvať ľudí",
+ "getting_started.invite": "Pozvi ľudí",
"getting_started.open_source_notice": "Mastodon je softvér s otvoreným kódom. Nahlásiť chyby, alebo prispievať môžeš na GitHube v {github}.",
"getting_started.security": "Zabezpečenie",
"getting_started.terms": "Podmienky prevozu",
@@ -253,7 +253,7 @@
"navigation_bar.profile_directory": "Katalóg profilov",
"navigation_bar.public_timeline": "Federovaná časová os",
"navigation_bar.security": "Zabezbečenie",
- "notification.and_n_others": "and {count, plural, one {# other} other {# others}}",
+ "notification.and_n_others": "a {count, plural,one {# ostatní} other {# ostatných}}",
"notification.favourite": "{name} si obľúbil/a tvoj príspevok",
"notification.follow": "{name} ťa začal/a následovať",
"notification.mention": "{name} ťa spomenul/a",
@@ -281,7 +281,7 @@
"notifications.filter.polls": "Výsledky ankiet",
"notifications.group": "{count} oboznámení",
"poll.closed": "Uzatvorená",
- "poll.refresh": "Aktualizuj",
+ "poll.refresh": "Obnov",
"poll.total_votes": "{count, plural, one {# hlas} few {# hlasov} many {# hlasov} other {# hlasov}}",
"poll.vote": "Hlasuj",
"poll_button.add_poll": "Pridaj anketu",
@@ -373,20 +373,20 @@
"time_remaining.moments": "Ostáva už iba chviľka",
"time_remaining.seconds": "Ostáva {number, plural, one {# sekunda} few {# sekúnd} many {# sekúnd} other {# sekúnd}}",
"trends.count_by_accounts": "{count} {rawCount, plural, one {človek vraví} other {ľudia vravia}}",
- "trends.refresh": "Refresh",
+ "trends.refresh": "Obnov",
"ui.beforeunload": "Čo máš rozpísané sa stratí, ak opustíš Mastodon.",
"upload_area.title": "Pretiahni a pusť pre nahratie",
"upload_button.label": "Pridaj médiálny súbor (JPEG, PNG, GIF, WebM, MP4, MOV)",
"upload_error.limit": "Limit pre nahrávanie súborov bol prekročený.",
"upload_error.poll": "Nahrávanie súborov pri anketách nieje možné.",
"upload_form.description": "Opis pre slabo vidiacich",
- "upload_form.edit": "Edit",
+ "upload_form.edit": "Uprav",
"upload_form.undo": "Vymaž",
"upload_modal.analyzing_picture": "Analyzing picture…",
- "upload_modal.apply": "Apply",
+ "upload_modal.apply": "Použi",
"upload_modal.description_placeholder": "A quick brown fox jumps over the lazy dog",
"upload_modal.detect_text": "Detect text from picture",
- "upload_modal.edit_media": "Edit media",
+ "upload_modal.edit_media": "Uprav médiá",
"upload_modal.hint": "Click or drag the circle on the preview to choose the focal point which will always be in view on all thumbnails.",
"upload_modal.preview_label": "Preview ({ratio})",
"upload_progress.label": "Nahráva sa...",
diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json
index 17a9dd9b0..af988b320 100644
--- a/app/javascript/mastodon/locales/zh-TW.json
+++ b/app/javascript/mastodon/locales/zh-TW.json
@@ -1,8 +1,8 @@
{
- "account.add_or_remove_from_list": "從名單中新增或移除",
+ "account.add_or_remove_from_list": "從列表新增或移除",
"account.badges.bot": "機器人",
"account.block": "封鎖 @{name}",
- "account.block_domain": "隱藏來自 {domain} 的所有嘟文",
+ "account.block_domain": "隱藏來自 {domain} 的所有內容",
"account.blocked": "已封鎖",
"account.cancel_follow_request": "Cancel follow request",
"account.direct": "傳私訊給 @{name}",
@@ -11,13 +11,13 @@
"account.endorse": "在個人資料推薦對方",
"account.follow": "關注",
"account.followers": "關注者",
- "account.followers.empty": "還沒有人關注這位使用者。",
+ "account.followers.empty": "尚沒有人關注這位使用者。",
"account.follows": "正在關注",
- "account.follows.empty": "這個使用者尚未關注任何使用者。",
+ "account.follows.empty": "這位使用者尚未關注任何使用者。",
"account.follows_you": "關注了你",
"account.hide_reblogs": "隱藏來自 @{name} 的轉推",
- "account.link_verified_on": "此連結的所有權已在 {date} 檢查",
- "account.locked_info": "此帳號的隱私狀態被設為鎖定,擁有者將手動審核可關注此帳號的人。",
+ "account.link_verified_on": "已在 {date} 檢查此連結的擁有者權限",
+ "account.locked_info": "這隻帳戶的隱私狀態被設成鎖定。該擁有者會手動審核能關注這隻帳號的人。",
"account.media": "媒體",
"account.mention": "提及 @{name}",
"account.moved_to": "{name} 已遷移至:",
@@ -34,23 +34,23 @@
"account.unblock_domain": "取消隱藏 {domain}",
"account.unendorse": "不再於個人資料頁面推薦對方",
"account.unfollow": "取消關注",
- "account.unmute": "不再靜音 @{name}",
- "account.unmute_notifications": "不再靜音來自 @{name} 的通知",
+ "account.unmute": "取消靜音 @{name}",
+ "account.unmute_notifications": "重新接收來自 @{name} 的通知",
"alert.unexpected.message": "發生了非預期的錯誤。",
"alert.unexpected.title": "哎呀!",
"autosuggest_hashtag.per_week": "{count} per week",
"boost_modal.combo": "下次您可以按 {combo} 跳過",
- "bundle_column_error.body": "載入此組件時發生錯誤。",
+ "bundle_column_error.body": "載入此元件時發生錯誤。",
"bundle_column_error.retry": "重試",
"bundle_column_error.title": "網路錯誤",
"bundle_modal_error.close": "關閉",
- "bundle_modal_error.message": "載入此組件時發生錯誤。",
+ "bundle_modal_error.message": "載入此元件時發生錯誤。",
"bundle_modal_error.retry": "重試",
"column.blocks": "封鎖的使用者",
- "column.community": "本地時間軸",
+ "column.community": "本機時間軸",
"column.direct": "私訊",
"column.domain_blocks": "隱藏的網域",
- "column.favourites": "最愛",
+ "column.favourites": "收藏",
"column.follow_requests": "關注請求",
"column.home": "主頁",
"column.lists": "名單",
@@ -66,44 +66,44 @@
"column_header.show_settings": "顯示設定",
"column_header.unpin": "取消釘選",
"column_subheading.settings": "設定",
- "community.column_settings.media_only": "僅媒體",
- "compose_form.direct_message_warning": "這條嘟文只有被提及的使用者才能看到。",
+ "community.column_settings.media_only": "只有媒體",
+ "compose_form.direct_message_warning": "這條嘟文只有被提及的使用者才看得到。",
"compose_form.direct_message_warning_learn_more": "了解更多",
- "compose_form.hashtag_warning": "因這則嘟文設成「不公開」,因此它不會列在任何「#」標籤下。只有公開嘟文才能用「#」標籤找到。",
- "compose_form.lock_disclaimer": "您的帳戶尚未{locked}。任何人都能關注您並看到您設定成僅關注者能看的嘟文。",
+ "compose_form.hashtag_warning": "由於這則嘟文被設定成「不公開」,所以它將不會被列在任何主題標籤下。只有公開的嘟文才能藉主題標籤找到。",
+ "compose_form.lock_disclaimer": "您的帳戶尚未{locked}。任何人都能關注您並看到您設定成只有關注者能看的嘟文。",
"compose_form.lock_disclaimer.lock": "上鎖",
"compose_form.placeholder": "您正在想些什麼?",
"compose_form.poll.add_option": "新增選擇",
"compose_form.poll.duration": "投票期限",
"compose_form.poll.option_placeholder": "第 {number} 個選擇",
"compose_form.poll.remove_option": "移除此選擇",
- "compose_form.publish": "嘟掉",
+ "compose_form.publish": "嘟出去",
"compose_form.publish_loud": "{publish}!",
"compose_form.sensitive.hide": "Mark media as sensitive",
"compose_form.sensitive.marked": "此媒體被標記為敏感內容",
- "compose_form.sensitive.unmarked": "此媒體未被標記為敏感內容",
- "compose_form.spoiler.marked": "正文已隱藏在警告之後",
+ "compose_form.sensitive.unmarked": "此媒體未標記為敏感內容",
+ "compose_form.spoiler.marked": "正文已隱藏到警告之後",
"compose_form.spoiler.unmarked": "正文未被隱藏",
"compose_form.spoiler_placeholder": "請在此處寫入警告訊息",
"confirmation_modal.cancel": "取消",
"confirmations.block.block_and_report": "Block & Report",
"confirmations.block.confirm": "封鎖",
- "confirmations.block.message": "你確定要封鎖 {name} ?",
+ "confirmations.block.message": "確定封鎖 {name} ?",
"confirmations.delete.confirm": "刪除",
"confirmations.delete.message": "你確定要刪除這條嘟文?",
"confirmations.delete_list.confirm": "刪除",
- "confirmations.delete_list.message": "確定要永久刪除此名單?",
+ "confirmations.delete_list.message": "確定永久刪除此名單?",
"confirmations.domain_block.confirm": "隱藏整個網域",
- "confirmations.domain_block.message": "確定封鎖整個 {domain} 嗎?多數情況下,封鎖或靜音幾個特定使用者應該就能滿足你的需求了。您將不能在任何公開時間軸或通知中看到來自該網域的內容。來自該網域的關注者將被移除。",
+ "confirmations.domain_block.message": "真的非常確定封鎖整個 {domain} 嗎?大部分情況下,你只需要封鎖或靜音少數特定的人就能滿足需求了。你將不能在任何公開的時間軸及通知中看到那個網域的內容。你來自該網域的關注者也會被移除。",
"confirmations.mute.confirm": "靜音",
"confirmations.mute.message": "確定靜音 {name} ?",
"confirmations.redraft.confirm": "刪除並重新編輯",
- "confirmations.redraft.message": "你確定要刪除這條嘟文並重新編輯它嗎?這麼做將失去轉嘟和最愛,而對原始嘟文的回覆將被孤立。",
+ "confirmations.redraft.message": "確定刪掉這則嘟文並重新編輯嗎?將會失去這則嘟文的轉嘟及收藏,且回覆這則的嘟文將會變成獨立的嘟文。",
"confirmations.reply.confirm": "回覆",
"confirmations.reply.message": "現在回覆將蓋掉您目前正在撰寫的訊息。是否仍要回覆?",
"confirmations.unfollow.confirm": "取消關注",
"confirmations.unfollow.message": "真的要取消關注 {name} 嗎?",
- "embed.instructions": "要嵌入此嘟文,請將以下代碼貼進你的網站。",
+ "embed.instructions": "要嵌入此嘟文,請將以下程式碼貼進你的網站。",
"embed.preview": "他會顯示成這樣:",
"emoji_button.activity": "活動",
"emoji_button.custom": "自訂",
@@ -111,7 +111,7 @@
"emoji_button.food": "飲食",
"emoji_button.label": "插入表情符號",
"emoji_button.nature": "大自然",
- "emoji_button.not_found": "就沒這表情符號吼!! (╯°□°)╯︵ ┻━┻",
+ "emoji_button.not_found": "啊就沒這表情符號吼!! (╯°□°)╯︵ ┻━┻",
"emoji_button.objects": "物件",
"emoji_button.people": "使用者",
"emoji_button.recent": "最常使用",
@@ -125,15 +125,15 @@
"empty_column.community": "本地時間軸是空的。快公開嘟些文搶頭香啊!",
"empty_column.direct": "您還沒有任何私訊。當您私訊別人或收到私訊時,它將於此顯示。",
"empty_column.domain_blocks": "尚未隱藏任何網域。",
- "empty_column.favourited_statuses": "你還沒有將任何嘟文標為最愛。最愛的嘟文將顯示於此。",
- "empty_column.favourites": "還沒有人將此嘟文標為最愛。如果有人標成最愛,則會顯示在這裡。",
- "empty_column.follow_requests": "您尚未收到任何關注請求。收到時會顯示於此。",
- "empty_column.hashtag": "這個「#」標籤下什麼都沒有。",
+ "empty_column.favourited_statuses": "你還沒收藏任何嘟文。這裡將會顯示你收藏的嘟文。",
+ "empty_column.favourites": "還沒有人收藏這則嘟文。這裡將會顯示被收藏的嘟文。",
+ "empty_column.follow_requests": "您尚未收到任何關注請求。這裡將會顯示收到的關注請求。",
+ "empty_column.hashtag": "這個主題標籤下什麼也沒有。",
"empty_column.home": "您的首頁時間軸是空的!前往 {public} 或使用搜尋功能來認識其他人。",
"empty_column.home.public_timeline": "公開時間軸",
- "empty_column.list": "此份名單還沒有東西。當此名單的成員嘟出了新的嘟文時,它們就會出現在這裡。",
- "empty_column.lists": "你還沒有建立任何名單。你建立的名單將會顯示在這裡。",
- "empty_column.mutes": "你還沒有靜音任何使用者。",
+ "empty_column.list": "這份名單還沒有東西。當此名單的成員嘟出了新的嘟文時,它們就會顯示於此。",
+ "empty_column.lists": "你還沒有建立任何名單。這裡將會顯示你所建立的名單。",
+ "empty_column.mutes": "你尚未靜音任何使用者。",
"empty_column.notifications": "您尚未收到任何通知,和別人互動開啟對話吧。",
"empty_column.public": "這裡什麼都沒有!嘗試寫些公開的嘟文,或著自己關注其他伺服器的使用者後就會有嘟文出現了",
"follow_request.authorize": "授權",
@@ -148,61 +148,61 @@
"getting_started.terms": "服務條款",
"hashtag.column_header.tag_mode.all": "以及{additional}",
"hashtag.column_header.tag_mode.any": "或是{additional}",
- "hashtag.column_header.tag_mode.none": "而不用{additional}",
+ "hashtag.column_header.tag_mode.none": "而無需{additional}",
"hashtag.column_settings.select.no_options_message": "找不到建議",
- "hashtag.column_settings.select.placeholder": "輸入「#」標籤…",
+ "hashtag.column_settings.select.placeholder": "輸入主題標籤…",
"hashtag.column_settings.tag_mode.all": "全部",
"hashtag.column_settings.tag_mode.any": "任一",
- "hashtag.column_settings.tag_mode.none": "全都不要",
- "hashtag.column_settings.tag_toggle": "對此欄位加入額外標籤",
+ "hashtag.column_settings.tag_mode.none": "全不",
+ "hashtag.column_settings.tag_toggle": "將額外標籤加入到這個欄位",
"home.column_settings.basic": "基本",
- "home.column_settings.show_reblogs": "顯示轉推",
+ "home.column_settings.show_reblogs": "顯示轉嘟",
"home.column_settings.show_replies": "顯示回覆",
"home.column_settings.update_live": "Update in real-time",
"intervals.full.days": "{number, plural, one {# 天} other {# 天}}",
"intervals.full.hours": "{number, plural, one {# 小時} other {# 小時}}",
"intervals.full.minutes": "{number, plural, one {# 分鐘} other {# 分鐘}}",
"introduction.federation.action": "下一步",
- "introduction.federation.federated.headline": "聯邦",
- "introduction.federation.federated.text": "來自聯邦網路中其他伺服器的公開嘟文將會在聯邦網路時間軸中顯示。",
+ "introduction.federation.federated.headline": "站台聯盟",
+ "introduction.federation.federated.text": "來自聯盟宇宙中其他站台的公開嘟文將會在站點聯盟時間軸中顯示。",
"introduction.federation.home.headline": "首頁",
- "introduction.federation.home.text": "您所關注使用者所發的嘟文將顯示在首頁的訊息來源。您能關注任何伺服器上的任何人!",
- "introduction.federation.local.headline": "本地",
- "introduction.federation.local.text": "跟您同伺服器之使用者所發的公開嘟文將會顯示在本地時間軸中。",
+ "introduction.federation.home.text": "你關注使用者的嘟文將會在首頁動態中顯示。你可以關注任何伺服器上的任何人!",
+ "introduction.federation.local.headline": "本機",
+ "introduction.federation.local.text": "跟您同伺服器之使用者所發的公開嘟文將會顯示在本機時間軸中。",
"introduction.interactions.action": "完成教學!",
- "introduction.interactions.favourite.headline": "最愛",
- "introduction.interactions.favourite.text": "您能稍候儲存嘟文,或者將嘟文加到最愛,讓作者知道您喜歡這嘟文。",
+ "introduction.interactions.favourite.headline": "關注",
+ "introduction.interactions.favourite.text": "您能儲存嘟文供稍候觀看,或者收藏嘟文,讓作者知道您喜歡這則嘟文。",
"introduction.interactions.reblog.headline": "轉嘟",
- "introduction.interactions.reblog.text": "您能透過轉嘟他人嘟文來分享給您的關注者。",
+ "introduction.interactions.reblog.text": "您能藉由轉嘟他人嘟文來分享給您的關注者。",
"introduction.interactions.reply.headline": "回覆",
- "introduction.interactions.reply.text": "您能回覆其他人或自己的嘟文。將會把這些回覆串成一串對話。",
- "introduction.welcome.action": "開始!",
+ "introduction.interactions.reply.text": "您能回覆其他人或自己的嘟文,這麼做會把這些回覆串成一串對話。",
+ "introduction.welcome.action": "開始旅程吧!",
"introduction.welcome.headline": "第一步",
- "introduction.welcome.text": "歡迎來到聯邦!稍候您將可以廣播訊息並跨各種各式各樣的伺服器與朋友聊天。但這台伺服器,{domain},十分特殊 -- 它寄管了您的個人資料,所以請記住這台伺服器的名稱。",
+ "introduction.welcome.text": "歡迎來到聯盟宇宙!等等你就可以廣播訊息及跨越各種各式各樣的伺服器與朋友聊天。但這台伺服器,{domain},非常特別 - 它寄管了你的個人資料,所以請記住它的名字。",
"keyboard_shortcuts.back": "返回上一頁",
- "keyboard_shortcuts.blocked": "開啟「封鎖的使用者」名單",
+ "keyboard_shortcuts.blocked": "開啟「封鎖使用者」名單",
"keyboard_shortcuts.boost": "轉嘟",
"keyboard_shortcuts.column": "將焦點放在其中一欄的嘟文",
"keyboard_shortcuts.compose": "將焦點移至撰寫文字區塊",
"keyboard_shortcuts.description": "描述",
"keyboard_shortcuts.direct": "開啟私訊欄",
- "keyboard_shortcuts.down": "在名單中往下移動",
+ "keyboard_shortcuts.down": "往下移動名單項目",
"keyboard_shortcuts.enter": "檢視嘟文",
- "keyboard_shortcuts.favourite": "加入最愛",
- "keyboard_shortcuts.favourites": "開啟最愛名單",
- "keyboard_shortcuts.federated": "開啟聯邦時間軸",
+ "keyboard_shortcuts.favourite": "收藏",
+ "keyboard_shortcuts.favourites": "開啟收藏名單",
+ "keyboard_shortcuts.federated": "開啟站點聯盟時間軸",
"keyboard_shortcuts.heading": "鍵盤快速鍵",
"keyboard_shortcuts.home": "開啟首頁時間軸",
"keyboard_shortcuts.hotkey": "快速鍵",
- "keyboard_shortcuts.legend": "顯示此說明",
- "keyboard_shortcuts.local": "開啟本地時間軸",
+ "keyboard_shortcuts.legend": "顯示此列表",
+ "keyboard_shortcuts.local": "開啟本機時間軸",
"keyboard_shortcuts.mention": "提及作者",
"keyboard_shortcuts.muted": "開啟靜音使用者名單",
"keyboard_shortcuts.my_profile": "開啟個人資料頁面",
"keyboard_shortcuts.notifications": "開啟通知欄",
"keyboard_shortcuts.pinned": "開啟釘選的嘟文名單",
- "keyboard_shortcuts.profile": "開啟作者的個人資料頁",
- "keyboard_shortcuts.reply": "回應嘟文",
+ "keyboard_shortcuts.profile": "開啟作者的個人資料頁面",
+ "keyboard_shortcuts.reply": "回覆",
"keyboard_shortcuts.requests": "開啟關注請求名單",
"keyboard_shortcuts.search": "將焦點移至搜尋框",
"keyboard_shortcuts.start": "開啟「開始使用」欄位",
@@ -210,7 +210,7 @@
"keyboard_shortcuts.toggle_sensitivity": "to show/hide media",
"keyboard_shortcuts.toot": "開始發出新嘟文",
"keyboard_shortcuts.unfocus": "取消輸入文字區塊 / 搜尋的焦點",
- "keyboard_shortcuts.up": "在名單中往上移動",
+ "keyboard_shortcuts.up": "往上移動名單項目",
"lightbox.close": "關閉",
"lightbox.next": "下一步",
"lightbox.previous": "上一步",
@@ -229,16 +229,16 @@
"media_gallery.toggle_visible": "切換可見性",
"missing_indicator.label": "找不到",
"missing_indicator.sublabel": "找不到此資源",
- "mute_modal.hide_notifications": "隱藏來自這個使用者的通知?",
- "navigation_bar.apps": "行動應用程式",
- "navigation_bar.blocks": "封鎖的使用者",
- "navigation_bar.community_timeline": "本地時間軸",
+ "mute_modal.hide_notifications": "隱藏來自這位使用者的通知?",
+ "navigation_bar.apps": "封鎖的使用者",
+ "navigation_bar.blocks": "封鎖使用者",
+ "navigation_bar.community_timeline": "本機時間軸",
"navigation_bar.compose": "撰寫新嘟文",
"navigation_bar.direct": "私訊",
"navigation_bar.discover": "探索",
"navigation_bar.domain_blocks": "隱藏的網域",
"navigation_bar.edit_profile": "編輯個人資料",
- "navigation_bar.favourites": "最愛內容",
+ "navigation_bar.favourites": "收藏",
"navigation_bar.filters": "靜音詞彙",
"navigation_bar.follow_requests": "關注請求",
"navigation_bar.follows_and_followers": "Follows and followers",
diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js
index 089d920c3..c62ab0dfd 100644
--- a/app/javascript/mastodon/reducers/alerts.js
+++ b/app/javascript/mastodon/reducers/alerts.js
@@ -14,6 +14,7 @@ export default function alerts(state = initialState, action) {
key: state.size > 0 ? state.last().get('key') + 1 : 0,
title: action.title,
message: action.message,
+ message_values: action.message_values,
}));
case ALERT_DISMISS:
return state.filterNot(item => item.get('key') === action.alert.key);
diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js
index 7b0cdd5a5..268237846 100644
--- a/app/javascript/mastodon/reducers/compose.js
+++ b/app/javascript/mastodon/reducers/compose.js
@@ -17,6 +17,7 @@ import {
COMPOSE_SUGGESTIONS_CLEAR,
COMPOSE_SUGGESTIONS_READY,
COMPOSE_SUGGESTION_SELECT,
+ COMPOSE_SUGGESTION_TAGS_UPDATE,
COMPOSE_TAG_HISTORY_UPDATE,
COMPOSE_SENSITIVITY_CHANGE,
COMPOSE_SPOILERNESS_CHANGE,
@@ -205,16 +206,36 @@ const expiresInFromExpiresAt = expires_at => {
return [300, 1800, 3600, 21600, 86400, 259200, 604800].find(expires_in => expires_in >= delta) || 24 * 3600;
};
-const normalizeSuggestions = (state, { accounts, emojis, tags }) => {
+const mergeLocalHashtagResults = (suggestions, prefix, tagHistory) => {
+ prefix = prefix.toLowerCase();
+ if (suggestions.length < 4) {
+ const localTags = tagHistory.filter(tag => tag.toLowerCase().startsWith(prefix) && !suggestions.some(suggestion => suggestion.type === 'hashtag' && suggestion.name.toLowerCase() === tag.toLowerCase()));
+ return suggestions.concat(localTags.slice(0, 4 - suggestions.length).toJS().map(tag => ({ type: 'hashtag', name: tag })));
+ } else {
+ return suggestions;
+ }
+};
+
+const normalizeSuggestions = (state, { accounts, emojis, tags, token }) => {
if (accounts) {
return accounts.map(item => ({ id: item.id, type: 'account' }));
} else if (emojis) {
return emojis.map(item => ({ ...item, type: 'emoji' }));
} else {
- return sortHashtagsByUse(state, tags.map(item => ({ ...item, type: 'hashtag' })));
+ return mergeLocalHashtagResults(sortHashtagsByUse(state, tags.map(item => ({ ...item, type: 'hashtag' }))), token.slice(1), state.get('tagHistory'));
}
};
+const updateSuggestionTags = (state, token) => {
+ const prefix = token.slice(1);
+
+ const suggestions = state.get('suggestions').toJS();
+ return state.merge({
+ suggestions: ImmutableList(mergeLocalHashtagResults(suggestions, prefix, state.get('tagHistory'))),
+ suggestion_token: token,
+ });
+};
+
export default function compose(state = initialState, action) {
switch(action.type) {
case STORE_HYDRATE:
@@ -328,6 +349,8 @@ export default function compose(state = initialState, action) {
return state.set('suggestions', ImmutableList(normalizeSuggestions(state, action))).set('suggestion_token', action.token);
case COMPOSE_SUGGESTION_SELECT:
return insertSuggestion(state, action.position, action.token, action.completion, action.path);
+ case COMPOSE_SUGGESTION_TAGS_UPDATE:
+ return updateSuggestionTags(state, action.token);
case COMPOSE_TAG_HISTORY_UPDATE:
return state.set('tagHistory', fromJS(action.tags));
case TIMELINE_DELETE:
diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js
index 8db18c5dc..08e94022f 100644
--- a/app/javascript/mastodon/reducers/user_lists.js
+++ b/app/javascript/mastodon/reducers/user_lists.js
@@ -20,6 +20,14 @@ import {
MUTES_FETCH_SUCCESS,
MUTES_EXPAND_SUCCESS,
} from '../actions/mutes';
+import {
+ DIRECTORY_FETCH_REQUEST,
+ DIRECTORY_FETCH_SUCCESS,
+ DIRECTORY_FETCH_FAIL,
+ DIRECTORY_EXPAND_REQUEST,
+ DIRECTORY_EXPAND_SUCCESS,
+ DIRECTORY_EXPAND_FAIL,
+} from 'mastodon/actions/directory';
import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
const initialState = ImmutableMap({
@@ -74,6 +82,16 @@ export default function userLists(state = initialState, action) {
return state.setIn(['mutes', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
case MUTES_EXPAND_SUCCESS:
return state.updateIn(['mutes', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['mutes', 'next'], action.next);
+ case DIRECTORY_FETCH_SUCCESS:
+ return state.setIn(['directory', 'items'], ImmutableList(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
+ case DIRECTORY_EXPAND_SUCCESS:
+ return state.updateIn(['directory', 'items'], list => list.concat(action.accounts.map(item => item.id))).setIn(['directory', 'isLoading'], false);
+ case DIRECTORY_FETCH_REQUEST:
+ case DIRECTORY_EXPAND_REQUEST:
+ return state.setIn(['directory', 'isLoading'], true);
+ case DIRECTORY_FETCH_FAIL:
+ case DIRECTORY_EXPAND_FAIL:
+ return state.setIn(['directory', 'isLoading'], false);
default:
return state;
}
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index c87654547..6f1ce9602 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -128,6 +128,7 @@ export const getAlerts = createSelector([getAlertsBase], (base) => {
base.forEach(item => {
arr.push({
message: item.get('message'),
+ message_values: item.get('message_values'),
title: item.get('title'),
key: item.get('key'),
dismissAfter: 5000,
diff --git a/app/javascript/mastodon/utils/log_out.js b/app/javascript/mastodon/utils/log_out.js
new file mode 100644
index 000000000..b43417f4b
--- /dev/null
+++ b/app/javascript/mastodon/utils/log_out.js
@@ -0,0 +1,33 @@
+import Rails from 'rails-ujs';
+
+export const logOut = () => {
+ const form = document.createElement('form');
+
+ const methodInput = document.createElement('input');
+ methodInput.setAttribute('name', '_method');
+ methodInput.setAttribute('value', 'delete');
+ methodInput.setAttribute('type', 'hidden');
+ form.appendChild(methodInput);
+
+ const csrfToken = Rails.csrfToken();
+ const csrfParam = Rails.csrfParam();
+
+ if (csrfParam && csrfToken) {
+ const csrfInput = document.createElement('input');
+ csrfInput.setAttribute('name', csrfParam);
+ csrfInput.setAttribute('value', csrfToken);
+ csrfInput.setAttribute('type', 'hidden');
+ form.appendChild(csrfInput);
+ }
+
+ const submitButton = document.createElement('input');
+ submitButton.setAttribute('type', 'submit');
+ form.appendChild(submitButton);
+
+ form.method = 'post';
+ form.action = '/auth/sign_out';
+ form.style.display = 'none';
+
+ document.body.appendChild(form);
+ submitButton.click();
+};
diff --git a/app/javascript/styles/mailer.scss b/app/javascript/styles/mailer.scss
index b4fb1d709..e25a80c04 100644
--- a/app/javascript/styles/mailer.scss
+++ b/app/javascript/styles/mailer.scss
@@ -457,6 +457,13 @@ h5 {
.status {
padding-bottom: 32px;
+ &--highlighted {
+ border: 1px solid lighten($ui-base-color, 8%);
+ border-radius: 4px;
+ padding-bottom: 16px;
+ margin-bottom: 16px;
+ }
+
.status-header {
td {
font-size: 14px;
diff --git a/app/javascript/styles/mastodon-light/diff.scss b/app/javascript/styles/mastodon-light/diff.scss
index ee8a7d265..e7114ed07 100644
--- a/app/javascript/styles/mastodon-light/diff.scss
+++ b/app/javascript/styles/mastodon-light/diff.scss
@@ -104,7 +104,8 @@ html {
.box-widget input[type="email"],
.box-widget input[type="password"],
.box-widget textarea,
-.statuses-grid .detailed-status {
+.statuses-grid .detailed-status,
+.audio-player {
border: 1px solid lighten($ui-base-color, 8%);
}
@@ -700,3 +701,10 @@ html {
.compose-form .compose-form__warning {
box-shadow: none;
}
+
+.audio-player .video-player__controls button,
+.audio-player .video-player__time-sep,
+.audio-player .video-player__time-current,
+.audio-player .video-player__time-total {
+ color: $primary-text-color;
+}
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 1a589bdc0..75a64119f 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -967,7 +967,8 @@
opacity: 1;
animation: fade 150ms linear;
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 8px;
}
@@ -1062,7 +1063,8 @@
white-space: normal;
}
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 8px;
max-width: 250px;
}
@@ -1173,7 +1175,8 @@
}
}
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 8px;
}
}
@@ -2108,13 +2111,23 @@ a.account__display-name {
padding: 0;
}
- //.column {
- // margin-top: 0;
+ .directory__list {
+ display: grid;
+ grid-gap: 10px;
+ grid-template-columns: minmax(0, 50%) minmax(0, 50%);
- // @media screen and (min-width: $no-gap-breakpoint) {
- // margin-top: 10px;
- // }
- //}
+ @media screen and (max-width: $no-gap-breakpoint) {
+ display: block;
+ }
+ }
+
+ .directory__card {
+ margin-bottom: 0;
+ }
+
+ .filter-form {
+ display: flex;
+ }
.autosuggest-textarea__textarea {
font-size: 16px;
@@ -2149,7 +2162,8 @@ a.account__display-name {
padding: 15px;
.media-gallery,
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 15px;
}
}
@@ -2191,7 +2205,8 @@ a.account__display-name {
.media-gallery,
&__action-bar,
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 10px;
}
}
@@ -2772,6 +2787,15 @@ a.account__display-name {
background: $ui-base-color;
flex: 0 1 auto;
+ h4 {
+ font-size: 12px;
+ text-transform: uppercase;
+ color: $darker-text-color;
+ padding: 10px;
+ font-weight: 500;
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
+ }
+
@media screen and (max-height: 810px) {
.trends__item:nth-child(3) {
display: none;
@@ -3456,6 +3480,14 @@ a.status-card.compact:hover {
background: rgba($base-overlay-background, 0.8);
}
}
+
+ &:disabled {
+ cursor: not-allowed;
+
+ .spoiler-button__overlay__label {
+ background: rgba($base-overlay-background, 0.5);
+ }
+ }
}
}
@@ -4971,59 +5003,6 @@ a.status-card.compact:hover {
}
/* End Media Gallery */
-/* Status Video Player */
-.status__video-player {
- background: $base-overlay-background;
- box-sizing: border-box;
- cursor: default; /* May not be needed */
- margin-top: 8px;
- overflow: hidden;
- position: relative;
-}
-
-.status__video-player-video {
- height: 100%;
- object-fit: cover;
- position: relative;
- top: 50%;
- transform: translateY(-50%);
- width: 100%;
- z-index: 1;
-}
-
-.status__video-player-expand,
-.status__video-player-mute {
- color: $primary-text-color;
- opacity: 0.8;
- position: absolute;
- right: 4px;
- text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
-}
-
-.status__video-player-spoiler {
- display: none;
- color: $primary-text-color;
- left: 4px;
- position: absolute;
- text-shadow: 0 1px 1px $base-shadow-color, 1px 0 1px $base-shadow-color;
- top: 4px;
- z-index: 100;
-
- &.status__video-player-spoiler--visible {
- display: block;
- }
-}
-
-.status__video-player-expand {
- bottom: 4px;
- z-index: 100;
-}
-
-.status__video-player-mute {
- top: 4px;
- z-index: 5;
-}
-
.detailed,
.fullscreen {
.video-player__volume__current,
@@ -5037,15 +5016,63 @@ a.status-card.compact:hover {
}
+.audio-player {
+ box-sizing: border-box;
+ position: relative;
+ background: darken($ui-base-color, 8%);
+ border-radius: 4px;
+ padding-bottom: 44px;
+
+ &.editable {
+ border-radius: 0;
+ height: 100%;
+ }
+
+ &__waveform {
+ padding: 15px 0;
+ position: relative;
+ overflow: hidden;
+
+ &::before {
+ content: "";
+ display: block;
+ position: absolute;
+ border-top: 1px solid lighten($ui-base-color, 4%);
+ width: 100%;
+ height: 0;
+ left: 0;
+ top: calc(50% + 1px);
+ }
+ }
+
+ &__progress-placeholder {
+ background-color: rgba(lighten($ui-highlight-color, 8%), 0.5);
+ }
+
+ &__wave-placeholder {
+ background-color: lighten($ui-base-color, 16%);
+ }
+
+ .video-player__controls {
+ padding: 0 15px;
+ padding-top: 10px;
+ background: darken($ui-base-color, 8%);
+ border-top: 1px solid lighten($ui-base-color, 4%);
+ border-radius: 0 0 4px 4px;
+ }
+}
+
.video-player {
overflow: hidden;
position: relative;
background: $base-shadow-color;
max-width: 100%;
border-radius: 4px;
+ box-sizing: border-box;
&.editable {
border-radius: 0;
+ height: 100% !important;
}
&:focus {
@@ -5328,28 +5355,137 @@ a.status-card.compact:hover {
}
}
-.media-spoiler-video {
- background-size: cover;
- background-repeat: no-repeat;
- background-position: center;
- cursor: pointer;
- margin-top: 8px;
- position: relative;
- border: 0;
- display: block;
-}
+.directory {
+ &__list {
+ width: 100%;
+ margin: 10px 0;
+ transition: opacity 100ms ease-in;
-.media-spoiler-video-play-icon {
- border-radius: 100px;
- color: rgba($primary-text-color, 0.8);
- font-size: 36px;
- left: 50%;
- padding: 5px;
- position: absolute;
- top: 50%;
- transform: translate(-50%, -50%);
+ &.loading {
+ opacity: 0.7;
+ }
+
+ @media screen and (max-width: $no-gap-breakpoint) {
+ margin: 0;
+ }
+ }
+
+ &__card {
+ box-sizing: border-box;
+ margin-bottom: 10px;
+
+ &__img {
+ height: 125px;
+ position: relative;
+ background: darken($ui-base-color, 12%);
+ overflow: hidden;
+
+ img {
+ display: block;
+ width: 100%;
+ height: 100%;
+ margin: 0;
+ object-fit: cover;
+ }
+ }
+
+ &__bar {
+ display: flex;
+ align-items: center;
+ background: lighten($ui-base-color, 4%);
+ padding: 10px;
+
+ &__name {
+ flex: 1 1 auto;
+ display: flex;
+ align-items: center;
+ text-decoration: none;
+ overflow: hidden;
+ }
+
+ &__relationship {
+ width: 23px;
+ min-height: 1px;
+ flex: 0 0 auto;
+ }
+
+ .avatar {
+ flex: 0 0 auto;
+ width: 48px;
+ height: 48px;
+ padding-top: 2px;
+
+ img {
+ width: 100%;
+ height: 100%;
+ display: block;
+ margin: 0;
+ border-radius: 4px;
+ background: darken($ui-base-color, 8%);
+ object-fit: cover;
+ }
+ }
+
+ .display-name {
+ margin-left: 15px;
+ text-align: left;
+
+ strong {
+ font-size: 15px;
+ color: $primary-text-color;
+ font-weight: 500;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+
+ span {
+ display: block;
+ font-size: 14px;
+ color: $darker-text-color;
+ font-weight: 400;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ }
+ }
+ }
+
+ &__extra {
+ background: $ui-base-color;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+
+ .accounts-table__count {
+ width: 33.33%;
+ flex: 0 0 auto;
+ padding: 15px 0;
+ }
+
+ .account__header__content {
+ box-sizing: border-box;
+ padding: 15px 10px;
+ border-bottom: 1px solid lighten($ui-base-color, 8%);
+ width: 100%;
+ min-height: 18px + 30px;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+
+ p {
+ display: none;
+
+ &:first-child {
+ display: inline;
+ }
+ }
+
+ br {
+ display: none;
+ }
+ }
+ }
+ }
}
-/* End Video Player */
.account-gallery__container {
display: flex;
@@ -5425,6 +5561,73 @@ a.status-card.compact:hover {
}
}
}
+
+ &.directory__section-headline {
+ background: darken($ui-base-color, 2%);
+ border-bottom-color: transparent;
+
+ a,
+ button {
+ &.active {
+ &::before {
+ display: none;
+ }
+
+ &::after {
+ border-color: transparent transparent darken($ui-base-color, 7%);
+ }
+ }
+ }
+ }
+}
+
+.filter-form {
+ background: $ui-base-color;
+
+ &__column {
+ padding: 10px 15px;
+ }
+
+ .radio-button {
+ display: block;
+ }
+}
+
+.radio-button {
+ font-size: 14px;
+ position: relative;
+ display: inline-block;
+ padding: 6px 0;
+ line-height: 18px;
+ cursor: default;
+ white-space: nowrap;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ cursor: pointer;
+
+ input[type=radio],
+ input[type=checkbox] {
+ display: none;
+ }
+
+ &__input {
+ display: inline-block;
+ position: relative;
+ border: 1px solid $ui-primary-color;
+ box-sizing: border-box;
+ width: 18px;
+ height: 18px;
+ flex: 0 0 auto;
+ margin-right: 10px;
+ top: -1px;
+ border-radius: 50%;
+ vertical-align: middle;
+
+ &.checked {
+ border-color: lighten($ui-highlight-color, 8%);
+ background: lighten($ui-highlight-color, 8%);
+ }
+ }
}
::-webkit-scrollbar-thumb {
diff --git a/app/javascript/styles/mastodon/containers.scss b/app/javascript/styles/mastodon/containers.scss
index 2b6794ee2..e769c495b 100644
--- a/app/javascript/styles/mastodon/containers.scss
+++ b/app/javascript/styles/mastodon/containers.scss
@@ -763,6 +763,24 @@
}
}
+ .directory__list {
+ display: grid;
+ grid-gap: 10px;
+ grid-template-columns: minmax(0, 50%) minmax(0, 50%);
+
+ @media screen and (max-width: $no-gap-breakpoint) {
+ display: block;
+ }
+
+ .icon-button {
+ font-size: 18px;
+ }
+ }
+
+ .directory__card {
+ margin-bottom: 0;
+ }
+
.card-grid {
display: flex;
flex-wrap: wrap;
diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb
index 1f2b40c15..345060462 100644
--- a/app/lib/activitypub/activity/delete.rb
+++ b/app/lib/activitypub/activity/delete.rb
@@ -70,7 +70,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
end
def delete_now!
- RemoveStatusService.new.call(@status)
+ RemoveStatusService.new.call(@status, redraft: false)
end
def payload
diff --git a/app/lib/activitypub/adapter.rb b/app/lib/activitypub/adapter.rb
index a1d84de2f..1c58be8c0 100644
--- a/app/lib/activitypub/adapter.rb
+++ b/app/lib/activitypub/adapter.rb
@@ -20,6 +20,7 @@ class ActivityPub::Adapter < ActiveModelSerializers::Adapter::Base
focal_point: { 'toot' => 'http://joinmastodon.org/ns#', 'focalPoint' => { '@container' => '@list', '@id' => 'toot:focalPoint' } },
identity_proof: { 'toot' => 'http://joinmastodon.org/ns#', 'IdentityProof' => 'toot:IdentityProof' },
blurhash: { 'toot' => 'http://joinmastodon.org/ns#', 'blurhash' => 'toot:blurhash' },
+ discoverable: { 'toot' => 'http://joinmastodon.org/ns#', 'discoverable' => 'toot:discoverable' },
}.freeze
def self.default_key_transform
diff --git a/app/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 8f3a4ab3a..b41004acc 100644
--- a/app/mailers/user_mailer.rb
+++ b/app/mailers/user_mailer.rb
@@ -5,6 +5,7 @@ class UserMailer < Devise::Mailer
helper :application
helper :instance
+ helper :statuses
add_template_helper RoutingHelper
@@ -79,10 +80,11 @@ class UserMailer < Devise::Mailer
end
end
- def warning(user, warning)
+ def warning(user, warning, status_ids = nil)
@resource = user
@warning = warning
@instance = Rails.configuration.x.local_domain
+ @statuses = Status.where(id: status_ids).includes(:account) if status_ids.is_a?(Array)
I18n.with_locale(@resource.locale || I18n.default_locale) do
mail to: @resource.email,
diff --git a/app/models/account.rb b/app/models/account.rb
index 392cc625f..8c9388b95 100644
--- a/app/models/account.rb
+++ b/app/models/account.rb
@@ -51,7 +51,6 @@
class Account < ApplicationRecord
USERNAME_RE = /[a-z0-9_]+([a-z0-9_\.-]+[a-z0-9_]+)?/i
MENTION_RE = /(?<=^|[^\/[:word:]])@((#{USERNAME_RE})(?:@[a-z0-9\.\-]+[a-z0-9]+)?)/i
- MIN_FOLLOWERS_DISCOVERY = 10
include AccountAssociations
include AccountAvatar
@@ -100,11 +99,13 @@ class Account < ApplicationRecord
scope :matches_display_name, ->(value) { where(arel_table[:display_name].matches("#{value}%")) }
scope :matches_domain, ->(value) { where(arel_table[:domain].matches("%#{value}%")) }
scope :searchable, -> { without_suspended.where(moved_to_account_id: nil) }
- scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).joins(:account_stat).where(AccountStat.arel_table[:followers_count].gteq(MIN_FOLLOWERS_DISCOVERY)) }
+ scope :discoverable, -> { searchable.without_silenced.where(discoverable: true).left_outer_joins(:account_stat) }
scope :tagged_with, ->(tag) { joins(:accounts_tags).where(accounts_tags: { tag_id: tag }) }
- scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc')) }
+ scope :by_recent_status, -> { order(Arel.sql('(case when account_stats.last_status_at is null then 1 else 0 end) asc, account_stats.last_status_at desc, accounts.id desc')) }
scope :popular, -> { order('account_stats.followers_count desc') }
scope :by_domain_and_subdomains, ->(domain) { where(domain: domain).or(where(arel_table[:domain].matches('%.' + domain))) }
+ scope :not_excluded_by_account, ->(account) { where.not(id: account.excluded_from_timeline_account_ids) }
+ scope :not_domain_blocked_by_account, ->(account) { where(arel_table[:domain].eq(nil).or(arel_table[:domain].not_in(account.excluded_from_timeline_domains))) }
delegate :email,
:unconfirmed_email,
diff --git a/app/models/admin/account_action.rb b/app/models/admin/account_action.rb
index bdbd342fb..c7da8b52c 100644
--- a/app/models/admin/account_action.rb
+++ b/app/models/admin/account_action.rb
@@ -19,20 +19,25 @@ class Admin::AccountAction
:report_id,
:warning_preset_id
- attr_reader :warning, :send_email_notification
+ attr_reader :warning, :send_email_notification, :include_statuses
def send_email_notification=(value)
@send_email_notification = ActiveModel::Type::Boolean.new.cast(value)
end
+ def include_statuses=(value)
+ @include_statuses = ActiveModel::Type::Boolean.new.cast(value)
+ end
+
def save!
ApplicationRecord.transaction do
process_action!
process_warning!
end
- queue_email!
+ process_email!
process_reports!
+ process_queue!
end
def report
@@ -110,7 +115,6 @@ class Admin::AccountAction
authorize(target_account, :suspend?)
log_action(:suspend, target_account)
target_account.suspend!
- queue_suspension_worker!
end
def text_for_warning
@@ -121,16 +125,22 @@ class Admin::AccountAction
Admin::SuspensionWorker.perform_async(target_account.id)
end
- def queue_email!
- return unless warnable?
+ def process_queue!
+ queue_suspension_worker! if type == 'suspend'
+ end
- UserMailer.warning(target_account.user, warning).deliver_later!
+ def process_email!
+ UserMailer.warning(target_account.user, warning, status_ids).deliver_now! if warnable?
end
def warnable?
send_email_notification && target_account.local?
end
+ def status_ids
+ @report.status_ids if @report && include_statuses
+ end
+
def warning_preset
@warning_preset ||= AccountWarningPreset.find(warning_preset_id) if warning_preset_id.present?
end
diff --git a/app/models/feed.rb b/app/models/feed.rb
index 0e8943ff8..36e0c1e0a 100644
--- a/app/models/feed.rb
+++ b/app/models/feed.rb
@@ -9,6 +9,11 @@ class Feed
end
def get(limit, max_id = nil, since_id = nil, min_id = nil)
+ limit = limit.to_i
+ max_id = max_id.to_i if max_id.present?
+ since_id = since_id.to_i if since_id.present?
+ min_id = min_id.to_i if min_id.present?
+
from_redis(limit, max_id, since_id, min_id)
end
diff --git a/app/models/form/status_batch.rb b/app/models/form/status_batch.rb
index 933dfdaca..e09cc2594 100644
--- a/app/models/form/status_batch.rb
+++ b/app/models/form/status_batch.rb
@@ -34,7 +34,8 @@ class Form::StatusBatch
def delete_statuses
Status.where(id: status_ids).reorder(nil).find_each do |status|
- RemovalWorker.perform_async(status.id)
+ status.discard
+ RemovalWorker.perform_async(status.id, redraft: false)
Tombstone.find_or_create_by(uri: status.uri, account: status.account, by_moderator: true)
log_action :destroy, status
end
diff --git a/app/models/media_attachment.rb b/app/models/media_attachment.rb
index fe76b36aa..44f8e6be6 100644
--- a/app/models/media_attachment.rb
+++ b/app/models/media_attachment.rb
@@ -28,12 +28,12 @@ class MediaAttachment < ApplicationRecord
IMAGE_FILE_EXTENSIONS = %w(.jpg .jpeg .png .gif).freeze
VIDEO_FILE_EXTENSIONS = %w(.webm .mp4 .m4v .mov).freeze
- AUDIO_FILE_EXTENSIONS = %w(.ogg .oga .mp3 .wav .flac .opus .aac .m4a .3gp).freeze
+ AUDIO_FILE_EXTENSIONS = %w(.ogg .oga .mp3 .wav .flac .opus .aac .m4a .3gp .wma).freeze
IMAGE_MIME_TYPES = %w(image/jpeg image/png image/gif).freeze
VIDEO_MIME_TYPES = %w(video/webm video/mp4 video/quicktime video/ogg).freeze
VIDEO_CONVERTIBLE_MIME_TYPES = %w(video/webm video/quicktime).freeze
- AUDIO_MIME_TYPES = %w(audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/ogg audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/3gpp).freeze
+ AUDIO_MIME_TYPES = %w(audio/wave audio/wav audio/x-wav audio/x-pn-wave audio/ogg audio/mpeg audio/mp3 audio/webm audio/flac audio/aac audio/m4a audio/x-m4a audio/mp4 audio/3gpp video/x-ms-asf).freeze
BLURHASH_OPTIONS = {
x_comp: 4,
diff --git a/app/models/remote_follow.rb b/app/models/remote_follow.rb
index 93df11724..52dd3f67b 100644
--- a/app/models/remote_follow.rb
+++ b/app/models/remote_follow.rb
@@ -6,7 +6,7 @@ class RemoteFollow
attr_accessor :acct, :addressable_template
- validates :acct, presence: true
+ validates :acct, presence: true, domain: { acct: true }
def initialize(attrs = {})
@acct = normalize_acct(attrs[:acct])
@@ -21,7 +21,7 @@ class RemoteFollow
end
def subscribe_address_for(account)
- addressable_template.expand(uri: account.local_username_and_domain).to_s
+ addressable_template.expand(uri: ActivityPub::TagManager.instance.uri_for(account)).to_s
end
def interact_address_for(status)
@@ -44,6 +44,8 @@ class RemoteFollow
end
[username, domain].compact.join('@')
+ rescue Addressable::URI::InvalidURIError
+ value
end
def fetch_template!
diff --git a/app/models/report.rb b/app/models/report.rb
index 5192ceef7..1e707ff1c 100644
--- a/app/models/report.rb
+++ b/app/models/report.rb
@@ -43,7 +43,7 @@ class Report < ApplicationRecord
end
def statuses
- Status.where(id: status_ids).includes(:account, :media_attachments, :mentions)
+ Status.with_discarded.where(id: status_ids).includes(:account, :media_attachments, :mentions)
end
def media_attachments
diff --git a/app/models/status.rb b/app/models/status.rb
index 32dade30b..5b92ce7cf 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -22,15 +22,19 @@
# application_id :bigint(8)
# in_reply_to_account_id :bigint(8)
# poll_id :bigint(8)
+# deleted_at :datetime
#
class Status < ApplicationRecord
before_destroy :unlink_from_conversations
+ include Discard::Model
include Paginable
include Cacheable
include StatusThreadingConcern
+ self.discard_column = :deleted_at
+
# If `override_timestamps` is set at creation time, Snowflake ID creation
# will be based on current time instead of `created_at`
attr_accessor :override_timestamps
@@ -73,7 +77,7 @@ class Status < ApplicationRecord
accepts_nested_attributes_for :poll
- default_scope { recent }
+ default_scope { recent.kept }
scope :recent, -> { reorder(id: :desc) }
scope :remote, -> { where(local: false).where.not(uri: nil) }
diff --git a/app/models/tag.rb b/app/models/tag.rb
index 5094d973d..945e3a3c6 100644
--- a/app/models/tag.rb
+++ b/app/models/tag.rb
@@ -114,7 +114,7 @@ class Tag < ApplicationRecord
class << self
def find_or_create_by_names(name_or_names)
Array(name_or_names).map(&method(:normalize)).uniq { |str| str.mb_chars.downcase.to_s }.map do |normalized_name|
- tag = matching_name(normalized_name).first || create(name: normalized_name)
+ tag = matching_name(normalized_name).first || create!(name: normalized_name)
yield tag if block_given?
diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb
index 0bd7aed2e..222e17c99 100644
--- a/app/serializers/activitypub/actor_serializer.rb
+++ b/app/serializers/activitypub/actor_serializer.rb
@@ -6,12 +6,14 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
context :security
context_extensions :manually_approves_followers, :featured, :also_known_as,
- :moved_to, :property_value, :hashtag, :emoji, :identity_proof
+ :moved_to, :property_value, :hashtag, :emoji, :identity_proof,
+ :discoverable
attributes :id, :type, :following, :followers,
:inbox, :outbox, :featured,
:preferred_username, :name, :summary,
- :url, :manually_approves_followers
+ :url, :manually_approves_followers,
+ :discoverable
has_one :public_key, serializer: ActivityPub::PublicKeySerializer
diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb
index 272e3eb9c..75b6cf13b 100644
--- a/app/serializers/rest/account_serializer.rb
+++ b/app/serializers/rest/account_serializer.rb
@@ -5,7 +5,7 @@ class REST::AccountSerializer < ActiveModel::Serializer
attributes :id, :username, :acct, :display_name, :locked, :bot, :created_at,
:note, :url, :avatar, :avatar_static, :header, :header_static,
- :followers_count, :following_count, :statuses_count
+ :followers_count, :following_count, :statuses_count, :last_status_at
has_one :moved_to_account, key: :moved, serializer: REST::AccountSerializer, if: :moved_and_not_nested?
has_many :emojis, serializer: REST::CustomEmojiSerializer
diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb
index 603e27ed9..cef658e19 100644
--- a/app/services/activitypub/process_account_service.rb
+++ b/app/services/activitypub/process_account_service.rb
@@ -83,6 +83,7 @@ class ActivityPub::ProcessAccountService < BaseService
@account.fields = property_values || {}
@account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) }
@account.actor_type = actor_type
+ @account.discoverable = @json['discoverable'] || false
end
def set_fetchable_attributes!
diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index 6df8d4769..3638134be 100644
--- a/app/services/batched_remove_status_service.rb
+++ b/app/services/batched_remove_status_service.rb
@@ -8,7 +8,7 @@ class BatchedRemoveStatusService < BaseService
# Dispatch Salmon deletes, unique per domain, of the deleted statuses, but only local ones
# Remove statuses from home feeds
# Push delete events to streaming API for home feeds and public feeds
- # @param [Status] statuses A preferably batched array of statuses
+ # @param [Enumerable
] statuses A preferably batched array of statuses
# @param [Hash] options
# @option [Boolean] :skip_side_effects
def call(statuses, **options)
diff --git a/app/services/remove_status_service.rb b/app/services/remove_status_service.rb
index 91c934181..685c1d4bf 100644
--- a/app/services/remove_status_service.rb
+++ b/app/services/remove_status_service.rb
@@ -4,6 +4,11 @@ class RemoveStatusService < BaseService
include Redisable
include Payloadable
+ # Delete a status
+ # @param [Status] status
+ # @param [Hash] options
+ # @option [Boolean] :redraft
+ # @options [Boolean] :original_removed
def call(status, **options)
@payload = Oj.dump(event: :delete, payload: status.id.to_s)
@status = status
@@ -24,6 +29,7 @@ class RemoveStatusService < BaseService
remove_from_public
remove_from_media if status.media_attachments.any?
remove_from_spam_check
+ remove_media
@status.destroy!
else
@@ -143,6 +149,12 @@ class RemoveStatusService < BaseService
redis.publish('timeline:public:local:media', @payload) if @status.local?
end
+ def remove_media
+ return if @options[:redraft]
+
+ @status.media_attachments.destroy_all
+ end
+
def remove_from_spam_check
redis.zremrangebyscore("spam_check:#{@status.account_id}", @status.id, @status.id)
end
diff --git a/app/validators/domain_validator.rb b/app/validators/domain_validator.rb
index ae07f1798..6e4a854ff 100644
--- a/app/validators/domain_validator.rb
+++ b/app/validators/domain_validator.rb
@@ -4,14 +4,22 @@ class DomainValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
return if value.blank?
- record.errors.add(attribute, I18n.t('domain_validator.invalid_domain')) unless compliant?(value)
+ domain = begin
+ if options[:acct]
+ value.split('@').last
+ else
+ value
+ end
+ end
+
+ record.errors.add(attribute, I18n.t('domain_validator.invalid_domain')) unless compliant?(domain)
end
private
def compliant?(value)
Addressable::URI.new.tap { |uri| uri.host = value }
- rescue Addressable::URI::InvalidURIError
+ rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
false
end
end
diff --git a/app/validators/email_mx_validator.rb b/app/validators/email_mx_validator.rb
index 96fbedcfc..9b5009966 100644
--- a/app/validators/email_mx_validator.rb
+++ b/app/validators/email_mx_validator.rb
@@ -14,6 +14,7 @@ class EmailMxValidator < ActiveModel::Validator
return true if domain.nil?
+ domain = TagManager.instance.normalize_domain(domain)
hostnames = []
ips = []
@@ -29,6 +30,8 @@ class EmailMxValidator < ActiveModel::Validator
end
ips.empty? || on_blacklist?(hostnames + ips)
+ rescue Addressable::URI::InvalidURIError
+ true
end
def on_blacklist?(values)
diff --git a/app/views/admin/account_actions/new.html.haml b/app/views/admin/account_actions/new.html.haml
index 97286c8e5..20fbeef33 100644
--- a/app/views/admin/account_actions/new.html.haml
+++ b/app/views/admin/account_actions/new.html.haml
@@ -13,6 +13,10 @@
.fields-group
= f.input :send_email_notification, as: :boolean, wrapper: :with_label
+ - if params[:report_id].present?
+ .fields-group
+ = f.input :include_statuses, as: :boolean, wrapper: :with_label
+
%hr.spacer/
- unless @warning_presets.empty?
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 2fe1feb55..f044d9f86 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -103,7 +103,7 @@
%li
= feature_hint(t('admin.dashboard.authorized_fetch_mode'), @authorized_fetch)
%li
- = feature_hint(t('admin.dashboard.whitelist_mode'), @whitelist_mode)
+ = feature_hint(t('admin.dashboard.whitelist_mode'), @whitelist_enabled)
%li
= feature_hint('LDAP', @ldap_enabled)
%li
diff --git a/app/views/admin/reports/_status.html.haml b/app/views/admin/reports/_status.html.haml
index 9376db7ff..6facc0a56 100644
--- a/app/views/admin/reports/_status.html.haml
+++ b/app/views/admin/reports/_status.html.haml
@@ -16,11 +16,14 @@
- video = status.proper.media_attachments.first
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description
- else
- = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
+ = react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.proper.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif, media: status.proper.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json }
.detailed-status__meta
= link_to ActivityPub::TagManager.instance.url_for(status), class: 'detailed-status__datetime', target: stream_link_target, rel: 'noopener' do
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
+ - if status.discarded?
+ ·
+ %span.negative-hint= t('admin.statuses.deleted')
·
- if status.reblog?
= fa_icon('retweet fw')
diff --git a/app/views/admin/tags/show.html.haml b/app/views/admin/tags/show.html.haml
index c3779d48c..f9677874a 100644
--- a/app/views/admin/tags/show.html.haml
+++ b/app/views/admin/tags/show.html.haml
@@ -38,8 +38,10 @@
.table-wrapper
%table.table
%tbody
+ - total = @usage_by_domain.sum(&:statuses_count).to_f
+
- @usage_by_domain.each do |(domain, count)|
%tr
%th= domain || site_hostname
- %td= number_to_percentage((count / @tag.history[0][:uses].to_f) * 100)
+ %td= number_to_percentage((count / total) * 100, precision: 1)
%td= number_with_delimiter count
diff --git a/app/views/application/_card.html.haml b/app/views/application/_card.html.haml
index 00254c40c..8719ce484 100644
--- a/app/views/application/_card.html.haml
+++ b/app/views/application/_card.html.haml
@@ -9,7 +9,7 @@
= image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
.display-name
- %span{id: "default_account_display_name", style: "display:none;"}= account.username
+ %span{ id: "default_account_display_name", style: "display: none" }= account.username
%bdi
%strong.emojify.p-name= display_name(account, custom_emojify: true)
%span
diff --git a/app/views/directories/index.html.haml b/app/views/directories/index.html.haml
index a8aa68cc4..30daa6bb1 100644
--- a/app/views/directories/index.html.haml
+++ b/app/views/directories/index.html.haml
@@ -14,58 +14,43 @@
%h1= t('directories.explore_mastodon', title: site_title)
%p= t('directories.explanation')
-.grid
- .column-0
- - if @accounts.empty?
- = nothing_here
- - else
- .directory
- %table.accounts-table
- %tbody
- - @accounts.each do |account|
- %tr
- %td= account_link_to account
- %td.accounts-table__count.optional
- = number_to_human account.statuses_count, strip_insignificant_zeros: true
- %small= t('accounts.posts', count: account.statuses_count).downcase
- %td.accounts-table__count.optional
- = number_to_human account.followers_count, strip_insignificant_zeros: true
- %small= t('accounts.followers', count: account.followers_count).downcase
- %td.accounts-table__count
- - if account.last_status_at.present?
- %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
- - else
- \-
- %small= t('accounts.last_active')
+- if @accounts.empty?
+ = nothing_here
+- else
+ .directory__list
+ - @accounts.each do |account|
+ .directory__card
+ .directory__card__img
+ = image_tag account.header.url, alt: ''
+ .directory__card__bar
+ = link_to TagManager.instance.url_for(account), class: 'directory__card__bar__name' do
+ .avatar
+ = image_tag account.avatar.url, alt: '', width: 48, height: 48, class: 'u-photo'
- = paginate @accounts
+ .display-name
+ %span{ id: "default_account_display_name", style: "display: none" }= account.username
+ %bdi
+ %strong.emojify.p-name= display_name(account, custom_emojify: true)
+ %span= acct(account)
+ .directory__card__bar__relationship.account__relationship
+ = minimal_account_action_button(account)
- .column-1
- - if user_signed_in?
- .box-widget.notice-widget
- - if current_account.discoverable?
- - if current_account.followers_count < Account::MIN_FOLLOWERS_DISCOVERY
- %p= t('directories.enabled_but_waiting', min_followers: Account::MIN_FOLLOWERS_DISCOVERY)
- - else
- %p= t('directories.enabled')
- - else
- %p= t('directories.how_to_enable')
+ .directory__card__extra
+ .account__header__content.emojify= Formatter.instance.simplified_format(account, custom_emojify: true)
- = link_to settings_profile_path do
- = t('settings.edit_profile')
- = fa_icon 'chevron-right fw'
+ .directory__card__extra
+ .accounts-table__count
+ = number_to_human account.statuses_count, strip_insignificant_zeros: true
+ %small= t('accounts.posts', count: account.statuses_count).downcase
+ .accounts-table__count
+ = number_to_human account.followers_count, strip_insignificant_zeros: true
+ %small= t('accounts.followers', count: account.followers_count).downcase
+ .accounts-table__count
+ - if account.last_status_at.present?
+ %time.time-ago{ datetime: account.last_status_at.iso8601, title: l(account.last_status_at) }= l account.last_status_at
+ - else
+ = t('invites.expires_in_prompt')
- - if @tags.empty? && !user_signed_in?
- .nothing-here
- - else
- - @tags.each do |tag|
- .directory__tag{ class: tag.id == @tag&.id ? 'active' : nil }
- = link_to explore_hashtag_path(tag) do
- %h4
- = fa_icon 'hashtag'
- = tag.name
- %small= t('directories.people', count: tag.accounts_count)
+ %small= t('accounts.last_active')
- .avatar-stack
- - tag.cached_sample_accounts.each do |account|
- = image_tag current_account&.user&.setting_auto_play_gif ? account.avatar_original_url : account.avatar_static_url, width: 48, height: 48, alt: '', class: 'account__avatar'
+ = paginate @accounts
diff --git a/app/views/errors/400.html.haml b/app/views/errors/400.html.haml
new file mode 100644
index 000000000..11fbdd40c
--- /dev/null
+++ b/app/views/errors/400.html.haml
@@ -0,0 +1,5 @@
+- content_for :page_title do
+ = t('errors.400')
+
+- content_for :content do
+ = t('errors.400')
diff --git a/app/views/errors/406.html.haml b/app/views/errors/406.html.haml
new file mode 100644
index 000000000..0ef815df3
--- /dev/null
+++ b/app/views/errors/406.html.haml
@@ -0,0 +1,5 @@
+- content_for :page_title do
+ = t('errors.406')
+
+- content_for :content do
+ = t('errors.406')
diff --git a/app/views/errors/503.html.haml b/app/views/errors/503.html.haml
new file mode 100644
index 000000000..b0c895aa5
--- /dev/null
+++ b/app/views/errors/503.html.haml
@@ -0,0 +1,5 @@
+- content_for :page_title do
+ = t('errors.503')
+
+- content_for :content do
+ = t('errors.503')
diff --git a/app/views/notification_mailer/_status.html.haml b/app/views/notification_mailer/_status.html.haml
index 57b5688bd..40f3aa88a 100644
--- a/app/views/notification_mailer/_status.html.haml
+++ b/app/views/notification_mailer/_status.html.haml
@@ -1,4 +1,5 @@
- i ||= 0
+- highlighted ||= false
%table.email-table{ cellspacing: 0, cellpadding: 0, dir: 'ltr' }
%tbody
@@ -14,7 +15,7 @@
%table.column{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
- %td.column-cell.padded.status
+ %td.column-cell.padded.status{ class: highlighted ? 'status--highlighted' : '' }
%table.status-header{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
@@ -32,5 +33,10 @@
%div{ dir: rtl_status?(status) ? 'rtl' : 'ltr' }
= Formatter.instance.format(status)
+ - if status.media_attachments.size > 0
+ %p
+ - status.media_attachments.each do |a|
+ = link_to medium_url(a), medium_url(a)
+
%p.status-footer
= link_to l(status.created_at), web_url("statuses/#{status.id}")
diff --git a/app/views/settings/profiles/show.html.haml b/app/views/settings/profiles/show.html.haml
index f8a8fddd3..f042011d6 100644
--- a/app/views/settings/profiles/show.html.haml
+++ b/app/views/settings/profiles/show.html.haml
@@ -28,7 +28,7 @@
- if Setting.profile_directory
.fields-group
- = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable_html', min_followers: Account::MIN_FOLLOWERS_DISCOVERY, path: explore_path), recommended: true
+ = f.input :discoverable, as: :boolean, wrapper: :with_label, hint: t('simple_form.hints.defaults.discoverable'), recommended: true
%hr.spacer/
diff --git a/app/views/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index 8686c2033..12f03ccdd 100644
--- a/app/views/statuses/_detailed_status.html.haml
+++ b/app/views/statuses/_detailed_status.html.haml
@@ -27,10 +27,14 @@
= render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
- if !status.media_attachments.empty?
- - if status.media_attachments.first.audio_or_video?
+ - if status.media_attachments.first.video?
- video = status.media_attachments.first
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 670, height: 380, detailed: true, inline: true, alt: video.description do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
+ - elsif status.media_attachments.first.audio?
+ - audio = status.media_attachments.first
+ = react_component :audio, src: audio.file.url(:original), height: 130, alt: audio.description, preload: true, duration: audio.file.meta.dig(:original, :duration) do
+ = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- else
= react_component :media_gallery, height: 380, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, standalone: true, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, 'reduceMotion': current_account&.user&.setting_reduce_motion, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
diff --git a/app/views/statuses/_simple_status.html.haml b/app/views/statuses/_simple_status.html.haml
index 38fde1be8..ca3c8fdd5 100644
--- a/app/views/statuses/_simple_status.html.haml
+++ b/app/views/statuses/_simple_status.html.haml
@@ -31,10 +31,14 @@
= render partial: 'statuses/poll', locals: { status: status, poll: status.preloadable_poll, autoplay: autoplay }
- if !status.media_attachments.empty?
- - if status.media_attachments.first.audio_or_video?
+ - if status.media_attachments.first.video?
- video = status.media_attachments.first
= react_component :video, src: video.file.url(:original), preview: video.file.url(:small), blurhash: video.blurhash, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, width: 610, height: 343, inline: true, alt: video.description do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
+ - elsif status.media_attachments.first.audio?
+ - audio = status.media_attachments.first
+ = react_component :audio, src: audio.file.url(:original), height: 110, alt: audio.description, duration: audio.file.meta.dig(:original, :duration) do
+ = render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
- else
= react_component :media_gallery, height: 343, sensitive: !current_account&.user&.show_all_media? && status.sensitive? || current_account&.user&.hide_all_media?, 'autoPlayGif': current_account&.user&.setting_auto_play_gif || autoplay, media: status.media_attachments.map { |a| ActiveModelSerializers::SerializableResource.new(a, serializer: REST::MediaAttachmentSerializer).as_json } do
= render partial: 'statuses/attachment_list', locals: { attachments: status.media_attachments }
diff --git a/app/views/user_mailer/warning.html.haml b/app/views/user_mailer/warning.html.haml
index 72ea5e5d2..1105f2062 100644
--- a/app/views/user_mailer/warning.html.haml
+++ b/app/views/user_mailer/warning.html.haml
@@ -42,6 +42,14 @@
- unless @warning.text.blank?
= Formatter.instance.linkify(@warning.text)
+ - unless @statuses&.empty?
+ %p
+ %strong= t('user_mailer.warning.statuses')
+
+- unless @statuses&.empty?
+ - @statuses.each_with_index do |status, i|
+ = render 'notification_mailer/status', status: status, i: i + 1, highlighted: true
+
%table.email-table{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
@@ -50,7 +58,7 @@
%table.content-section{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
- %td.content-cell
+ %td.content-cell{ class: @statuses.empty? ? '' : 'content-start' }
%table.column{ cellspacing: 0, cellpadding: 0 }
%tbody
%tr
@@ -61,3 +69,20 @@
%td.button-primary
= link_to about_more_url do
%span= t 'user_mailer.warning.review_server_policies'
+
+%table.email-table{ cellspacing: 0, cellpadding: 0 }
+ %tbody
+ %tr
+ %td.email-body
+ .email-container
+ %table.content-section{ cellspacing: 0, cellpadding: 0 }
+ %tbody
+ %tr
+ %td.content-cell
+ .email-row
+ .col-6
+ %table.column{ cellspacing: 0, cellpadding: 0 }
+ %tbody
+ %tr
+ %td.column-cell.text-center
+ %p= t 'user_mailer.warning.get_in_touch', instance: @instance
diff --git a/app/views/user_mailer/warning.text.erb b/app/views/user_mailer/warning.text.erb
index b4f2402cb..45ad3b64d 100644
--- a/app/views/user_mailer/warning.text.erb
+++ b/app/views/user_mailer/warning.text.erb
@@ -7,3 +7,16 @@
<% end %>
<%= @warning.text %>
+<% unless @statuses&.empty? %>
+<%= t('user_mailer.warning.statuses') %>
+
+<% @statuses.each do |status| %>
+
+<%= render 'notification_mailer/status', status: status %>
+---
+<% end %>
+<% else %>
+---
+<% end %>
+
+<%= t 'user_mailer.warning.get_in_touch', instance: @instance %>
diff --git a/app/workers/removal_worker.rb b/app/workers/removal_worker.rb
index 19a660dd3..2a1eaa89b 100644
--- a/app/workers/removal_worker.rb
+++ b/app/workers/removal_worker.rb
@@ -3,8 +3,8 @@
class RemovalWorker
include Sidekiq::Worker
- def perform(status_id)
- RemoveStatusService.new.call(Status.find(status_id))
+ def perform(status_id, options = {})
+ RemoveStatusService.new.call(Status.with_discarded.find(status_id), **options.symbolize_keys)
rescue ActiveRecord::RecordNotFound
true
end
diff --git a/config/initializers/content_security_policy.rb b/config/initializers/content_security_policy.rb
index 895768ef1..d49fcfa99 100644
--- a/config/initializers/content_security_policy.rb
+++ b/config/initializers/content_security_policy.rb
@@ -34,10 +34,12 @@ Rails.application.config.content_security_policy do |p|
webpacker_urls = %w(ws http).map { |protocol| "#{protocol}#{Webpacker.dev_server.https? ? 's' : ''}://#{Webpacker.dev_server.host_with_port}" }
p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url, *webpacker_urls
- p.script_src :self, :blob, :unsafe_inline, :unsafe_eval, assets_host
+ p.script_src :self, :unsafe_inline, :unsafe_eval, assets_host
+ p.worker_src :self, :blob, assets_host
else
p.connect_src :self, :data, :blob, assets_host, media_host, Rails.configuration.x.streaming_api_base_url
- p.script_src :self, :blob, assets_host
+ p.script_src :self, assets_host
+ p.worker_src :self, :blob, assets_host
end
end
diff --git a/config/locales/co.yml b/config/locales/co.yml
index 9f0ca8b2c..94c363511 100644
--- a/config/locales/co.yml
+++ b/config/locales/co.yml
@@ -45,6 +45,8 @@ co:
what_is_mastodon: Quale hè Mastodon?
accounts:
choices_html: "%{name} ricumanda:"
+ endorsements_hint: Pudete appughjà i conti chì siguitate dapoi l'interfaccia web, è saranu mustrati quì.
+ featured_tags_hint: Pudete mette in mostra qualchì hashtag chì saranu affissatu quì.
follow: Siguità
followers:
one: Abbunatu·a
@@ -421,6 +423,13 @@ co:
custom_css:
desc_html: Mudificà l'apparenza cù CSS caricatu nant'à ogni pagina
title: CSS persunalizatu
+ domain_blocks:
+ all: À tutti
+ disabled: À nimu
+ title: Mustrà blucchime di duminiu
+ users: À l'utilizatori lucali cunnettati
+ domain_blocks_rationale:
+ title: Vede ragiò
hero:
desc_html: Affissatu nant’a pagina d’accolta. Ricumandemu almenu 600x100px. S’ellu ùn hè micca definiti, a vignetta di u servore sarà usata
title: Ritrattu di cuprendula
@@ -628,6 +637,23 @@ co:
people:
one: "%{count} persona"
other: "%{count} persone"
+ domain_blocks:
+ blocked_domains: Lista di dumini bluccati è limitati
+ description: Quessa ghjè a lista di i servori limitati da o cù quelli %{instance} righjetta a federazione.
+ domain: Duminiu
+ media_block: Blucchime di media
+ no_domain_blocks: "(Nisun blucchime di duminiu)"
+ severity: Severità
+ severity_legend:
+ media_block: I fugliali media chì venenu sa stu servore ùn saranu mai ricuperati, cunservati, o affissati à l'utilizatore.
+ silence: I conti nant'à i servori silenzati ponu esse trovi, siguitati è spartuti/favurizati, mà i statuti ùn saranu micca in e linee pubbliche, è l'utilizatori lucali ch'ùn sò micca abbunati à sti conti ùn anu micca da riceve e so nutificazione.
+ suspension: U cuntinutu da i servori suspesi ùn hè mai cunservatu o affissatu, è u cuntinutu di stu servore ùn li hè micca mandatu. L'interazzione da sti servori suspesi sò ignurate.
+ suspension_disclaimer: I servori suspesi ponu ognitantu ricuperà i cuntinuti pubblichi da stu servore.
+ title: Severità
+ show_rationale: Vede ragiò
+ silence: Silenziu
+ suspension: Suspensione
+ title: Lista di servori bluccati da %{instance}
domain_validator:
invalid_domain: ùn hè micca un nome di duminiu currettu
errors:
@@ -664,6 +690,7 @@ co:
add_new: Aghjunghje
errors:
limit: Avete digià messu in mostra u numeru massimale di hashtag
+ hint_html: "Quale sò i hashtag in mostra? Sò messi in vista nant'à u vostru prufile pubblicu è permettenu à a ghjente di vede i vostri statuti ch'annu stu hashtag. Sò una bona manere di mustrà e vostre opere creative o i prughjetti à longu termine."
filters:
contexts:
home: Accolta
@@ -684,6 +711,7 @@ co:
developers: Sviluppatori
more: Di più…
resources: Risorze
+ trending_now: Tindenze d'avà
generic:
all: Tuttu
changes_saved_msg: Cambiamenti salvati!
diff --git a/config/locales/de.yml b/config/locales/de.yml
index 534ccadbf..1e3e78878 100644
--- a/config/locales/de.yml
+++ b/config/locales/de.yml
@@ -45,6 +45,8 @@ de:
what_is_mastodon: Was ist Mastodon?
accounts:
choices_html: "%{name} empfiehlt:"
+ endorsements_hint: Du kannst Personen unterstützen, die du von der Web-Schnittstelle folgen kannst, und sie werden hier angezeigt.
+ featured_tags_hint: Du kannst spezifische Hashtags, die hier angezeigt werden, angeben.
follow: Folgen
followers:
one: Folger_innen
@@ -421,6 +423,13 @@ de:
custom_css:
desc_html: Verändere das Aussehen mit CSS, dass auf jeder Seite geladen wird
title: Benutzerdefiniertes CSS
+ domain_blocks:
+ all: An alle
+ disabled: An niemanden
+ title: Zeige Domain-Blockaden
+ users: Für angemeldete lokale Benutzer
+ domain_blocks_rationale:
+ title: Rationale anzeigen
hero:
desc_html: Wird auf der Startseite angezeigt. Mindestens 600x100px sind empfohlen. Wenn es nicht gesetzt wurde, wird das Server-Thumbnail dafür verwendet
title: Bild für Einstiegsseite
@@ -507,7 +516,7 @@ de:
review: Prüfstatus
reviewed: Überprüft
title: Hashtags
- trending_right_now: Gerade angesagt
+ trending_right_now: Aktuell in den Trends
unique_uses_today: "%{count} Beiträge heute"
unreviewed: Nicht überprüft
updated_msg: Hashtageinstellungen wurden erfolgreich aktualisiert
@@ -527,7 +536,7 @@ de:
body_remote: Jemand von %{domain} hat %{target} gemeldet
subject: Neue Meldung auf %{instance} (#%{id})
new_trending_tag:
- body: 'Der Hashtag #%{name} ist heute angesagt, aber wurde vorher noch nicht überprüft. Er wird nicht öffentlich angezeigt, es sei denn du erlaubst es oder speicherst das Formular ab und vergisst es.'
+ body: 'Der Hashtag #%{name} ist heute am trenden, aber wurde vorher noch nicht überprüft. Er wird nicht öffentlich angezeigt, es sei denn du erlaubst es oder speicherst das Formular ab und vergisst es.'
subject: Neuer Hashtag zur Überprüfung auf %{instance} verfügbar (#%{name})
appearance:
advanced_web_interface: Fortgeschrittene Benutzeroberfläche
@@ -628,6 +637,23 @@ de:
people:
one: "%{count} Person"
other: "%{count} Leute"
+ domain_blocks:
+ blocked_domains: Liste der begrenzten und blockierten Domains
+ description: Dies ist die Liste der Server, die %{instance} limitiert oder dessen Föderation ablehnt.
+ domain: Domain
+ media_block: Medienblockade
+ no_domain_blocks: "(Keine Domain-Blockaden)"
+ severity: Schweregrad
+ severity_legend:
+ media_block: Mediendateien, die vom Server stammen, werden weder vom Benutzer abgerufen, gespeichert noch angezeigt.
+ silence: Konten von stummgeschalteten Servern können gefunden und gefolgt werden und man kann mit ihnen interagieren, aber ihre Beiträge werden nicht in der öffentlichen Zeitleiste erscheinen und Benachrichtigungen von ihnen werden nicht zu lokalen Benutzern gesendet, die sie nicht folgen.
+ suspension: Keine Inhalte von gesperrten Servern werden gespeichert oder angezeigt, und es werden auch keine Inhalte an sie gesendet. Die Interaktionen von gesperrten Servern werden ignoriert.
+ suspension_disclaimer: Gesperrte Server können gelegentlich öffentliche Inhalte von diesem Server abrufen.
+ title: Schweregrade
+ show_rationale: Rationale anzeigen
+ silence: Stummschalten
+ suspension: Sperre
+ title: "%{instance} Liste der blockierten Instanzen"
domain_validator:
invalid_domain: ist kein gültiger Domain-Name
errors:
@@ -664,6 +690,7 @@ de:
add_new: Neu hinzufügen
errors:
limit: Du hast bereits die maximale Anzahl an empfohlenen Hashtags erreicht
+ hint_html: "Was sind empfohlene Hashtags? Sie werden in deinem öffentlichen Profil deutlich angezeigt und ermöglichen es den Menschen, deine öffentlichen Beiträge speziell unter diesen Hashtags zu durchsuchen. Sie sind ein großartiges Werkzeug, um kreative Werke oder langfristige Projekte zu verfolgen."
filters:
contexts:
home: Startseite
@@ -684,6 +711,7 @@ de:
developers: Entwickler
more: Mehr…
resources: Ressourcen
+ trending_now: In den Trends
generic:
all: Alle
changes_saved_msg: Änderungen gespeichert!
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 30a49022b..184855329 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -499,6 +499,7 @@ en:
delete: Delete
nsfw_off: Mark as not sensitive
nsfw_on: Mark as sensitive
+ deleted: Deleted
failed_to_execute: Failed to execute
media:
title: Media
@@ -629,14 +630,8 @@ en:
warning_title: Disseminated content availability
directories:
directory: Profile directory
- enabled: You are currently listed in the directory.
- enabled_but_waiting: You have opted-in to be listed in the directory, but you do not have the minimum number of followers (%{min_followers}) to be listed yet.
explanation: Discover users based on their interests
explore_mastodon: Explore %{title}
- how_to_enable: You are not currently opted-in to the directory. You can opt-in below. Use hashtags in your bio text to be listed under specific hashtags!
- people:
- one: "%{count} person"
- other: "%{count} people"
domain_blocks:
blocked_domains: List of limited and blocked domains
description: This is the list of servers that %{instance} limits or reject federation with.
@@ -657,8 +652,10 @@ en:
domain_validator:
invalid_domain: is not a valid domain name
errors:
+ '400': The request you submitted was invalid or malformed.
'403': You don't have permission to view this page.
'404': The page you are looking for isn't here.
+ '406': This page is not available in the requested format.
'410': The page you were looking for doesn't exist here anymore.
'422':
content: Security verification failed. Are you blocking cookies?
@@ -667,6 +664,7 @@ en:
'500':
content: We're sorry, but something went wrong on our end.
title: This page is not correct
+ '503': The page could not be served due to a temporary server failure.
noscript_html: To use the Mastodon web application, please enable JavaScript. Alternatively, try one of the native apps for Mastodon for your platform.
existing_username_validator:
not_found: could not find a local user with that username
@@ -1118,7 +1116,9 @@ en:
disable: While your account is frozen, your account data remains intact, but you cannot perform any actions until it is unlocked.
silence: While your account is limited, only people who are already following you will see your toots on this server, and you may be excluded from various public listings. However, others may still manually follow you.
suspend: Your account has been suspended, and all of your toots and your uploaded media files have been irreversibly removed from this server, and servers where you had followers.
+ get_in_touch: You can reply to this e-mail to get in touch with the staff of %{instance}.
review_server_policies: Review server policies
+ statuses: 'Specifically, for:'
subject:
disable: Your account %{acct} has been frozen
none: Warning for %{acct}
diff --git a/config/locales/ja.yml b/config/locales/ja.yml
index 32ff844a5..158a56b10 100644
--- a/config/locales/ja.yml
+++ b/config/locales/ja.yml
@@ -255,7 +255,7 @@ ja:
features: 機能
hidden_service: 秘匿サービスとの連合
open_reports: 未解決の通報
- pending_tags: 承認待ちのハッシュタグ
+ pending_tags: 審査待ちのハッシュタグ
pending_users: 承認待ちの人数
recent_users: 最近登録したユーザー
search: 全文検索
@@ -465,7 +465,7 @@ ja:
site_title: サーバーの名前
spam_check_enabled:
desc_html: 求められていないメッセージを繰り返し送信するアカウントを自動でサイレンスにし通報することができます。誤検知を含む可能性もあります。
- title: スパム対策
+ title: スパム対策を有効にする
thumbnail:
desc_html: OpenGraphとAPIによるプレビューに使用されます。サイズは1200×630px推奨です
title: サーバーのサムネイル
@@ -475,7 +475,7 @@ ja:
title: サイト設定
trends:
desc_html: 現在トレンドになっている承認済みのハッシュタグを公開します
- title: トレンドタグ
+ title: トレンドタグを有効にする
statuses:
back_to_account: アカウントページに戻る
batch:
@@ -497,11 +497,11 @@ ja:
directory: ディレクトリに使用
in_directory: "%{count} 人がディレクトリに使用"
review: 審査状況
- reviewed: 承認済み
+ reviewed: 審査済み
title: ハッシュタグ
trending_right_now: 現在のトレンド
unique_uses_today: 本日 %{count} 人がトゥートに使用
- unreviewed: 未承認
+ unreviewed: 未審査
updated_msg: ハッシュタグ設定の更新に成功しました
title: 管理
warning_presets:
@@ -520,7 +520,7 @@ ja:
subject: "%{instance} の新しい通報 (#%{id})"
new_trending_tag:
body: 'ハッシュタグ #%{name} が本日のトレンドになっていますが、審査がまだ行われていないためトレンドタグには表示されていません。一度許可すれば次回からこの操作は不要です。'
- subject: "%{instance} で新しいハッシュタグ (#%{name}) が承認待ちです"
+ subject: "%{instance} で新しいハッシュタグ (#%{name}) が審査待ちです"
appearance:
advanced_web_interface: 上級者向け UI
advanced_web_interface_hint: ディスプレイを幅いっぱいまで活用したい場合、上級者向け UI をおすすめします。ホーム、通知、連合タイムライン、更にはリストやハッシュタグなど、様々な異なるカラムから望む限りの情報を一度に受け取れるような設定が可能になります。
@@ -674,6 +674,7 @@ ja:
developers: 開発者向け
more: さらに…
resources: リソース
+ trending_now: トレンドタグ
generic:
all: すべて
changes_saved_msg: 正常に変更されました!
diff --git a/config/locales/ko.yml b/config/locales/ko.yml
index 11f1498a7..a2b98ebe2 100644
--- a/config/locales/ko.yml
+++ b/config/locales/ko.yml
@@ -43,6 +43,8 @@ ko:
what_is_mastodon: 마스토돈이란?
accounts:
choices_html: "%{name}의 추천:"
+ endorsements_hint: 내가 팔로우 하고 있는 사람들을 여기에 추천 할 수 있습니다.
+ featured_tags_hint: 특정한 해시태그들을 여기에 표시되도록 할 수 있습니다.
follow: 팔로우
followers:
other: 팔로워
@@ -417,6 +419,13 @@ ko:
custom_css:
desc_html: 모든 페이지에 적용할 CSS
title: 커스텀 CSS
+ domain_blocks:
+ all: 모두에게
+ disabled: 아무에게도 안 함
+ title: 도메인 차단 보여주기
+ users: 로그인 한 유저에게
+ domain_blocks_rationale:
+ title: 사유 보여주기
hero:
desc_html: 프론트페이지에 표시 됩니다. 최소 600x100픽셀을 권장합니다. 만약 설정되지 않았다면, 서버의 썸네일이 사용 됩니다
title: 히어로 이미지
@@ -623,6 +632,23 @@ ko:
how_to_enable: 아직 디렉터리에 참여하지 않았습니다. 아래에서 참여할 수 있습니다. 바이오 텍스트에 해시태그를 사용해 특정 해시태그 디렉터리에 표시 될 수 있습니다!
people:
other: "%{count}명"
+ domain_blocks:
+ blocked_domains: 제한 되거나 차단 된 도메인 목록
+ description: 이것은 %{instance}가 제한하거나 연합을 거부한 서버들의 목록입니다.
+ domain: 도메인
+ media_block: 미디어 차단
+ no_domain_blocks: "(도메인 차단 없음)"
+ severity: 심각도
+ severity_legend:
+ media_block: 이 서버의 미디어는 불러오거나, 저장 되거나, 유저에게 보여지지 않습니다.
+ silence: 침묵 된 서버의 계정은 발견 되고, 팔로우 하고, 그들과 상호작용 할 수 있습니다, 그러나 그들의 툿은 툿 공개 타임라인에 나타나지 않으며 그들에게서 오는 알림은 그들을 팔로우 하지 않는 로컬 유저에게는 전달되지 않습니다.
+ suspension: 정지 된 서버의 어떤 콘텐츠도 저장 되거나 보여지거나 하지 않고 어떤 콘텐츠도 그 서버로 전달 되지 않습니다. 정지 된 서버에서의 상호작용은 무시됩니다.
+ suspension_disclaimer: 정지 된 서버는 때때로 이 서버의 공개 콘텐츠를 받아 갈 수도 잇습니다.
+ title: 심각도
+ show_rationale: 사유 보여주기
+ silence: 침묵
+ suspension: 정지
+ title: "%{instance}의 차단한 인스턴스 목록"
domain_validator:
invalid_domain: 올바른 도메인 네임이 아닙니다
errors:
@@ -659,6 +685,7 @@ ko:
add_new: 추가
errors:
limit: 이미 추천 해시태그의 개수가 최대입니다
+ hint_html: "추천 해시태그가 무엇이죠? 당신의 공개 프로필 페이지에 눈에 띄게 표현 되며 사람들이 그 해시태그를 포함한 당신의 글을 찾아 볼 수 있도록 합니다. 창작활동이나 긴 기간을 가지는 프로젝트를 쭉 따라가기에 좋은 도구입니다."
filters:
contexts:
home: 홈 타임라인
@@ -679,6 +706,7 @@ ko:
developers: 개발자
more: 더 보기…
resources: 리소스
+ trending_now: 지금 유행중
generic:
all: 모두
changes_saved_msg: 정상적으로 변경되었습니다!
diff --git a/config/locales/ru.yml b/config/locales/ru.yml
index 8306ed6cc..ec639c7a9 100644
--- a/config/locales/ru.yml
+++ b/config/locales/ru.yml
@@ -119,7 +119,7 @@ ru:
followers: Подписчики
followers_url: URL подписчиков
follows: Подписки
- header: Заголовок
+ header: Шапка
inbox_url: URL входящих
invited_by: Приглашение выдал(а)
ip: IP
@@ -191,6 +191,7 @@ ru:
username: Имя пользователя
warn: Предупредить
web: Веб
+ whitelisted: В белом списке
action_logs:
actions:
assigned_to_self_report: "%{name} назначил(а) жалобу %{target} на себя"
@@ -263,6 +264,8 @@ ru:
features: Возможности
hidden_service: Федерация со скрытыми сервисами
open_reports: открытых жалоб
+ pending_tags: хэштеги, ожидающие проверки
+ pending_users: пользователи, ожидающие проверки
recent_users: Недавние пользователи
search: Полнотекстовый поиск
single_user_mode: Однопользовательский режим
@@ -274,11 +277,17 @@ ru:
week_interactions: взаимодействий на этой неделе
week_users_active: активно на этой неделе
week_users_new: пользователей на этой неделе
+ whitelist_mode: Белый список
+ domain_allows:
+ created_msg: Домен добавлен в белый список
+ destroyed_msg: Домен убран из белого списка
+ undo: Убрать из белого списка
domain_blocks:
add_new: Заблокировать домен
created_msg: Блокировка домена обрабатывается
destroyed_msg: Блокировка домена снята
domain: Домен
+ edit: Редактировать блокировку
new:
create: Создать блокировку
hint: Блокировка домена не предотвратит создание новых аккаунтов в базе данных, но ретроактивно и автоматически применит указанные методы модерации для этих аккаунтов.
@@ -288,6 +297,8 @@ ru:
silence: Глушение
suspend: Блокировка
title: Новая доменная блокировка
+ private_comment: Приватный комментарий
+ public_comment: Публичный комментарий
reject_media: Запретить медиаконтент
reject_media_hint: Удаляет локально хранимый медиаконтент и запрещает его загрузку в будущем. Не имеет значения в случае блокировки.
reject_reports: Отклонять жалобы
@@ -530,7 +541,7 @@ ru:
login: Войти
logout: Выйти
migrate_account: Перенести аккаунт
- migrate_account_html: Если Вы хотите перенести этот аккаунт на другой, вы можете сделать это здесь .
+ migrate_account_html: Если вы хотите перенаправить подписчиков на другой аккаунт, это можно настроить здесь .
or_log_in_with: Или войти с помощью
providers:
cas: CAS
diff --git a/config/locales/simple_form.en.yml b/config/locales/simple_form.en.yml
index 98f0843d0..2e5982de9 100644
--- a/config/locales/simple_form.en.yml
+++ b/config/locales/simple_form.en.yml
@@ -5,6 +5,7 @@ en:
account_warning_preset:
text: You can use toot syntax, such as URLs, hashtags and mentions
admin_account_action:
+ include_statuses: The user will see which toots have caused the moderation action or warning
send_email_notification: The user will receive an explanation of what happened with their account
text_html: Optional. You can use toot syntax. You can add warning presets to save time
type_html: Choose what to do with %{acct}
@@ -15,7 +16,7 @@ en:
bot: This account mainly performs automated actions and might not be monitored
context: One or multiple contexts where the filter should apply
digest: Only sent after a long period of inactivity and only if you have received any personal messages in your absence
- discoverable_html: The directory lets people find accounts based on interests and activity. Requires at least %{min_followers} followers
+ discoverable: The profile directory is another way by which your account can reach a wider audience
email: You will be sent a confirmation e-mail
fields: You can have up to 4 items displayed as a table on your profile
header: PNG, GIF or JPG. At most %{size}. Will be downscaled to %{dimensions}px
@@ -60,6 +61,7 @@ en:
account_warning_preset:
text: Preset text
admin_account_action:
+ include_statuses: Include reported toots in the e-mail
send_email_notification: Notify the user per e-mail
text: Custom warning
type: Action
@@ -143,6 +145,7 @@ en:
trending_tag: Send e-mail when an unreviewed hashtag is trending
tag:
listable: Allow this hashtag to appear in searches and on the profile directory
+ name: Hashtag
trendable: Allow this hashtag to appear under trends
usable: Allow toots to use this hashtag
'no': 'No'
diff --git a/config/locales/simple_form.ja.yml b/config/locales/simple_form.ja.yml
index a710b38be..4a0d16028 100644
--- a/config/locales/simple_form.ja.yml
+++ b/config/locales/simple_form.ja.yml
@@ -47,7 +47,7 @@ ja:
sessions:
otp: '携帯電話のアプリで生成された二段階認証コードを入力するか、リカバリーコードを使用してください:'
tag:
- name: 視認性向上などのために大文字 / 小文字の変更のみ行うことができます
+ name: 視認性向上などのためにアルファベット大文字小文字の変更のみ行うことができます
user:
chosen_languages: 選択すると、選択した言語のトゥートのみが公開タイムラインに表示されるようになります
labels:
@@ -138,9 +138,9 @@ ja:
pending_account: 新しいアカウントの承認が必要な時にメールで通知する
reblog: トゥートがブーストされた時にメールで通知する
report: 通報を受けた時にメールで通知する
- trending_tag: 未承認のハッシュタグが人気の時にメールで通知する
+ trending_tag: 未審査のハッシュタグが人気の時にメールで通知する
tag:
- listable: ディレクトリへの使用を許可する
+ listable: 検索とディレクトリへの使用を許可する
trendable: トレンドへの表示を許可する
usable: トゥートへの使用を許可する
'no': いいえ
diff --git a/config/locales/simple_form.ru.yml b/config/locales/simple_form.ru.yml
index fcc1c2827..2835cdb30 100644
--- a/config/locales/simple_form.ru.yml
+++ b/config/locales/simple_form.ru.yml
@@ -11,18 +11,18 @@ ru:
warning_preset_id: Необязательно. Вы можете добавить собственный текст в конце шаблона
defaults:
autofollow: Люди, пришедшие по этому приглашению, автоматически будут подписаны на вас
- avatar: PNG, GIF или JPG. Максимально %{size}. Будет уменьшено до %{dimensions}px
+ avatar: Поддерживается PNG, GIF и JPG. Максимальный размер — %{size}. Будет уменьшен до %{dimensions}px
bot: Этот аккаунт обычно выполяет автоматизированные действия и может не просматриваться владельцем
context: Один или несколько контекстов, к которым должны быть применены фильтры
digest: Отсылается лишь после длительной неактивности, если вы в это время получали личные сообщения
discoverable_html: Каталог позволяет пользователям искать людей по интересам и активности. Необходимо наличие не менее %{min_followers} подписчиков
email: Вам будет отправлено электронное письмо с подтверждением
fields: В профиле можно отобразить до 4 пунктов как таблицу
- header: PNG, GIF или JPG. Максимально %{size}. Будет уменьшено до %{dimensions}px
+ header: Поддерживается PNG, GIF и JPG. Максимальный размер — %{size}. Будет уменьшена до %{dimensions}px
inbox_url: Копировать URL с главной страницы ретранслятора, который вы хотите использовать
irreversible: Отфильтрованные статусы будут утеряны навсегда, даже если в будущем фильтр будет убран
locale: Язык интерфейса, e-mail писем и push-уведомлений
- locked: Потребует от вас ручного подтверждения подписчиков, изменит приватность постов по умолчанию на "только для подписчиков"
+ locked: Подписчиков нужно будет подтверждать самостоятельно
password: Укажите не менее 8 символов
phrase: Будет сопоставлено независимо от присутствия в тексте или предупреждения о содержании статуса
scopes: Какие API приложению будет позволено использовать. Если вы выберете самый верхний, нижестоящие будут выбраны автоматически.
@@ -34,8 +34,12 @@ ru:
setting_hide_network: Те, на кого вы подписаны и кто подписан на Вас, не будут отображены в вашем профиле
setting_noindex: Относится к вашему публичному профилю и страницам статусов
setting_show_application: В окне просмотра вашего статуса будет видно, с какого приложения он был отправлен
+ setting_use_blurhash: Градиенты основаны на цветах скрытых медиа, но скрывают любые детали
+ setting_use_pending_items: Показывать обновления в ленте только после клика вместо автоматической прокрутки
username: Ваш юзернейм будет уникальным на %{domain}
whole_word: Если слово или фраза состоит только из букв и цифр, сопоставление произойдёт только по полному совпадению
+ domain_allow:
+ domain: Этот домен сможет получать данные с этого сервера и его входящие данные будут обрабатываться и сохранены
featured_tag:
name: 'Возможно, вы захотите выбрать из них:'
imports:
@@ -44,6 +48,8 @@ ru:
text: Это поможет нам рассмотреть вашу заявку
sessions:
otp: 'Введите код двухфакторной аутентификации, сгенерированный в мобильном приложении, или используйте один из ваших кодов восстановления:'
+ tag:
+ name: Вы можете изменить только регистр букв чтобы, например, сделать тег более читаемым
user:
chosen_languages: Если выбрано, то в публичных лентах будут показаны только посты на выбранных языках
labels:
@@ -74,7 +80,7 @@ ru:
current_password: Текущий пароль
data: Данные
discoverable: Показывать этот аккаунт в каталоге
- display_name: Показываемое имя
+ display_name: Отображаемое имя
email: Адрес e-mail
expires_in: Истекает через
fields: Метаданные профиля
@@ -85,7 +91,7 @@ ru:
locked: Сделать аккаунт закрытым
max_uses: Максимальное число использований
new_password: Новый пароль
- note: О Вас
+ note: О себе
otp_attempt: Двухфакторный код
password: Пароль
phrase: Слово или фраза
@@ -108,7 +114,10 @@ ru:
setting_show_application: Раскрывать приложение, с которого отправляются статусы
setting_system_font_ui: Использовать шрифт системы по умолчанию
setting_theme: Тема сайта
+ setting_trends: Показывать сегодняшние тренды
setting_unfollow_modal: Показывать диалог подтверждения перед тем, как отписаться от аккаунта
+ setting_use_blurhash: Показать цветные градиенты для скрытых медиа
+ setting_use_pending_items: Медленный режим
severity: Строгость
type: Тип импорта
username: Имя пользователя
@@ -131,6 +140,11 @@ ru:
pending_account: Отправлять e-mail при наличии новых заявок на присоединение
reblog: Уведомлять по e-mail, когда кто-то продвинул ваш статус
report: Уведомлять по e-mail при создании жалобы
+ trending_tag: Отправлять e-mail при непроверенных хэштегах в трендах
+ tag:
+ listable: Разрешить показ хэштега в поиске или в каталоге профилей
+ trendable: Разрешить показ хэштега в трендах
+ usable: Разрешить использовать этот хэштег в постах
'no': Нет
recommended: Рекомендуется
required:
diff --git a/config/locales/sk.yml b/config/locales/sk.yml
index 42556286d..dc75706b1 100644
--- a/config/locales/sk.yml
+++ b/config/locales/sk.yml
@@ -430,6 +430,12 @@ sk:
custom_css:
desc_html: Uprav vzhľad pomocou CSS, ktoré je načítané na každej stránke
title: Vlastné CSS
+ domain_blocks:
+ all: Všetkým
+ disabled: Nikomu
+ title: Ukáž blokované domény
+ domain_blocks_rationale:
+ title: Ukáž zdôvodnenie
hero:
desc_html: Zobrazuje sa na hlavnej stránke. Doporučené je rozlišenie aspoň 600x100px. Pokiaľ nič nieje dodané, bude nastavený základný orázok serveru.
title: Obrázok hrdinu
diff --git a/config/locales/th.yml b/config/locales/th.yml
index c1c4d76bc..73e6fb178 100644
--- a/config/locales/th.yml
+++ b/config/locales/th.yml
@@ -1,6 +1,7 @@
---
th:
about:
+ about_hashtag_html: มีโพสต์สาธารณะที่ถูกแท็กด้วย #%{hashtag} คุณสามารถโต้ตอบได้หากคุณมีบัญชีที่ไหนก็ได้ในเฟดิเวิร์ส
about_mastodon_html: Mastodon เป็นเครือข่ายสังคมที่ทำงานบนโปรโตคอลเว็บแบบเปิดและซอฟต์แวร์เสรีที่เปิดต้นฉบับ กระจายศูนย์เหมือนอีเมล
about_this: เกี่ยวกับ
active_count_after: ที่ใช้งาน
@@ -18,9 +19,11 @@ th:
documentation: เอกสารประกอบ
extended_description_html: |
สถานที่ที่ดีสำหรับกฎ
- ยังไม่ได้ตั้งคำอธิบายแบบขยาย
+ ยังไม่ได้ตั้งคำอธิบายเพิ่มเติม
+ federation_hint_html: เมื่อคุณมีบัญชีที่ %{instance} แล้ว คุณสามารถติดตามผู้คนบนเซิร์ฟเวอร์ Mastodon เซิร์ฟเวอร์ใดก็ได้
generic_description: "%{domain} เป็นเซิร์ฟเวอร์หนึ่งในเครือข่าย"
get_apps: ลองแอปสำหรับมือถือ
+ hosted_on: Mastodon ให้บริการบน %{domain}
learn_more: เรียนรู้เพิ่มเติม
privacy_policy: นโยบายความเป็นส่วนตัว
see_whats_happening: ดูสิ่งที่กำลังเกิดขึ้น
@@ -28,7 +31,7 @@ th:
source_code: โค้ดต้นฉบับ
status_count_after:
other: สถานะ
- status_count_before: ผู้สร้าง
+ status_count_before: ผู้ใช้เหล่านั้นได้สร้าง
terms: เงื่อนไขการให้บริการ
user_count_after:
other: ผู้ใช้
diff --git a/config/routes.rb b/config/routes.rb
index 469a93a79..118900b5d 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -6,6 +6,8 @@ require 'sidekiq-scheduler/web'
Sidekiq::Web.set :session_secret, Rails.application.secrets[:secret_key_base]
Rails.application.routes.draw do
+ root 'home#index'
+
mount LetterOpenerWeb::Engine, at: 'letter_opener' if Rails.env.development?
authenticate :user, lambda { |u| u.admin? } do
@@ -329,6 +331,7 @@ Rails.application.routes.draw do
end
resource :domain_blocks, only: [:show, :create, :destroy]
+ resource :directory, only: [:show]
resources :follow_requests, only: [:index] do
member do
@@ -432,10 +435,6 @@ Rails.application.routes.draw do
get '/about/blocks', to: 'about#blocks'
get '/terms', to: 'about#terms'
- root 'home#index'
-
- match '*unmatched_route',
- via: :all,
- to: 'application#raise_not_found',
- format: false
+ match '/', via: [:post, :put, :patch, :delete], to: 'application#raise_not_found', format: false
+ match '*unmatched_route', via: :all, to: 'application#raise_not_found', format: false
end
diff --git a/db/migrate/20190819134503_add_deleted_at_to_statuses.rb b/db/migrate/20190819134503_add_deleted_at_to_statuses.rb
new file mode 100644
index 000000000..5af109097
--- /dev/null
+++ b/db/migrate/20190819134503_add_deleted_at_to_statuses.rb
@@ -0,0 +1,5 @@
+class AddDeletedAtToStatuses < ActiveRecord::Migration[5.2]
+ def change
+ add_column :statuses, :deleted_at, :datetime
+ end
+end
diff --git a/db/migrate/20190820003045_update_statuses_index.rb b/db/migrate/20190820003045_update_statuses_index.rb
new file mode 100644
index 000000000..5c2ea1f6a
--- /dev/null
+++ b/db/migrate/20190820003045_update_statuses_index.rb
@@ -0,0 +1,13 @@
+class UpdateStatusesIndex < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def up
+ safety_assured { add_index :statuses, [:account_id, :id, :visibility, :updated_at], where: 'deleted_at IS NULL', order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20190820 }
+ remove_index :statuses, name: :index_statuses_20180106
+ end
+
+ def down
+ safety_assured { add_index :statuses, [:account_id, :id, :visibility, :updated_at], order: { id: :desc }, algorithm: :concurrently, name: :index_statuses_20180106 }
+ remove_index :statuses, name: :index_statuses_20190820
+ end
+end
diff --git a/db/migrate/20190823221802_add_local_index_to_statuses.rb b/db/migrate/20190823221802_add_local_index_to_statuses.rb
new file mode 100644
index 000000000..deca25c35
--- /dev/null
+++ b/db/migrate/20190823221802_add_local_index_to_statuses.rb
@@ -0,0 +1,11 @@
+class AddLocalIndexToStatuses < ActiveRecord::Migration[5.2]
+ disable_ddl_transaction!
+
+ def up
+ add_index :statuses, [:id, :account_id], name: :index_statuses_local_20190824, algorithm: :concurrently, order: { id: :desc }, where: '(local OR (uri IS NULL)) AND deleted_at IS NULL AND visibility = 0 AND reblog_of_id IS NULL AND ((NOT reply) OR (in_reply_to_account_id = account_id))'
+ end
+
+ def down
+ remove_index :statuses, name: :index_statuses_local_20190824
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 961b5c6bc..078a363b7 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 2019_08_15_225426) do
+ActiveRecord::Schema.define(version: 2019_08_23_221802) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -654,7 +654,9 @@ ActiveRecord::Schema.define(version: 2019_08_15_225426) do
t.bigint "application_id"
t.bigint "in_reply_to_account_id"
t.bigint "poll_id"
- t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20180106", order: { id: :desc }
+ t.datetime "deleted_at"
+ t.index ["account_id", "id", "visibility", "updated_at"], name: "index_statuses_20190820", order: { id: :desc }, where: "(deleted_at IS NULL)"
+ t.index ["id", "account_id"], name: "index_statuses_local_20190824", order: { id: :desc }, where: "((local OR (uri IS NULL)) AND (deleted_at IS NULL) AND (visibility = 0) AND (reblog_of_id IS NULL) AND ((NOT reply) OR (in_reply_to_account_id = account_id)))"
t.index ["in_reply_to_account_id"], name: "index_statuses_on_in_reply_to_account_id"
t.index ["in_reply_to_id"], name: "index_statuses_on_in_reply_to_id"
t.index ["reblog_of_id", "account_id"], name: "index_statuses_on_reblog_of_id_and_account_id"
diff --git a/dist/nginx.conf b/dist/nginx.conf
index 7c429bad4..b6591e897 100644
--- a/dist/nginx.conf
+++ b/dist/nginx.conf
@@ -19,7 +19,7 @@ server {
listen [::]:443 ssl http2;
server_name example.com;
- ssl_protocols TLSv1.2;
+ ssl_protocols TLSv1.2 TLSv1.3;
ssl_ciphers HIGH:!MEDIUM:!LOW:!aNULL:!NULL:!SHA;
ssl_prefer_server_ciphers on;
ssl_session_cache shared:SSL:10m;
diff --git a/package.json b/package.json
index 43aedb68c..f79fa4912 100644
--- a/package.json
+++ b/package.json
@@ -2,7 +2,7 @@
"name": "mastodon",
"license": "AGPL-3.0-or-later",
"engines": {
- "node": ">=8.12 <12"
+ "node": ">=8.12 <13"
},
"scripts": {
"postversion": "git push --tags",
@@ -60,7 +60,7 @@
"private": true,
"dependencies": {
"@babel/core": "^7.4.5",
- "@babel/plugin-proposal-class-properties": "^7.5.0",
+ "@babel/plugin-proposal-class-properties": "^7.5.5",
"@babel/plugin-proposal-decorators": "^7.4.4",
"@babel/plugin-proposal-object-rest-spread": "^7.4.4",
"@babel/plugin-syntax-dynamic-import": "^7.2.0",
@@ -95,7 +95,7 @@
"escape-html": "^1.0.3",
"exif-js": "^2.3.0",
"express": "^4.17.1",
- "file-loader": "^4.1.0",
+ "file-loader": "^4.2.0",
"font-awesome": "^4.7.0",
"glob": "^7.1.1",
"http-link-header": "^1.0.2",
@@ -134,7 +134,7 @@
"react-motion": "^0.5.2",
"react-notification": "^6.8.4",
"react-overlays": "^0.8.3",
- "react-redux": "^7.1.0",
+ "react-redux": "^7.1.1",
"react-redux-loading-bar": "^4.0.8",
"react-router-dom": "^4.1.1",
"react-router-scroll-4": "^1.0.0-beta.1",
@@ -144,13 +144,13 @@
"react-textarea-autosize": "^7.1.0",
"react-toggle": "^4.0.1",
"redis": "^2.7.1",
- "redux": "^4.0.1",
+ "redux": "^4.0.4",
"redux-immutable": "^4.0.0",
"redux-thunk": "^2.2.0",
"rellax": "^1.10.0",
"requestidlecallback": "^0.3.0",
"reselect": "^4.0.0",
- "rimraf": "^2.6.3",
+ "rimraf": "^3.0.0",
"sass": "^1.22.9",
"sass-loader": "^7.0.3",
"stringz": "^2.0.0",
@@ -160,10 +160,11 @@
"throng": "^4.0.0",
"tiny-queue": "^0.2.1",
"uuid": "^3.1.0",
+ "wavesurfer.js": "^3.0.0",
"webpack": "^4.35.3",
"webpack-assets-manifest": "^3.1.1",
"webpack-bundle-analyzer": "^3.3.2",
- "webpack-cli": "^3.3.6",
+ "webpack-cli": "^3.3.7",
"webpack-merge": "^4.2.1",
"websocket.js": "^0.1.12"
},
diff --git a/spec/controllers/admin/reported_statuses_controller_spec.rb b/spec/controllers/admin/reported_statuses_controller_spec.rb
index c358506d6..bd146b795 100644
--- a/spec/controllers/admin/reported_statuses_controller_spec.rb
+++ b/spec/controllers/admin/reported_statuses_controller_spec.rb
@@ -47,7 +47,7 @@ describe Admin::ReportedStatusesController do
it 'removes a status' do
allow(RemovalWorker).to receive(:perform_async)
subject.call
- expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first)
+ expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, redraft: false)
end
end
diff --git a/spec/controllers/admin/statuses_controller_spec.rb b/spec/controllers/admin/statuses_controller_spec.rb
index 1a08c10b7..6b06343ef 100644
--- a/spec/controllers/admin/statuses_controller_spec.rb
+++ b/spec/controllers/admin/statuses_controller_spec.rb
@@ -65,7 +65,7 @@ describe Admin::StatusesController do
it 'removes a status' do
allow(RemovalWorker).to receive(:perform_async)
subject.call
- expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first)
+ expect(RemovalWorker).to have_received(:perform_async).with(status_ids.first, redraft: false)
end
end
diff --git a/spec/controllers/remote_follow_controller_spec.rb b/spec/controllers/remote_follow_controller_spec.rb
index 5088c2e65..d79dd2949 100644
--- a/spec/controllers/remote_follow_controller_spec.rb
+++ b/spec/controllers/remote_follow_controller_spec.rb
@@ -66,9 +66,7 @@ describe RemoteFollowController do
end
it 'redirects to the remote location' do
- address = "http://example.com/follow_me?acct=test_user%40#{Rails.configuration.x.local_domain}"
-
- expect(response).to redirect_to(address)
+ expect(response).to redirect_to("http://example.com/follow_me?acct=https%3A%2F%2F#{Rails.configuration.x.local_domain}%2Fusers%2Ftest_user")
end
end
end
diff --git a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
index 478f24585..2222a7559 100644
--- a/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentication/confirmations_controller_spec.rb
@@ -50,7 +50,8 @@ describe Settings::TwoFactorAuthentication::ConfirmationsController do
describe 'when form_two_factor_confirmation parameter is not provided' do
it 'raises ActionController::ParameterMissing' do
- expect { post :create, params: {} }.to raise_error(ActionController::ParameterMissing)
+ post :create, params: {}
+ expect(response).to have_http_status(400)
end
end
diff --git a/spec/controllers/settings/two_factor_authentications_controller_spec.rb b/spec/controllers/settings/two_factor_authentications_controller_spec.rb
index 9f27222ad..f7c628756 100644
--- a/spec/controllers/settings/two_factor_authentications_controller_spec.rb
+++ b/spec/controllers/settings/two_factor_authentications_controller_spec.rb
@@ -112,7 +112,8 @@ describe Settings::TwoFactorAuthenticationsController do
end
it 'raises ActionController::ParameterMissing if code is missing' do
- expect { post :destroy }.to raise_error(ActionController::ParameterMissing)
+ post :destroy
+ expect(response).to have_http_status(400)
end
end
diff --git a/spec/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb
index 53c836494..ead3b3baa 100644
--- a/spec/mailers/previews/user_mailer_preview.rb
+++ b/spec/mailers/previews/user_mailer_preview.rb
@@ -42,6 +42,6 @@ class UserMailerPreview < ActionMailer::Preview
# Preview this email at http://localhost:3000/rails/mailers/user_mailer/warning
def warning
- UserMailer.warning(User.first, AccountWarning.new(text: '', action: :silence))
+ UserMailer.warning(User.first, AccountWarning.new(text: '', action: :silence), [Status.first.id])
end
end
diff --git a/spec/models/admin/account_action_spec.rb b/spec/models/admin/account_action_spec.rb
index a3db60cfc..87fc28500 100644
--- a/spec/models/admin/account_action_spec.rb
+++ b/spec/models/admin/account_action_spec.rb
@@ -58,8 +58,8 @@ RSpec.describe Admin::AccountAction, type: :model do
end.to change { Admin::ActionLog.count }.by 1
end
- it 'calls queue_email!' do
- expect(account_action).to receive(:queue_email!)
+ it 'calls process_email!' do
+ expect(account_action).to receive(:process_email!)
subject
end
diff --git a/spec/models/form/status_batch_spec.rb b/spec/models/form/status_batch_spec.rb
index 00c790a11..f9c58c90f 100644
--- a/spec/models/form/status_batch_spec.rb
+++ b/spec/models/form/status_batch_spec.rb
@@ -41,12 +41,12 @@ describe Form::StatusBatch do
it 'call RemovalWorker' do
form.save
- expect(RemovalWorker).to have_received(:perform_async).with(status.id)
+ expect(RemovalWorker).to have_received(:perform_async).with(status.id, redraft: false)
end
it 'do not call RemovalWorker' do
form.save
- expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id)
+ expect(RemovalWorker).not_to have_received(:perform_async).with(another_status.id, redraft: false)
end
end
end
diff --git a/spec/models/remote_follow_spec.rb b/spec/models/remote_follow_spec.rb
index ed2667b28..5b4c19b5b 100644
--- a/spec/models/remote_follow_spec.rb
+++ b/spec/models/remote_follow_spec.rb
@@ -61,7 +61,7 @@ RSpec.describe RemoteFollow do
subject { remote_follow.subscribe_address_for(account) }
it 'returns subscribe address' do
- is_expected.to eq 'https://quitter.no/main/ostatussub?profile=alice%40cb6e6126.ngrok.io'
+ is_expected.to eq 'https://quitter.no/main/ostatussub?profile=https%3A%2F%2Fcb6e6126.ngrok.io%2Fusers%2Falice'
end
end
end
diff --git a/yarn.lock b/yarn.lock
index f486e3eae..a02b5a081 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -121,16 +121,16 @@
"@babel/traverse" "^7.4.4"
"@babel/types" "^7.4.4"
-"@babel/helper-create-class-features-plugin@^7.4.4", "@babel/helper-create-class-features-plugin@^7.5.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.0.tgz#02edb97f512d44ba23b3227f1bf2ed43454edac5"
- integrity sha512-EAoMc3hE5vE5LNhMqDOwB1usHvmRjCDAnH8CD4PVkX9/Yr3W/tcz8xE8QvdZxfsFBDICwZnF2UTHIqslRpvxmA==
+"@babel/helper-create-class-features-plugin@^7.4.4", "@babel/helper-create-class-features-plugin@^7.5.5":
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.5.5.tgz#401f302c8ddbc0edd36f7c6b2887d8fa1122e5a4"
+ integrity sha512-ZsxkyYiRA7Bg+ZTRpPvB6AbOFKTFFK4LrvTet8lInm0V468MWCaSYJE+I7v2z2r8KNLtYiV+K5kTCnR7dvyZjg==
dependencies:
"@babel/helper-function-name" "^7.1.0"
- "@babel/helper-member-expression-to-functions" "^7.0.0"
+ "@babel/helper-member-expression-to-functions" "^7.5.5"
"@babel/helper-optimise-call-expression" "^7.0.0"
"@babel/helper-plugin-utils" "^7.0.0"
- "@babel/helper-replace-supers" "^7.4.4"
+ "@babel/helper-replace-supers" "^7.5.5"
"@babel/helper-split-export-declaration" "^7.4.4"
"@babel/helper-define-map@^7.5.5":
@@ -173,13 +173,6 @@
dependencies:
"@babel/types" "^7.4.4"
-"@babel/helper-member-expression-to-functions@^7.0.0":
- version "7.0.0"
- resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.0.0.tgz#8cd14b0a0df7ff00f009e7d7a436945f47c7a16f"
- integrity sha512-avo+lm/QmZlv27Zsi0xEor2fKcqWG56D5ae9dzklpIaY7cQMK5N8VSpaNVPPagiqmy7LrEjK1IWdGMOqPu5csg==
- dependencies:
- "@babel/types" "^7.0.0"
-
"@babel/helper-member-expression-to-functions@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.5.5.tgz#1fb5b8ec4453a93c439ee9fe3aeea4a84b76b590"
@@ -236,16 +229,6 @@
"@babel/traverse" "^7.1.0"
"@babel/types" "^7.0.0"
-"@babel/helper-replace-supers@^7.4.4":
- version "7.4.4"
- resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.4.4.tgz#aee41783ebe4f2d3ab3ae775e1cc6f1a90cefa27"
- integrity sha512-04xGEnd+s01nY1l15EuMS1rfKktNF+1CkKmHoErDppjAAZL+IUBZpzT748x262HF7fibaQPhbvWUl5HeSt1EXg==
- dependencies:
- "@babel/helper-member-expression-to-functions" "^7.0.0"
- "@babel/helper-optimise-call-expression" "^7.0.0"
- "@babel/traverse" "^7.4.4"
- "@babel/types" "^7.4.4"
-
"@babel/helper-replace-supers@^7.5.5":
version "7.5.5"
resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.5.5.tgz#f84ce43df031222d2bad068d2626cb5799c34bc2"
@@ -327,12 +310,12 @@
"@babel/helper-remap-async-to-generator" "^7.1.0"
"@babel/plugin-syntax-async-generators" "^7.2.0"
-"@babel/plugin-proposal-class-properties@^7.5.0":
- version "7.5.0"
- resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.0.tgz#5bc6a0537d286fcb4fd4e89975adbca334987007"
- integrity sha512-9L/JfPCT+kShiiTTzcnBJ8cOwdKVmlC1RcCf9F0F9tERVrM4iWtWnXtjWCRqNm2la2BxO1MPArWNsU9zsSJWSQ==
+"@babel/plugin-proposal-class-properties@^7.5.5":
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.5.5.tgz#a974cfae1e37c3110e71f3c6a2e48b8e71958cd4"
+ integrity sha512-AF79FsnWFxjlaosgdi421vmYG6/jg79bVD0dpD44QdgobzHKuLZ6S3vl8la9qIeSwGi8i1fS0O1mfuDAAdo1/A==
dependencies:
- "@babel/helper-create-class-features-plugin" "^7.5.0"
+ "@babel/helper-create-class-features-plugin" "^7.5.5"
"@babel/helper-plugin-utils" "^7.0.0"
"@babel/plugin-proposal-decorators@^7.4.4":
@@ -811,10 +794,10 @@
dependencies:
regenerator-runtime "^0.12.0"
-"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4":
- version "7.5.4"
- resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.4.tgz#cb7d1ad7c6d65676e66b47186577930465b5271b"
- integrity sha512-Na84uwyImZZc3FKf4aUF1tysApzwf3p2yuFBIyBfbzT5glzKTdvYI4KVW4kcgjrzoGUjC7w3YyCHcJKaRxsr2Q==
+"@babel/runtime@^7.1.2", "@babel/runtime@^7.4.2", "@babel/runtime@^7.4.5", "@babel/runtime@^7.5.4", "@babel/runtime@^7.5.5":
+ version "7.5.5"
+ resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
+ integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
dependencies:
regenerator-runtime "^0.13.2"
@@ -3854,14 +3837,16 @@ eslint-scope@^5.0.0:
estraverse "^4.1.1"
eslint-utils@^1.3.1:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.3.1.tgz#9a851ba89ee7c460346f97cf8939c7298827e512"
- integrity sha512-Z7YjnIldX+2XMcjr7ZkgEsOj/bREONV60qYeB/bjMAqqqZ4zxKyWX+BOUkdmRmA9riiIPVvo5x86m5elviOk0Q==
+ version "1.4.2"
+ resolved "https://registry.yarnpkg.com/eslint-utils/-/eslint-utils-1.4.2.tgz#166a5180ef6ab7eb462f162fd0e6f2463d7309ab"
+ integrity sha512-eAZS2sEUMlIeCjBeubdj45dmBHQwPHWyBcT1VSYB7o9x9WRRqKxyUoiXlRjyAwzN7YEzHJlYg0NmzDRWx6GP4Q==
+ dependencies:
+ eslint-visitor-keys "^1.0.0"
eslint-visitor-keys@^1.0.0:
- version "1.0.0"
- resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#3f3180fb2e291017716acb4c9d6d5b5c34a6a81d"
- integrity sha512-qzm/XxIbxm/FHyH341ZrbnMUpe+5Bocte9xkmFMzPMjRaZMcXww+MpBptFvtU+79L362nqiLhekCxCxDPaUMBQ==
+ version "1.1.0"
+ resolved "https://registry.yarnpkg.com/eslint-visitor-keys/-/eslint-visitor-keys-1.1.0.tgz#e2a82cea84ff246ad6fb57f9bde5b46621459ec2"
+ integrity sha512-8y9YjtM1JBJU/A9Kc+SbaOV4y29sSWckBwMHa+FGtVj5gN/sbnKDf6xJUl+8g7FAij9LVaP8C24DUiH/f/2Z9A==
eslint@^2.7.0:
version "2.13.1"
@@ -4272,10 +4257,10 @@ file-entry-cache@^5.0.1:
dependencies:
flat-cache "^2.0.1"
-file-loader@^4.1.0:
- version "4.1.0"
- resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-4.1.0.tgz#3a763391bc9502da7c59612fe348e38fc1980336"
- integrity sha512-ajDk1nlByoalZAGR4b0H6oD+EGlWnyW1qbSxzaUc7RFiqmn+RbXQQRbTc72jsiUIlVusJ4Et58ltds8ZwTfnAw==
+file-loader@^4.2.0:
+ version "4.2.0"
+ resolved "https://registry.yarnpkg.com/file-loader/-/file-loader-4.2.0.tgz#5fb124d2369d7075d70a9a5abecd12e60a95215e"
+ integrity sha512-+xZnaK5R8kBJrHK0/6HRlrKNamvVS5rjyuju+rnyxRGuwUJwpAMsVzUl5dz6rK8brkzjV6JpcFNjp6NqV0g1OQ==
dependencies:
loader-utils "^1.2.3"
schema-utils "^2.0.0"
@@ -6756,9 +6741,9 @@ mississippi@^3.0.0:
through2 "^2.0.0"
mixin-deep@^1.2.0:
- version "1.3.1"
- resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.1.tgz#a49e7268dce1a0d9698e45326c5626df3543d0fe"
- integrity sha512-8ZItLHeEgaqEvd5lYBXfm4EZSFCX29Jb9K+lAHhDKzReKBQKj3R+7NOF6tjqYi9t4oI8VUfaWITJQm86wnXGNQ==
+ version "1.3.2"
+ resolved "https://registry.yarnpkg.com/mixin-deep/-/mixin-deep-1.3.2.tgz#1120b43dc359a785dce65b55b82e257ccf479566"
+ integrity sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==
dependencies:
for-in "^1.0.2"
is-extendable "^1.0.1"
@@ -8476,10 +8461,10 @@ react-intl@^2.9.0:
intl-relativeformat "^2.1.0"
invariant "^2.1.1"
-react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6:
- version "16.8.6"
- resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.8.6.tgz#5bbc1e2d29141c9fbdfed456343fe2bc430a6a16"
- integrity sha512-aUk3bHfZ2bRSVFFbbeVS4i+lNPZr3/WM5jT2J5omUVV1zzcs1nAaf3l51ctA5FFvCRbhrH0bdAsRRQddFJZPtA==
+react-is@^16.3.2, react-is@^16.7.0, react-is@^16.8.1, react-is@^16.8.4, react-is@^16.8.6, react-is@^16.9.0:
+ version "16.9.0"
+ resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.9.0.tgz#21ca9561399aad0ff1a7701c01683e8ca981edcb"
+ integrity sha512-tJBzzzIgnnRfEm046qRcURvwQnZVXmuCbscxUO5RWrGTXpon2d4c8mI0D8WE6ydVIm29JiLB6+RslkIvym9Rjw==
react-lifecycles-compat@^3.0.2, react-lifecycles-compat@^3.0.4:
version "3.0.4"
@@ -8531,17 +8516,17 @@ react-redux-loading-bar@^4.0.8:
prop-types "^15.6.2"
react-lifecycles-compat "^3.0.2"
-react-redux@^7.1.0:
- version "7.1.0"
- resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.0.tgz#72af7cf490a74acdc516ea9c1dd80e25af9ea0b2"
- integrity sha512-hyu/PoFK3vZgdLTg9ozbt7WF3GgX5+Yn3pZm5/96/o4UueXA+zj08aiSC9Mfj2WtD1bvpIb3C5yvskzZySzzaw==
+react-redux@^7.1.1:
+ version "7.1.1"
+ resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-7.1.1.tgz#ce6eee1b734a7a76e0788b3309bf78ff6b34fa0a"
+ integrity sha512-QsW0vcmVVdNQzEkrgzh2W3Ksvr8cqpAv5FhEk7tNEft+5pp7rXxAudTz3VOPawRkLIepItpkEIyLcN/VVXzjTg==
dependencies:
- "@babel/runtime" "^7.4.5"
+ "@babel/runtime" "^7.5.5"
hoist-non-react-statics "^3.3.0"
invariant "^2.2.4"
loose-envify "^1.4.0"
prop-types "^15.7.2"
- react-is "^16.8.6"
+ react-is "^16.9.0"
react-router-dom@^4.1.1:
version "4.3.1"
@@ -8790,10 +8775,10 @@ redux-thunk@^2.2.0:
resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
integrity sha512-km6dclyFnmcvxhAcrQV2AkZmPQjzPDjgVlQtR0EQjxZPyJ0BnMf3in1ryuR8A2qU0HldVRfxYXbFSKlI3N7Slw==
-redux@^4.0.1:
- version "4.0.1"
- resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.1.tgz#436cae6cc40fbe4727689d7c8fae44808f1bfef5"
- integrity sha512-R7bAtSkk7nY6O/OYMVR9RiBI+XghjF9rlbl5806HJbQph0LJVHZrU5oaO4q70eUKiqMRqm4y07KLTlMZ2BlVmg==
+redux@^4.0.4:
+ version "4.0.4"
+ resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.4.tgz#4ee1aeb164b63d6a1bcc57ae4aa0b6e6fa7a3796"
+ integrity sha512-vKv4WdiJxOWKxK0yRoaK3Y4pxxB0ilzVx6dszU2W8wLxlb2yikRph4iV/ymtdJ6ZxpBLFbyrxklnT5yBbQSl3Q==
dependencies:
loose-envify "^1.4.0"
symbol-observable "^1.2.0"
@@ -9084,6 +9069,13 @@ rimraf@2.6.3, rimraf@^2.5.4, rimraf@^2.6.1, rimraf@^2.6.2, rimraf@^2.6.3, rimraf
dependencies:
glob "^7.1.3"
+rimraf@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.0.tgz#614176d4b3010b75e5c390eb0ee96f6dc0cebb9b"
+ integrity sha512-NDGVxTsjqfunkds7CqsOiEnxln4Bo7Nddl3XhS4pXg5OzwkLqJ971ZVAAnB+DDLnF76N+VnDEiBHaVV8I06SUg==
+ dependencies:
+ glob "^7.1.3"
+
ripemd160@^2.0.0, ripemd160@^2.0.1:
version "2.0.2"
resolved "https://registry.yarnpkg.com/ripemd160/-/ripemd160-2.0.2.tgz#a1c1a6f624751577ba5d07914cbc92850585890c"
@@ -10459,6 +10451,11 @@ watchpack@^1.5.0:
graceful-fs "^4.1.2"
neo-async "^2.5.0"
+wavesurfer.js@^3.0.0:
+ version "3.0.0"
+ resolved "https://registry.yarnpkg.com/wavesurfer.js/-/wavesurfer.js-3.0.0.tgz#35f36d76d59c749dca453cf4e10ee0ec49f454f8"
+ integrity sha512-DANu206c6gb9pSUbYFevsSiXMy8+Ri+CNtqm0UsouUdsn9fVQRtYs8uxzBtXK+rUPlIc6FlO54DU8uWeW3lDzw==
+
wbuf@^1.1.0, wbuf@^1.7.3:
version "1.7.3"
resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df"
@@ -10503,10 +10500,10 @@ webpack-bundle-analyzer@^3.3.2:
opener "^1.5.1"
ws "^6.0.0"
-webpack-cli@^3.3.6:
- version "3.3.6"
- resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.6.tgz#2c8c399a2642133f8d736a359007a052e060032c"
- integrity sha512-0vEa83M7kJtxK/jUhlpZ27WHIOndz5mghWL2O53kiDoA9DIxSKnfqB92LoqEn77cT4f3H2cZm1BMEat/6AZz3A==
+webpack-cli@^3.3.7:
+ version "3.3.7"
+ resolved "https://registry.yarnpkg.com/webpack-cli/-/webpack-cli-3.3.7.tgz#77c8580dd8e92f69d635e0238eaf9d9c15759a91"
+ integrity sha512-OhTUCttAsr+IZSMVwGROGRHvT+QAs8H6/mHIl4SvhAwYywjiylYjpwybGx7WQ9Hkb45FhjtsymkwiRRbGJ1SZQ==
dependencies:
chalk "2.4.2"
cross-spawn "6.0.5"