Merge commit 'a32a126cac42c73236236b5a9bd660765b9c58ee' into glitch-soc/merge-upstream

Conflicts:
- `spec/lib/sanitize/config_spec.rb`:
  Conflict due to glitch-soc having factored the file differently.
  Ported upstream's changes.
shrike
Claire 2024-03-13 20:14:18 +01:00
commit 65ca37bbaa
19 changed files with 109 additions and 64 deletions

View File

@ -131,7 +131,7 @@ class ApplicationController < ActionController::Base
end end
def single_user_mode? def single_user_mode?
@single_user_mode ||= Rails.configuration.x.single_user_mode && Account.where('id > 0').exists? @single_user_mode ||= Rails.configuration.x.single_user_mode && Account.without_internal.exists?
end end
def use_seamless_external_login? def use_seamless_external_login?

View File

@ -214,7 +214,7 @@ module ApplicationHelper
state_params[:moved_to_account] = current_account.moved_to_account state_params[:moved_to_account] = current_account.moved_to_account
end end
state_params[:owner] = Account.local.without_suspended.where('id > 0').first if single_user_mode? state_params[:owner] = Account.local.without_suspended.without_internal.first if single_user_mode?
json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json json = ActiveModelSerializers::SerializableResource.new(InitialStatePresenter.new(state_params), serializer: InitialStateSerializer).to_json
# rubocop:disable Rails/OutputSafety # rubocop:disable Rails/OutputSafety

View File

@ -22,6 +22,7 @@ import Card from '../features/status/components/card';
// to use the progress bar to show download progress // to use the progress bar to show download progress
import Bundle from '../features/ui/components/bundle'; import Bundle from '../features/ui/components/bundle';
import { MediaGallery, Video, Audio } from '../features/ui/util/async-components'; import { MediaGallery, Video, Audio } from '../features/ui/util/async-components';
import { SensitiveMediaContext } from '../features/ui/util/sensitive_media_context';
import { displayMedia } from '../initial_state'; import { displayMedia } from '../initial_state';
import { Avatar } from './avatar'; import { Avatar } from './avatar';
@ -78,6 +79,8 @@ const messages = defineMessages({
class Status extends ImmutablePureComponent { class Status extends ImmutablePureComponent {
static contextType = SensitiveMediaContext;
static propTypes = { static propTypes = {
status: ImmutablePropTypes.map, status: ImmutablePropTypes.map,
account: ImmutablePropTypes.record, account: ImmutablePropTypes.record,
@ -133,19 +136,21 @@ class Status extends ImmutablePureComponent {
]; ];
state = { state = {
showMedia: defaultMediaVisibility(this.props.status), showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault),
statusId: undefined,
forceFilter: undefined, forceFilter: undefined,
}; };
static getDerivedStateFromProps(nextProps, prevState) { componentDidUpdate (prevProps) {
if (nextProps.status && nextProps.status.get('id') !== prevState.statusId) { // This will potentially cause a wasteful redraw, but in most cases `Status` components are used
return { // with a `key` directly depending on their `id`, preventing re-use of the component across
showMedia: defaultMediaVisibility(nextProps.status), // different IDs.
statusId: nextProps.status.get('id'), // But just in case this does change, reset the state on status change.
};
} else { if (this.props.status?.get('id') !== prevProps.status?.get('id')) {
return null; this.setState({
showMedia: defaultMediaVisibility(this.props.status) && !(this.context?.hideMediaByDefault),
forceFilter: undefined,
});
} }
} }

View File

@ -15,6 +15,7 @@ import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header'; import ColumnHeader from 'mastodon/components/column_header';
import { IconButton } from 'mastodon/components/icon_button'; import { IconButton } from 'mastodon/components/icon_button';
import ScrollableList from 'mastodon/components/scrollable_list'; import ScrollableList from 'mastodon/components/scrollable_list';
import { SensitiveMediaContextProvider } from 'mastodon/features/ui/util/sensitive_media_context';
import NotificationContainer from './containers/notification_container'; import NotificationContainer from './containers/notification_container';
@ -106,6 +107,7 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
)} )}
/> />
<SensitiveMediaContextProvider hideMediaByDefault>
<ScrollableList <ScrollableList
scrollKey={`notification_requests/${id}`} scrollKey={`notification_requests/${id}`}
trackScroll={!multiColumn} trackScroll={!multiColumn}
@ -125,6 +127,7 @@ export const NotificationRequest = ({ multiColumn, params: { id } }) => {
/> />
))} ))}
</ScrollableList> </ScrollableList>
</SensitiveMediaContextProvider>
<Helmet> <Helmet>
<title>{columnTitle}</title> <title>{columnTitle}</title>

View File

@ -0,0 +1,28 @@
import { createContext, useContext, useMemo } from 'react';
export const SensitiveMediaContext = createContext<{
hideMediaByDefault: boolean;
}>({
hideMediaByDefault: false,
});
export function useSensitiveMediaContext() {
return useContext(SensitiveMediaContext);
}
type ContextValue = React.ContextType<typeof SensitiveMediaContext>;
export const SensitiveMediaContextProvider: React.FC<
React.PropsWithChildren<{ hideMediaByDefault: boolean }>
> = ({ hideMediaByDefault, children }) => {
const contextValue = useMemo<ContextValue>(
() => ({ hideMediaByDefault }),
[hideMediaByDefault],
);
return (
<SensitiveMediaContext.Provider value={contextValue}>
{children}
</SensitiveMediaContext.Provider>
);
};

View File

@ -115,6 +115,7 @@ class Account < ApplicationRecord
normalizes :username, with: ->(username) { username.squish } normalizes :username, with: ->(username) { username.squish }
scope :without_internal, -> { where(id: 1...) }
scope :remote, -> { where.not(domain: nil) } scope :remote, -> { where.not(domain: nil) }
scope :local, -> { where(domain: nil) } scope :local, -> { where(domain: nil) }
scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) } scope :partitioned, -> { order(Arel.sql('row_number() over (partition by domain)')) }

View File

@ -41,6 +41,14 @@ RSpec.describe InstanceActorsController do
it_behaves_like 'shared behavior' it_behaves_like 'shared behavior'
end end
context 'with a suspended instance actor' do
let(:authorized_fetch_mode) { false }
before { Account.representative.update(suspended_at: 10.days.ago) }
it_behaves_like 'shared behavior'
end
end end
end end
end end

View File

@ -11,7 +11,7 @@ RSpec.describe FeedManager do
end end
it 'tracks at least as many statuses as reblogs', :skip_stub do it 'tracks at least as many statuses as reblogs', :skip_stub do
expect(FeedManager::REBLOG_FALLOFF).to be <= FeedManager::MAX_ITEMS expect(described_class::REBLOG_FALLOFF).to be <= described_class::MAX_ITEMS
end end
describe '#key' do describe '#key' do
@ -232,12 +232,12 @@ RSpec.describe FeedManager do
it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do it 'trims timelines if they will have more than FeedManager::MAX_ITEMS' do
account = Fabricate(:account) account = Fabricate(:account)
status = Fabricate(:status) status = Fabricate(:status)
members = Array.new(FeedManager::MAX_ITEMS) { |count| [count, count] } members = Array.new(described_class::MAX_ITEMS) { |count| [count, count] }
redis.zadd("feed:home:#{account.id}", members) redis.zadd("feed:home:#{account.id}", members)
described_class.instance.push_to_home(account, status) described_class.instance.push_to_home(account, status)
expect(redis.zcard("feed:home:#{account.id}")).to eq FeedManager::MAX_ITEMS expect(redis.zcard("feed:home:#{account.id}")).to eq described_class::MAX_ITEMS
end end
context 'with reblogs' do context 'with reblogs' do
@ -267,7 +267,7 @@ RSpec.describe FeedManager do
described_class.instance.push_to_home(account, reblogged) described_class.instance.push_to_home(account, reblogged)
# Fill the feed with intervening statuses # Fill the feed with intervening statuses
FeedManager::REBLOG_FALLOFF.times do described_class::REBLOG_FALLOFF.times do
described_class.instance.push_to_home(account, Fabricate(:status)) described_class.instance.push_to_home(account, Fabricate(:status))
end end
@ -328,7 +328,7 @@ RSpec.describe FeedManager do
described_class.instance.push_to_home(account, reblogs.first) described_class.instance.push_to_home(account, reblogs.first)
# Fill the feed with intervening statuses # Fill the feed with intervening statuses
FeedManager::REBLOG_FALLOFF.times do described_class::REBLOG_FALLOFF.times do
described_class.instance.push_to_home(account, Fabricate(:status)) described_class.instance.push_to_home(account, Fabricate(:status))
end end
@ -474,7 +474,7 @@ RSpec.describe FeedManager do
status = Fabricate(:status, reblog: reblogged) status = Fabricate(:status, reblog: reblogged)
described_class.instance.push_to_home(receiver, reblogged) described_class.instance.push_to_home(receiver, reblogged)
FeedManager::REBLOG_FALLOFF.times { described_class.instance.push_to_home(receiver, Fabricate(:status)) } described_class::REBLOG_FALLOFF.times { described_class.instance.push_to_home(receiver, Fabricate(:status)) }
described_class.instance.push_to_home(receiver, status) described_class.instance.push_to_home(receiver, status)
# The reblogging status should show up under normal conditions. # The reblogging status should show up under normal conditions.

View File

@ -58,7 +58,7 @@ describe Sanitize::Config do
end end
describe '::MASTODON_OUTGOING' do describe '::MASTODON_OUTGOING' do
subject { Sanitize::Config::MASTODON_OUTGOING } subject { described_class::MASTODON_OUTGOING }
around do |example| around do |example|
original_web_domain = Rails.configuration.x.web_domain original_web_domain = Rails.configuration.x.web_domain

View File

@ -27,7 +27,7 @@ RSpec.describe SignatureParser do
let(:header) { 'hello this is malformed!' } let(:header) { 'hello this is malformed!' }
it 'raises an error' do it 'raises an error' do
expect { subject }.to raise_error(SignatureParser::ParsingError) expect { subject }.to raise_error(described_class::ParsingError)
end end
end end
end end

View File

@ -46,7 +46,7 @@ describe WebfingerResource do
expect do expect do
described_class.new(resource).username described_class.new(resource).username
end.to raise_error(WebfingerResource::InvalidRequest) end.to raise_error(described_class::InvalidRequest)
end end
it 'finds the username in a valid https route' do it 'finds the username in a valid https route' do
@ -137,7 +137,7 @@ describe WebfingerResource do
expect do expect do
described_class.new(resource).username described_class.new(resource).username
end.to raise_error(WebfingerResource::InvalidRequest) end.to raise_error(described_class::InvalidRequest)
end end
end end
end end

View File

@ -678,7 +678,7 @@ RSpec.describe Account do
end end
describe 'MENTION_RE' do describe 'MENTION_RE' do
subject { Account::MENTION_RE } subject { described_class::MENTION_RE }
it 'matches usernames in the middle of a sentence' do it 'matches usernames in the middle of a sentence' do
expect(subject.match('Hello to @alice from me')[1]).to eq 'alice' expect(subject.match('Hello to @alice from me')[1]).to eq 'alice'
@ -888,7 +888,7 @@ RSpec.describe Account do
{ username: 'b', domain: 'b' }, { username: 'b', domain: 'b' },
].map(&method(:Fabricate).curry(2).call(:account)) ].map(&method(:Fabricate).curry(2).call(:account))
expect(described_class.where('id > 0').alphabetic).to eq matches expect(described_class.without_internal.alphabetic).to eq matches
end end
end end
@ -939,7 +939,7 @@ RSpec.describe Account do
it 'returns an array of accounts who do not have a domain' do it 'returns an array of accounts who do not have a domain' do
local_account = Fabricate(:account, domain: nil) local_account = Fabricate(:account, domain: nil)
_account_with_domain = Fabricate(:account, domain: 'example.com') _account_with_domain = Fabricate(:account, domain: 'example.com')
expect(described_class.where('id > 0').local).to contain_exactly(local_account) expect(described_class.without_internal.local).to contain_exactly(local_account)
end end
end end
@ -950,14 +950,14 @@ RSpec.describe Account do
matches[index] = Fabricate(:account, domain: matches[index]) matches[index] = Fabricate(:account, domain: matches[index])
end end
expect(described_class.where('id > 0').partitioned).to match_array(matches) expect(described_class.without_internal.partitioned).to match_array(matches)
end end
end end
describe 'recent' do describe 'recent' do
it 'returns a relation of accounts sorted by recent creation' do it 'returns a relation of accounts sorted by recent creation' do
matches = Array.new(2) { Fabricate(:account) } matches = Array.new(2) { Fabricate(:account) }
expect(described_class.where('id > 0').recent).to match_array(matches) expect(described_class.without_internal.recent).to match_array(matches)
end end
end end

View File

@ -30,7 +30,7 @@ RSpec.describe Form::Import do
it 'has errors' do it 'has errors' do
subject.validate subject.validate
expect(subject.errors[:data]).to include(I18n.t('imports.errors.over_rows_processing_limit', count: Form::Import::ROWS_PROCESSING_LIMIT)) expect(subject.errors[:data]).to include(I18n.t('imports.errors.over_rows_processing_limit', count: described_class::ROWS_PROCESSING_LIMIT))
end end
end end

View File

@ -8,7 +8,7 @@ describe PrivacyPolicy do
it 'has the privacy text' do it 'has the privacy text' do
policy = described_class.current policy = described_class.current
expect(policy.text).to eq(PrivacyPolicy::DEFAULT_PRIVACY_POLICY) expect(policy.text).to eq(described_class::DEFAULT_PRIVACY_POLICY)
end end
end end

View File

@ -22,7 +22,7 @@ RSpec.describe Tag do
end end
describe 'HASHTAG_RE' do describe 'HASHTAG_RE' do
subject { Tag::HASHTAG_RE } subject { described_class::HASHTAG_RE }
it 'does not match URLs with anchors with non-hashtag characters' do it 'does not match URLs with anchors with non-hashtag characters' do
expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil expect(subject.match('Check this out https://medium.com/@alice/some-article#.abcdef123')).to be_nil

View File

@ -8,7 +8,7 @@ RSpec.describe UserRole do
describe '#can?' do describe '#can?' do
context 'with a single flag' do context 'with a single flag' do
it 'returns true if any of them are present' do it 'returns true if any of them are present' do
subject.permissions = UserRole::FLAGS[:manage_reports] subject.permissions = described_class::FLAGS[:manage_reports]
expect(subject.can?(:manage_reports)).to be true expect(subject.can?(:manage_reports)).to be true
end end
@ -19,7 +19,7 @@ RSpec.describe UserRole do
context 'with multiple flags' do context 'with multiple flags' do
it 'returns true if any of them are present' do it 'returns true if any of them are present' do
subject.permissions = UserRole::FLAGS[:manage_users] subject.permissions = described_class::FLAGS[:manage_users]
expect(subject.can?(:manage_reports, :manage_users)).to be true expect(subject.can?(:manage_reports, :manage_users)).to be true
end end
@ -51,7 +51,7 @@ RSpec.describe UserRole do
describe '#permissions_as_keys' do describe '#permissions_as_keys' do
before do before do
subject.permissions = UserRole::FLAGS[:invite_users] | UserRole::FLAGS[:view_dashboard] | UserRole::FLAGS[:manage_reports] subject.permissions = described_class::FLAGS[:invite_users] | described_class::FLAGS[:view_dashboard] | described_class::FLAGS[:manage_reports]
end end
it 'returns an array' do it 'returns an array' do
@ -70,7 +70,7 @@ RSpec.describe UserRole do
let(:input) { %w(manage_users) } let(:input) { %w(manage_users) }
it 'sets permission flags' do it 'sets permission flags' do
expect(subject.permissions).to eq UserRole::FLAGS[:manage_users] expect(subject.permissions).to eq described_class::FLAGS[:manage_users]
end end
end end
@ -78,7 +78,7 @@ RSpec.describe UserRole do
let(:input) { %w(manage_users manage_reports) } let(:input) { %w(manage_users manage_reports) }
it 'sets permission flags' do it 'sets permission flags' do
expect(subject.permissions).to eq UserRole::FLAGS[:manage_users] | UserRole::FLAGS[:manage_reports] expect(subject.permissions).to eq described_class::FLAGS[:manage_users] | described_class::FLAGS[:manage_reports]
end end
end end
@ -86,7 +86,7 @@ RSpec.describe UserRole do
let(:input) { %w(foo) } let(:input) { %w(foo) }
it 'does not set permission flags' do it 'does not set permission flags' do
expect(subject.permissions).to eq UserRole::Flags::NONE expect(subject.permissions).to eq described_class::Flags::NONE
end end
end end
end end
@ -96,7 +96,7 @@ RSpec.describe UserRole do
subject { described_class.nobody } subject { described_class.nobody }
it 'returns none' do it 'returns none' do
expect(subject.computed_permissions).to eq UserRole::Flags::NONE expect(subject.computed_permissions).to eq described_class::Flags::NONE
end end
end end
@ -110,11 +110,11 @@ RSpec.describe UserRole do
context 'when role has the administrator flag' do context 'when role has the administrator flag' do
before do before do
subject.permissions = UserRole::FLAGS[:administrator] subject.permissions = described_class::FLAGS[:administrator]
end end
it 'returns all permissions' do it 'returns all permissions' do
expect(subject.computed_permissions).to eq UserRole::Flags::ALL expect(subject.computed_permissions).to eq described_class::Flags::ALL
end end
end end
@ -135,7 +135,7 @@ RSpec.describe UserRole do
end end
it 'has default permissions' do it 'has default permissions' do
expect(subject.permissions).to eq UserRole::FLAGS[:invite_users] expect(subject.permissions).to eq described_class::FLAGS[:invite_users]
end end
it 'has negative position' do it 'has negative position' do
@ -155,7 +155,7 @@ RSpec.describe UserRole do
end end
it 'has no permissions' do it 'has no permissions' do
expect(subject.permissions).to eq UserRole::Flags::NONE expect(subject.permissions).to eq described_class::Flags::NONE
end end
it 'has negative position' do it 'has negative position' do

View File

@ -24,7 +24,7 @@ RSpec.describe UserSettings do
context 'when setting was not defined' do context 'when setting was not defined' do
it 'raises error' do it 'raises error' do
expect { subject[:foo] }.to raise_error UserSettings::KeyError expect { subject[:foo] }.to raise_error described_class::KeyError
end end
end end
end end
@ -93,7 +93,7 @@ RSpec.describe UserSettings do
describe '.definition_for' do describe '.definition_for' do
context 'when key is defined' do context 'when key is defined' do
it 'returns a setting' do it 'returns a setting' do
expect(described_class.definition_for(:always_send_emails)).to be_a UserSettings::Setting expect(described_class.definition_for(:always_send_emails)).to be_a described_class::Setting
end end
end end

View File

@ -150,7 +150,7 @@ RSpec.describe PostStatusService do
expect do expect do
subject.call(account, text: '@alice hm, @bob is really annoying lately', allowed_mentions: [mentioned_account.id]) subject.call(account, text: '@alice hm, @bob is really annoying lately', allowed_mentions: [mentioned_account.id])
end.to raise_error(an_instance_of(PostStatusService::UnexpectedMentionsError).and(having_attributes(accounts: [unexpected_mentioned_account]))) end.to raise_error(an_instance_of(described_class::UnexpectedMentionsError).and(having_attributes(accounts: [unexpected_mentioned_account])))
end end
it 'processes duplicate mentions correctly' do it 'processes duplicate mentions correctly' do

View File

@ -56,7 +56,7 @@ RSpec.describe FollowLimitValidator do
follow.valid? follow.valid?
expect(follow.errors[:base]).to include(I18n.t('users.follow_limit_reached', limit: FollowLimitValidator::LIMIT)) expect(follow.errors[:base]).to include(I18n.t('users.follow_limit_reached', limit: described_class::LIMIT))
end end
end end