Merge commit '41a505513fb36f7c28c8d8a4270d5ee192169462' into glitch-soc/merge-upstream

Conflicts:
- `app/serializers/initial_state_serializer.rb`:
  Upstream renamed an initial state parameter, where we had extra ones.
  Renamed as upstream did.
- `app/workers/feed_insert_worker.rb`:
  Upstream wrapped database query in a block, we had extra database
  queries because of the DM timeline.
  Moved everything in the block.
This commit is contained in:
Claire 2023-07-12 15:27:32 +02:00
commit 82eebd0482
28 changed files with 355 additions and 289 deletions

View File

@ -812,7 +812,6 @@ Style/FrozenStringLiteralComment:
- 'config/initializers/httplog.rb' - 'config/initializers/httplog.rb'
- 'config/initializers/inflections.rb' - 'config/initializers/inflections.rb'
- 'config/initializers/mail_delivery_job.rb' - 'config/initializers/mail_delivery_job.rb'
- 'config/initializers/makara.rb'
- 'config/initializers/mime_types.rb' - 'config/initializers/mime_types.rb'
- 'config/initializers/oj.rb' - 'config/initializers/oj.rb'
- 'config/initializers/omniauth.rb' - 'config/initializers/omniauth.rb'

View File

@ -11,7 +11,6 @@ gem 'rack', '~> 2.2.7'
gem 'haml-rails', '~>2.0' gem 'haml-rails', '~>2.0'
gem 'pg', '~> 1.5' gem 'pg', '~> 1.5'
gem 'makara', '~> 0.5'
gem 'pghero' gem 'pghero'
gem 'dotenv-rails', '~> 2.8' gem 'dotenv-rails', '~> 2.8'

View File

@ -399,8 +399,6 @@ GEM
net-imap net-imap
net-pop net-pop
net-smtp net-smtp
makara (0.5.1)
activerecord (>= 5.2.0)
marcel (1.0.2) marcel (1.0.2)
mario-redis-lock (1.2.1) mario-redis-lock (1.2.1)
redis (>= 3.0.5) redis (>= 3.0.5)
@ -815,7 +813,6 @@ DEPENDENCIES
letter_opener_web (~> 2.0) letter_opener_web (~> 2.0)
link_header (~> 0.0) link_header (~> 0.0)
lograge (~> 0.12) lograge (~> 0.12)
makara (~> 0.5)
mario-redis-lock (~> 1.2) mario-redis-lock (~> 1.2)
memory_profiler memory_profiler
mime-types (~> 3.4.1) mime-types (~> 3.4.1)

View File

@ -6,11 +6,14 @@ class Api::V1::Timelines::HomeController < Api::BaseController
after_action :insert_pagination_headers, unless: -> { @statuses.empty? } after_action :insert_pagination_headers, unless: -> { @statuses.empty? }
def show 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, render json: @statuses,
each_serializer: REST::StatusSerializer, each_serializer: REST::StatusSerializer,
relationships: StatusRelationshipsPresenter.new(@statuses, current_user&.account_id), relationships: @relationships,
status: account_home_feed.regenerating? ? 206 : 200 status: account_home_feed.regenerating? ? 206 : 200
end end

View File

@ -12,52 +12,48 @@ export const ALERT_DISMISS = 'ALERT_DISMISS';
export const ALERT_CLEAR = 'ALERT_CLEAR'; export const ALERT_CLEAR = 'ALERT_CLEAR';
export const ALERT_NOOP = 'ALERT_NOOP'; export const ALERT_NOOP = 'ALERT_NOOP';
export function dismissAlert(alert) { export const dismissAlert = alert => ({
return { type: ALERT_DISMISS,
type: ALERT_DISMISS, alert,
alert, });
};
}
export function clearAlert() { export const clearAlert = () => ({
return { type: ALERT_CLEAR,
type: ALERT_CLEAR, });
};
}
export function showAlert(title = messages.unexpectedTitle, message = messages.unexpectedMessage, message_values = undefined) { export const showAlert = alert => ({
return { type: ALERT_SHOW,
type: ALERT_SHOW, alert,
title, });
message,
message_values,
};
}
export function showAlertForError(error, skipNotFound = false) { export const showAlertForError = (error, skipNotFound = false) => {
if (error.response) { if (error.response) {
const { data, status, statusText, headers } = 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)) { if (skipNotFound && (status === 404 || status === 410)) {
// Skip these errors as they are reflected in the UI
return { type: ALERT_NOOP }; return { type: ALERT_NOOP };
} }
// Rate limit errors
if (status === 429 && headers['x-ratelimit-reset']) { if (status === 429 && headers['x-ratelimit-reset']) {
const reset_date = new Date(headers['x-ratelimit-reset']); return showAlert({
return showAlert(messages.rateLimitedTitle, messages.rateLimitedMessage, { 'retry_time': reset_date }); title: messages.rateLimitedTitle,
message: messages.rateLimitedMessage,
values: { 'retry_time': new Date(headers['x-ratelimit-reset']) },
});
} }
let message = statusText; return showAlert({
let title = `${status}`; title: `${status}`,
message: data.error || statusText,
if (data.error) { });
message = data.error;
}
return showAlert(title, message);
} else {
console.error(error);
return showAlert();
} }
console.error(error);
return showAlert({
title: messages.unexpectedTitle,
message: messages.unexpectedMessage,
});
} }

View File

@ -82,6 +82,8 @@ export const COMPOSE_FOCUS = 'COMPOSE_FOCUS';
const messages = defineMessages({ const messages = defineMessages({
uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' }, uploadErrorLimit: { id: 'upload_error.limit', defaultMessage: 'File upload limit exceeded.' },
uploadErrorPoll: { id: 'upload_error.poll', defaultMessage: 'File upload not allowed with polls.' }, 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) => { export const ensureComposeIsVisible = (getState, routerHistory) => {
@ -242,6 +244,13 @@ export function submitCompose(routerHistory) {
} }
insertIfOnline(`account:${response.data.account.id}`); 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) { }).catch(function (error) {
dispatch(submitComposeFail(error)); dispatch(submitComposeFail(error));
}); });
@ -271,18 +280,19 @@ export function submitComposeFail(error) {
export function uploadCompose(files) { export function uploadCompose(files) {
return function (dispatch, getState) { return function (dispatch, getState) {
const uploadLimit = 4; const uploadLimit = 4;
const media = getState().getIn(['compose', 'media_attachments']); const media = getState().getIn(['compose', 'media_attachments']);
const pending = getState().getIn(['compose', 'pending_media_attachments']); const pending = getState().getIn(['compose', 'pending_media_attachments']);
const progress = new Array(files.length).fill(0); const progress = new Array(files.length).fill(0);
let total = Array.from(files).reduce((a, v) => a + v.size, 0); let total = Array.from(files).reduce((a, v) => a + v.size, 0);
if (files.length + media.size + pending > uploadLimit) { if (files.length + media.size + pending > uploadLimit) {
dispatch(showAlert(undefined, messages.uploadErrorLimit)); dispatch(showAlert({ message: messages.uploadErrorLimit }));
return; return;
} }
if (getState().getIn(['compose', 'poll'])) { if (getState().getIn(['compose', 'poll'])) {
dispatch(showAlert(undefined, messages.uploadErrorPoll)); dispatch(showAlert({ message: messages.uploadErrorPoll }));
return; return;
} }

View File

@ -237,7 +237,6 @@ class StatusActionBar extends ImmutablePureComponent {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props; const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.context.identity; const { signedIn, permissions } = this.context.identity;
const anonymousAccess = !signedIn;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility')); const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility')); const pinnableStatus = ['public', 'unlisted', 'private'].includes(status.get('visibility'));
const mutingConversation = status.get('muted'); 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({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
} }
menu.push(null); if (signedIn) {
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 });
menu.push(null); menu.push(null);
if (relationship && relationship.get('muting')) { menu.push({ text: intl.formatMessage(status.get('bookmarked') ? messages.removeBookmark : messages.bookmark), action: this.handleBookmarkClick });
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
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 { } else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true }); 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 (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(null); menu.push(null);
if (relationship && relationship.get('domain_blocking')) { if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else { } 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)) { if (relationship && relationship.get('blocking')) {
menu.push(null); menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { } else {
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.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
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) {
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]; 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 {
<div className='status__action-bar__dropdown'> <div className='status__action-bar__dropdown'>
<DropdownMenuContainer <DropdownMenuContainer
scrollKey={scrollKey} scrollKey={scrollKey}
disabled={anonymousAccess}
status={status} status={status}
items={menu} items={menu}
icon='ellipsis-h' icon='ellipsis-h'

View File

@ -290,7 +290,6 @@ class Header extends ImmutablePureComponent {
if (isRemote) { if (isRemote) {
menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') }); menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: account.get('url') });
menu.push(null);
} }
if ('share' in navigator) { if ('share' in navigator) {

View File

@ -11,7 +11,7 @@ import { connect } from 'react-redux';
import Column from 'mastodon/components/column'; import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import Search from 'mastodon/features/compose/containers/search_container'; import Search from 'mastodon/features/compose/containers/search_container';
import { showTrends } from 'mastodon/initial_state'; import { trendsEnabled } from 'mastodon/initial_state';
import Links from './links'; import Links from './links';
import SearchResults from './results'; import SearchResults from './results';
@ -26,7 +26,7 @@ const messages = defineMessages({
const mapStateToProps = state => ({ const mapStateToProps = state => ({
layout: state.getIn(['meta', 'layout']), layout: state.getIn(['meta', 'layout']),
isSearching: state.getIn(['search', 'submitted']) || !showTrends, isSearching: state.getIn(['search', 'submitted']) || !trendsEnabled,
}); });
class Explore extends PureComponent { class Explore extends PureComponent {

View File

@ -32,7 +32,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === 'granted') { if (permission === 'granted') {
dispatch(changePushNotifications(path.slice(1), checked)); dispatch(changePushNotifications(path.slice(1), checked));
} else { } else {
dispatch(showAlert(undefined, messages.permissionDenied)); dispatch(showAlert({ message: messages.permissionDenied }));
} }
})); }));
} else { } else {
@ -47,7 +47,7 @@ const mapDispatchToProps = (dispatch, { intl }) => ({
if (permission === 'granted') { if (permission === 'granted') {
dispatch(changeSetting(['notifications', ...path], checked)); dispatch(changeSetting(['notifications', ...path], checked));
} else { } else {
dispatch(showAlert(undefined, messages.permissionDenied)); dispatch(showAlert({ message: messages.permissionDenied }));
} }
})); }));
} else { } else {

View File

@ -195,71 +195,74 @@ class ActionBar extends PureComponent {
let menu = []; let menu = [];
if (publicStatus) { if (publicStatus && isRemote) {
if (isRemote) { menu.push({ text: intl.formatMessage(messages.openOriginalPage), href: status.get('url') });
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 (writtenByMe) { menu.push({ text: intl.formatMessage(messages.copy), action: this.handleCopy });
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 }); if (publicStatus && 'share' in navigator) {
menu.push(null); menu.push({ text: intl.formatMessage(messages.share), action: this.handleShare });
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 }); if (publicStatus) {
} else { menu.push({ text: intl.formatMessage(messages.embed), action: this.handleEmbed });
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick }); }
if (signedIn) {
menu.push(null); menu.push(null);
if (relationship && relationship.get('muting')) { if (writtenByMe) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick }); 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 { } else {
menu.push({ text: intl.formatMessage(messages.mute, { name: account.get('username') }), action: this.handleMuteClick, dangerous: true }); menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
}
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(null); menu.push(null);
if (relationship && relationship.get('domain_blocking')) { if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unblockDomain, { domain }), action: this.handleUnblockDomain }); menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else { } 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)) { if (relationship && relationship.get('blocking')) {
menu.push(null); menu.push({ text: intl.formatMessage(messages.unblock, { name: account.get('username') }), action: this.handleBlockClick });
if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { } else {
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.block, { name: account.get('username') }), action: this.handleBlockClick, dangerous: true });
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) {
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]; 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 {
<div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div> <div className='detailed-status__button'><IconButton className='bookmark-icon' disabled={!signedIn} active={status.get('bookmarked')} title={intl.formatMessage(messages.bookmark)} icon='bookmark' onClick={this.handleBookmarkClick} /></div>
<div className='detailed-status__action-bar-dropdown'> <div className='detailed-status__action-bar-dropdown'>
<DropdownMenuContainer size={18} icon='ellipsis-h' disabled={!signedIn} status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} /> <DropdownMenuContainer size={18} icon='ellipsis-h' status={status} items={menu} direction='left' title={intl.formatMessage(messages.more)} />
</div> </div>
</div> </div>
); );

View File

@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
import { WordmarkLogo } from 'mastodon/components/logo'; import { WordmarkLogo } from 'mastodon/components/logo';
import NavigationPortal from 'mastodon/components/navigation_portal'; 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 ColumnLink from './column_link';
import DisabledAccountBanner from './disabled_account_banner'; import DisabledAccountBanner from './disabled_account_banner';
@ -65,7 +65,7 @@ class NavigationPanel extends Component {
</> </>
)} )}
{showTrends ? ( {trendsEnabled ? (
<ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} /> <ColumnLink transparent to='/explore' icon='hashtag' text={intl.formatMessage(messages.explore)} />
) : ( ) : (
<ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} /> <ColumnLink transparent to='/search' icon='search' text={intl.formatMessage(messages.search)} />

View File

@ -7,26 +7,27 @@ import { NotificationStack } from 'react-notification';
import { dismissAlert } from '../../../actions/alerts'; import { dismissAlert } from '../../../actions/alerts';
import { getAlerts } from '../../../selectors'; import { getAlerts } from '../../../selectors';
const mapStateToProps = (state, { intl }) => { const formatIfNeeded = (intl, message, values) => {
const notifications = getAlerts(state); if (typeof message === 'object') {
return intl.formatMessage(message, values);
}
notifications.forEach(notification => ['title', 'message'].forEach(key => { return message;
const value = notification[key];
if (typeof value === 'object') {
notification[key] = intl.formatMessage(value, notification[`${key}_values`]);
}
}));
return { notifications };
}; };
const mapDispatchToProps = (dispatch) => { const mapStateToProps = (state, { intl }) => ({
return { notifications: getAlerts(state).map(alert => ({
onDismiss: alert => { ...alert,
dispatch(dismissAlert(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)); export default injectIntl(connect(mapStateToProps, mapDispatchToProps)(NotificationStack));

View File

@ -22,7 +22,7 @@ import { clearHeight } from '../../actions/height_cache';
import { expandNotifications } from '../../actions/notifications'; import { expandNotifications } from '../../actions/notifications';
import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server'; import { fetchServer, fetchServerTranslationLanguages } from '../../actions/server';
import { expandHomeTimeline } from '../../actions/timelines'; 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 BundleColumnError from './components/bundle_column_error';
import Header from './components/header'; import Header from './components/header';
@ -170,7 +170,7 @@ class SwitchingColumnsArea extends PureComponent {
} }
} else if (singleUserMode && owner && initialState?.accounts[owner]) { } else if (singleUserMode && owner && initialState?.accounts[owner]) {
redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />; redirect = <Redirect from='/' to={`/@${initialState.accounts[owner].username}`} exact />;
} else if (showTrends && trendsAsLanding) { } else if (trendsEnabled && trendsAsLanding) {
redirect = <Redirect from='/' to='/explore' exact />; redirect = <Redirect from='/' to='/explore' exact />;
} else { } else {
redirect = <Redirect from='/' to='/about' exact />; redirect = <Redirect from='/' to='/about' exact />;

View File

@ -69,12 +69,13 @@
* @property {boolean} reduce_motion * @property {boolean} reduce_motion
* @property {string} repository * @property {string} repository
* @property {boolean} search_enabled * @property {boolean} search_enabled
* @property {boolean} trends_enabled
* @property {boolean} single_user_mode * @property {boolean} single_user_mode
* @property {string} source_url * @property {string} source_url
* @property {string} streaming_api_base_url * @property {string} streaming_api_base_url
* @property {boolean} timeline_preview * @property {boolean} timeline_preview
* @property {string} title * @property {string} title
* @property {boolean} trends * @property {boolean} show_trends
* @property {boolean} trends_as_landing_page * @property {boolean} trends_as_landing_page
* @property {boolean} unfollow_modal * @property {boolean} unfollow_modal
* @property {boolean} use_blurhash * @property {boolean} use_blurhash
@ -122,7 +123,8 @@ export const reduceMotion = getMeta('reduce_motion');
export const registrationsOpen = getMeta('registrations_open'); export const registrationsOpen = getMeta('registrations_open');
export const repository = getMeta('repository'); export const repository = getMeta('repository');
export const searchEnabled = getMeta('search_enabled'); 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 singleUserMode = getMeta('single_user_mode');
export const source_url = getMeta('source_url'); export const source_url = getMeta('source_url');
export const timelinePreview = getMeta('timeline_preview'); export const timelinePreview = getMeta('timeline_preview');

View File

@ -135,6 +135,8 @@
"community.column_settings.remote_only": "Remote only", "community.column_settings.remote_only": "Remote only",
"compose.language.change": "Change language", "compose.language.change": "Change language",
"compose.language.search": "Search languages...", "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.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.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.", "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.",

View File

@ -1,4 +1,4 @@
import { Map as ImmutableMap, List as ImmutableList } from 'immutable'; import { List as ImmutableList } from 'immutable';
import { import {
ALERT_SHOW, ALERT_SHOW,
@ -8,17 +8,20 @@ import {
const initialState = ImmutableList([]); const initialState = ImmutableList([]);
let id = 0;
const addAlert = (state, alert) =>
state.push({
key: id++,
...alert,
});
export default function alerts(state = initialState, action) { export default function alerts(state = initialState, action) {
switch(action.type) { switch(action.type) {
case ALERT_SHOW: case ALERT_SHOW:
return state.push(ImmutableMap({ return addAlert(state, action.alert);
key: state.size > 0 ? state.last().get('key') + 1 : 0,
title: action.title,
message: action.message,
message_values: action.message_values,
}));
case ALERT_DISMISS: 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: case ALERT_CLEAR:
return state.clear(); return state.clear();
default: default:

View File

@ -26,7 +26,6 @@ import lists from './lists';
import markers from './markers'; import markers from './markers';
import media_attachments from './media_attachments'; import media_attachments from './media_attachments';
import meta from './meta'; import meta from './meta';
import { missedUpdatesReducer } from './missed_updates';
import { modalReducer } from './modal'; import { modalReducer } from './modal';
import mutes from './mutes'; import mutes from './mutes';
import notifications from './notifications'; import notifications from './notifications';
@ -82,7 +81,6 @@ const reducers = {
suggestions, suggestions,
polls, polls,
trends, trends,
missed_updates: missedUpdatesReducer,
markers, markers,
picture_in_picture, picture_in_picture,
history, history,

View File

@ -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<MissedUpdatesState>({
focused: true,
unread: 0,
})();
export function missedUpdatesReducer(
state = initialState,
action: Action<string>
) {
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;
}
}

View File

@ -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) => { export const getAlerts = createSelector(state => state.get('alerts'), alerts =>
let arr = []; alerts.map(item => ({
...ALERT_DEFAULTS,
base.forEach(item => { ...item,
arr.push({ })).toArray());
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 makeGetNotification = () => createSelector([ export const makeGetNotification = () => createSelector([
(_, base) => base, (_, base) => base,

View File

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

View File

@ -4,13 +4,14 @@ class ActivityPub::Activity::Flag < ActivityPub::Activity
def perform def perform
return if skip_reports? return if skip_reports?
target_accounts = object_uris.filter_map { |uri| account_from_uri(uri) }.select(&:local?) target_accounts = object_uris.filter_map { |uri| account_from_uri(uri) }
target_statuses_by_account = object_uris.filter_map { |uri| status_from_uri(uri) }.select(&:local?).group_by(&:account_id) target_statuses_by_account = object_uris.filter_map { |uri| status_from_uri(uri) }.group_by(&:account_id)
target_accounts.each do |target_account| 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( ReportService.new.call(
@account, @account,

View File

@ -39,7 +39,7 @@ class InitialStateSerializer < ActiveModel::Serializer
limited_federation_mode: Rails.configuration.x.whitelist_mode, limited_federation_mode: Rails.configuration.x.whitelist_mode,
mascot: instance_presenter.mascot&.file&.url, mascot: instance_presenter.mascot&.file&.url,
profile_directory: Setting.profile_directory, profile_directory: Setting.profile_directory,
trends: Setting.trends, trends_enabled: Setting.trends,
registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode, registrations_open: Setting.registrations_mode != 'none' && !Rails.configuration.x.single_user_mode,
timeline_preview: Setting.timeline_preview, timeline_preview: Setting.timeline_preview,
activity_api_enabled: Setting.activity_api_enabled, 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[:advanced_layout] = object.current_account.user.setting_advanced_layout
store[:use_blurhash] = object.current_account.user.setting_use_blurhash store[:use_blurhash] = object.current_account.user.setting_use_blurhash
store[:use_pending_items] = object.current_account.user.setting_use_pending_items 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[:default_content_type] = object.current_account.user.setting_default_content_type
store[:system_emoji_font] = object.current_account.user.setting_system_emoji_font 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 store[:crop_images] = object.current_account.user.setting_crop_images
else else
store[:auto_play_gif] = Setting.auto_play_gif store[:auto_play_gif] = Setting.auto_play_gif

View File

@ -45,11 +45,15 @@ class ReportService < BaseService
end end
def forward_to_origin! def forward_to_origin!
ActivityPub::DeliveryWorker.perform_async( # Send report to the server where the account originates from
payload, ActivityPub::DeliveryWorker.perform_async(payload, some_local_account.id, @target_account.inbox_url)
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 end
def forward? def forward?

View File

@ -4,21 +4,25 @@ class FeedInsertWorker
include Sidekiq::Worker include Sidekiq::Worker
def perform(status_id, id, type = 'home', options = {}) def perform(status_id, id, type = 'home', options = {})
@type = type.to_sym ApplicationRecord.connected_to(role: :primary) do
@status = Status.find(status_id) @type = type.to_sym
@options = options.symbolize_keys @status = Status.find(status_id)
@options = options.symbolize_keys
case @type case @type
when :home, :tags when :home, :tags
@follower = Account.find(id) @follower = Account.find(id)
when :list when :list
@list = List.find(id) @list = List.find(id)
@follower = @list.account @follower = @list.account
when :direct when :direct
@account = Account.find(id) @account = Account.find(id)
end
end end
check_and_insert ApplicationRecord.connected_to(role: :read, prevent_writes: true) do
check_and_insert
end
rescue ActiveRecord::RecordNotFound rescue ActiveRecord::RecordNotFound
true true
end end

View File

@ -27,10 +27,20 @@ test:
port: <%= ENV['DB_PORT'] %> port: <%= ENV['DB_PORT'] %>
production: production:
<<: *default primary:
database: <%= ENV['DB_NAME'] || 'mastodon_production' %> <<: *default
username: <%= ENV['DB_USER'] || 'mastodon' %> database: <%= ENV['DB_NAME'] || 'mastodon_production' %>
password: <%= (ENV['DB_PASS'] || '').to_json %> username: <%= ENV['DB_USER'] || 'mastodon' %>
host: <%= ENV['DB_HOST'] || 'localhost' %> password: <%= (ENV['DB_PASS'] || '').to_json %>
port: <%= ENV['DB_PORT'] || 5432 %> host: <%= ENV['DB_HOST'] || 'localhost' %>
prepared_statements: <%= ENV['PREPARED_STATEMENTS'] || 'true' %> 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

View File

@ -1,2 +0,0 @@
Makara::Cookie::DEFAULT_OPTIONS[:same_site] = :lax
Makara::Cookie::DEFAULT_OPTIONS[:secure] = Rails.env.production? || ENV['LOCAL_HTTPS'] == 'true'

View File

@ -17,24 +17,45 @@ RSpec.describe ReportService, type: :service do
context 'with a remote account' do context 'with a remote account' do
let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') } let(:remote_account) { Fabricate(:account, domain: 'example.com', protocol: :activitypub, inbox_url: 'http://example.com/inbox') }
let(:forward) { false }
before do before do
stub_request(:post, 'http://example.com/inbox').to_return(status: 200) stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
end end
it 'sends ActivityPub payload when forward is true' do context 'when forward is true' do
subject.call(source_account, remote_account, forward: true) let(:forward) { true }
expect(a_request(:post, 'http://example.com/inbox')).to have_been_made
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 end
it 'does not send anything when forward is false' do context 'when forward is false' do
subject.call(source_account, remote_account, forward: false) it 'does not send anything' do
expect(a_request(:post, 'http://example.com/inbox')).to_not have_been_made subject.call(source_account, remote_account, forward: forward)
end 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
end end
end end