From e65103bd3de034b89196f7f0d86a6e9eb157971e Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 21 Jan 2026 10:18:16 +0100 Subject: [PATCH 001/230] Fix rendering of initial state when `collections` feature is enabled (#37556) --- app/serializers/initial_state_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index fe2a857d50970d..a8e4b1d7f79713 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -140,7 +140,7 @@ def object_account_user end def serialized_account(account) - ActiveModelSerializers::SerializableResource.new(account, serializer: REST::AccountSerializer) + ActiveModelSerializers::SerializableResource.new(account, serializer: REST::AccountSerializer, scope_name: :current_user, scope: object.current_account&.user) end def instance_presenter From e7c6600d83d043a91769142e249a8818cba855d6 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 21 Jan 2026 13:02:41 +0100 Subject: [PATCH 002/230] Fix cross-server conversation tracking (#37559) --- app/lib/activitypub/activity/create.rb | 1 + app/lib/activitypub/tag_manager.rb | 16 +++++++----- app/lib/ostatus/tag_manager.rb | 10 +++----- spec/lib/activitypub/activity/create_spec.rb | 26 +++++++++++++++++++- spec/lib/activitypub/tag_manager_spec.rb | 8 ------ 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 43c7bb1fe7103f..a7d2be35ed0310 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -379,6 +379,7 @@ def fetch_and_verify_quote def conversation_from_uri(uri) return nil if uri.nil? return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri) + return ActivityPub::TagManager.instance.uri_to_resource(uri, Conversation) if ActivityPub::TagManager.instance.local_uri?(uri) begin Conversation.find_or_create_by!(uri: uri) diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index e6714c51abb062..f9cb90f548cd70 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -243,12 +243,6 @@ def local_uri?(uri) !host.nil? && (::TagManager.instance.local_domain?(host) || ::TagManager.instance.web_domain?(host)) end - def uri_to_local_id(uri, param = :id) - path_params = Rails.application.routes.recognize_path(uri) - path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors' - path_params[param] - end - def uris_to_local_accounts(uris) usernames = [] ids = [] @@ -266,6 +260,14 @@ def uri_to_actor(uri) uri_to_resource(uri, Account) end + def uri_to_local_conversation(uri) + path_params = Rails.application.routes.recognize_path(uri) + return unless path_params[:controller] == 'activitypub/contexts' + + account_id, conversation_id = path_params[:id].split('-') + Conversation.find_by(parent_account_id: account_id, id: conversation_id) + end + def uri_to_resource(uri, klass) return if uri.nil? @@ -273,6 +275,8 @@ def uri_to_resource(uri, klass) case klass.name when 'Account' uris_to_local_accounts([uri]).first + when 'Conversation' + uri_to_local_conversation(uri) else StatusFinder.new(uri).status end diff --git a/app/lib/ostatus/tag_manager.rb b/app/lib/ostatus/tag_manager.rb index cb0c9f89668685..7d0f23c4dc1a7d 100644 --- a/app/lib/ostatus/tag_manager.rb +++ b/app/lib/ostatus/tag_manager.rb @@ -11,16 +11,12 @@ def unique_tag(date, id, type) def unique_tag_to_local_id(tag, expected_type) return nil unless local_id?(tag) - if ActivityPub::TagManager.instance.local_uri?(tag) - ActivityPub::TagManager.instance.uri_to_local_id(tag) - else - matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) - matches[1] unless matches.nil? - end + matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) + matches[1] unless matches.nil? end def local_id?(id) - id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id) + id.start_with?("tag:#{Rails.configuration.x.local_domain}") end def uri_for(target) diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 1e8a2a29db4b30..19b6014af158cd 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -471,7 +471,7 @@ def activity_for_object(json) end end - context 'with a reply' do + context 'with a reply without explicitly setting a conversation' do let(:original_status) { Fabricate(:status) } let(:object_json) do @@ -493,6 +493,30 @@ def activity_for_object(json) end end + context 'with a reply explicitly setting a conversation' do + let(:original_status) { Fabricate(:status) } + + let(:object_json) do + build_object( + inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), + conversation: ActivityPub::TagManager.instance.uri_for(original_status.conversation), + context: ActivityPub::TagManager.instance.uri_for(original_status.conversation) + ) + end + + it 'creates status' do + expect { subject.perform }.to change(sender.statuses, :count).by(1) + + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.thread).to eq original_status + expect(status.reply?).to be true + expect(status.in_reply_to_account).to eq original_status.account + expect(status.conversation).to eq original_status.conversation + end + end + context 'with mentions' do let(:recipient) { Fabricate(:account) } diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 55e54ede5e10b9..a15529057cb409 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -629,14 +629,6 @@ end end - describe '#uri_to_local_id' do - let(:account) { Fabricate(:account, id_scheme: :username_ap_id) } - - it 'returns the local ID' do - expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username - end - end - describe '#uris_to_local_accounts' do it 'returns the expected local accounts' do account = Fabricate(:account) From 783504f36a394a7eaa4e34552116cfeb0c7cd1c6 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 21 Jan 2026 13:30:07 +0100 Subject: [PATCH 003/230] Do not return undiscoverable collections (#37560) --- .../api/v1_alpha/collections_controller.rb | 1 + app/models/collection.rb | 1 + .../requests/api/v1_alpha/collections_spec.rb | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1_alpha/collections_controller.rb index 9d6b2f9a381072..4b07b5012a2099 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1_alpha/collections_controller.rb @@ -74,6 +74,7 @@ def set_collections .order(created_at: :desc) .offset(offset_param) .limit(limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections = @collections.discoverable unless @account == current_account end def set_collection diff --git a/app/models/collection.rb b/app/models/collection.rb index 334318b73d37f9..3681c41d84fefc 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -43,6 +43,7 @@ class Collection < ApplicationRecord scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) } scope :with_tag, -> { includes(:tag) } + scope :discoverable, -> { where(discoverable: true) } def remote? !local? diff --git a/spec/requests/api/v1_alpha/collections_spec.rb b/spec/requests/api/v1_alpha/collections_spec.rb index b529fc2d92f311..de79dcf72308b8 100644 --- a/spec/requests/api/v1_alpha/collections_spec.rb +++ b/spec/requests/api/v1_alpha/collections_spec.rb @@ -55,6 +55,32 @@ ) end end + + context 'when some collections are not discoverable' do + before do + Fabricate(:collection, account:, discoverable: false) + end + + context 'when requesting user is a third party' do + it 'hides the collections that are not discoverable' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body.size).to eq 3 + end + end + + context 'when requesting user owns the collection' do + let(:account) { user.account } + + it 'returns all collections, including the ones that are not discoverable' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body.size).to eq 4 + end + end + end end describe 'GET /api/v1_alpha/collections/:id' do From 24ffa00bca165e8c180a5e45ba0380a2d2b37f81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:31:19 +0000 Subject: [PATCH 004/230] Update dependency pino to v10.2.1 (#37543) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index f37750a4cc74b3..d5efe483cbae61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10681,8 +10681,8 @@ __metadata: linkType: hard "pino@npm:^10.0.0": - version: 10.2.0 - resolution: "pino@npm:10.2.0" + version: 10.2.1 + resolution: "pino@npm:10.2.1" dependencies: "@pinojs/redact": "npm:^0.4.0" atomic-sleep: "npm:^1.0.0" @@ -10697,7 +10697,7 @@ __metadata: thread-stream: "npm:^4.0.0" bin: pino: bin.js - checksum: 10c0/8f88a2e205508d47ef04d2a6ec26ec450abb4b344d2d998d2e24b9e624e1a1ef7184f260ca5be06bc3733aa1ad76704657e373b359c7b71489a11709227e26da + checksum: 10c0/2eaed48bb7fb8865e27ac6d6709383f5c117f1e59c818734c7cc22b362e9aa5846a0547e7fd9cde64088a3b48aa314e1dab07ee16da8dc3b87897970eb56843e languageName: node linkType: hard From 5d82d48af3e6cdeabe2559e1e17ca48c4d716837 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:31:35 +0000 Subject: [PATCH 005/230] Update dependency stylelint-config-standard-scss to v17 (#37511) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 83 ++++++++++++++++++++++++---------------------------- 2 files changed, 39 insertions(+), 46 deletions(-) diff --git a/package.json b/package.json index 701719d7cff075..987467458d36de 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "storybook": "^10.0.5", "stylelint": "^16.19.1", "stylelint-config-prettier-scss": "^1.0.0", - "stylelint-config-standard-scss": "^16.0.0", + "stylelint-config-standard-scss": "^17.0.0", "typescript": "~5.9.0", "typescript-eslint": "^8.45.0", "typescript-plugin-css-modules": "^5.2.0", diff --git a/yarn.lock b/yarn.lock index d5efe483cbae61..e36a0cca262939 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2990,7 +2990,7 @@ __metadata: stringz: "npm:^2.1.0" stylelint: "npm:^16.19.1" stylelint-config-prettier-scss: "npm:^1.0.0" - stylelint-config-standard-scss: "npm:^16.0.0" + stylelint-config-standard-scss: "npm:^17.0.0" substring-trie: "npm:^1.0.2" tesseract.js: "npm:^7.0.0" tiny-queue: "npm:^0.2.1" @@ -9311,13 +9311,6 @@ __metadata: languageName: node linkType: hard -"known-css-properties@npm:^0.36.0": - version: 0.36.0 - resolution: "known-css-properties@npm:0.36.0" - checksum: 10c0/098c8f956408a7ce26a639c2354e0184fb2bb2772bb7d1ba23192b6b6cf5818cbb8a0acfb4049705ea103d9916065703bc540fa084a6349fdb41bf745aada4bc - languageName: node - linkType: hard - "known-css-properties@npm:^0.37.0": version: 0.37.0 resolution: "known-css-properties@npm:0.37.0" @@ -9679,10 +9672,10 @@ __metadata: languageName: node linkType: hard -"mdn-data@npm:^2.21.0": - version: 2.21.0 - resolution: "mdn-data@npm:2.21.0" - checksum: 10c0/cd26902551af2cc29f06f130893cb04bca9ee278939fce3ffbcb759497cc80d53a6f4abdef2ae2f3ed3c95ac8d651f53fc141defd580ebf4ae2f93aea325957b +"mdn-data@npm:^2.25.0": + version: 2.26.0 + resolution: "mdn-data@npm:2.26.0" + checksum: 10c0/e5f17f4dac247f3e260c081761628d371e23659a7ff13413f83f5bd7fd0f2d8317e72277bb77f0e13115041334ff728a5363db64aabaf376c0e1b0b31016d0b8 languageName: node linkType: hard @@ -13185,74 +13178,74 @@ __metadata: languageName: node linkType: hard -"stylelint-config-recommended-scss@npm:^16.0.1": - version: 16.0.2 - resolution: "stylelint-config-recommended-scss@npm:16.0.2" +"stylelint-config-recommended-scss@npm:^17.0.0": + version: 17.0.0 + resolution: "stylelint-config-recommended-scss@npm:17.0.0" dependencies: postcss-scss: "npm:^4.0.9" - stylelint-config-recommended: "npm:^17.0.0" - stylelint-scss: "npm:^6.12.1" + stylelint-config-recommended: "npm:^18.0.0" + stylelint-scss: "npm:^7.0.0" peerDependencies: postcss: ^8.3.3 - stylelint: ^16.24.0 + stylelint: ^17.0.0 peerDependenciesMeta: postcss: optional: true - checksum: 10c0/d4e30a881e248d8b039347bf967526f6afe6d6a07f18e2747e14568de32273e819ba478be7a61a0dd63178931b4e891050a34e73d296ab533aa434209a7f3146 + checksum: 10c0/05b2e8d4316c2a8cc66eed0a2a8f01237e0ee8966a2e73d0b3c6706694f7630be165daa5a0cef511bc51f7e3fcb07a84c55d948c15fe6193a7e13cf9bb67c913 languageName: node linkType: hard -"stylelint-config-recommended@npm:^17.0.0": - version: 17.0.0 - resolution: "stylelint-config-recommended@npm:17.0.0" +"stylelint-config-recommended@npm:^18.0.0": + version: 18.0.0 + resolution: "stylelint-config-recommended@npm:18.0.0" peerDependencies: - stylelint: ^16.23.0 - checksum: 10c0/49e5d1c0f58197b2c5585b85fad814fed9bdec44c9870368c46a762664c5ff158c1145b6337456ae194409d692992b5b87421d62880422f71d8a3360417f5ad1 + stylelint: ^17.0.0 + checksum: 10c0/c7f8ff45c76ec23f4c8c0438894726976fd5e872c59d489f959b728d9879bba20dbf0040cd29ad3bbc00eb32befd95f5b6ca150002bb8aea74b0797bc42ccc17 languageName: node linkType: hard -"stylelint-config-standard-scss@npm:^16.0.0": - version: 16.0.0 - resolution: "stylelint-config-standard-scss@npm:16.0.0" +"stylelint-config-standard-scss@npm:^17.0.0": + version: 17.0.0 + resolution: "stylelint-config-standard-scss@npm:17.0.0" dependencies: - stylelint-config-recommended-scss: "npm:^16.0.1" - stylelint-config-standard: "npm:^39.0.0" + stylelint-config-recommended-scss: "npm:^17.0.0" + stylelint-config-standard: "npm:^40.0.0" peerDependencies: postcss: ^8.3.3 - stylelint: ^16.23.1 + stylelint: ^17.0.0 peerDependenciesMeta: postcss: optional: true - checksum: 10c0/eb77f23824c5d649b193cb71d7f9b538b32b8cc1769451b2993270361127243d4011baf891ec265711b8e34e69ce28acb57ab6c3947b51fa3713ac26f4276439 + checksum: 10c0/0506537ba896f3d5e0fb002608090fcb41aa8ba7b65f1de8533702ce7c70e3f92b275782788a8356b5b687c86c53468c223e082226dda62780294b1cba324a36 languageName: node linkType: hard -"stylelint-config-standard@npm:^39.0.0": - version: 39.0.1 - resolution: "stylelint-config-standard@npm:39.0.1" +"stylelint-config-standard@npm:^40.0.0": + version: 40.0.0 + resolution: "stylelint-config-standard@npm:40.0.0" dependencies: - stylelint-config-recommended: "npm:^17.0.0" + stylelint-config-recommended: "npm:^18.0.0" peerDependencies: - stylelint: ^16.23.0 - checksum: 10c0/70a9862a2cedcc2a1807bd92fc91c40877270cf8a39576b91ae056d6de51d3b68104b26f71056ff22461b4319e9ec988d009abf10ead513b2ec15569d82e865a + stylelint: ^17.0.0 + checksum: 10c0/d8942552d53a3afda59b64d0c49503bb626fe5cef39a9e8c9583fcd60869f21431125ef4480ff27a59f7f2cf0da8af810d377129ef1d670ddc5def4defe2880c languageName: node linkType: hard -"stylelint-scss@npm:^6.12.1": - version: 6.12.1 - resolution: "stylelint-scss@npm:6.12.1" +"stylelint-scss@npm:^7.0.0": + version: 7.0.0 + resolution: "stylelint-scss@npm:7.0.0" dependencies: css-tree: "npm:^3.0.1" is-plain-object: "npm:^5.0.0" - known-css-properties: "npm:^0.36.0" - mdn-data: "npm:^2.21.0" + known-css-properties: "npm:^0.37.0" + mdn-data: "npm:^2.25.0" postcss-media-query-parser: "npm:^0.2.3" postcss-resolve-nested-selector: "npm:^0.1.6" - postcss-selector-parser: "npm:^7.1.0" + postcss-selector-parser: "npm:^7.1.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: - stylelint: ^16.0.2 - checksum: 10c0/9a0903d34be3c75a72bef32402899db5f6b94c0823c5944fdf1acb2c3dc61c1f70fbb322558f8cb7e42dd01ed5e0dec22ed298f03b7bacc9f467c28330acae71 + stylelint: ^16.8.2 || ^17.0.0 + checksum: 10c0/07d0f20c6bcb34b8b0b6bfb1d4367b4825b52a7eef7dde2adfbaec11ebc67242e6b99dccf70dfbef1eb0a9bf8712fe0ab49d183ff6e4cca9c7f89752f7e27027 languageName: node linkType: hard From e79d51ce19274504dc85f4bb3fbf8502c0cfdc58 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 21 Jan 2026 14:08:08 +0100 Subject: [PATCH 006/230] Profile redesign: verified badges (#37538) --- app/javascript/images/icons/icon_verified.svg | 10 ++++ .../mastodon/components/mini_card/list.tsx | 5 +- .../account_timeline/components/fields.tsx | 57 ++++++++++++------- .../components/fields_modal.tsx | 28 ++++++--- .../components/redesign.module.scss | 38 +++++++++++++ .../styles/mastodon/theme/_dark.scss | 1 + .../styles/mastodon/theme/_light.scss | 1 + 7 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 app/javascript/images/icons/icon_verified.svg diff --git a/app/javascript/images/icons/icon_verified.svg b/app/javascript/images/icons/icon_verified.svg new file mode 100644 index 00000000000000..65873b9dc43495 --- /dev/null +++ b/app/javascript/images/icons/icon_verified.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/javascript/mastodon/components/mini_card/list.tsx b/app/javascript/mastodon/components/mini_card/list.tsx index f775e70aac893c..318c5849538d7e 100644 --- a/app/javascript/mastodon/components/mini_card/list.tsx +++ b/app/javascript/mastodon/components/mini_card/list.tsx @@ -10,7 +10,9 @@ import type { MiniCardProps } from '.'; import classes from './styles.module.css'; interface MiniCardListProps { - cards?: (Pick & { key?: Key })[]; + cards?: (Pick & { + key?: Key; + })[]; className?: string; onOverflowClick?: MouseEventHandler; } @@ -42,6 +44,7 @@ export const MiniCardList: FC = ({ label={card.label} value={card.value} hidden={hasOverflow && index >= hiddenIndex} + className={card.className} /> ))} diff --git a/app/javascript/mastodon/features/account_timeline/components/fields.tsx b/app/javascript/mastodon/features/account_timeline/components/fields.tsx index a73d92c1b6988e..ab29a8299e46b8 100644 --- a/app/javascript/mastodon/features/account_timeline/components/fields.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/fields.tsx @@ -3,10 +3,14 @@ import type { FC } from 'react'; import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; + +import IconVerified from '@/images/icons/icon_verified.svg?react'; import { openModal } from '@/mastodon/actions/modal'; import { AccountFields } from '@/mastodon/components/account_fields'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; +import { Icon } from '@/mastodon/components/icon'; import { MiniCardList } from '@/mastodon/components/mini_card/list'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; import { useAccount } from '@/mastodon/hooks/useAccount'; @@ -55,25 +59,40 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => { const htmlHandlers = useElementHandledLink(); const cards = useMemo( () => - account.fields.toArray().map(({ value_emojified, name_emojified }) => ({ - label: ( - - ), - value: ( - - ), - })), + account.fields + .toArray() + .map(({ value_emojified, name_emojified, verified_at }) => ({ + label: ( + <> + + {!!verified_at && ( + + )} + + ), + value: ( + + ), + className: classNames( + classes.fieldCard, + !!verified_at && classes.fieldCardVerified, + ), + })), [account.emojis, account.fields, htmlHandlers], ); diff --git a/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx index 715f6097f45bca..103fffca5058b7 100644 --- a/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx @@ -2,9 +2,11 @@ import type { FC } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; +import IconVerified from '@/images/icons/icon_verified.svg?react'; import { DisplayName } from '@/mastodon/components/display_name'; import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { Icon } from '@/mastodon/components/icon'; import { IconButton } from '@/mastodon/components/icon_button'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; @@ -56,7 +58,10 @@ export const AccountFieldsModal: FC<{
{account.fields.map((field, index) => ( -
+
- +
+ + {!!field.verified_at && ( + + )} +
))}
diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss index 5ccdb1f310019d..4bc64d05a98cca 100644 --- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -64,6 +64,39 @@ svg.badgeIcon { margin-top: 16px; } +.fieldCard { + position: relative; + + a { + color: var(--color-text-brand); + text-decoration: none; + } +} + +.fieldCardVerified { + background-color: var(--color-bg-brand-softer); + + dt { + padding-right: 1rem; + } + + .fieldIconVerified { + position: absolute; + top: 4px; + right: 4px; + } +} + +.fieldIconVerified { + width: 1rem; + height: 1rem; + + // Need to override .icon path. + path { + fill: revert-layer; + } +} + .fieldNumbersWrapper { a { font-weight: unset; @@ -106,4 +139,9 @@ svg.badgeIcon { font-weight: 600; font-size: 15px; } + + .fieldIconVerified { + vertical-align: middle; + margin-left: 4px; + } } diff --git a/app/javascript/styles/mastodon/theme/_dark.scss b/app/javascript/styles/mastodon/theme/_dark.scss index e6fd6d3cc14cd5..9485464e09913f 100644 --- a/app/javascript/styles/mastodon/theme/_dark.scss +++ b/app/javascript/styles/mastodon/theme/_dark.scss @@ -142,6 +142,7 @@ var(--border-strength-primary) )}; --color-border-media: rgb(252 248 255 / 15%); + --color-border-verified: rgb(220, 3, 240); --color-border-on-bg-secondary: #{utils.css-alpha( var(--color-indigo-200), calc(var(--border-strength-primary) / 1.5) diff --git a/app/javascript/styles/mastodon/theme/_light.scss b/app/javascript/styles/mastodon/theme/_light.scss index f0dc1bdfbc30bf..534a18367ca0f9 100644 --- a/app/javascript/styles/mastodon/theme/_light.scss +++ b/app/javascript/styles/mastodon/theme/_light.scss @@ -140,6 +140,7 @@ var(--color-grey-950) var(--border-strength-primary) ); --color-border-media: rgb(252 248 255 / 15%); + --color-border-verified: rgb(220, 3, 240); --color-border-on-bg-secondary: var(--color-grey-200); --color-border-on-bg-brand-softer: var(--color-indigo-200); --color-border-on-bg-error-softer: #{utils.css-alpha( From 1468f945093671f67c7444fcc2c56fc42858653d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:08:16 +0000 Subject: [PATCH 007/230] New Crowdin Translations (automated) (#37555) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/es.json | 4 ++++ config/locales/ca.yml | 1 + config/locales/simple_form.de.yml | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 2b7d1905350a1c..8a3672ad4c9291 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "Cuenta conmemorativa.", + "account.joined_long": "Se unió el {date}", "account.joined_short": "Se unió", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "La propiedad de este enlace fue verificada el {date}", @@ -90,6 +91,8 @@ "account.unmute": "Dejar de silenciar a @{name}", "account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_short": "Dejar de silenciar", + "account_fields_modal.close": "Cerrar", + "account_fields_modal.title": "Información de {name}", "account_note.placeholder": "Haz clic para añadir nota", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después del registro", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después del registro", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar de las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 0cbbb08f83ec3b..6adcf1871c7fae 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -2162,6 +2162,7 @@ ca: error: Hi ha hagut un problema al esborrar la teva clau de seguretat. Tornau-ho a provar. success: La teva clau de seguretat s'ha esborrat correctament. invalid_credential: Clau de seguretat invàlida + nickname: Sobrenom nickname_hint: Introdueix el sobrenom de la teva clau de seguretat nova not_enabled: Encara no has activat WebAuthn not_supported: Aquest navegador no suporta claus de seguretat diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 98defb79cdb60f..a39cf4435948c3 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -175,7 +175,7 @@ de: labels: account: attribution_domains: Websites, die auf dich verweisen dürfen - discoverable: Profil und Beiträge in Suchalgorithmen berücksichtigen + discoverable: Profil und Beiträge in Empfehlungsalgorithmen berücksichtigen fields: name: Beschriftung value: Inhalt From 22e438d7bdefdb467ddb759acf2c5a5ef584af26 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:14:48 +0100 Subject: [PATCH 008/230] Update dependency @csstools/stylelint-formatter-github to v2 (#37515) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 987467458d36de..acdd57e3fd6733 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "private": true, "dependencies": { - "@csstools/stylelint-formatter-github": "^1.0.0", + "@csstools/stylelint-formatter-github": "^2.0.0", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", diff --git a/yarn.lock b/yarn.lock index e36a0cca262939..9c14c9e85e2f2b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1926,12 +1926,12 @@ __metadata: languageName: node linkType: hard -"@csstools/stylelint-formatter-github@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/stylelint-formatter-github@npm:1.0.0" +"@csstools/stylelint-formatter-github@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/stylelint-formatter-github@npm:2.0.0" peerDependencies: - stylelint: ^16.6.0 - checksum: 10c0/2052c4e4d89656b2b4176a6d07508ef73278d33c24a7408a3555d07f26ec853f85da95525590c51751fb3150a2ebb5e3083d8200dc6597af2cd8e93198695269 + stylelint: ^17.0.0 + checksum: 10c0/1eddcb749eb93efff2e2d7edb4405459bf558ceaa6d90e792408802f30c55e3482a4cead9e69fd651f04a927e863782fc6cf813c37433da9ff1f068910080a06 languageName: node linkType: hard @@ -2861,7 +2861,7 @@ __metadata: version: 0.0.0-use.local resolution: "@mastodon/mastodon@workspace:." dependencies: - "@csstools/stylelint-formatter-github": "npm:^1.0.0" + "@csstools/stylelint-formatter-github": "npm:^2.0.0" "@dnd-kit/core": "npm:^6.1.0" "@dnd-kit/sortable": "npm:^10.0.0" "@dnd-kit/utilities": "npm:^3.2.2" From 6897475f9be80d9f3f81d06087dec0a3b6d7cc44 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 21 Jan 2026 16:54:52 +0100 Subject: [PATCH 009/230] Adds theming to Storybook (#37562) --- .storybook/preview.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index abbd193c68190e..10d45acfe65e37 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -50,9 +50,19 @@ const preview: Preview = { dynamicTitle: true, }, }, + theme: { + description: 'Theme for the story', + toolbar: { + title: 'Theme', + icon: 'circlehollow', + items: [{ value: 'light' }, { value: 'dark' }], + dynamicTitle: true, + }, + }, }, initialGlobals: { locale: 'en', + theme: 'light', }, decorators: [ (Story, { parameters, globals, args, argTypes }) => { @@ -135,6 +145,13 @@ const preview: Preview = { ); }, + (Story, { globals }) => { + const theme = (globals.theme as string) || 'light'; + useEffect(() => { + document.body.setAttribute('data-color-scheme', theme); + }, [theme]); + return ; + }, (Story) => ( From 3219373d5600ee5c6f38b947eb0bae6a56647643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 21 Jan 2026 17:01:33 +0100 Subject: [PATCH 010/230] Add profile field limits to instance serializer (#37535) --- app/serializers/rest/instance_serializer.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 75d3acfea5895c..1900330475c463 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -71,6 +71,9 @@ def configuration accounts: { max_featured_tags: FeaturedTag::LIMIT, max_pinned_statuses: StatusPinValidator::PIN_LIMIT, + max_profile_fields: Account::DEFAULT_FIELDS_SIZE, + profile_field_name_limit: Account::Field::MAX_CHARACTERS_LOCAL, + profile_field_value_limit: Account::Field::MAX_CHARACTERS_LOCAL, }, statuses: { From 562ea656f495f0619e393b7d93bd07c5abd28e5a Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 21 Jan 2026 11:11:38 -0500 Subject: [PATCH 011/230] Add coverage for `TagManager#normalize_domain` (#35994) --- app/lib/tag_manager.rb | 2 +- spec/lib/tag_manager_spec.rb | 40 ++++++++++++++++++-- spec/models/instance_moderation_note_spec.rb | 2 +- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index c1bd2973ed12bf..5a6284cc5b6281 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -18,7 +18,7 @@ def normalize_domain(domain) return if domain.nil? uri = Addressable::URI.new - uri.host = domain.delete_suffix('/') + uri.host = domain.strip.delete_suffix('/') uri.normalized_host end diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb index 38203a55f70ad9..927214bb40c50a 100644 --- a/spec/lib/tag_manager_spec.rb +++ b/spec/lib/tag_manager_spec.rb @@ -54,12 +54,44 @@ end describe '#normalize_domain' do - it 'returns nil if the given parameter is nil' do - expect(described_class.instance.normalize_domain(nil)).to be_nil + subject { described_class.instance.normalize_domain(domain) } + + context 'with a nil value' do + let(:domain) { nil } + + it { is_expected.to be_nil } + end + + context 'with a blank value' do + let(:domain) { '' } + + it { is_expected.to be_blank } + end + + context 'with a mixed case string' do + let(:domain) { 'DoMaIn.Example.com' } + + it { is_expected.to eq('domain.example.com') } end - it 'returns normalized domain' do - expect(described_class.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com' + context 'with a trailing slash string' do + let(:domain) { 'domain.example.com/' } + + it { is_expected.to eq('domain.example.com') } + end + + context 'with a space padded string' do + let(:domain) { ' domain.example.com ' } + + it { is_expected.to eq('domain.example.com') } + end + + context 'with an invalid domain string' do + let(:domain) { ' !@#$@#$@$@# ' } + + it 'raises invalid uri error' do + expect { subject }.to raise_error(Addressable::URI::InvalidURIError) + end end end diff --git a/spec/models/instance_moderation_note_spec.rb b/spec/models/instance_moderation_note_spec.rb index 4d77d497122cd3..011b001cc72e8f 100644 --- a/spec/models/instance_moderation_note_spec.rb +++ b/spec/models/instance_moderation_note_spec.rb @@ -5,7 +5,7 @@ RSpec.describe InstanceModerationNote do describe 'chronological' do it 'returns the instance notes sorted by oldest first' do - instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain('mastodon.example')) + instance = Instance.find_or_initialize_by(domain: 'mastodon.example') note1 = Fabricate(:instance_moderation_note, domain: instance.domain) note2 = Fabricate(:instance_moderation_note, domain: instance.domain) From 42b2fdb0acc41b06a397b25ca095fa222b96b9bf Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 22 Jan 2026 13:04:15 +0100 Subject: [PATCH 012/230] Re-download Material Icons (#37571) --- app/javascript/mastodon/features/compose/components/upload.tsx | 2 +- app/javascript/material-icons/400-24px/audio.svg | 1 - app/javascript/material-icons/400-24px/block-fill.svg | 2 +- app/javascript/material-icons/400-24px/block.svg | 2 +- app/javascript/material-icons/400-24px/graphic_eq-fill.svg | 1 + app/javascript/material-icons/400-24px/graphic_eq.svg | 1 + 6 files changed, 5 insertions(+), 4 deletions(-) delete mode 100644 app/javascript/material-icons/400-24px/audio.svg create mode 100644 app/javascript/material-icons/400-24px/graphic_eq-fill.svg create mode 100644 app/javascript/material-icons/400-24px/graphic_eq.svg diff --git a/app/javascript/mastodon/features/compose/components/upload.tsx b/app/javascript/mastodon/features/compose/components/upload.tsx index 85fed0cbd3bf78..4190f3248e331f 100644 --- a/app/javascript/mastodon/features/compose/components/upload.tsx +++ b/app/javascript/mastodon/features/compose/components/upload.tsx @@ -10,8 +10,8 @@ import { useSortable } from '@dnd-kit/sortable'; import { CSS } from '@dnd-kit/utilities'; import CloseIcon from '@/material-icons/400-20px/close.svg?react'; -import SoundIcon from '@/material-icons/400-24px/audio.svg?react'; import EditIcon from '@/material-icons/400-24px/edit.svg?react'; +import SoundIcon from '@/material-icons/400-24px/graphic_eq.svg?react'; import WarningIcon from '@/material-icons/400-24px/warning.svg?react'; import { undoUploadCompose } from 'mastodon/actions/compose'; import { openModal } from 'mastodon/actions/modal'; diff --git a/app/javascript/material-icons/400-24px/audio.svg b/app/javascript/material-icons/400-24px/audio.svg deleted file mode 100644 index 417e47c18fee37..00000000000000 --- a/app/javascript/material-icons/400-24px/audio.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/block-fill.svg b/app/javascript/material-icons/400-24px/block-fill.svg index 20e9889ae8006c..2d3801613c97af 100644 --- a/app/javascript/material-icons/400-24px/block-fill.svg +++ b/app/javascript/material-icons/400-24px/block-fill.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/block.svg b/app/javascript/material-icons/400-24px/block.svg index 20e9889ae8006c..e9df4cdd35dfbc 100644 --- a/app/javascript/material-icons/400-24px/block.svg +++ b/app/javascript/material-icons/400-24px/block.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/graphic_eq-fill.svg b/app/javascript/material-icons/400-24px/graphic_eq-fill.svg new file mode 100644 index 00000000000000..a83414784d1e47 --- /dev/null +++ b/app/javascript/material-icons/400-24px/graphic_eq-fill.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/javascript/material-icons/400-24px/graphic_eq.svg b/app/javascript/material-icons/400-24px/graphic_eq.svg new file mode 100644 index 00000000000000..a83414784d1e47 --- /dev/null +++ b/app/javascript/material-icons/400-24px/graphic_eq.svg @@ -0,0 +1 @@ + \ No newline at end of file From 958103368eaa3ab2a044087ca8cffac6ec9f4454 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 22 Jan 2026 13:38:00 +0100 Subject: [PATCH 013/230] Shorten caching of quote posts pending approval (#37570) --- app/controllers/statuses_controller.rb | 2 +- app/models/quote.rb | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/app/controllers/statuses_controller.rb b/app/controllers/statuses_controller.rb index e673faca04536b..65db807d187ebe 100644 --- a/app/controllers/statuses_controller.rb +++ b/app/controllers/statuses_controller.rb @@ -29,7 +29,7 @@ def show end format.json do - expires_in 3.minutes, public: true if @status.distributable? && public_fetch_mode? + expires_in @status.quote&.pending? ? 5.seconds : 3.minutes, public: true if @status.distributable? && public_fetch_mode? render_with_cache json: @status, content_type: 'application/activity+json', serializer: ActivityPub::NoteSerializer, adapter: ActivityPub::Adapter end end diff --git a/app/models/quote.rb b/app/models/quote.rb index 4ad393e3a579ec..425cf6305574d7 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -47,6 +47,8 @@ class Quote < ApplicationRecord def accept! update!(state: :accepted) + + reset_parent_cache! if attribute_changed?(:state) end def reject! @@ -75,6 +77,15 @@ def schedule_refresh_if_stale! private + def reset_parent_cache! + return if status_id.nil? + + Rails.cache.delete("v3:statuses/#{status_id}") + + # This clears the web cache for the ActivityPub representation + Rails.cache.delete("statuses/show:v3:statuses/#{status_id}") + end + def set_accounts self.account = status.account self.quoted_account = quoted_status&.account From 52ca91c43d0134fd6a3600a208ce019fa0e22159 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 14:50:23 +0100 Subject: [PATCH 014/230] Update dependency pg-connection-string to v2.10.1 (#37558) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9c14c9e85e2f2b..15f3d5048dc5ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10512,9 +10512,9 @@ __metadata: linkType: hard "pg-connection-string@npm:^2.10.0, pg-connection-string@npm:^2.6.0": - version: 2.10.0 - resolution: "pg-connection-string@npm:2.10.0" - checksum: 10c0/6639d3e12f66bc3cc5fa1e5794b4b22a4212e30424cbf001422e041c77c598ee0cd19395a9d79cb99a962da612b03c021e8148c3437cf01a29a18437bad193eb + version: 2.10.1 + resolution: "pg-connection-string@npm:2.10.1" + checksum: 10c0/f218a72b59c661022caca9a7f2116655632b1d7e7d6dc9a8ee9f238744e0927e0d6f44e12f50d9767c6d9cd47d9b3766aa054b77504b15c6bf503400530e053e languageName: node linkType: hard From 8dcd3881890a70c0df396c5d69ab8ed9d5494150 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:50:59 +0000 Subject: [PATCH 015/230] Update dependency aws-sdk-s3 to v1.212.0 (#37536) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 3ee1f77aa0b218..646162cffe406c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,8 +96,8 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1201.0) - aws-sdk-core (3.241.3) + aws-partitions (1.1206.0) + aws-sdk-core (3.241.4) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -105,11 +105,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.120.0) - aws-sdk-core (~> 3, >= 3.241.3) + aws-sdk-kms (1.121.0) + aws-sdk-core (~> 3, >= 3.241.4) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.211.0) - aws-sdk-core (~> 3, >= 3.241.3) + aws-sdk-s3 (1.212.0) + aws-sdk-core (~> 3, >= 3.241.4) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) From 7b9479239aa52fddba1d4949ceebebe274b194f3 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 22 Jan 2026 08:59:36 -0500 Subject: [PATCH 016/230] Typo fix in federation document (#37564) --- FEDERATION.md | 4 ++-- spec/models/account_spec.rb | 2 ++ spec/models/custom_emoji_spec.rb | 8 ++++++- spec/models/custom_filter_keyword_spec.rb | 5 +++++ spec/models/custom_filter_spec.rb | 3 ++- spec/models/list_spec.rb | 1 + spec/requests/activitypub/inboxes_spec.rb | 13 +++++++++++ .../api/web/push_subscriptions_spec.rb | 11 ++++++++++ .../services/fan_out_on_write_service_spec.rb | 22 +++++++++++++++++-- 9 files changed, 63 insertions(+), 6 deletions(-) diff --git a/FEDERATION.md b/FEDERATION.md index eb91d9545fe1ea..d5a176807b2d31 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -52,8 +52,8 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques ## Size limits Mastodon imposes a few hard limits on federated content. -These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accomodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons. -The following table attempts to summary those limits. +These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accommodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons. +The following table summarizes those limits. | Limited property | Size limit | Consequence of exceeding the limit | | ------------------------------------------------------------- | ---------- | ---------------------------------- | diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index 89a2f78c2e1039..c9adaaff397408 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -564,6 +564,8 @@ it { is_expected.to_not allow_values('username', 'Username').for(:username) } end + it { is_expected.to validate_length_of(:username).is_at_most(described_class::USERNAME_LENGTH_HARD_LIMIT) } + it { is_expected.to allow_values('the-doctor', username_over_limit).for(:username) } it { is_expected.to_not allow_values('the doctor').for(:username) } diff --git a/spec/models/custom_emoji_spec.rb b/spec/models/custom_emoji_spec.rb index 244d0d126f106a..ab757d396835c1 100644 --- a/spec/models/custom_emoji_spec.rb +++ b/spec/models/custom_emoji_spec.rb @@ -90,8 +90,14 @@ subject { Fabricate.build :custom_emoji } it { is_expected.to validate_uniqueness_of(:shortcode).scoped_to(:domain) } - it { is_expected.to validate_length_of(:shortcode).is_at_least(described_class::MINIMUM_SHORTCODE_SIZE) } + it { is_expected.to validate_length_of(:shortcode).is_at_least(described_class::MINIMUM_SHORTCODE_SIZE).is_at_most(described_class::MAX_SHORTCODE_SIZE) } it { is_expected.to allow_values('cats').for(:shortcode) } it { is_expected.to_not allow_values('@#$@#$', 'X').for(:shortcode) } + + context 'when remote' do + subject { Fabricate.build :custom_emoji, domain: 'host.example' } + + it { is_expected.to validate_length_of(:shortcode).is_at_most(described_class::MAX_FEDERATED_SHORTCODE_SIZE) } + end end end diff --git a/spec/models/custom_filter_keyword_spec.rb b/spec/models/custom_filter_keyword_spec.rb index 4e3ab060a042d1..cc6558ea693f7f 100644 --- a/spec/models/custom_filter_keyword_spec.rb +++ b/spec/models/custom_filter_keyword_spec.rb @@ -3,6 +3,11 @@ require 'rails_helper' RSpec.describe CustomFilterKeyword do + describe 'Validations' do + it { is_expected.to validate_length_of(:keyword).is_at_most(described_class::KEYWORD_LENGTH_LIMIT) } + it { is_expected.to validate_presence_of(:keyword) } + end + describe '#to_regex' do context 'when whole_word is true' do it 'builds a regex with boundaries and the keyword' do diff --git a/spec/models/custom_filter_spec.rb b/spec/models/custom_filter_spec.rb index 03914fa6b48525..8a60f1dd49167e 100644 --- a/spec/models/custom_filter_spec.rb +++ b/spec/models/custom_filter_spec.rb @@ -6,8 +6,9 @@ it_behaves_like 'Expireable' describe 'Validations' do - it { is_expected.to validate_presence_of(:title) } + it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_LIMIT) } it { is_expected.to validate_presence_of(:context) } + it { is_expected.to validate_presence_of(:title) } it { is_expected.to_not allow_values([], %w(invalid)).for(:context) } it { is_expected.to allow_values(%w(home)).for(:context) } diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb index e2d91835ec76a9..bc6e67d8bff33f 100644 --- a/spec/models/list_spec.rb +++ b/spec/models/list_spec.rb @@ -6,6 +6,7 @@ describe 'Validations' do subject { Fabricate.build :list } + it { is_expected.to validate_length_of(:title).is_at_most(described_class::TITLE_LENGTH_LIMIT) } it { is_expected.to validate_presence_of(:title) } context 'when account has hit max list limit' do diff --git a/spec/requests/activitypub/inboxes_spec.rb b/spec/requests/activitypub/inboxes_spec.rb index b21881b10fd1d3..e33afa53c9b2ee 100644 --- a/spec/requests/activitypub/inboxes_spec.rb +++ b/spec/requests/activitypub/inboxes_spec.rb @@ -20,6 +20,19 @@ end end + context 'with an excessively large payload' do + subject { post inbox_path, params: { this: :that, those: :these }.to_json, sign_with: remote_account } + + before { stub_const('ActivityPub::Activity::MAX_JSON_SIZE', 1.byte) } + + it 'returns http content too large' do + subject + + expect(response) + .to have_http_status(413) + end + end + context 'with a specific account' do subject { post account_inbox_path(account_username: account.username), params: {}.to_json, sign_with: remote_account } diff --git a/spec/requests/api/web/push_subscriptions_spec.rb b/spec/requests/api/web/push_subscriptions_spec.rb index 88c0302f860f04..3c33f0d2d29321 100644 --- a/spec/requests/api/web/push_subscriptions_spec.rb +++ b/spec/requests/api/web/push_subscriptions_spec.rb @@ -190,6 +190,17 @@ .to eq(alerts_payload[:data][:alerts][type.to_sym].to_s) end end + + context 'when using other user subscription' do + let(:subscription) { Fabricate(:web_push_subscription) } + + it 'does not change settings' do + put api_web_push_subscription_path(subscription), params: alerts_payload + + expect(response) + .to have_http_status(404) + end + end end def created_push_subscription diff --git a/spec/services/fan_out_on_write_service_spec.rb b/spec/services/fan_out_on_write_service_spec.rb index c6dd020cdff4b6..9f488995606ced 100644 --- a/spec/services/fan_out_on_write_service_spec.rb +++ b/spec/services/fan_out_on_write_service_spec.rb @@ -23,18 +23,30 @@ Fabricate(:media_attachment, status: status, account: alice) allow(redis).to receive(:publish) - - subject.call(status) end def home_feed_of(account) HomeFeed.new(account).get(10).map(&:id) end + context 'when status account is suspended' do + let(:visibility) { 'public' } + + before { alice.suspend! } + + it 'does not execute or broadcast' do + expect(subject.call(status)) + .to be_nil + expect_no_broadcasting + end + end + context 'when status is public' do let(:visibility) { 'public' } it 'adds status to home feed of author and followers and broadcasts', :inline_jobs do + subject.call(status) + expect(status.id) .to be_in(home_feed_of(alice)) .and be_in(home_feed_of(bob)) @@ -52,6 +64,8 @@ def home_feed_of(account) let(:visibility) { 'limited' } it 'adds status to home feed of author and mentioned followers and does not broadcast', :inline_jobs do + subject.call(status) + expect(status.id) .to be_in(home_feed_of(alice)) .and be_in(home_feed_of(bob)) @@ -66,6 +80,8 @@ def home_feed_of(account) let(:visibility) { 'private' } it 'adds status to home feed of author and followers and does not broadcast', :inline_jobs do + subject.call(status) + expect(status.id) .to be_in(home_feed_of(alice)) .and be_in(home_feed_of(bob)) @@ -79,6 +95,8 @@ def home_feed_of(account) let(:visibility) { 'direct' } it 'is added to the home feed of its author and mentioned followers and does not broadcast', :inline_jobs do + subject.call(status) + expect(status.id) .to be_in(home_feed_of(alice)) .and be_in(home_feed_of(bob)) From 3a84b73d80b2ad8eeecf882f0c735e1182f40404 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 15:26:33 +0100 Subject: [PATCH 017/230] New Crowdin Translations (automated) (#37569) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/nl.json | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 1411a222f810f7..bec46ab52edba8 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ga naar profiel", "account.hide_reblogs": "Boosts van @{name} verbergen", "account.in_memoriam": "In memoriam.", + "account.joined_long": "Lid geworden op {date}", "account.joined_short": "Geregistreerd op", "account.languages": "Getoonde talen wijzigen", "account.link_verified_on": "Eigendom van deze link is gecontroleerd op {date}", @@ -90,6 +91,8 @@ "account.unmute": "@{name} niet langer negeren", "account.unmute_notifications_short": "Meldingen niet langer negeren", "account.unmute_short": "Niet langer negeren", + "account_fields_modal.close": "Sluiten", + "account_fields_modal.title": "{name}'s info", "account_note.placeholder": "Klik om een opmerking toe te voegen", "admin.dashboard.daily_retention": "Retentiegraad van gebruikers per dag, vanaf registratie", "admin.dashboard.monthly_retention": "Retentiegraad van gebruikers per maand, vanaf registratie", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nieuw item} other {# nieuwe items}}", "loading_indicator.label": "Laden…", "media_gallery.hide": "Verberg", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Omdat je naar {movedToAccount} bent verhuisd is jouw account {disabledAccount} momenteel uitgeschakeld.", "mute_modal.hide_from_notifications": "Onder meldingen verbergen", "mute_modal.hide_options": "Opties verbergen", From 157d8c0d9912c62c0e533d51ff41b2c552c2f5cd Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 22 Jan 2026 09:57:19 -0500 Subject: [PATCH 018/230] Remove deprecated usage of imagemagick (#37488) --- .devcontainer/Dockerfile | 2 +- .github/workflows/test-ruby.yml | 87 ------------------- Dockerfile | 2 - Vagrantfile | 1 - .../dimension/software_versions_dimension.rb | 26 +----- app/models/concerns/account/avatar.rb | 2 +- app/models/concerns/account/header.rb | 2 +- app/models/preview_card.rb | 4 +- config/application.rb | 8 +- config/imagemagick/policy.xml | 27 ------ config/initializers/deprecations.rb | 6 -- config/initializers/paperclip.rb | 7 -- config/initializers/vips.rb | 46 +++++----- lib/paperclip/blurhash_transcoder.rb | 10 +-- lib/paperclip/color_extractor.rb | 13 +-- spec/models/media_attachment_spec.rb | 4 +- spec/models/preview_card_spec.rb | 6 -- spec/requests/api/v1/media_spec.rb | 2 +- spec/requests/api/v2/media_spec.rb | 2 +- 19 files changed, 35 insertions(+), 222 deletions(-) delete mode 100644 config/imagemagick/policy.xml diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index 3aa0bbf7da4ec3..ed8484f5b80b26 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -9,7 +9,7 @@ RUN /bin/bash --login -i -c "nvm install" # Install additional OS packages RUN apt-get update && \ export DEBIAN_FRONTEND=noninteractive && \ - apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg imagemagick libvips42 libpam-dev + apt-get -y install --no-install-recommends libicu-dev libidn11-dev ffmpeg libvips42 libpam-dev # Disable download prompt for Corepack ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 diff --git a/.github/workflows/test-ruby.yml b/.github/workflows/test-ruby.yml index 8f05812d600505..316bf831b6fd1b 100644 --- a/.github/workflows/test-ruby.yml +++ b/.github/workflows/test-ruby.yml @@ -173,93 +173,6 @@ jobs: env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - test-imagemagick: - name: ImageMagick tests - runs-on: ubuntu-latest - - needs: - - build - - services: - postgres: - image: postgres:14-alpine - env: - POSTGRES_PASSWORD: postgres - POSTGRES_USER: postgres - options: >- - --health-cmd pg_isready - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 5432:5432 - - redis: - image: redis:7-alpine - options: >- - --health-cmd "redis-cli ping" - --health-interval 10ms - --health-timeout 3s - --health-retries 50 - ports: - - 6379:6379 - - env: - DB_HOST: localhost - DB_USER: postgres - DB_PASS: postgres - COVERAGE: ${{ matrix.ruby-version == '.ruby-version' }} - RAILS_ENV: test - ALLOW_NOPAM: true - PAM_ENABLED: true - PAM_DEFAULT_SERVICE: pam_test - PAM_CONTROLLED_SERVICE: pam_test_controlled - OIDC_ENABLED: true - OIDC_SCOPE: read - SAML_ENABLED: true - CAS_ENABLED: true - BUNDLE_WITH: 'pam_authentication test' - GITHUB_RSPEC: ${{ matrix.ruby-version == '.ruby-version' && github.event.pull_request && 'true' }} - MASTODON_USE_LIBVIPS: false - - strategy: - fail-fast: false - matrix: - ruby-version: - - '3.2' - - '3.3' - - '.ruby-version' - steps: - - uses: actions/checkout@v5 - - - uses: actions/download-artifact@v6 - with: - path: './' - name: ${{ github.sha }} - - - name: Expand archived asset artifacts - run: | - tar xvzf artifacts.tar.gz - - - name: Set up Ruby environment - uses: ./.github/actions/setup-ruby - with: - ruby-version: ${{ matrix.ruby-version}} - additional-system-dependencies: ffmpeg imagemagick libpam-dev - - - name: Load database schema - run: './bin/rails db:create db:schema:load db:seed' - - - run: bin/rspec --tag attachment_processing - - - name: Upload coverage reports to Codecov - if: matrix.ruby-version == '.ruby-version' - uses: codecov/codecov-action@v5 - with: - files: coverage/lcov/mastodon.lcov - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - test-e2e: name: End to End testing runs-on: ubuntu-latest diff --git a/Dockerfile b/Dockerfile index b9dcbe59fd2637..c06bc84a3395dc 100644 --- a/Dockerfile +++ b/Dockerfile @@ -70,8 +70,6 @@ ENV \ PATH="${PATH}:/opt/ruby/bin:/opt/mastodon/bin" \ # Optimize jemalloc 5.x performance MALLOC_CONF="narenas:2,background_thread:true,thp:never,dirty_decay_ms:1000,muzzy_decay_ms:0" \ - # Enable libvips, should not be changed - MASTODON_USE_LIBVIPS=true \ # Sidekiq will touch tmp/sidekiq_process_has_started_and_will_begin_processing_jobs to indicate it is ready. This can be used for a readiness check in Kubernetes MASTODON_SIDEKIQ_READY_FILENAME=sidekiq_process_has_started_and_will_begin_processing_jobs diff --git a/Vagrantfile b/Vagrantfile index 0a34367024070a..a2c0b13b146031 100644 --- a/Vagrantfile +++ b/Vagrantfile @@ -29,7 +29,6 @@ sudo apt-get install \ libpq-dev \ libxml2-dev \ libxslt1-dev \ - imagemagick \ nodejs \ redis-server \ redis-tools \ diff --git a/app/lib/admin/metrics/dimension/software_versions_dimension.rb b/app/lib/admin/metrics/dimension/software_versions_dimension.rb index e64a6b1180af61..032abb752502c8 100644 --- a/app/lib/admin/metrics/dimension/software_versions_dimension.rb +++ b/app/lib/admin/metrics/dimension/software_versions_dimension.rb @@ -10,7 +10,7 @@ def key protected def perform_query - [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version, libvips_version, imagemagick_version, ffmpeg_version].compact + [mastodon_version, ruby_version, postgresql_version, redis_version, elasticsearch_version, libvips_version, ffmpeg_version].compact end def mastodon_version @@ -70,8 +70,6 @@ def elasticsearch_version end def libvips_version - return unless Rails.configuration.x.use_vips - { key: 'libvips', human_key: 'libvips', @@ -80,28 +78,6 @@ def libvips_version } end - def imagemagick_version - return if Rails.configuration.x.use_vips - - imagemagick_binary = Paperclip.options[:is_windows] ? 'magick convert' : 'convert' - - version_output = Terrapin::CommandLine.new(imagemagick_binary, '-version').run - version_match = version_output.match(/Version: ImageMagick (\S+)/)[1].strip - - return nil unless version_match - - version = version_match - - { - key: 'imagemagick', - human_key: 'ImageMagick', - value: version, - human_value: version, - } - rescue Terrapin::CommandNotFoundError, Terrapin::ExitStatusError, Paperclip::Errors::CommandNotFoundError, Paperclip::Errors::CommandFailedError - nil - end - def ffmpeg_version version_output = Terrapin::CommandLine.new(Rails.configuration.x.ffprobe_binary, '-show_program_version -v 0 -of json').run version = Oj.load(version_output, mode: :strict, symbol_keys: true).dig(:program_version, :version) diff --git a/app/models/concerns/account/avatar.rb b/app/models/concerns/account/avatar.rb index a60a289d5b0ada..0be46788818ec3 100644 --- a/app/models/concerns/account/avatar.rb +++ b/app/models/concerns/account/avatar.rb @@ -4,7 +4,7 @@ module Account::Avatar extend ActiveSupport::Concern AVATAR_IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze - AVATAR_LIMIT = Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes + AVATAR_LIMIT = 8.megabytes AVATAR_DIMENSIONS = [400, 400].freeze AVATAR_GEOMETRY = [AVATAR_DIMENSIONS.first, AVATAR_DIMENSIONS.last].join('x') diff --git a/app/models/concerns/account/header.rb b/app/models/concerns/account/header.rb index 662ee7caf78c61..066c42cb6cde38 100644 --- a/app/models/concerns/account/header.rb +++ b/app/models/concerns/account/header.rb @@ -4,7 +4,7 @@ module Account::Header extend ActiveSupport::Concern HEADER_IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze - HEADER_LIMIT = Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes + HEADER_LIMIT = 8.megabytes HEADER_DIMENSIONS = [1500, 500].freeze HEADER_GEOMETRY = [HEADER_DIMENSIONS.first, HEADER_DIMENSIONS.last].join('x') HEADER_MAX_PIXELS = HEADER_DIMENSIONS.first * HEADER_DIMENSIONS.last diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb index 8e0e13cdb94b77..644be2671a98be 100644 --- a/app/models/preview_card.rb +++ b/app/models/preview_card.rb @@ -39,7 +39,7 @@ class PreviewCard < ApplicationRecord include Attachmentable IMAGE_MIME_TYPES = ['image/jpeg', 'image/png', 'image/gif', 'image/webp'].freeze - LIMIT = Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes + LIMIT = 8.megabytes BLURHASH_OPTIONS = { x_comp: 4, @@ -63,7 +63,7 @@ class PreviewCard < ApplicationRecord belongs_to :author_account, class_name: 'Account', optional: true has_attached_file :image, - processors: [Rails.configuration.x.use_vips ? :lazy_thumbnail : :thumbnail, :blurhash_transcoder], + processors: [:lazy_thumbnail, :blurhash_transcoder], styles: ->(f) { image_styles(f) }, convert_options: { all: '-quality 90 +profile "!icc,*" +set date:modify +set date:create +set date:timestamp' }, validate_media_type: false diff --git a/config/application.rb b/config/application.rb index 90cfe47428fcf2..4e58bd9f6c213e 100644 --- a/config/application.rb +++ b/config/application.rb @@ -94,13 +94,7 @@ class Application < Rails::Application require 'mastodon/redis_configuration' ::REDIS_CONFIGURATION = Mastodon::RedisConfiguration.new - config.x.use_vips = ENV['MASTODON_USE_LIBVIPS'] != 'false' - - if config.x.use_vips - require_relative '../lib/paperclip/vips_lazy_thumbnail' - else - require_relative '../lib/paperclip/lazy_thumbnail' - end + require_relative '../lib/paperclip/vips_lazy_thumbnail' end config.x.cache_buster = config_for(:cache_buster) diff --git a/config/imagemagick/policy.xml b/config/imagemagick/policy.xml deleted file mode 100644 index e2aa202f274433..00000000000000 --- a/config/imagemagick/policy.xml +++ /dev/null @@ -1,27 +0,0 @@ - - - - - - - - - - - - - - - - diff --git a/config/initializers/deprecations.rb b/config/initializers/deprecations.rb index 520707e59ff415..2c1057e5058f48 100644 --- a/config/initializers/deprecations.rb +++ b/config/initializers/deprecations.rb @@ -17,12 +17,6 @@ abort message # rubocop:disable Rails/Exit end -if ENV['MASTODON_USE_LIBVIPS'] == 'false' - warn <<~MESSAGE - WARNING: Mastodon support for ImageMagick is deprecated and will be removed in future versions. Please consider using libvips instead. - MESSAGE -end - if ENV.key?('WHITELIST_MODE') warn(<<~MESSAGE.squish) WARNING: The environment variable WHITELIST_MODE has been replaced with diff --git a/config/initializers/paperclip.rb b/config/initializers/paperclip.rb index b444c5611b0c79..18d35a27f181dd 100644 --- a/config/initializers/paperclip.rb +++ b/config/initializers/paperclip.rb @@ -182,10 +182,3 @@ class NetworkingError < StandardError; end end end end - -# Set our ImageMagick security policy, but allow admins to override it -ENV['MAGICK_CONFIGURE_PATH'] = begin - imagemagick_config_paths = ENV.fetch('MAGICK_CONFIGURE_PATH', '').split(File::PATH_SEPARATOR) - imagemagick_config_paths << Rails.root.join('config', 'imagemagick').expand_path.to_s - imagemagick_config_paths.join(File::PATH_SEPARATOR) -end diff --git a/config/initializers/vips.rb b/config/initializers/vips.rb index 09210d60ebe4f5..cd8d2410b92a37 100644 --- a/config/initializers/vips.rb +++ b/config/initializers/vips.rb @@ -1,35 +1,33 @@ # frozen_string_literal: true -if Rails.configuration.x.use_vips - ENV['VIPS_BLOCK_UNTRUSTED'] = 'true' +ENV['VIPS_BLOCK_UNTRUSTED'] = 'true' - require 'vips' +require 'vips' - unless Vips.at_least_libvips?(8, 13) - abort <<~ERROR.squish # rubocop:disable Rails/Exit - Incompatible libvips version (#{Vips.version_string}), please install libvips >= 8.13 - ERROR - end +unless Vips.at_least_libvips?(8, 13) + abort <<~ERROR.squish # rubocop:disable Rails/Exit + Incompatible libvips version (#{Vips.version_string}), please install libvips >= 8.13 + ERROR +end - Vips.block('VipsForeign', true) +Vips.block('VipsForeign', true) - %w( - VipsForeignLoadNsgif - VipsForeignLoadJpeg - VipsForeignLoadPng - VipsForeignLoadWebp - VipsForeignLoadHeif - VipsForeignSavePng - VipsForeignSaveSpng - VipsForeignSaveJpeg - VipsForeignSaveWebp - ).each do |operation| - Vips.block(operation, false) - end - - Vips.block_untrusted(true) +%w( + VipsForeignLoadNsgif + VipsForeignLoadJpeg + VipsForeignLoadPng + VipsForeignLoadWebp + VipsForeignLoadHeif + VipsForeignSavePng + VipsForeignSaveSpng + VipsForeignSaveJpeg + VipsForeignSaveWebp +).each do |operation| + Vips.block(operation, false) end +Vips.block_untrusted(true) + # In some places of the code, we rescue this exception, but we don't always # load libvips, so it may be an undefined constant: unless defined?(Vips) diff --git a/lib/paperclip/blurhash_transcoder.rb b/lib/paperclip/blurhash_transcoder.rb index b4ff4a12a0e0f6..e9ff1dd9dd8067 100644 --- a/lib/paperclip/blurhash_transcoder.rb +++ b/lib/paperclip/blurhash_transcoder.rb @@ -19,14 +19,8 @@ def make private def blurhash_params - if Rails.configuration.x.use_vips - image = Vips::Image.thumbnail(@file.path, 100) - [image.width, image.height, image.colourspace(:srgb).extract_band(0, n: 3).to_a.flatten] - else - pixels = convert(':source -depth 8 RGB:-', source: File.expand_path(@file.path)).unpack('C*') - geometry = options.fetch(:file_geometry_parser).from_file(@file) - [geometry.width, geometry.height, pixels] - end + image = Vips::Image.thumbnail(@file.path, 100) + [image.width, image.height, image.colourspace(:srgb).extract_band(0, n: 3).to_a.flatten] end end end diff --git a/lib/paperclip/color_extractor.rb b/lib/paperclip/color_extractor.rb index 62daa077952cde..1c9ef4bd3d6b3f 100644 --- a/lib/paperclip/color_extractor.rb +++ b/lib/paperclip/color_extractor.rb @@ -10,7 +10,7 @@ class ColorExtractor < Paperclip::Processor BINS = 10 def make - background_palette, foreground_palette = Rails.configuration.x.use_vips ? palettes_from_libvips : palettes_from_imagemagick + background_palette, foreground_palette = palettes_from_libvips background_color = background_palette.first || foreground_palette.first foreground_colors = [] @@ -93,17 +93,6 @@ def palettes_from_libvips [background_palette, foreground_palette] end - def palettes_from_imagemagick - depth = 8 - - # Determine background palette by getting colors close to the image's edge only - background_palette = palette_from_im_histogram(convert(':source -alpha set -gravity Center -region 75%x75% -fill None -colorize 100% -alpha transparent +region -format %c -colors :quantity -depth :depth histogram:info:', source: File.expand_path(@file.path), quantity: 10, depth: depth), 10) - - # Determine foreground palette from the whole image - foreground_palette = palette_from_im_histogram(convert(':source -format %c -colors :quantity -depth :depth histogram:info:', source: File.expand_path(@file.path), quantity: 10, depth: depth), 10) - [background_palette, foreground_palette] - end - def downscaled_image image = Vips::Image.new_from_file(@file.path, access: :random).thumbnail_image(100) diff --git a/spec/models/media_attachment_spec.rb b/spec/models/media_attachment_spec.rb index a712cdde1dcb11..cbabb1d0dd3bc9 100644 --- a/spec/models/media_attachment_spec.rb +++ b/spec/models/media_attachment_spec.rb @@ -219,9 +219,7 @@ describe 'ogg with cover art' do let(:media) { Fabricate(:media_attachment, file: attachment_fixture('boop.ogg')) } let(:expected_media_duration) { 0.235102 } - - # The libvips and ImageMagick implementations produce different results - let(:expected_background_color) { Rails.configuration.x.use_vips ? '#268cd9' : '#3088d4' } + let(:expected_background_color) { '#268cd9' } it 'sets correct file metadata' do expect(media) diff --git a/spec/models/preview_card_spec.rb b/spec/models/preview_card_spec.rb index 0fe76c37b07cda..bac29046eac964 100644 --- a/spec/models/preview_card_spec.rb +++ b/spec/models/preview_card_spec.rb @@ -3,12 +3,6 @@ require 'rails_helper' RSpec.describe PreviewCard do - describe 'file size limit', :attachment_processing do - it 'is set differently whether vips is enabled or not' do - expect(described_class::LIMIT).to eq(Rails.configuration.x.use_vips ? 8.megabytes : 2.megabytes) - end - end - describe 'Validations' do describe 'url' do it { is_expected.to allow_values('http://example.host/path', 'https://example.host/path').for(:url) } diff --git a/spec/requests/api/v1/media_spec.rb b/spec/requests/api/v1/media_spec.rb index 347ff4b27979df..89e34998db7c16 100644 --- a/spec/requests/api/v1/media_spec.rb +++ b/spec/requests/api/v1/media_spec.rb @@ -102,7 +102,7 @@ allow(user.account).to receive(:media_attachments).and_return(media_attachments) end - context 'when imagemagick cannot identify the file type' do + context 'when file type cannot be identified' do it 'returns http unprocessable entity' do allow(media_attachments).to receive(:create!).and_raise(Paperclip::Errors::NotIdentifiedByImageMagickError) diff --git a/spec/requests/api/v2/media_spec.rb b/spec/requests/api/v2/media_spec.rb index 04e48bc02c3942..1f149b9634d5e7 100644 --- a/spec/requests/api/v2/media_spec.rb +++ b/spec/requests/api/v2/media_spec.rb @@ -71,7 +71,7 @@ allow(user.account).to receive(:media_attachments).and_return(media_attachments) end - context 'when imagemagick cannot identify the file type' do + context 'when file type cannot be identified' do before do allow(media_attachments).to receive(:create!).and_raise(Paperclip::Errors::NotIdentifiedByImageMagickError) end From 3806d15d99c468394f56494fcf1d3a152e23f780 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 16:00:53 +0100 Subject: [PATCH 019/230] Update dependency pg to v8.17.2 (#37557) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/yarn.lock b/yarn.lock index 15f3d5048dc5ab..eaf9fb1eda3da2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10511,7 +10511,7 @@ __metadata: languageName: node linkType: hard -"pg-connection-string@npm:^2.10.0, pg-connection-string@npm:^2.6.0": +"pg-connection-string@npm:^2.10.1, pg-connection-string@npm:^2.6.0": version: 2.10.1 resolution: "pg-connection-string@npm:2.10.1" checksum: 10c0/f218a72b59c661022caca9a7f2116655632b1d7e7d6dc9a8ee9f238744e0927e0d6f44e12f50d9767c6d9cd47d9b3766aa054b77504b15c6bf503400530e053e @@ -10555,11 +10555,11 @@ __metadata: linkType: hard "pg@npm:^8.5.0": - version: 8.17.1 - resolution: "pg@npm:8.17.1" + version: 8.17.2 + resolution: "pg@npm:8.17.2" dependencies: pg-cloudflare: "npm:^1.3.0" - pg-connection-string: "npm:^2.10.0" + pg-connection-string: "npm:^2.10.1" pg-pool: "npm:^3.11.0" pg-protocol: "npm:^1.11.0" pg-types: "npm:2.2.0" @@ -10572,7 +10572,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 10c0/39a92391adfc73f793d195b4062bc2d21aa3537073e3973f3979d72901d92a59e129f31f42577ff916038a6c3f9fe423b6024717529609ae8548fda21248cfe7 + checksum: 10c0/74b022587f92953f498dba747ccf9c7c90767af70326595d30c7ab0e2f00b2b468226c8abae54caef63ab444a8ac6f1597d859174386c7ba7c318c225d711c5f languageName: node linkType: hard From 1809048105a7e04095f6a0d1d466adeb0fdcffa5 Mon Sep 17 00:00:00 2001 From: Shlee Date: Fri, 23 Jan 2026 02:31:44 +1030 Subject: [PATCH 020/230] Safefy: Updated Admin::AccountDeletionWorker to match AccountDeletionWorker (#37577) --- app/workers/admin/account_deletion_worker.rb | 5 ++++- spec/workers/admin/account_deletion_worker_spec.rb | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/app/workers/admin/account_deletion_worker.rb b/app/workers/admin/account_deletion_worker.rb index 5dfdfb6e73c14d..cd0d9c1f5e530d 100644 --- a/app/workers/admin/account_deletion_worker.rb +++ b/app/workers/admin/account_deletion_worker.rb @@ -6,7 +6,10 @@ class Admin::AccountDeletionWorker sidekiq_options queue: 'pull', lock: :until_executed, lock_ttl: 1.week.to_i def perform(account_id) - DeleteAccountService.new.call(Account.find(account_id), reserve_username: true, reserve_email: true) + delete_account = Account.find(account_id) + return unless delete_account.unavailable? + + DeleteAccountService.new.call(delete_account, reserve_username: true, reserve_email: true) rescue ActiveRecord::RecordNotFound true end diff --git a/spec/workers/admin/account_deletion_worker_spec.rb b/spec/workers/admin/account_deletion_worker_spec.rb index e41b734f214711..7fc56e4289173b 100644 --- a/spec/workers/admin/account_deletion_worker_spec.rb +++ b/spec/workers/admin/account_deletion_worker_spec.rb @@ -6,7 +6,7 @@ let(:worker) { described_class.new } describe 'perform' do - let(:account) { Fabricate(:account) } + let(:account) { Fabricate(:account, suspended: true) } let(:service) { instance_double(DeleteAccountService, call: true) } it 'calls delete account service' do From 0924171c0f108f59c858c73362367f0f13cccc08 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Thu, 22 Jan 2026 17:08:57 +0100 Subject: [PATCH 021/230] Add form field components: `TextInputField`, `TextAreaField`, `SelectField` (#37578) --- .../mastodon/components/form_fields/index.ts | 3 + .../form_fields/select_field.stories.tsx | 55 ++++++++++ .../components/form_fields/select_field.tsx | 38 +++++++ .../form_fields/text_area_field.stories.tsx | 45 ++++++++ .../form_fields/text_area_field.tsx | 31 ++++++ .../form_fields/text_input_field.stories.tsx | 45 ++++++++ .../form_fields/text_input_field.tsx | 36 +++++++ .../components/form_fields/wrapper.tsx | 100 ++++++++++++++++++ .../mastodon/features/lists/new.tsx | 100 +++++++----------- .../mastodon/features/onboarding/profile.tsx | 60 ++++------- app/javascript/mastodon/locales/en.json | 1 + 11 files changed, 418 insertions(+), 96 deletions(-) create mode 100644 app/javascript/mastodon/components/form_fields/index.ts create mode 100644 app/javascript/mastodon/components/form_fields/select_field.stories.tsx create mode 100644 app/javascript/mastodon/components/form_fields/select_field.tsx create mode 100644 app/javascript/mastodon/components/form_fields/text_area_field.stories.tsx create mode 100644 app/javascript/mastodon/components/form_fields/text_area_field.tsx create mode 100644 app/javascript/mastodon/components/form_fields/text_input_field.stories.tsx create mode 100644 app/javascript/mastodon/components/form_fields/text_input_field.tsx create mode 100644 app/javascript/mastodon/components/form_fields/wrapper.tsx diff --git a/app/javascript/mastodon/components/form_fields/index.ts b/app/javascript/mastodon/components/form_fields/index.ts new file mode 100644 index 00000000000000..2aa87645144e93 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/index.ts @@ -0,0 +1,3 @@ +export { TextInputField } from './text_input_field'; +export { TextAreaField } from './text_area_field'; +export { SelectField } from './select_field'; diff --git a/app/javascript/mastodon/components/form_fields/select_field.stories.tsx b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx new file mode 100644 index 00000000000000..30897adda1feca --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/select_field.stories.tsx @@ -0,0 +1,55 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { SelectField } from './select_field'; + +const meta = { + title: 'Components/Form Fields/SelectField', + component: SelectField, + args: { + label: 'Fruit preference', + hint: 'Select your favourite fruit or not. Up to you.', + }, + render(args) { + // Component styles require a wrapper class at the moment + return ( +
+ + + + + + + + + + + +
+ ); + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Simple: Story = {}; + +export const Required: Story = { + args: { + required: true, + }, +}; + +export const Optional: Story = { + args: { + required: false, + }, +}; + +export const WithError: Story = { + args: { + required: false, + hasError: true, + }, +}; diff --git a/app/javascript/mastodon/components/form_fields/select_field.tsx b/app/javascript/mastodon/components/form_fields/select_field.tsx new file mode 100644 index 00000000000000..aa058fc782e848 --- /dev/null +++ b/app/javascript/mastodon/components/form_fields/select_field.tsx @@ -0,0 +1,38 @@ +import type { ComponentPropsWithoutRef } from 'react'; +import { forwardRef } from 'react'; + +import { FormFieldWrapper } from './wrapper'; +import type { CommonFieldWrapperProps } from './wrapper'; + +interface Props + extends ComponentPropsWithoutRef<'select'>, CommonFieldWrapperProps {} + +/** + * A simple form field for single-item selections. + * Provide selectable items via nested `