diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index c1d580e515..653af9583a 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -812,7 +812,6 @@ Style/FrozenStringLiteralComment:
- 'config/initializers/httplog.rb'
- 'config/initializers/inflections.rb'
- 'config/initializers/mail_delivery_job.rb'
- - 'config/initializers/makara.rb'
- 'config/initializers/mime_types.rb'
- 'config/initializers/oj.rb'
- 'config/initializers/omniauth.rb'
diff --git a/Gemfile b/Gemfile
index 7a0fbdc82d..6bd881c906 100644
--- a/Gemfile
+++ b/Gemfile
@@ -11,7 +11,6 @@ gem 'rack', '~> 2.2.7'
gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.5'
-gem 'makara', '~> 0.5'
gem 'pghero'
gem 'dotenv-rails', '~> 2.8'
diff --git a/Gemfile.lock b/Gemfile.lock
index 985e36c20c..9bd708d61e 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -399,8 +399,6 @@ GEM
net-imap
net-pop
net-smtp
- makara (0.5.1)
- activerecord (>= 5.2.0)
marcel (1.0.2)
mario-redis-lock (1.2.1)
redis (>= 3.0.5)
@@ -815,7 +813,6 @@ DEPENDENCIES
letter_opener_web (~> 2.0)
link_header (~> 0.0)
lograge (~> 0.12)
- makara (~> 0.5)
mario-redis-lock (~> 1.2)
memory_profiler
mime-types (~> 3.4.1)
diff --git a/app/controllers/api/v1/timelines/home_controller.rb b/app/controllers/api/v1/timelines/home_controller.rb
index ae6dbcb8b3..0ee28ef042 100644
--- a/app/controllers/api/v1/timelines/home_controller.rb
+++ b/app/controllers/api/v1/timelines/home_controller.rb
@@ -6,11 +6,14 @@ class Api::V1::Timelines::HomeController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show
- @statuses = load_statuses
+ ApplicationRecord.connected_to(role: :read, prevent_writes: true) do
+ @statuses = load_statuses
+ @relationships = StatusRelationshipsPresenter.new(@statuses, current_user&.account_id)
+ end
render json: @statuses,
each_serializer: REST::StatusSerializer,
- relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id),
+ relationships: @relationships,
status: account_home_feed.regenerating? ? 206 : 200
end
diff --git a/app/javascript/mastodon/actions/alerts.js b/app/javascript/mastodon/actions/alerts.js
index 0220b0af58..051a9675b3 100644
--- a/app/javascript/mastodon/actions/alerts.js
+++ b/app/javascript/mastodon/actions/alerts.js
@@ -12,52 +12,48 @@ export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP';
-export function dismissAlert(alert) {
- return {
- type: ALERT_DISMISS,
- alert,
- };
-}
+export const dismissAlert = alert => ({
+ type: ALERT_DISMISS,
+ alert,
+});
-export function clearAlert() {
- return {
- type: ALERT_CLEAR,
- };
-}
+export const clearAlert = () => ({
+ type: ALERT_CLEAR,
+});
-export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) {
- return {
- type: ALERT_SHOW,
- title,
- message,
- message_values,
- };
-}
+export const showAlert = alert => ({
+ type: ALERT_SHOW,
+ alert,
+});
-export function showAlertForError(error, skipNotFound = false) {
+export const showAlertForError = (error, skipNotFound = false) => {
if (error.response) {
const { data, status, statusText, headers } = error.response;
+ // Skip these errors as they are reflected in the UI
if (skipNotFound && (status === 404 || status === 410)) {
- // Skip these errors as they are reflected in the UI
return { type: ALERT_NOOP };
}
+ // Rate limit errors
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 });
+ return showAlert({
+ title: messages.rateLimitedTitle,
+ message: messages.rateLimitedMessage,
+ values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
+ });
}
- let message = statusText;
- let title = `${status}`;
-
- if (data.error) {
- message = data.error;
- }
-
- return showAlert(title, message);
- } else {
- console.error(error);
- return showAlert();
+ return showAlert({
+ title: `${status}`,
+ message: data.error || statusText,
+ });
}
+
+ console.error(error);
+
+ return showAlert({
+ title: messages.unexpectedTitle,
+ message: messages.unexpectedMessage,
+ });
}
diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js
index 7cc8516fdb..5f7b8e949f 100644
--- a/app/javascript/mastodon/actions/compose.js
+++ b/app/javascript/mastodon/actions/compose.js
@@ -82,6 +82,8 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' },
+ open: { id: 'compose.published.open', defaultMessage: 'Open' },
+ published: { id: 'compose.published.body', defaultMessage: 'Post published.' },
});
export const ensureComposeIsVisible = (getState, routerHistory) => {
@@ -242,6 +244,13 @@ export function submitCompose(routerHistory) {
}
insertIfOnline(`account:${response.data.account.id}`);
}
+
+ dispatch(showAlert({
+ message: messages.published,
+ action: messages.open,
+ dismissAfter: 10000,
+ onClick: () => routerHistory.push(`/@${response.data.account.username}/${response.data.id}`),
+ }));
}).catch(function (error) {
dispatch(submitComposeFail(error));
});
@@ -271,18 +280,19 @@ export function submitComposeFail(error) {
export function uploadCompose(files) {
return function (dispatch, getState) {
const uploadLimit = 4;
- const media = getState().getIn(['compose', 'media_attachments']);
- const pending = getState().getIn(['compose', 'pending_media_attachments']);
+ const media = getState().getIn(['compose', 'media_attachments']);
+ const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0);
+
let total = Array.from(files).reduce((a, v) => a + v.size, 0);
if (files.length + media.size + pending > uploadLimit) {
- dispatch(showAlert(undefined, messages.uploadErrorLimit));
+ dispatch(showAlert({ message: messages.uploadErrorLimit }));
return;
}
if (getState().getIn(['compose', 'poll'])) {
- dispatch(showAlert(undefined, messages.uploadErrorPoll));
+ dispatch(showAlert({ message: messages.uploadErrorPoll }));
return;
}
diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx
index 8b3c20f824..ab9dac27e5 100644
--- a/app/javascript/mastodon/components/status_action_bar.jsx
+++ b/app/javascript/mastodon/components/status_action_bar.jsx
@@ -237,7 +237,6 @@ class StatusActionBar extends ImmutablePureComponent {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.context.identity;
- const anonymousAccess = !signedIn;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
const mutingConversation = status.get('muted');
@@ -263,71 +262,73 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
}
- menu.push(null);
-
- menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
-
- if (writtenByMe && pinnableStatus) {
- menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
- }
-
- menu.push(null);
-
- if (writtenByMe || withDismiss) {
- menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
- menu.push(null);
- }
-
- if (writtenByMe) {
- menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
- menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
- menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
- } else {
- menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
- menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
+ if (signedIn) {
menu.push(null);
- if (relationship && relationship.get('muting')) {
- menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
+ menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
+
+ if (writtenByMe && pinnableStatus) {
+ menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+ }
+
+ menu.push(null);
+
+ if (writtenByMe || withDismiss) {
+ menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+ menu.push(null);
+ }
+
+ if (writtenByMe) {
+ menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
+ menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
+ menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else {
- menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
- }
-
- if (relationship && relationship.get('blocking')) {
- menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
- } else {
- menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
- }
-
- if (!this.props.onFilter) {
- menu.push(null);
- menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
- menu.push(null);
- }
-
- menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true });
-
- if (account.get('acct') !== account.get('username')) {
- const domain = account.get('acct').split('@')[1];
-
+ menu.push({ text: intl.formatMessage(messages.mention, { name: account.get('username') }), action: this.handleMentionClick });
+ menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
- if (relationship && relationship.get('domain_blocking')) {
- menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
+ if (relationship && relationship.get('muting')) {
+ menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {
- menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
+ menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
}
- }
- if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
- menu.push(null);
- if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
- menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
- menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+ if (relationship && relationship.get('blocking')) {
+ menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
+ } else {
+ menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
}
- if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
+
+ if (!this.props.onFilter) {
+ menu.push(null);
+ menu.push({ text: intl.formatMessage(messages.filter), action: this.handleFilterClick, dangerous: true });
+ menu.push(null);
+ }
+
+ menu.push({ text: intl.formatMessage(messages.report, { name: account.get('username') }), action: this.handleReport, dangerous: true });
+
+ if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
- menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
+
+ menu.push(null);
+
+ if (relationship && relationship.get('domain_blocking')) {
+ menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
+ } else {
+ menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
+ }
+ }
+
+ if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
+ menu.push(null);
+ if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
+ menu.push({ text: intl.formatMessage(messages.admin_account, { name: account.get('username') }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
+ menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+ }
+ if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
+ const domain = account.get('acct').split('@')[1];
+ menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
+ }
}
}
}
@@ -371,7 +372,6 @@ class StatusActionBar extends ImmutablePureComponent {
({
layout: state.getIn(['meta', 'layout']),
- isSearching: state.getIn(['search', 'submitted']) || !showTrends,
+ isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled,
});
class Explore extends PureComponent {
diff --git a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js
index b63796a8b2..1e62ed9a5a 100644
--- a/app/javascript/mastodon/features/notifications/containers/column_settings_container.js
+++ b/app/javascript/mastodon/features/notifications/containers/column_settings_container.js
@@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === 'granted') {
dispatch(changePushNotifications(path.slice(1), checked));
} else {
- dispatch(showAlert(undefined, messages.permissionDenied));
+ dispatch(showAlert({ message: messages.permissionDenied }));
}
}));
} else {
@@ -47,7 +47,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === 'granted') {
dispatch(changeSetting(['notifications', ...path], checked));
} else {
- dispatch(showAlert(undefined, messages.permissionDenied));
+ dispatch(showAlert({ message: messages.permissionDenied }));
}
}));
} else {
diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx
index bc90ce592c..d3295913c7 100644
--- a/app/javascript/mastodon/features/status/components/action_bar.jsx
+++ b/app/javascript/mastodon/features/status/components/action_bar.jsx
@@ -195,71 +195,74 @@ class ActionBar extends PureComponent {
let menu = [];
- if (publicStatus) {
- if (isRemote) {
- menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
- }
-
- menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
-
- if ('share' in navigator) {
- menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
- }
-
- menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
- menu.push(null);
+ if (publicStatus && isRemote) {
+ menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
}
- if (writtenByMe) {
- if (pinnableStatus) {
- menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
- menu.push(null);
- }
+ menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
- menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
- menu.push(null);
- menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
- menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
- menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
- } else {
- menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
+ if (publicStatus && 'share' in navigator) {
+ menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
+ }
+
+ if (publicStatus) {
+ menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
+ }
+
+ if (signedIn) {
menu.push(null);
- if (relationship && relationship.get('muting')) {
- menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
+ if (writtenByMe) {
+ if (pinnableStatus) {
+ menu.push({ text: intl.formatMessage(status.get('pinned') ? messages.unpin : messages.pin), action: this.handlePinClick });
+ menu.push(null);
+ }
+
+ menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
+ menu.push(null);
+ menu.push({ text: intl.formatMessage(messages.edit), action: this.handleEditClick });
+ menu.push({ text: intl.formatMessage(messages.delete), action: this.handleDeleteClick, dangerous: true });
+ menu.push({ text: intl.formatMessage(messages.redraft), action: this.handleRedraftClick, dangerous: true });
} else {
- menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
- }
-
- if (relationship && relationship.get('blocking')) {
- menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
- } else {
- menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
- }
-
- menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
-
- if (account.get('acct') !== account.get('username')) {
- const domain = account.get('acct').split('@')[1];
-
+ menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push(null);
- if (relationship && relationship.get('domain_blocking')) {
- menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
+ if (relationship && relationship.get('muting')) {
+ menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {
- menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
+ menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true });
}
- }
- if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
- menu.push(null);
- if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
- menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
- menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+ if (relationship && relationship.get('blocking')) {
+ menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
+ } else {
+ menu.push({ text: intl.formatMessage(messages.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
}
- if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
+
+ menu.push({ text: intl.formatMessage(messages.report, { name: status.getIn(['account', 'username']) }), action: this.handleReport, dangerous: true });
+
+ if (account.get('acct') !== account.get('username')) {
const domain = account.get('acct').split('@')[1];
- menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
+
+ menu.push(null);
+
+ if (relationship && relationship.get('domain_blocking')) {
+ menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain });
+ } else {
+ menu.push({ text: intl.formatMessage(messages.blockDomain, { domain }), action: this.handleBlockDomain, dangerous: true });
+ }
+ }
+
+ if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION)) {
+ menu.push(null);
+ if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) {
+ menu.push({ text: intl.formatMessage(messages.admin_account, { name: status.getIn(['account', 'username']) }), href: `/admin/accounts/${status.getIn(['account', 'id'])}` });
+ menu.push({ text: intl.formatMessage(messages.admin_status), href: `/admin/accounts/${status.getIn(['account', 'id'])}/statuses/${status.get('id')}` });
+ }
+ if (isRemote && (permissions & PERMISSION_MANAGE_FEDERATION) === PERMISSION_MANAGE_FEDERATION) {
+ const domain = account.get('acct').split('@')[1];
+ menu.push({ text: intl.formatMessage(messages.admin_domain, { domain: domain }), href: `/admin/instances/${domain}` });
+ }
}
}
}
@@ -292,7 +295,7 @@ class ActionBar extends PureComponent {
-
+
);
diff --git a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
index d5e98461aa..dc406fa55c 100644
--- a/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
+++ b/app/javascript/mastodon/features/ui/components/navigation_panel.jsx
@@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
import { WordmarkLogo } from 'mastodon/components/logo';
import NavigationPortal from 'mastodon/components/navigation_portal';
-import { timelinePreview, showTrends } from 'mastodon/initial_state';
+import { timelinePreview, trendsEnabled } from 'mastodon/initial_state';
import ColumnLink from './column_link';
import DisabledAccountBanner from './disabled_account_banner';
@@ -65,7 +65,7 @@ class NavigationPanel extends Component {
>
)}
- {showTrends ? (
+ {trendsEnabled ? (
) : (
diff --git a/app/javascript/mastodon/features/ui/containers/notifications_container.js b/app/javascript/mastodon/features/ui/containers/notifications_container.js
index c1d19f7100..3d60cfdad1 100644
--- a/app/javascript/mastodon/features/ui/containers/notifications_container.js
+++ b/app/javascript/mastodon/features/ui/containers/notifications_container.js
@@ -7,26 +7,27 @@ import { NotificationStack } from 'react-notification';
import { dismissAlert } from '../../../actions/alerts';
import { getAlerts } from '../../../selectors';
-const mapStateToProps = (state, { intl }) => {
- const notifications = getAlerts(state);
+const formatIfNeeded = (intl, message, values) => {
+ if (typeof message === 'object') {
+ return intl.formatMessage(message, values);
+ }
- notifications.forEach(notification => ['title', 'message'].forEach(key => {
- const value = notification[key];
-
- if (typeof value === 'object') {
- notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
- }
- }));
-
- return { notifications };
+ return message;
};
-const mapDispatchToProps = (dispatch) => {
- return {
- onDismiss: alert => {
- dispatch(dismissAlert(alert));
- },
- };
-};
+const mapStateToProps = (state, { intl }) => ({
+ notifications: getAlerts(state).map(alert => ({
+ ...alert,
+ action: formatIfNeeded(intl, alert.action, alert.values),
+ title: formatIfNeeded(intl, alert.title, alert.values),
+ message: formatIfNeeded(intl, alert.message, alert.values),
+ })),
+});
+
+const mapDispatchToProps = (dispatch) => ({
+ onDismiss (alert) {
+ dispatch(dismissAlert(alert));
+ },
+});
export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));
diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx
index 59327f0496..b38acfc14d 100644
--- a/app/javascript/mastodon/features/ui/index.jsx
+++ b/app/javascript/mastodon/features/ui/index.jsx
@@ -22,7 +22,7 @@ import { clearHeight } from '../../actions/height_cache';
import { expandNotifications } from '../../actions/notifications';
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
import { expandHomeTimeline } from '../../actions/timelines';
-import initialState, { me, owner, singleUserMode, showTrends, trendsAsLanding } from '../../initial_state';
+import initialState, { me, owner, singleUserMode, trendsEnabled, trendsAsLanding } from '../../initial_state';
import BundleColumnError from './components/bundle_column_error';
import Header from './components/header';
@@ -170,7 +170,7 @@ class SwitchingColumnsArea extends PureComponent {
}
} else if (singleUserMode && owner && initialState?.accounts[owner]) {
redirect = ;
- } else if (showTrends && trendsAsLanding) {
+ } else if (trendsEnabled && trendsAsLanding) {
redirect = ;
} else {
redirect = ;
diff --git a/app/javascript/mastodon/initial_state.js b/app/javascript/mastodon/initial_state.js
index 6a220f37da..edac55c1bc 100644
--- a/app/javascript/mastodon/initial_state.js
+++ b/app/javascript/mastodon/initial_state.js
@@ -69,12 +69,13 @@
* @property {boolean} reduce_motion
* @property {string} repository
* @property {boolean} search_enabled
+ * @property {boolean} trends_enabled
* @property {boolean} single_user_mode
* @property {string} source_url
* @property {string} streaming_api_base_url
* @property {boolean} timeline_preview
* @property {string} title
- * @property {boolean} trends
+ * @property {boolean} show_trends
* @property {boolean} trends_as_landing_page
* @property {boolean} unfollow_modal
* @property {boolean} use_blurhash
@@ -122,7 +123,8 @@ export const reduceMotion = getMeta('reduce_motion');
export const registrationsOpen = getMeta('registrations_open');
export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled');
-export const showTrends = getMeta('trends');
+export const trendsEnabled = getMeta('trends_enabled');
+export const showTrends = getMeta('show_trends');
export const singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url');
export const timelinePreview = getMeta('timeline_preview');
diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json
index 2afac7e7e8..8705e6cd68 100644
--- a/app/javascript/mastodon/locales/en.json
+++ b/app/javascript/mastodon/locales/en.json
@@ -135,6 +135,8 @@
"community.column_settings.remote_only": "Remote only",
"compose.language.change": "Change language",
"compose.language.search": "Search languages...",
+ "compose.published.body": "Post published.",
+ "compose.published.open": "Open",
"compose_form.direct_message_warning_learn_more": "Learn more",
"compose_form.encryption_warning": "Posts on Mastodon are not end-to-end encrypted. Do not share any sensitive information over Mastodon.",
"compose_form.hashtag_warning": "This post won't be listed under any hashtag as it is not public. Only public posts can be searched by hashtag.",
diff --git a/app/javascript/mastodon/reducers/alerts.js b/app/javascript/mastodon/reducers/alerts.js
index bd49d748f9..1ca9b62a02 100644
--- a/app/javascript/mastodon/reducers/alerts.js
+++ b/app/javascript/mastodon/reducers/alerts.js
@@ -1,4 +1,4 @@
-import { Map as ImmutableMap, List as ImmutableList } from 'immutable';
+import { List as ImmutableList } from 'immutable';
import {
ALERT_SHOW,
@@ -8,17 +8,20 @@ import {
const initialState = ImmutableList([]);
+let id = 0;
+
+const addAlert = (state, alert) =>
+ state.push({
+ key: id++,
+ ...alert,
+ });
+
export default function alerts(state = initialState, action) {
switch(action.type) {
case ALERT_SHOW:
- return state.push(ImmutableMap({
- key: state.size > 0 ? state.last().get('key') + 1 : 0,
- title: action.title,
- message: action.message,
- message_values: action.message_values,
- }));
+ return addAlert(state, action.alert);
case ALERT_DISMISS:
- return state.filterNot(item => item.get('key') === action.alert.key);
+ return state.filterNot(item => item.key === action.alert.key);
case ALERT_CLEAR:
return state.clear();
default:
diff --git a/app/javascript/mastodon/reducers/index.ts b/app/javascript/mastodon/reducers/index.ts
index 67aa5f6c5e..ad3077e37d 100644
--- a/app/javascript/mastodon/reducers/index.ts
+++ b/app/javascript/mastodon/reducers/index.ts
@@ -26,7 +26,6 @@ import lists from './lists';
import markers from './markers';
import media_attachments from './media_attachments';
import meta from './meta';
-import { missedUpdatesReducer } from './missed_updates';
import { modalReducer } from './modal';
import mutes from './mutes';
import notifications from './notifications';
@@ -82,7 +81,6 @@ const reducers = {
suggestions,
polls,
trends,
- missed_updates: missedUpdatesReducer,
markers,
picture_in_picture,
history,
diff --git a/app/javascript/mastodon/reducers/missed_updates.ts b/app/javascript/mastodon/reducers/missed_updates.ts
deleted file mode 100644
index a587fcb036..0000000000
--- a/app/javascript/mastodon/reducers/missed_updates.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-import { Record } from 'immutable';
-
-import type { Action } from 'redux';
-
-import { focusApp, unfocusApp } from '../actions/app';
-import { NOTIFICATIONS_UPDATE } from '../actions/notifications';
-
-interface MissedUpdatesState {
- focused: boolean;
- unread: number;
-}
-const initialState = Record({
- focused: true,
- unread: 0,
-})();
-
-export function missedUpdatesReducer(
- state = initialState,
- action: Action
-) {
- switch (action.type) {
- case focusApp.type:
- return state.set('focused', true).set('unread', 0);
- case unfocusApp.type:
- return state.set('focused', false);
- case NOTIFICATIONS_UPDATE:
- return state.get('focused')
- ? state
- : state.update('unread', (x) => x + 1);
- default:
- return state;
- }
-}
diff --git a/app/javascript/mastodon/selectors/index.js b/app/javascript/mastodon/selectors/index.js
index f92e7fe48d..0968fb090b 100644
--- a/app/javascript/mastodon/selectors/index.js
+++ b/app/javascript/mastodon/selectors/index.js
@@ -84,26 +84,16 @@ export const makeGetPictureInPicture = () => {
}));
};
-const getAlertsBase = state => state.get('alerts');
+const ALERT_DEFAULTS = {
+ dismissAfter: 5000,
+ style: false,
+};
-export const getAlerts = createSelector([getAlertsBase], (base) => {
- let arr = [];
-
- 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,
- barStyle: {
- zIndex: 200,
- },
- });
- });
-
- return arr;
-});
+export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
+ alerts.map(item => ({
+ ...ALERT_DEFAULTS,
+ ...item,
+ })).toArray());
export const makeGetNotification = () => createSelector([
(_, base) => base,
diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss
index 0d0d5a8569..959dc2f715 100644
--- a/app/javascript/styles/mastodon/components.scss
+++ b/app/javascript/styles/mastodon/components.scss
@@ -9077,3 +9077,62 @@ noscript {
}
}
}
+
+.notification-list {
+ position: fixed;
+ bottom: 2rem;
+ inset-inline-start: 0;
+ z-index: 999;
+ display: flex;
+ flex-direction: column;
+ gap: 4px;
+}
+
+.notification-bar {
+ flex: 0 0 auto;
+ position: relative;
+ inset-inline-start: -100%;
+ width: auto;
+ padding: 15px;
+ margin: 0;
+ color: $primary-text-color;
+ background: rgba($black, 0.85);
+ backdrop-filter: blur(8px);
+ border: 1px solid rgba(lighten($ui-base-color, 4%), 0.85);
+ border-radius: 8px;
+ box-shadow: 0 10px 15px -3px rgba($base-shadow-color, 0.25),
+ 0 4px 6px -4px rgba($base-shadow-color, 0.25);
+ cursor: default;
+ transition: 0.5s cubic-bezier(0.89, 0.01, 0.5, 1.1);
+ transform: translateZ(0);
+ font-size: 15px;
+ line-height: 21px;
+
+ &.notification-bar-active {
+ inset-inline-start: 1rem;
+ }
+}
+
+.notification-bar-title {
+ margin-inline-end: 5px;
+}
+
+.notification-bar-title,
+.notification-bar-action {
+ font-weight: 700;
+}
+
+.notification-bar-action {
+ text-transform: uppercase;
+ margin-inline-start: 10px;
+ cursor: pointer;
+ color: $highlight-text-color;
+ border-radius: 4px;
+ padding: 0 4px;
+
+ &:hover,
+ &:focus,
+ &:active {
+ background: rgba($ui-base-color, 0.85);
+ }
+}
diff --git a/app/lib/activitypub/activity/flag.rb b/app/lib/activitypub/activity/flag.rb
index dc1932f597..304cf0ad2f 100644
--- a/app/lib/activitypub/activity/flag.rb
+++ b/app/lib/activitypub/activity/flag.rb
@@ -4,13 +4,14 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
def perform
return if skip_reports?
- target_accounts = object_uris.filter_map { |uri| account_from_uri(uri) }.select(&:local?)
- target_statuses_by_account = object_uris.filter_map { |uri| status_from_uri(uri) }.select(&:local?).group_by(&:account_id)
+ target_accounts = object_uris.filter_map { |uri| account_from_uri(uri) }
+ target_statuses_by_account = object_uris.filter_map { |uri| status_from_uri(uri) }.group_by(&:account_id)
target_accounts.each do |target_account|
- target_statuses = target_statuses_by_account[target_account.id]
+ target_statuses = target_statuses_by_account[target_account.id]
+ replied_to_accounts = Account.local.where(id: target_statuses.filter_map(&:in_reply_to_account_id))
- next if target_account.suspended?
+ next if target_account.suspended? || (!target_account.local? && replied_to_accounts.none?)
ReportService.new.call(
@account,
diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb
index 45ee06e12c..2aaa4cdec6 100644
--- a/app/serializers/initial_state_serializer.rb
+++ b/app/serializers/initial_state_serializer.rb
@@ -39,7 +39,7 @@ class InitialStateSerializer < ActiveModel::Serializer
limited_federation_mode: Rails.configuration.x.whitelist_mode,
mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory,
- trends: Setting.trends,
+ trends_enabled: Setting.trends,
registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode,
timeline_preview: Setting.timeline_preview,
activity_api_enabled: Setting.activity_api_enabled,
@@ -62,9 +62,9 @@ class InitialStateSerializer < ActiveModel::Serializer
store[:advanced_layout] = object.current_account.user.setting_advanced_layout
store[:use_blurhash] = object.current_account.user.setting_use_blurhash
store[:use_pending_items] = object.current_account.user.setting_use_pending_items
- store[:trends] = Setting.trends && object.current_account.user.setting_trends
store[:default_content_type] = object.current_account.user.setting_default_content_type
store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font
+ store[:show_trends] = Setting.trends && object.current_account.user.setting_trends
store[:crop_images] = object.current_account.user.setting_crop_images
else
store[:auto_play_gif] = Setting.auto_play_gif
diff --git a/app/services/report_service.rb b/app/services/report_service.rb
index 0ce525b071..3444e1dfaf 100644
--- a/app/services/report_service.rb
+++ b/app/services/report_service.rb
@@ -45,11 +45,15 @@ class ReportService < BaseService
end
def forward_to_origin!
- ActivityPub::DeliveryWorker.perform_async(
- payload,
- some_local_account.id,
- @target_account.inbox_url
- )
+ # Send report to the server where the account originates from
+ ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, @target_account.inbox_url)
+
+ # Send report to servers to which the account was replying to, so they also have a chance to act
+ inbox_urls = Account.remote.where(id: Status.where(id: reported_status_ids).where.not(in_reply_to_account_id: nil).select(:in_reply_to_account_id)).inboxes - [@target_account.inbox_url]
+
+ inbox_urls.each do |inbox_url|
+ ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, inbox_url)
+ end
end
def forward?
diff --git a/app/workers/feed_insert_worker.rb b/app/workers/feed_insert_worker.rb
index ee9a3cadc1..a72acbbe80 100644
--- a/app/workers/feed_insert_worker.rb
+++ b/app/workers/feed_insert_worker.rb
@@ -4,21 +4,25 @@ class FeedInsertWorker
include Sidekiq::Worker
def perform(status_id, id, type = 'home', options = {})
- @type = type.to_sym
- @status = Status.find(status_id)
- @options = options.symbolize_keys
+ ApplicationRecord.connected_to(role: :primary) do
+ @type = type.to_sym
+ @status = Status.find(status_id)
+ @options = options.symbolize_keys
- case @type
- when :home, :tags
- @follower = Account.find(id)
- when :list
- @list = List.find(id)
- @follower = @list.account
- when :direct
- @account = Account.find(id)
+ case @type
+ when :home, :tags
+ @follower = Account.find(id)
+ when :list
+ @list = List.find(id)
+ @follower = @list.account
+ when :direct
+ @account = Account.find(id)
+ end
end
- check_and_insert
+ ApplicationRecord.connected_to(role: :read, prevent_writes: true) do
+ check_and_insert
+ end
rescue ActiveRecord::RecordNotFound
true
end
diff --git a/config/database.yml b/config/database.yml
index 34acf2f19a..f7ecbd9814 100644
--- a/config/database.yml
+++ b/config/database.yml
@@ -27,10 +27,20 @@ test:
port: <%= ENV['DB_PORT'] %>
production:
- <<: *default
- database: <%= ENV['DB_NAME'] || 'mastodon_production' %>
- username: <%= ENV['DB_USER'] || 'mastodon' %>
- password: <%= (ENV['DB_PASS'] || '').to_json %>
- host: <%= ENV['DB_HOST'] || 'localhost' %>
- port: <%= ENV['DB_PORT'] || 5432 %>
- prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %>
+ primary:
+ <<: *default
+ database: <%= ENV['DB_NAME'] || 'mastodon_production' %>
+ username: <%= ENV['DB_USER'] || 'mastodon' %>
+ password: <%= (ENV['DB_PASS'] || '').to_json %>
+ host: <%= ENV['DB_HOST'] || 'localhost' %>
+ port: <%= ENV['DB_PORT'] || 5432 %>
+ prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %>
+ read:
+ <<: *default
+ database: <%= ENV['DB_REPLICA_NAME'] ||ENV['DB_NAME'] || 'mastodon_production' %>
+ username: <%= ENV['DB_REPLICA_USER'] ||ENV['DB_USER'] || 'mastodon' %>
+ password: <%= (ENV['DB_REPLICA_PASS'] || ENV['DB_PASS'] || '').to_json %>
+ host: <%= ENV['DB_REPLICA_HOST'] ||ENV['DB_HOST'] || 'localhost' %>
+ port: <%= ENV['DB_REPLICA_PORT'] ||ENV['DB_PORT'] || 5432 %>
+ prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %>
+ replica: true
diff --git a/config/initializers/makara.rb b/config/initializers/makara.rb
deleted file mode 100644
index dc88fa63cd..0000000000
--- a/config/initializers/makara.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-Makara::Cookie::DEFAULT_OPTIONS[:same_site] = :lax
-Makara::Cookie::DEFAULT_OPTIONS[:secure] = Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'
diff --git a/spec/services/report_service_spec.rb b/spec/services/report_service_spec.rb
index b8ceedb851..660ce3db23 100644
--- a/spec/services/report_service_spec.rb
+++ b/spec/services/report_service_spec.rb
@@ -17,24 +17,45 @@ RSpec.describe ReportService, type: :service do
context 'with a remote account' do
let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }
+ let(:forward) { false }
before do
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
end
- it 'sends ActivityPub payload when forward is true' do
- subject.call(source_account, remote_account, forward: true)
- expect(a_request(:post, 'http://example.com/inbox')).to have_been_made
+ context 'when forward is true' do
+ let(:forward) { true }
+
+ it 'sends ActivityPub payload when forward is true' do
+ subject.call(source_account, remote_account, forward: forward)
+ expect(a_request(:post, 'http://example.com/inbox')).to have_been_made
+ end
+
+ it 'has an uri' do
+ report = subject.call(source_account, remote_account, forward: forward)
+ expect(report.uri).to_not be_nil
+ end
+
+ context 'when reporting a reply' do
+ let(:remote_thread_account) { Fabricate(:account, domain: 'foo.com', protocol: :activitypub, inbox_url: 'http://foo.com/inbox') }
+ let(:reported_status) { Fabricate(:status, account: remote_account, thread: Fabricate(:status, account: remote_thread_account)) }
+
+ before do
+ stub_request(:post, 'http://foo.com/inbox').to_return(status: 200)
+ end
+
+ it 'sends ActivityPub payload to the author of the replied-to post' do
+ subject.call(source_account, remote_account, status_ids: [reported_status.id], forward: forward)
+ expect(a_request(:post, 'http://foo.com/inbox')).to have_been_made
+ end
+ end
end
- it 'does not send anything when forward is false' do
- subject.call(source_account, remote_account, forward: false)
- expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made
- end
-
- it 'has an uri' do
- report = subject.call(source_account, remote_account, forward: true)
- expect(report.uri).to_not be_nil
+ context 'when forward is false' do
+ it 'does not send anything' do
+ subject.call(source_account, remote_account, forward: forward)
+ expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made
+ end
end
end