From dff7d55a6d2363d28398a7c909d5e4b3bbb9fe4b Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 11 Mar 2026 08:42:36 +0100 Subject: [PATCH 1/5] Prevent hover card from showing unintentionally (#38112) --- .../components/hover_card_controller.tsx | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx index 43ca37f50f..2e5924d4a0 100644 --- a/app/javascript/mastodon/components/hover_card_controller.tsx +++ b/app/javascript/mastodon/components/hover_card_controller.tsx @@ -14,6 +14,10 @@ import { useTimeout } from 'mastodon/hooks/useTimeout'; const offset = [-12, 4] as OffsetValue; const enterDelay = 750; const leaveDelay = 150; +// Only open the card if the mouse was moved within this time, +// to avoid triggering the card without intentional mouse movement +// (e.g. when content changed underneath the mouse cursor) +const activeMovementThreshold = 150; const popperConfig = { strategy: 'fixed' } as UsePopperOptions; const isHoverCardAnchor = (element: HTMLElement) => @@ -23,10 +27,10 @@ export const HoverCardController: React.FC = () => { const [open, setOpen] = useState(false); const [accountId, setAccountId] = useState(); const [anchor, setAnchor] = useState(null); - const isUsingTouchRef = useRef(false); const cardRef = useRef(null); const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout(); const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout(); + const [setMoveTimeout, cancelMoveTimeout] = useTimeout(); const [setScrollTimeout] = useTimeout(); const location = useLocation(); @@ -43,6 +47,8 @@ export const HoverCardController: React.FC = () => { useEffect(() => { let isScrolling = false; + let isUsingTouch = false; + let isActiveMouseMovement = false; let currentAnchor: HTMLElement | null = null; let currentTitle: string | null = null; @@ -64,7 +70,7 @@ export const HoverCardController: React.FC = () => { const handleTouchStart = () => { // Keeping track of touch events to prevent the // hover card from being displayed on touch devices - isUsingTouchRef.current = true; + isUsingTouch = true; }; const handleMouseEnter = (e: MouseEvent) => { @@ -76,13 +82,14 @@ export const HoverCardController: React.FC = () => { return; } - // Bail out if a touch is active - if (isUsingTouchRef.current) { + // Bail out if we're scrolling, a touch is active, + // or if there was no active mouse movement + if (isScrolling || !isActiveMouseMovement || isUsingTouch) { return; } // We've entered an anchor - if (!isScrolling && isHoverCardAnchor(target)) { + if (isHoverCardAnchor(target)) { cancelLeaveTimeout(); currentAnchor?.removeAttribute('aria-describedby'); @@ -97,10 +104,7 @@ export const HoverCardController: React.FC = () => { } // We've entered the hover card - if ( - !isScrolling && - (target === currentAnchor || target === cardRef.current) - ) { + if (target === currentAnchor || target === cardRef.current) { cancelLeaveTimeout(); } }; @@ -139,10 +143,17 @@ export const HoverCardController: React.FC = () => { }; const handleMouseMove = () => { - if (isUsingTouchRef.current) { - isUsingTouchRef.current = false; + if (isUsingTouch) { + isUsingTouch = false; } + delayEnterTimeout(enterDelay); + + cancelMoveTimeout(); + isActiveMouseMovement = true; + setMoveTimeout(() => { + isActiveMouseMovement = false; + }, activeMovementThreshold); }; document.body.addEventListener('touchstart', handleTouchStart, { @@ -186,6 +197,8 @@ export const HoverCardController: React.FC = () => { setOpen, setAccountId, setAnchor, + setMoveTimeout, + cancelMoveTimeout, ]); return ( From ac91d30a5ad05b81527c50ffb8c23f983c14ff3c Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 11 Mar 2026 10:49:52 +0100 Subject: [PATCH 2/5] Change HTTP signatures to skip the `Accept` header (#38132) --- app/lib/request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/request.rb b/app/lib/request.rb index 59c0e72526..0e71cec764 100644 --- a/app/lib/request.rb +++ b/app/lib/request.rb @@ -179,7 +179,7 @@ class Request return end - signature_value = @signing.sign(signed_headers.without('User-Agent', 'Accept-Encoding'), @verb, Addressable::URI.parse(request.uri)) + signature_value = @signing.sign(signed_headers.without('User-Agent', 'Accept-Encoding', 'Accept'), @verb, Addressable::URI.parse(request.uri)) request.headers['Signature'] = signature_value end From 9171fa49b6444b8523d32c582de35ebeb9103b4b Mon Sep 17 00:00:00 2001 From: Hugo Gameiro Date: Thu, 12 Mar 2026 10:15:49 +0000 Subject: [PATCH 3/5] Fix OpenStack Swift Keystone token rate limiting (#38145) --- config/initializers/fog_connection_cache.rb | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 config/initializers/fog_connection_cache.rb diff --git a/config/initializers/fog_connection_cache.rb b/config/initializers/fog_connection_cache.rb new file mode 100644 index 0000000000..c9cf310d83 --- /dev/null +++ b/config/initializers/fog_connection_cache.rb @@ -0,0 +1,17 @@ +# frozen_string_literal: true + +if ENV['SWIFT_ENABLED'] == 'true' + module PaperclipFogConnectionCache + def connection + @connection ||= begin + key = fog_credentials.hash + Thread.current[:paperclip_fog_connections] ||= {} + Thread.current[:paperclip_fog_connections][key] ||= ::Fog::Storage.new(fog_credentials) + end + end + end + + Rails.application.config.after_initialize do + Paperclip::Storage::Fog.prepend(PaperclipFogConnectionCache) + end +end From f37dc6c59e88aa3a119ecbe76ee9fba480d13daa Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 13 Mar 2026 05:33:02 -0400 Subject: [PATCH 4/5] Normalize `current_username` on account migration (#38183) --- app/models/account_migration.rb | 5 ++- spec/models/account_migration_spec.rb | 4 +++ spec/system/settings/migrations_spec.rb | 42 ++++++++++++++++++++----- 3 files changed, 42 insertions(+), 9 deletions(-) diff --git a/app/models/account_migration.rb b/app/models/account_migration.rb index 6cdc7128ef..39aaf8e794 100644 --- a/app/models/account_migration.rb +++ b/app/models/account_migration.rb @@ -25,7 +25,10 @@ class AccountMigration < ApplicationRecord before_validation :set_target_account before_validation :set_followers_count + attribute :current_username, :string + normalizes :acct, with: ->(acct) { acct.strip.delete_prefix('@') } + normalizes :current_username, with: ->(value) { value.strip.delete_prefix('@') } validates :acct, presence: true, domain: { acct: true } validate :validate_migration_cooldown @@ -33,7 +36,7 @@ class AccountMigration < ApplicationRecord scope :within_cooldown, -> { where(created_at: cooldown_duration_ago..) } - attr_accessor :current_password, :current_username + attr_accessor :current_password def self.cooldown_duration_ago Time.current - COOLDOWN_PERIOD diff --git a/spec/models/account_migration_spec.rb b/spec/models/account_migration_spec.rb index b92771e8f5..1bb238f7ef 100644 --- a/spec/models/account_migration_spec.rb +++ b/spec/models/account_migration_spec.rb @@ -7,6 +7,10 @@ RSpec.describe AccountMigration do describe 'acct' do it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') } end + + describe 'current_username' do + it { is_expected.to normalize(:current_username).from(' @username ').to('username') } + end end describe 'Validations' do diff --git a/spec/system/settings/migrations_spec.rb b/spec/system/settings/migrations_spec.rb index fecde36f35..d95636a609 100644 --- a/spec/system/settings/migrations_spec.rb +++ b/spec/system/settings/migrations_spec.rb @@ -33,20 +33,36 @@ RSpec.describe 'Settings Migrations' do end describe 'Creating migrations' do - let(:user) { Fabricate(:user, password: '12345678') } + let(:user) { Fabricate(:user, password:) } + let(:password) { '12345678' } before { sign_in(user) } context 'when migration account is changed' do let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) } - it 'updates moved to account' do - visit settings_migration_path + context 'when user has encrypted password' do + it 'updates moved to account' do + visit settings_migration_path - expect { fill_in_and_submit } - .to(change { user.account.reload.moved_to_account_id }.to(acct.id)) - expect(page) - .to have_content(I18n.t('settings.migrate')) + expect { fill_in_and_submit } + .to(change { user.account.reload.moved_to_account_id }.to(acct.id)) + expect(page) + .to have_content(I18n.t('settings.migrate')) + end + end + + context 'when user has blank encrypted password value' do + before { user.update! encrypted_password: '' } + + it 'updates moved to account using at-username value' do + visit settings_migration_path + + expect { fill_in_and_submit_via_username("@#{user.account.username}") } + .to(change { user.account.reload.moved_to_account_id }.to(acct.id)) + expect(page) + .to have_content(I18n.t('settings.migrate')) + end end end @@ -92,8 +108,18 @@ RSpec.describe 'Settings Migrations' do def fill_in_and_submit fill_in 'account_migration_acct', with: acct.username - fill_in 'account_migration_current_password', with: '12345678' + if block_given? + yield + else + fill_in 'account_migration_current_password', with: password + end click_on I18n.t('migrations.proceed_with_move') end + + def fill_in_and_submit_via_username(username) + fill_in_and_submit do + fill_in 'account_migration_current_username', with: username + end + end end end From a97811b056ebe30862ce9c77c2d384d4b64bca45 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 11 Mar 2026 08:42:36 +0100 Subject: [PATCH 5/5] [Glitch] Prevent hover card from showing unintentionally Port 316290ba9d25358f88a9616ba9cbc30b8ccef453 to glitch-soc Signed-off-by: Claire --- .../components/hover_card_controller.tsx | 35 +++++++++++++------ 1 file changed, 24 insertions(+), 11 deletions(-) diff --git a/app/javascript/flavours/glitch/components/hover_card_controller.tsx b/app/javascript/flavours/glitch/components/hover_card_controller.tsx index d232b6760c..415455bdeb 100644 --- a/app/javascript/flavours/glitch/components/hover_card_controller.tsx +++ b/app/javascript/flavours/glitch/components/hover_card_controller.tsx @@ -14,6 +14,10 @@ import { useTimeout } from 'flavours/glitch/hooks/useTimeout'; const offset = [-12, 4] as OffsetValue; const enterDelay = 750; const leaveDelay = 150; +// Only open the card if the mouse was moved within this time, +// to avoid triggering the card without intentional mouse movement +// (e.g. when content changed underneath the mouse cursor) +const activeMovementThreshold = 150; const popperConfig = { strategy: 'fixed' } as UsePopperOptions; const isHoverCardAnchor = (element: HTMLElement) => @@ -23,10 +27,10 @@ export const HoverCardController: React.FC = () => { const [open, setOpen] = useState(false); const [accountId, setAccountId] = useState(); const [anchor, setAnchor] = useState(null); - const isUsingTouchRef = useRef(false); const cardRef = useRef(null); const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout(); const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout(); + const [setMoveTimeout, cancelMoveTimeout] = useTimeout(); const [setScrollTimeout] = useTimeout(); const location = useLocation(); @@ -43,6 +47,8 @@ export const HoverCardController: React.FC = () => { useEffect(() => { let isScrolling = false; + let isUsingTouch = false; + let isActiveMouseMovement = false; let currentAnchor: HTMLElement | null = null; let currentTitle: string | null = null; @@ -64,7 +70,7 @@ export const HoverCardController: React.FC = () => { const handleTouchStart = () => { // Keeping track of touch events to prevent the // hover card from being displayed on touch devices - isUsingTouchRef.current = true; + isUsingTouch = true; }; const handleMouseEnter = (e: MouseEvent) => { @@ -76,13 +82,14 @@ export const HoverCardController: React.FC = () => { return; } - // Bail out if a touch is active - if (isUsingTouchRef.current) { + // Bail out if we're scrolling, a touch is active, + // or if there was no active mouse movement + if (isScrolling || !isActiveMouseMovement || isUsingTouch) { return; } // We've entered an anchor - if (!isScrolling && isHoverCardAnchor(target)) { + if (isHoverCardAnchor(target)) { cancelLeaveTimeout(); currentAnchor?.removeAttribute('aria-describedby'); @@ -97,10 +104,7 @@ export const HoverCardController: React.FC = () => { } // We've entered the hover card - if ( - !isScrolling && - (target === currentAnchor || target === cardRef.current) - ) { + if (target === currentAnchor || target === cardRef.current) { cancelLeaveTimeout(); } }; @@ -139,10 +143,17 @@ export const HoverCardController: React.FC = () => { }; const handleMouseMove = () => { - if (isUsingTouchRef.current) { - isUsingTouchRef.current = false; + if (isUsingTouch) { + isUsingTouch = false; } + delayEnterTimeout(enterDelay); + + cancelMoveTimeout(); + isActiveMouseMovement = true; + setMoveTimeout(() => { + isActiveMouseMovement = false; + }, activeMovementThreshold); }; document.body.addEventListener('touchstart', handleTouchStart, { @@ -186,6 +197,8 @@ export const HoverCardController: React.FC = () => { setOpen, setAccountId, setAnchor, + setMoveTimeout, + cancelMoveTimeout, ]); return (