From 22ec36857415db40c3c0af843bda5217f893494a Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 20 Jan 2026 16:15:49 +0100 Subject: [PATCH 01/12] Profile redesign: Badges (#37550) --- app/javascript/images/icons/icon_admin.svg | 10 ++++ app/javascript/mastodon/components/badge.jsx | 31 ------------- app/javascript/mastodon/components/badge.tsx | 46 +++++++++++++++++++ .../account_timeline/components/badges.tsx | 38 +++++++++++---- .../components/redesign.module.scss | 21 +++++++++ 5 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 app/javascript/images/icons/icon_admin.svg delete mode 100644 app/javascript/mastodon/components/badge.jsx create mode 100644 app/javascript/mastodon/components/badge.tsx diff --git a/app/javascript/images/icons/icon_admin.svg b/app/javascript/images/icons/icon_admin.svg new file mode 100644 index 0000000000..7e40dc4643 --- /dev/null +++ b/app/javascript/images/icons/icon_admin.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/javascript/mastodon/components/badge.jsx b/app/javascript/mastodon/components/badge.jsx deleted file mode 100644 index 2a335d7f50..0000000000 --- a/app/javascript/mastodon/components/badge.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import GroupsIcon from '@/material-icons/400-24px/group.svg?react'; -import PersonIcon from '@/material-icons/400-24px/person.svg?react'; -import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react'; - - -export const Badge = ({ icon = , label, domain, roleId }) => ( -
- {icon} - {label} - {domain && {domain}} -
-); - -Badge.propTypes = { - icon: PropTypes.node, - label: PropTypes.node, - domain: PropTypes.node, - roleId: PropTypes.string -}; - -export const GroupBadge = () => ( - } label={} /> -); - -export const AutomatedBadge = () => ( - } label={} /> -); diff --git a/app/javascript/mastodon/components/badge.tsx b/app/javascript/mastodon/components/badge.tsx new file mode 100644 index 0000000000..b7dc169edb --- /dev/null +++ b/app/javascript/mastodon/components/badge.tsx @@ -0,0 +1,46 @@ +import type { FC, ReactNode } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import GroupsIcon from '@/material-icons/400-24px/group.svg?react'; +import PersonIcon from '@/material-icons/400-24px/person.svg?react'; +import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react'; + +export const Badge: FC<{ + label: ReactNode; + icon?: ReactNode; + className?: string; + domain?: ReactNode; + roleId?: string; +}> = ({ icon = , label, className, domain, roleId }) => ( +
+ {icon} + {label} + {domain && {domain}} +
+); + +export const GroupBadge: FC<{ className?: string }> = ({ className }) => ( + } + label={ + + } + className={className} + /> +); + +export const AutomatedBadge: FC<{ className?: string }> = ({ className }) => ( + } + label={ + + } + className={className} + /> +); diff --git a/app/javascript/mastodon/features/account_timeline/components/badges.tsx b/app/javascript/mastodon/features/account_timeline/components/badges.tsx index cc6c456e9c..1c5942d90d 100644 --- a/app/javascript/mastodon/features/account_timeline/components/badges.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/badges.tsx @@ -1,9 +1,16 @@ -import type { FC } from 'react'; +import type { FC, ReactNode } from 'react'; +import IconAdmin from '@/images/icons/icon_admin.svg?react'; import { AutomatedBadge, Badge, GroupBadge } from '@/mastodon/components/badge'; +import { Icon } from '@/mastodon/components/icon'; import { useAccount } from '@/mastodon/hooks/useAccount'; +import type { AccountRole } from '@/mastodon/models/account'; import { useAppSelector } from '@/mastodon/store'; +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { const account = useAccount(accountId); const localDomain = useAppSelector( @@ -15,22 +22,32 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { return null; } + const className = isRedesignEnabled() ? classes.badge : ''; + if (account.bot) { - badges.push(); + badges.push(); } else if (account.group) { - badges.push(); + badges.push(); } const domain = account.acct.includes('@') ? account.acct.split('@')[1] : localDomain; account.roles.forEach((role) => { + let icon: ReactNode = undefined; + if (isAdminBadge(role)) { + icon = ( + + ); + } badges.push( {role.get('name')}} - domain={domain} - roleId={role.get('id')} + key={role.id} + label={role.name} + className={className} + domain={isRedesignEnabled() ? `(${domain})` : domain} + roleId={role.id} + icon={icon} />, ); }); @@ -39,5 +56,10 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { return null; } - return
{badges}
; + return
{badges}
; }; + +function isAdminBadge(role: AccountRole) { + const name = role.name.toLowerCase(); + return isRedesignEnabled() && (name === 'admin' || name === 'owner'); +} 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 757f5a4231..5ccdb1f310 100644 --- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -39,6 +39,27 @@ h1.name > small { } } +.badge { + background-color: var(--color-bg-secondary); + border: none; + color: var(--color-text-secondary); + font-weight: 600; + + > span { + font-weight: unset; + opacity: 1; + } +} + +svg.badgeIcon { + opacity: 1; + fill: revert-layer; + + path { + fill: revert-layer; + } +} + .fieldList { margin-top: 16px; } From e65103bd3de034b89196f7f0d86a6e9eb157971e Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 21 Jan 2026 10:18:16 +0100 Subject: [PATCH 02/12] 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 fe2a857d50..a8e4b1d7f7 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -140,7 +140,7 @@ class InitialStateSerializer < ActiveModel::Serializer 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 03/12] 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 43c7bb1fe7..a7d2be35ed 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -379,6 +379,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity 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 e6714c51ab..f9cb90f548 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -243,12 +243,6 @@ class ActivityPub::TagManager !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 @@ class ActivityPub::TagManager 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 @@ class ActivityPub::TagManager 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 cb0c9f8966..7d0f23c4dc 100644 --- a/app/lib/ostatus/tag_manager.rb +++ b/app/lib/ostatus/tag_manager.rb @@ -11,16 +11,12 @@ class OStatus::TagManager 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 1e8a2a29db..19b6014af1 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -471,7 +471,7 @@ RSpec.describe ActivityPub::Activity::Create do 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 @@ RSpec.describe ActivityPub::Activity::Create do 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 55e54ede5e..a15529057c 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -629,14 +629,6 @@ RSpec.describe ActivityPub::TagManager do 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 04/12] 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 9d6b2f9a38..4b07b5012a 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1_alpha/collections_controller.rb @@ -74,6 +74,7 @@ class Api::V1Alpha::CollectionsController < Api::BaseController .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 334318b73d..3681c41d84 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 b529fc2d92..de79dcf723 100644 --- a/spec/requests/api/v1_alpha/collections_spec.rb +++ b/spec/requests/api/v1_alpha/collections_spec.rb @@ -55,6 +55,32 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do ) 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 05/12] 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 f37750a4cc..d5efe483cb 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 06/12] 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 | 89 ++++++++++++++++++++++++---------------------------- 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 701719d7cf..987467458d 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 d5efe483cb..e36a0cca26 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": +"stylelint-config-recommended@npm:^18.0.0": + version: 18.0.0 + resolution: "stylelint-config-recommended@npm:18.0.0" + peerDependencies: + stylelint: ^17.0.0 + checksum: 10c0/c7f8ff45c76ec23f4c8c0438894726976fd5e872c59d489f959b728d9879bba20dbf0040cd29ad3bbc00eb32befd95f5b6ca150002bb8aea74b0797bc42ccc17 + languageName: node + linkType: hard + +"stylelint-config-standard-scss@npm:^17.0.0": version: 17.0.0 - resolution: "stylelint-config-recommended@npm:17.0.0" - peerDependencies: - stylelint: ^16.23.0 - checksum: 10c0/49e5d1c0f58197b2c5585b85fad814fed9bdec44c9870368c46a762664c5ff158c1145b6337456ae194409d692992b5b87421d62880422f71d8a3360417f5ad1 - 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" + 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 07/12] 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 0000000000..65873b9dc4 --- /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 f775e70aac..318c584953 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 a73d92c1b6..ab29a8299e 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 715f6097f4..103fffca50 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 5ccdb1f310..4bc64d05a9 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 e6fd6d3cc1..9485464e09 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 f0dc1bdfbc..534a18367c 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 08/12] 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 2b7d190535..8a3672ad4c 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 0cbbb08f83..6adcf1871c 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 98defb79cd..a39cf44359 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 09/12] 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 987467458d..acdd57e3fd 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 e36a0cca26..9c14c9e85e 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 10/12] 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 abbd193c68..10d45acfe6 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 11/12] 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 75d3acfea5..1900330475 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -71,6 +71,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer 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 12/12] 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 c1bd2973ed..5a6284cc5b 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -18,7 +18,7 @@ class TagManager 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 38203a55f7..927214bb40 100644 --- a/spec/lib/tag_manager_spec.rb +++ b/spec/lib/tag_manager_spec.rb @@ -54,12 +54,44 @@ RSpec.describe TagManager do 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 - it 'returns normalized domain' do - expect(described_class.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com' + 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 + + 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 4d77d49712..011b001cc7 100644 --- a/spec/models/instance_moderation_note_spec.rb +++ b/spec/models/instance_moderation_note_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' 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)