Remove the outgoing_quotes feature flag, making the feature unconditional (#36130)

This commit is contained in:
Claire 2025-09-24 10:58:08 +02:00 committed by GitHub
parent 6cbc857ee0
commit e1f7847b64
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
21 changed files with 27 additions and 132 deletions

View File

@ -4,7 +4,6 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base
include Api::InteractionPoliciesConcern
before_action -> { doorkeeper_authorize! :write, :'write:statuses' }
before_action -> { check_feature_enabled }
def update
authorize @status, :update?
@ -22,10 +21,6 @@ class Api::V1::Statuses::InteractionPoliciesController < Api::V1::Statuses::Base
params.permit(:quote_approval_policy)
end
def check_feature_enabled
raise ActionController::RoutingError unless Mastodon::Feature.outgoing_quotes_enabled?
end
def broadcast_updates!
DistributionWorker.perform_async(@status.id, { 'update' => true })
ActivityPub::StatusUpdateDistributionWorker.perform_async(@status.id, { 'updated_at' => Time.now.utc.iso8601 })

View File

@ -157,8 +157,6 @@ class Api::V1::StatusesController < Api::BaseController
end
def set_quoted_status
return unless Mastodon::Feature.outgoing_quotes_enabled?
@quoted_status = Status.find(status_params[:quoted_status_id]) if status_params[:quoted_status_id].present?
authorize(@quoted_status, :quote?) if @quoted_status.present?
rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError

View File

@ -4,8 +4,6 @@ module Api::InteractionPoliciesConcern
extend ActiveSupport::Concern
def quote_approval_policy
return nil unless Mastodon::Feature.outgoing_quotes_enabled?
case status_params[:quote_approval_policy].presence || current_user.setting_default_quote_policy
when 'public'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16

View File

@ -3,7 +3,7 @@ import type { Meta, StoryObj } from '@storybook/react-vite';
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
import { statusFactoryState } from '@/testing/factories';
import { LegacyReblogButton, StatusBoostButton } from './boost_button';
import { BoostButton } from './boost_button';
interface StoryProps {
visibility: StatusVisibility;
@ -38,10 +38,7 @@ const meta = {
},
},
render: (args) => (
<StatusBoostButton
status={argsToStatus(args)}
counters={args.reblogCount > 0}
/>
<BoostButton status={argsToStatus(args)} counters={args.reblogCount > 0} />
),
} satisfies Meta<StoryProps>;
@ -78,12 +75,3 @@ export const Mine: Story = {
},
},
};
export const Legacy: Story = {
render: (args) => (
<LegacyReblogButton
status={argsToStatus(args)}
counters={args.reblogCount > 0}
/>
),
};

View File

@ -1,5 +1,5 @@
import { useCallback, useMemo } from 'react';
import type { FC, KeyboardEvent, MouseEvent, MouseEventHandler } from 'react';
import type { FC, KeyboardEvent, MouseEvent } from 'react';
import { useIntl } from 'react-intl';
@ -11,7 +11,6 @@ import { openModal } from '@/mastodon/actions/modal';
import type { ActionMenuItem } from '@/mastodon/models/dropdown_menu';
import type { Status } from '@/mastodon/models/status';
import { useAppDispatch, useAppSelector } from '@/mastodon/store';
import { isFeatureEnabled } from '@/mastodon/utils/environment';
import type { SomeRequired } from '@/mastodon/utils/types';
import type { RenderItemFn, RenderItemFnHandlers } from '../dropdown_menu';
@ -47,10 +46,7 @@ interface ReblogButtonProps {
type ActionMenuItemWithIcon = SomeRequired<ActionMenuItem, 'icon'>;
export const StatusBoostButton: FC<ReblogButtonProps> = ({
status,
counters,
}) => {
export const BoostButton: FC<ReblogButtonProps> = ({ status, counters }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const statusState = useAppSelector((state) =>
@ -192,65 +188,3 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
</li>
);
};
// Legacy helpers
// Switch between the legacy and new reblog button based on feature flag.
export const BoostButton: FC<ReblogButtonProps> = (props) => {
if (isFeatureEnabled('outgoing_quotes')) {
return <StatusBoostButton {...props} />;
}
return <LegacyReblogButton {...props} />;
};
export const LegacyReblogButton: FC<ReblogButtonProps> = ({
status,
counters,
}) => {
const intl = useIntl();
const statusState = useAppSelector((state) =>
selectStatusState(state, status),
);
const { title, meta, iconComponent, disabled } = useMemo(
() => boostItemState(statusState),
[statusState],
);
const dispatch = useAppDispatch();
const handleClick: MouseEventHandler = useCallback(
(event) => {
if (statusState.isLoggedIn) {
dispatch(toggleReblog(status.get('id') as string, event.shiftKey));
} else {
dispatch(
openModal({
modalType: 'INTERACTION',
modalProps: {
accountId: status.getIn(['account', 'id']),
url: status.get('uri'),
},
}),
);
}
},
[dispatch, status, statusState.isLoggedIn],
);
return (
<IconButton
disabled={disabled}
active={!!status.get('reblogged')}
title={intl.formatMessage(meta ?? title)}
icon='retweet'
iconComponent={iconComponent}
onClick={!disabled ? handleClick : undefined}
counter={
counters
? (status.get('reblogs_count') as number) +
(status.get('quotes_count') as number)
: undefined
}
/>
);
};

View File

@ -23,7 +23,6 @@ import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../../initial_state';
import { IconButton } from '../icon_button';
import { isFeatureEnabled } from '../../utils/environment';
import { BoostButton } from '../status/boost_button';
import { RemoveQuoteHint } from './remove_quote_hint';
@ -281,7 +280,7 @@ class StatusActionBar extends ImmutablePureComponent {
if (writtenByMe || withDismiss) {
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
if (writtenByMe && isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) {
if (writtenByMe && !['private', 'direct'].includes(status.get('visibility'))) {
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
}
menu.push(null);

View File

@ -47,8 +47,6 @@ import Status from '../components/status';
import { deleteModal } from '../initial_state';
import { makeGetStatus, makeGetPictureInPicture } from '../selectors';
import { isFeatureEnabled } from 'mastodon/utils/environment';
const makeMapStateToProps = () => {
const getStatus = makeGetStatus();
const getPictureInPicture = makeGetPictureInPicture();
@ -81,9 +79,7 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
},
onQuote (status) {
if (isFeatureEnabled('outgoing_quotes')) {
dispatch(quoteComposeById(status.get('id')));
}
dispatch(quoteComposeById(status.get('id')));
},
onFavourite (status) {

View File

@ -12,14 +12,12 @@ import type { ApiQuotePolicy } from '@/mastodon/api_types/quotes';
import type { StatusVisibility } from '@/mastodon/api_types/statuses';
import { Icon } from '@/mastodon/components/icon';
import { useAppSelector, useAppDispatch } from '@/mastodon/store';
import { isFeatureEnabled } from '@/mastodon/utils/environment';
import AlternateEmailIcon from '@/material-icons/400-24px/alternate_email.svg?react';
import LockIcon from '@/material-icons/400-24px/lock.svg?react';
import PublicIcon from '@/material-icons/400-24px/public.svg?react';
import QuietTimeIcon from '@/material-icons/400-24px/quiet_time.svg?react';
import type { VisibilityModalCallback } from '../../ui/components/visibility_modal';
import PrivacyDropdownContainer from '../containers/privacy_dropdown_container';
import { messages as privacyMessages } from './privacy_dropdown';
@ -43,9 +41,6 @@ interface PrivacyDropdownProps {
}
export const VisibilityButton: FC<PrivacyDropdownProps> = (props) => {
if (!isFeatureEnabled('outgoing_quotes')) {
return <PrivacyDropdownContainer {...props} />;
}
return <PrivacyModalButton {...props} />;
};

View File

@ -9,7 +9,6 @@ import ImmutablePureComponent from 'react-immutable-pure-component';
import InfoIcon from '@/material-icons/400-24px/info.svg?react';
import Column from 'mastodon/components/column';
import ColumnHeader from 'mastodon/components/column_header';
import { isFeatureEnabled } from 'mastodon/utils/environment';
const messages = defineMessages({
heading: { id: 'keyboard_shortcuts.heading', defaultMessage: 'Keyboard Shortcuts' },
@ -63,12 +62,10 @@ class KeyboardShortcuts extends ImmutablePureComponent {
<td><kbd>b</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.boost' defaultMessage='to boost' /></td>
</tr>
{isFeatureEnabled('outgoing_quotes') && (
<tr>
<td><kbd>q</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.quote' defaultMessage='Quote post' /></td>
</tr>
)}
<tr>
<td><kbd>q</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.quote' defaultMessage='Quote post' /></td>
</tr>
<tr>
<td><kbd>enter</kbd>, <kbd>o</kbd></td>
<td><FormattedMessage id='keyboard_shortcuts.enter' defaultMessage='to open status' /></td>

View File

@ -19,7 +19,6 @@ import { PERMISSION_MANAGE_USERS, PERMISSION_MANAGE_FEDERATION } from 'mastodon/
import { IconButton } from '../../../components/icon_button';
import { Dropdown } from 'mastodon/components/dropdown_menu';
import { me } from '../../../initial_state';
import { isFeatureEnabled } from '@/mastodon/utils/environment';
import { BoostButton } from '@/mastodon/components/status/boost_button';
const messages = defineMessages({
@ -237,7 +236,7 @@ class ActionBar extends PureComponent {
}
menu.push({ text: intl.formatMessage(mutingConversation ? messages.unmuteConversation : messages.muteConversation), action: this.handleConversationMuteClick });
if (isFeatureEnabled('outgoing_quotes') && !['private', 'direct'].includes(status.get('visibility'))) {
if (!['private', 'direct'].includes(status.get('visibility'))) {
menu.push({ text: intl.formatMessage(messages.quotePolicyChange), action: this.handleQuotePolicyChange });
}
menu.push(null);

View File

@ -12,11 +12,7 @@ export function isProduction() {
else return import.meta.env.PROD;
}
export type Features =
| 'modern_emojis'
| 'outgoing_quotes'
| 'fasp'
| 'http_message_signatures';
export type Features = 'modern_emojis' | 'fasp' | 'http_message_signatures';
export function isFeatureEnabled(feature: Features) {
return initialState?.features.includes(feature) ?? false;

View File

@ -9,7 +9,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity
quoted_status = status_from_uri(object_uri)
return if quoted_status.nil? || !quoted_status.account.local? || !quoted_status.distributable?
if Mastodon::Feature.outgoing_quotes_enabled? && StatusPolicy.new(@account, quoted_status).quote?
if StatusPolicy.new(@account, quoted_status).quote?
accept_quote_request!(quoted_status)
else
reject_quote_request!(quoted_status)

View File

@ -36,7 +36,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
attribute :quote, key: :quote_uri, if: :quote?
attribute :quote_authorization, if: :quote_authorization?
attribute :interaction_policy, if: -> { Mastodon::Feature.outgoing_quotes_enabled? }
attribute :interaction_policy
def id
ActivityPub::TagManager.instance.uri_for(object)

View File

@ -6,5 +6,5 @@ class REST::ShallowStatusSerializer < REST::StatusSerializer
# It looks like redefining one `has_one` requires redefining all inherited ones
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? }
has_one :quote_approval
end

View File

@ -34,7 +34,7 @@ class REST::StatusSerializer < ActiveModel::Serializer
has_one :quote, key: :quote, serializer: REST::QuoteSerializer
has_one :preview_card, key: :card, serializer: REST::PreviewCardSerializer
has_one :preloadable_poll, key: :poll, serializer: REST::PollSerializer
has_one :quote_approval, if: -> { Mastodon::Feature.outgoing_quotes_enabled? }
has_one :quote_approval
def quote
object.quote if object.quote&.acceptable?

View File

@ -45,7 +45,7 @@ module Mastodon
def api_versions
{
mastodon: Mastodon::Feature.outgoing_quotes_enabled? ? 7 : 6,
mastodon: 7,
}
end

View File

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe ActivityPub::Activity::QuoteRequest, feature: :outgoing_quotes do
RSpec.describe ActivityPub::Activity::QuoteRequest do
let(:sender) { Fabricate(:account, domain: 'example.com') }
let(:recipient) { Fabricate(:account) }
let(:quoted_post) { Fabricate(:status, account: recipient) }

View File

@ -28,7 +28,7 @@ RSpec.describe StatusCacheHydrator do
end
end
context 'when handling a status with a quote policy', feature: :outgoing_quotes do
context 'when handling a status with a quote policy' do
let(:status) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
before do

View File

@ -2,7 +2,7 @@
require 'rails_helper'
RSpec.describe 'Interaction policies', feature: :outgoing_quotes do
RSpec.describe 'Interaction policies' do
let(:user) { Fabricate(:user) }
let(:scopes) { 'write:statuses' }
let(:token) { Fabricate(:accessible_access_token, resource_owner_id: user.id, scopes: scopes) }

View File

@ -158,7 +158,7 @@ RSpec.describe '/api/v1/statuses' do
end
end
context 'without a quote policy', feature: :outgoing_quotes do
context 'without a quote policy' do
let(:user) do
Fabricate(:user, settings: { default_quote_policy: 'followers' })
end
@ -180,7 +180,7 @@ RSpec.describe '/api/v1/statuses' do
end
end
context 'without a quote policy and the user defaults to nobody', feature: :outgoing_quotes do
context 'without a quote policy and the user defaults to nobody' do
let(:user) do
Fabricate(:user, settings: { default_quote_policy: 'nobody' })
end
@ -202,7 +202,7 @@ RSpec.describe '/api/v1/statuses' do
end
end
context 'with a quote policy', feature: :outgoing_quotes do
context 'with a quote policy' do
let(:quoted_status) { Fabricate(:status, account: user.account) }
let(:params) do
{
@ -227,7 +227,7 @@ RSpec.describe '/api/v1/statuses' do
end
end
context 'with a self-quote post', feature: :outgoing_quotes do
context 'with a self-quote post' do
let(:quoted_status) { Fabricate(:status, account: user.account) }
let(:params) do
{
@ -248,7 +248,7 @@ RSpec.describe '/api/v1/statuses' do
end
end
context 'with a self-quote post and a CW but no text', feature: :outgoing_quotes do
context 'with a self-quote post and a CW but no text' do
let(:quoted_status) { Fabricate(:status, account: user.account) }
let(:params) do
{
@ -420,7 +420,7 @@ RSpec.describe '/api/v1/statuses' do
context 'when updating only the quote policy' do
let(:params) { { status: status.text, quote_approval_policy: 'public' } }
it 'updates the status', :aggregate_failures, feature: :outgoing_quotes do
it 'updates the status', :aggregate_failures do
expect { subject }
.to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)

View File

@ -58,7 +58,7 @@ RSpec.describe ActivityPub::NoteSerializer do
end
end
context 'with a quote policy', feature: :outgoing_quotes do
context 'with a quote policy' do
let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
it 'has the expected shape' do