Merge commit '9f14bac22aa24a4b9971668a49d6920821b75fe2' into glitch-soc/merge-4.4

This commit is contained in:
Claire
2026-03-14 12:06:36 +01:00
6 changed files with 84 additions and 21 deletions

View File

@@ -14,6 +14,10 @@ import { useTimeout } from 'mastodon/hooks/useTimeout';
const offset = [-12, 4] as OffsetValue; const offset = [-12, 4] as OffsetValue;
const enterDelay = 750; const enterDelay = 750;
const leaveDelay = 150; 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 popperConfig = { strategy: 'fixed' } as UsePopperOptions;
const isHoverCardAnchor = (element: HTMLElement) => const isHoverCardAnchor = (element: HTMLElement) =>
@@ -23,10 +27,10 @@ export const HoverCardController: React.FC = () => {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [accountId, setAccountId] = useState<string | undefined>(); const [accountId, setAccountId] = useState<string | undefined>();
const [anchor, setAnchor] = useState<HTMLElement | null>(null); const [anchor, setAnchor] = useState<HTMLElement | null>(null);
const isUsingTouchRef = useRef(false);
const cardRef = useRef<HTMLDivElement | null>(null); const cardRef = useRef<HTMLDivElement | null>(null);
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout(); const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout(); const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
const [setMoveTimeout, cancelMoveTimeout] = useTimeout();
const [setScrollTimeout] = useTimeout(); const [setScrollTimeout] = useTimeout();
const location = useLocation(); const location = useLocation();
@@ -43,6 +47,8 @@ export const HoverCardController: React.FC = () => {
useEffect(() => { useEffect(() => {
let isScrolling = false; let isScrolling = false;
let isUsingTouch = false;
let isActiveMouseMovement = false;
let currentAnchor: HTMLElement | null = null; let currentAnchor: HTMLElement | null = null;
let currentTitle: string | null = null; let currentTitle: string | null = null;
@@ -64,7 +70,7 @@ export const HoverCardController: React.FC = () => {
const handleTouchStart = () => { const handleTouchStart = () => {
// Keeping track of touch events to prevent the // Keeping track of touch events to prevent the
// hover card from being displayed on touch devices // hover card from being displayed on touch devices
isUsingTouchRef.current = true; isUsingTouch = true;
}; };
const handleMouseEnter = (e: MouseEvent) => { const handleMouseEnter = (e: MouseEvent) => {
@@ -76,13 +82,14 @@ export const HoverCardController: React.FC = () => {
return; return;
} }
// Bail out if a touch is active // Bail out if we're scrolling, a touch is active,
if (isUsingTouchRef.current) { // or if there was no active mouse movement
if (isScrolling || !isActiveMouseMovement || isUsingTouch) {
return; return;
} }
// We've entered an anchor // We've entered an anchor
if (!isScrolling && isHoverCardAnchor(target)) { if (isHoverCardAnchor(target)) {
cancelLeaveTimeout(); cancelLeaveTimeout();
currentAnchor?.removeAttribute('aria-describedby'); currentAnchor?.removeAttribute('aria-describedby');
@@ -97,10 +104,7 @@ export const HoverCardController: React.FC = () => {
} }
// We've entered the hover card // We've entered the hover card
if ( if (target === currentAnchor || target === cardRef.current) {
!isScrolling &&
(target === currentAnchor || target === cardRef.current)
) {
cancelLeaveTimeout(); cancelLeaveTimeout();
} }
}; };
@@ -139,10 +143,17 @@ export const HoverCardController: React.FC = () => {
}; };
const handleMouseMove = () => { const handleMouseMove = () => {
if (isUsingTouchRef.current) { if (isUsingTouch) {
isUsingTouchRef.current = false; isUsingTouch = false;
} }
delayEnterTimeout(enterDelay); delayEnterTimeout(enterDelay);
cancelMoveTimeout();
isActiveMouseMovement = true;
setMoveTimeout(() => {
isActiveMouseMovement = false;
}, activeMovementThreshold);
}; };
document.body.addEventListener('touchstart', handleTouchStart, { document.body.addEventListener('touchstart', handleTouchStart, {
@@ -186,6 +197,8 @@ export const HoverCardController: React.FC = () => {
setOpen, setOpen,
setAccountId, setAccountId,
setAnchor, setAnchor,
setMoveTimeout,
cancelMoveTimeout,
]); ]);
return ( return (

View File

@@ -179,7 +179,7 @@ class Request
return return
end 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 request.headers['Signature'] = signature_value
end end

View File

@@ -25,7 +25,10 @@ class AccountMigration < ApplicationRecord
before_validation :set_target_account before_validation :set_target_account
before_validation :set_followers_count before_validation :set_followers_count
attribute :current_username, :string
normalizes :acct, with: ->(acct) { acct.strip.delete_prefix('@') } normalizes :acct, with: ->(acct) { acct.strip.delete_prefix('@') }
normalizes :current_username, with: ->(value) { value.strip.delete_prefix('@') }
validates :acct, presence: true, domain: { acct: true } validates :acct, presence: true, domain: { acct: true }
validate :validate_migration_cooldown validate :validate_migration_cooldown
@@ -33,7 +36,7 @@ class AccountMigration < ApplicationRecord
scope :within_cooldown, -> { where(created_at: cooldown_duration_ago..) } scope :within_cooldown, -> { where(created_at: cooldown_duration_ago..) }
attr_accessor :current_password, :current_username attr_accessor :current_password
def self.cooldown_duration_ago def self.cooldown_duration_ago
Time.current - COOLDOWN_PERIOD Time.current - COOLDOWN_PERIOD

View File

@@ -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

View File

@@ -7,6 +7,10 @@ RSpec.describe AccountMigration do
describe 'acct' do describe 'acct' do
it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') } it { is_expected.to normalize(:acct).from(' @username@domain ').to('username@domain') }
end end
describe 'current_username' do
it { is_expected.to normalize(:current_username).from(' @username ').to('username') }
end
end end
describe 'Validations' do describe 'Validations' do

View File

@@ -33,20 +33,36 @@ RSpec.describe 'Settings Migrations' do
end end
describe 'Creating migrations' do describe 'Creating migrations' do
let(:user) { Fabricate(:user, password: '12345678') } let(:user) { Fabricate(:user, password:) }
let(:password) { '12345678' }
before { sign_in(user) } before { sign_in(user) }
context 'when migration account is changed' do context 'when migration account is changed' do
let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) } let(:acct) { Fabricate(:account, also_known_as: [ActivityPub::TagManager.instance.uri_for(user.account)]) }
it 'updates moved to account' do context 'when user has encrypted password' do
visit settings_migration_path it 'updates moved to account' do
visit settings_migration_path
expect { fill_in_and_submit } expect { fill_in_and_submit }
.to(change { user.account.reload.moved_to_account_id }.to(acct.id)) .to(change { user.account.reload.moved_to_account_id }.to(acct.id))
expect(page) expect(page)
.to have_content(I18n.t('settings.migrate')) .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
end end
@@ -92,8 +108,18 @@ RSpec.describe 'Settings Migrations' do
def fill_in_and_submit def fill_in_and_submit
fill_in 'account_migration_acct', with: acct.username 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') click_on I18n.t('migrations.proceed_with_move')
end 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
end end