diff --git a/Gemfile b/Gemfile
index ace4b58e6c2e9..4305c42b0cc6b 100644
--- a/Gemfile
+++ b/Gemfile
@@ -31,7 +31,7 @@ gem 'charlock_holmes', '~> 0.7.6'
gem 'iso-639'
gem 'chewy', '~> 5.0'
gem 'cld3', '~> 3.2.4'
-gem 'devise', '~> 4.6'
+gem 'devise', '~> 4.7'
gem 'devise-two-factor', '~> 3.1'
group :pam_authentication, optional: true do
@@ -43,6 +43,7 @@ gem 'omniauth-cas', '~> 1.1'
gem 'omniauth-saml', '~> 1.10'
gem 'omniauth', '~> 1.9'
+gem 'discard', '~> 1.1'
gem 'doorkeeper', '~> 5.1'
gem 'fast_blank', '~> 1.0'
gem 'fastimage'
diff --git a/Gemfile.lock b/Gemfile.lock
index 0af2b2a890f17..caa8452a75006 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -112,7 +112,7 @@ GEM
aws-sigv4 (~> 1.1)
aws-sigv4 (1.1.0)
aws-eventstream (~> 1.0, >= 1.0.2)
- bcrypt (3.1.12)
+ bcrypt (3.1.13)
benchmark-ips (2.7.2)
better_errors (2.5.1)
coderay (>= 1.0.0)
@@ -127,7 +127,7 @@ GEM
brakeman (4.6.1)
browser (2.6.1)
builder (3.2.3)
- bullet (6.0.1)
+ bullet (6.0.2)
activesupport (>= 3.0.0)
uniform_notifier (~> 1.11)
bundler-audit (0.6.1)
@@ -188,10 +188,10 @@ GEM
rack (>= 1)
rake (> 10, < 13)
thor (~> 0.19)
- devise (4.6.2)
+ devise (4.7.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
- railties (>= 4.1.0, < 6.0)
+ railties (>= 4.1.0)
responders
warden (~> 1.2.3)
devise-two-factor (3.1.0)
@@ -204,6 +204,8 @@ GEM
devise (>= 4.0.0)
rpam2 (~> 4.0)
diff-lcs (1.3)
+ discard (1.1.0)
+ activerecord (>= 4.2, < 7)
docile (1.3.2)
domain_name (0.5.20180417)
unf (>= 0.0.5, < 1.0.0)
@@ -555,7 +557,7 @@ GEM
rainbow (>= 2.2.2, < 4.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 1.4.0, < 1.7)
- rubocop-rails (2.3.0)
+ rubocop-rails (2.3.1)
rack (>= 1.1)
rubocop (>= 0.72.0)
ruby-progressbar (1.10.1)
@@ -692,9 +694,10 @@ DEPENDENCIES
concurrent-ruby
connection_pool
derailed_benchmarks
- devise (~> 4.6)
+ devise (~> 4.7)
devise-two-factor (~> 3.1)
devise_pam_authenticatable2 (~> 9.2)
+ discard (~> 1.1)
doorkeeper (~> 5.1)
dotenv-rails (~> 2.7)
fabrication (~> 2.20)
diff --git a/app/controllers/admin/account_actions_controller.rb b/app/controllers/admin/account_actions_controller.rb
index a2cea461ee195..ea56fa0ac7229 100644
--- a/app/controllers/admin/account_actions_controller.rb
+++ b/app/controllers/admin/account_actions_controller.rb
@@ -5,7 +5,7 @@ module Admin
before_action :set_account
def new
- @account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true)
+ @account_action = Admin::AccountAction.new(type: params[:type], report_id: params[:report_id], send_email_notification: true, include_statuses: true)
@warning_presets = AccountWarningPreset.all
end
@@ -30,7 +30,7 @@ module Admin
end
def resource_params
- params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification)
+ params.require(:admin_account_action).permit(:type, :report_id, :warning_preset_id, :text, :send_email_notification, :include_statuses)
end
end
end
diff --git a/app/controllers/api/v1/reports_controller.rb b/app/controllers/api/v1/reports_controller.rb
index e182a9c6cf624..1b0b4b05b2e72 100644
--- a/app/controllers/api/v1/reports_controller.rb
+++ b/app/controllers/api/v1/reports_controller.rb
@@ -21,7 +21,7 @@ class Api::V1::ReportsController < Api::BaseController
private
def reported_status_ids
- reported_account.statuses.find(status_ids).pluck(:id)
+ reported_account.statuses.with_discarded.find(status_ids).pluck(:id)
end
def status_ids
diff --git a/app/controllers/api/v1/statuses/reblogs_controller.rb b/app/controllers/api/v1/statuses/reblogs_controller.rb
index ed4f551003d67..42381a37fdbe8 100644
--- a/app/controllers/api/v1/statuses/reblogs_controller.rb
+++ b/app/controllers/api/v1/statuses/reblogs_controller.rb
@@ -18,6 +18,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
@reblogs_map = { @status.id => false }
authorize status_for_destroy, :unreblog?
+ status_for_destroy.discard
RemovalWorker.perform_async(status_for_destroy.id)
render json: @status, serializer: REST::StatusSerializer, relationships: StatusRelationshipsPresenter.new([@status], current_user&.account_id, reblogs_map: @reblogs_map)
@@ -30,7 +31,7 @@ class Api::V1::Statuses::ReblogsController < Api::BaseController
end
def status_for_destroy
- current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
+ @status_for_destroy ||= current_user.account.statuses.where(reblog_of_id: params[:status_id]).first!
end
def reblog_params
diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb
index 4e7476a8489b2..486004f9cdc98 100644
--- a/app/controllers/api/v1/statuses_controller.rb
+++ b/app/controllers/api/v1/statuses_controller.rb
@@ -54,7 +54,8 @@ class Api::V1::StatusesController < Api::BaseController
@status = Status.where(account_id: current_user.account).find(params[:id])
authorize @status, :destroy?
- RemovalWorker.perform_async(@status.id)
+ @status.discard
+ RemovalWorker.perform_async(@status.id, redraft: true)
render json: @status, serializer: REST::StatusSerializer, source_requested: true
end
diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js
index ef2500e7bdd84..cd36d8007bcce 100644
--- a/app/javascript/mastodon/actions/alerts.js
+++ b/app/javascript/mastodon/actions/alerts.js
@@ -3,6 +3,8 @@ import { defineMessages } from 'react-intl';
const messages = defineMessages({
unexpectedTitle: { id: 'alert.unexpected.title', defaultMessage: 'Oops!' },
unexpectedMessage: { id: 'alert.unexpected.message', defaultMessage: 'An unexpected error occurred.' },
+ rateLimitedTitle: { id: 'alert.rate_limited.title', defaultMessage: 'Rate limited' },
+ rateLimitedMessage: { id: 'alert.rate_limited.message', defaultMessage: 'Please retry after {retry_time, time, medium}.' },
});
export const ALERT_SHOW = 'ALERT_SHOW';
@@ -23,23 +25,29 @@ export function clearAlert() {
};
};
-export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage) {
+export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
return {
type: ALERT_SHOW,
title,
message,
+ message_values,
};
};
export function showAlertForError(error) {
if (error.response) {
- const { data, status, statusText } = error.response;
+ const { data, status, statusText, headers } = error.response;
if (status === 404 || status === 410) {
// Skip these errors as they are reflected in the UI
return { type: ALERT_NOOP };
}
+ if (status === 429 && headers['x-ratelimit-reset']) {
+ const reset_date = new Date(headers['x-ratelimit-reset']);
+ return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date });
+ }
+
let message = statusText;
let title = `${status}`;
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index c27c53df0bfde..061a36bb8e756 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -356,6 +356,8 @@ const fetchComposeSuggestionsTags = throttle((dispatch, getState, token) => {
cancelFetchComposeSuggestionsTags();
}
+ dispatch(updateSuggestionTags(token));
+
api(getState).get('/api/v2/search', {
cancelToken: new CancelToken(cancel => {
cancelFetchComposeSuggestionsTags = cancel;
diff --git a/app/javascript/mastodon/components/autosuggest_hashtag.js b/app/javascript/mastodon/components/autosuggest_hashtag.js
index eabb8b1785fbe..e2f4e320dd1db 100644
--- a/app/javascript/mastodon/components/autosuggest_hashtag.js
+++ b/app/javascript/mastodon/components/autosuggest_hashtag.js
@@ -9,18 +9,18 @@ export default class AutosuggestHashtag extends React.PureComponent {
tag: PropTypes.shape({
name: PropTypes.string.isRequired,
url: PropTypes.string,
- history: PropTypes.array.isRequired,
+ history: PropTypes.array,
}).isRequired,
};
render () {
const { tag } = this.props;
- const weeklyUses = shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
+ const weeklyUses = tag.history && shortNumberFormat(tag.history.reduce((total, day) => total + (day.uses * 1), 0));
return (
+
{!!status.get('poll') &&
}
diff --git a/app/javascript/mastodon/containers/media_container.js b/app/javascript/mastodon/containers/media_container.js
index 8fddb6f54c6a7..db340032ae3f3 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/audio/index.js b/app/javascript/mastodon/features/audio/index.js
new file mode 100644
index 0000000000000..95e5675f3cf12
--- /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 d0303dbfbbf90..dd2632796c033 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 d8d49cb95cd53..840d0a3da3abb 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 eb9f3ea45e6e4..8606a642e4978 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 0731abcf4d5a8..e2de8b0e6a20d 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';
const messages = defineMessages({
start: { id: 'getting_started.heading', defaultMessage: 'Getting started' },
@@ -25,6 +27,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) => ({
@@ -61,6 +65,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));
}
@@ -92,7 +111,7 @@ class Compose extends React.PureComponent {
)}
-
+
);
}
diff --git a/app/javascript/mastodon/features/getting_started/components/trends.js b/app/javascript/mastodon/features/getting_started/components/trends.js
index 1dcacc8b39235..3b9a3075fbefe 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/status/components/detailed_status.js b/app/javascript/mastodon/features/status/components/detailed_status.js
index 4af157af13478..e97f18f08c134 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/ui/components/focal_point_modal.js b/app/javascript/mastodon/features/ui/components/focal_point_modal.js
index e0ef1a066a98f..735e445e88cc3 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 b481983dc8864..2b9bd3875e259 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/containers/notifications_container.js b/app/javascript/mastodon/features/ui/containers/notifications_container.js
index b60a0216f628e..3819da3d850c1 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 f0c3eff834ed1..9d284c2216cf7 100644
--- a/app/javascript/mastodon/features/ui/index.js
+++ b/app/javascript/mastodon/features/ui/index.js
@@ -141,14 +141,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();
diff --git a/app/javascript/mastodon/features/ui/util/async-components.js b/app/javascript/mastodon/features/ui/util/async-components.js
index 0a07aa75e1be1..a9b95c7b80b07 100644
--- a/app/javascript/mastodon/features/ui/util/async-components.js
+++ b/app/javascript/mastodon/features/ui/util/async-components.js
@@ -137,3 +137,7 @@ export function Search () {
export function Tesseract () {
return import(/*webpackChunkName: "tesseract" */'tesseract.js');
}
+
+export function Audio () {
+ return import(/* webpackChunkName: "features/audio" */'../../audio');
+}
diff --git a/app/javascript/mastodon/features/video/index.js b/app/javascript/mastodon/features/video/index.js
index da48c165e0669..5fe4e956f879f 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/defaultMessages.json b/app/javascript/mastodon/locales/defaultMessages.json
index 246c9bd0e955e..617328613ce6a 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 628ede3e314f7..28ea713a3223a 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -162,7 +162,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}}",
@@ -258,7 +257,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",
@@ -378,7 +376,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/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js
index 089d920c3e4ee..c62ab0dfdb9c8 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 7b0cdd5a5648f..268237846c633 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/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index c87654547d47f..6f1ce9602a498 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 0000000000000..b43417f4b4212
--- /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 b4fb1d709c02d..e25a80c043287 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 ee8a7d265a10a..e7114ed077506 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 5c30c1295c76a..8aaa068d387ab 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -948,7 +948,8 @@
opacity: 1;
animation: fade 150ms linear;
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 8px;
}
@@ -1043,7 +1044,8 @@
white-space: normal;
}
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 8px;
max-width: 250px;
}
@@ -1154,7 +1156,8 @@
}
}
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 8px;
}
}
@@ -2130,7 +2133,8 @@ a.account__display-name {
padding: 15px;
.media-gallery,
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 15px;
}
}
@@ -2172,7 +2176,8 @@ a.account__display-name {
.media-gallery,
&__action-bar,
- .video-player {
+ .video-player,
+ .audio-player {
margin-top: 10px;
}
}
@@ -2765,6 +2770,15 @@ a.account__display-name {
animation: fade 150ms linear;
margin-top: 10px;
+ 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;
@@ -5034,15 +5048,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 {
diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb
index 1f2b40c1506ac..3450604629b8b 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/mailers/user_mailer.rb b/app/mailers/user_mailer.rb
index 8f3a4ab3aa269..b41004acc6149 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/admin/account_action.rb b/app/models/admin/account_action.rb
index bdbd342fb3ccf..c7da8b52cecf4 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/form/status_batch.rb b/app/models/form/status_batch.rb
index 933dfdaca1ba5..e09cc2594e483 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/report.rb b/app/models/report.rb
index 5192ceef799cc..1e707ff1c4894 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 de790027d5b76..757deea06e356 100644
--- a/app/models/status.rb
+++ b/app/models/status.rb
@@ -25,15 +25,19 @@
# full_status_text :text default(""), not null
# poll_id :bigint(8)
# content_type :string
+# 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
@@ -77,7 +81,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/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb
index c9a9a5a6e054b..31237337aba95 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 c19fa2126fbcb..b2f7120893a4b 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
@@ -25,6 +30,7 @@ class RemoveStatusService < BaseService
remove_from_media if status.media_attachments.any?
remove_from_direct if status.direct_visibility?
remove_from_spam_check
+ remove_media
@status.destroy!
else
@@ -151,6 +157,12 @@ class RemoveStatusService < BaseService
end
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/views/admin/account_actions/new.html.haml b/app/views/admin/account_actions/new.html.haml
index 97286c8e5b5ef..20fbeef335beb 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 408d515cae7b4..af7a59802e43f 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -105,7 +105,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 9376db7ffe187..6facc0a568ba9 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/notification_mailer/_status.html.haml b/app/views/notification_mailer/_status.html.haml
index 57b5688bd6fff..40f3aa88a7365 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/statuses/_detailed_status.html.haml b/app/views/statuses/_detailed_status.html.haml
index 8686c20335031..12f03ccddee0b 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 27f6fc2270d5a..fe1591bf9ae54 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 72ea5e5d28f93..030a57bb452dd 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 b4f2402cb3743..24c1f86f2b7ed 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 19a660dd3e019..2a1eaa89b061d 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/locales/en.yml b/config/locales/en.yml
index be190f0f1376c..8e5ee8543b714 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -512,6 +512,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
@@ -1129,7 +1130,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/simple_form.en.yml b/config/locales/simple_form.en.yml
index df898c62195a8..14378b7bd3259 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}
@@ -65,6 +66,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
@@ -156,6 +158,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/db/migrate/20190819134503_add_deleted_at_to_statuses.rb b/db/migrate/20190819134503_add_deleted_at_to_statuses.rb
new file mode 100644
index 0000000000000..5af109097e8b1
--- /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 0000000000000..5c2ea1f6a2416
--- /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 0000000000000..deca25c3515ef
--- /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 7e62fe1f56e92..328506b50283d 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"
@@ -657,7 +657,9 @@ ActiveRecord::Schema.define(version: 2019_08_15_225426) do
t.boolean "local_only"
t.bigint "poll_id"
t.string "content_type"
- 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/package.json b/package.json
index 6f6730b9ab0b3..cba13911fcc26 100644
--- a/package.json
+++ b/package.json
@@ -61,7 +61,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",
@@ -137,7 +137,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",
@@ -163,10 +163,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 c358506d6dce8..bd146b79560a5 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 1a08c10b7e873..6b06343efbe4b 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/mailers/previews/user_mailer_preview.rb b/spec/mailers/previews/user_mailer_preview.rb
index 53c8364944579..ead3b3baa1c19 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 a3db60cfc85c9..87fc285007867 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 00c790a11f936..f9c58c90f88ca 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/yarn.lock b/yarn.lock
index ecc4e317c542e..ab20731ff5d44 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"
@@ -3858,14 +3841,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"
@@ -6764,9 +6749,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"
@@ -8484,10 +8469,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"
@@ -8539,17 +8524,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"
@@ -10474,6 +10459,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"
@@ -10518,10 +10508,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"