From e1f7847b64f8e963ad472a5fea91428cfd858910 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 24 Sep 2025 10:58:08 +0200 Subject: [PATCH] Remove the `outgoing_quotes` feature flag, making the feature unconditional (#36130) --- .../interaction_policies_controller.rb | 5 -- app/controllers/api/v1/statuses_controller.rb | 2 - .../api/interaction_policies_concern.rb | 2 - .../status/boost_button.stories.tsx | 16 +---- .../components/status/boost_button.tsx | 70 +------------------ .../components/status_action_bar/index.jsx | 3 +- .../mastodon/containers/status_container.jsx | 6 +- .../compose/components/visibility_button.tsx | 5 -- .../features/keyboard_shortcuts/index.jsx | 11 ++- .../features/status/components/action_bar.jsx | 3 +- app/javascript/mastodon/utils/environment.ts | 6 +- app/lib/activitypub/activity/quote_request.rb | 2 +- .../activitypub/note_serializer.rb | 2 +- .../rest/shallow_status_serializer.rb | 2 +- app/serializers/rest/status_serializer.rb | 2 +- lib/mastodon/version.rb | 2 +- .../activity/quote_request_spec.rb | 2 +- spec/lib/status_cache_hydrator_spec.rb | 2 +- .../v1/statuses/interaction_policies_spec.rb | 2 +- spec/requests/api/v1/statuses_spec.rb | 12 ++-- .../activitypub/note_serializer_spec.rb | 2 +- 21 files changed, 27 insertions(+), 132 deletions(-) diff --git a/app/controllers/api/v1/statuses/interaction_policies_controller.rb b/app/controllers/api/v1/statuses/interaction_policies_controller.rb index b8ec4fe140..6e2745806d 100644 --- a/app/controllers/api/v1/statuses/interaction_policies_controller.rb +++ b/app/controllers/api/v1/statuses/interaction_policies_controller.rb @@ -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 }) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 93dbd8f9d1..6c4e7619b7 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -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 diff --git a/app/controllers/concerns/api/interaction_policies_concern.rb b/app/controllers/concerns/api/interaction_policies_concern.rb index 5b63705a9b..f1e1480c0c 100644 --- a/app/controllers/concerns/api/interaction_policies_concern.rb +++ b/app/controllers/concerns/api/interaction_policies_concern.rb @@ -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 diff --git a/app/javascript/mastodon/components/status/boost_button.stories.tsx b/app/javascript/mastodon/components/status/boost_button.stories.tsx index e81d334a93..402695a829 100644 --- a/app/javascript/mastodon/components/status/boost_button.stories.tsx +++ b/app/javascript/mastodon/components/status/boost_button.stories.tsx @@ -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) => ( - 0} - /> + 0} /> ), } satisfies Meta; @@ -78,12 +75,3 @@ export const Mine: Story = { }, }, }; - -export const Legacy: Story = { - render: (args) => ( - 0} - /> - ), -}; diff --git a/app/javascript/mastodon/components/status/boost_button.tsx b/app/javascript/mastodon/components/status/boost_button.tsx index 49bdc953e1..337eca5071 100644 --- a/app/javascript/mastodon/components/status/boost_button.tsx +++ b/app/javascript/mastodon/components/status/boost_button.tsx @@ -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; -export const StatusBoostButton: FC = ({ - status, - counters, -}) => { +export const BoostButton: FC = ({ status, counters }) => { const intl = useIntl(); const dispatch = useAppDispatch(); const statusState = useAppSelector((state) => @@ -192,65 +188,3 @@ const ReblogMenuItem: FC = ({ ); }; - -// Legacy helpers - -// Switch between the legacy and new reblog button based on feature flag. -export const BoostButton: FC = (props) => { - if (isFeatureEnabled('outgoing_quotes')) { - return ; - } - return ; -}; - -export const LegacyReblogButton: FC = ({ - 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 ( - - ); -}; diff --git a/app/javascript/mastodon/components/status_action_bar/index.jsx b/app/javascript/mastodon/components/status_action_bar/index.jsx index 0e72a8cefe..3e82912ab1 100644 --- a/app/javascript/mastodon/components/status_action_bar/index.jsx +++ b/app/javascript/mastodon/components/status_action_bar/index.jsx @@ -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); diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index ba280ffd23..baf4157f96 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -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) { diff --git a/app/javascript/mastodon/features/compose/components/visibility_button.tsx b/app/javascript/mastodon/features/compose/components/visibility_button.tsx index fadb896b5e..1ea504ab1a 100644 --- a/app/javascript/mastodon/features/compose/components/visibility_button.tsx +++ b/app/javascript/mastodon/features/compose/components/visibility_button.tsx @@ -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 = (props) => { - if (!isFeatureEnabled('outgoing_quotes')) { - return ; - } return ; }; diff --git a/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx b/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx index a1f14f14f6..01a4f0e1fd 100644 --- a/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx +++ b/app/javascript/mastodon/features/keyboard_shortcuts/index.jsx @@ -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 { b - {isFeatureEnabled('outgoing_quotes') && ( - - q - - - )} + + q + + enter, o diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index fa9d6497ae..6156cf1916 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -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); diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index fc4448740f..2d544417e3 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -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; diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 7b49acd119..6d386f45dc 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -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) diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index ab4743bab4..4c5d3f4cf8 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -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) diff --git a/app/serializers/rest/shallow_status_serializer.rb b/app/serializers/rest/shallow_status_serializer.rb index d82ac32621..0b951f6caa 100644 --- a/app/serializers/rest/shallow_status_serializer.rb +++ b/app/serializers/rest/shallow_status_serializer.rb @@ -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 diff --git a/app/serializers/rest/status_serializer.rb b/app/serializers/rest/status_serializer.rb index 1e267df82a..a06ddc6861 100644 --- a/app/serializers/rest/status_serializer.rb +++ b/app/serializers/rest/status_serializer.rb @@ -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? diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index a6bbfcd24d..043f22b28e 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -45,7 +45,7 @@ module Mastodon def api_versions { - mastodon: Mastodon::Feature.outgoing_quotes_enabled? ? 7 : 6, + mastodon: 7, } end diff --git a/spec/lib/activitypub/activity/quote_request_spec.rb b/spec/lib/activitypub/activity/quote_request_spec.rb index 64627cbdfb..aae4ce0338 100644 --- a/spec/lib/activitypub/activity/quote_request_spec.rb +++ b/spec/lib/activitypub/activity/quote_request_spec.rb @@ -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) } diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb index f450997976..085866ef1d 100644 --- a/spec/lib/status_cache_hydrator_spec.rb +++ b/spec/lib/status_cache_hydrator_spec.rb @@ -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 diff --git a/spec/requests/api/v1/statuses/interaction_policies_spec.rb b/spec/requests/api/v1/statuses/interaction_policies_spec.rb index cdc33e40d7..aa447de17f 100644 --- a/spec/requests/api/v1/statuses/interaction_policies_spec.rb +++ b/spec/requests/api/v1/statuses/interaction_policies_spec.rb @@ -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) } diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index eb3e8aed5b..249abc2440 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -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) diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 336f394337..04179e9bf4 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -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