From 4bd353cdc71aaf90e6c1b9ae7ccc46e6cb9c792f Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 3 Mar 2026 16:26:56 +0100 Subject: [PATCH 1/7] Prevent hover card from showing on touch devices (#38039) --- .../components/hover_card_controller.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/javascript/mastodon/components/hover_card_controller.tsx b/app/javascript/mastodon/components/hover_card_controller.tsx index 057ef1aaed..6cd4783175 100644 --- a/app/javascript/mastodon/components/hover_card_controller.tsx +++ b/app/javascript/mastodon/components/hover_card_controller.tsx @@ -23,6 +23,7 @@ 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(); @@ -60,6 +61,12 @@ export const HoverCardController: React.FC = () => { setAccountId(undefined); }; + const handleTouchStart = () => { + // Keeping track of touch events to prevent the + // hover card from being displayed on touch devices + isUsingTouchRef.current = true; + }; + const handleMouseEnter = (e: MouseEvent) => { const { target } = e; @@ -69,6 +76,11 @@ export const HoverCardController: React.FC = () => { return; } + // Bail out if a touch is active + if (isUsingTouchRef.current) { + return; + } + // We've entered an anchor if (!isScrolling && isHoverCardAnchor(target)) { cancelLeaveTimeout(); @@ -127,9 +139,16 @@ export const HoverCardController: React.FC = () => { }; const handleMouseMove = () => { + if (isUsingTouchRef.current) { + isUsingTouchRef.current = false; + } delayEnterTimeout(enterDelay); }; + document.body.addEventListener('touchstart', handleTouchStart, { + passive: true, + }); + document.body.addEventListener('mouseenter', handleMouseEnter, { passive: true, capture: true, @@ -151,6 +170,7 @@ export const HoverCardController: React.FC = () => { }); return () => { + document.body.removeEventListener('touchstart', handleTouchStart); document.body.removeEventListener('mouseenter', handleMouseEnter); document.body.removeEventListener('mousemove', handleMouseMove); document.body.removeEventListener('mouseleave', handleMouseLeave); From 38782c6141e54d51140da4ecc4dc11020e550bd1 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 25 Feb 2026 15:30:01 +0100 Subject: [PATCH 2/7] Fix username availability check being wrongly applied on race conditions (#37975) --- app/javascript/entrypoints/public.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/javascript/entrypoints/public.tsx b/app/javascript/entrypoints/public.tsx index d33e00d5da..488481bf11 100644 --- a/app/javascript/entrypoints/public.tsx +++ b/app/javascript/entrypoints/public.tsx @@ -182,15 +182,25 @@ function loaded() { ({ target }) => { if (!(target instanceof HTMLInputElement)) return; - if (target.value && target.value.length > 0) { + const checkedUsername = target.value; + if (checkedUsername && checkedUsername.length > 0) { axios - .get('/api/v1/accounts/lookup', { params: { acct: target.value } }) + .get('/api/v1/accounts/lookup', { + params: { acct: checkedUsername }, + }) .then(() => { - target.setCustomValidity(formatMessage(messages.usernameTaken)); + // Only update the validity if the result is for the currently-typed username + if (checkedUsername === target.value) { + target.setCustomValidity(formatMessage(messages.usernameTaken)); + } + return true; }) .catch(() => { - target.setCustomValidity(''); + // Only update the validity if the result is for the currently-typed username + if (checkedUsername === target.value) { + target.setCustomValidity(''); + } }); } else { target.setCustomValidity(''); From 4bf9083d132dcedbc245836c7b49b5741ed85590 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Thu, 5 Mar 2026 05:37:12 -0500 Subject: [PATCH 3/7] Fix incorrect I18n string in webauthn mailers (#38062) --- app/views/user_mailer/webauthn_credential_added.text.erb | 4 ++-- app/views/user_mailer/webauthn_enabled.text.erb | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/views/user_mailer/webauthn_credential_added.text.erb b/app/views/user_mailer/webauthn_credential_added.text.erb index 4319dddbfc..d4482a69bb 100644 --- a/app/views/user_mailer/webauthn_credential_added.text.erb +++ b/app/views/user_mailer/webauthn_credential_added.text.erb @@ -1,7 +1,7 @@ -<%= t 'devise.mailer.two_factor_enabled.title' %> +<%= t 'devise.mailer.webauthn_credential.added.title' %> === -<%= t 'devise.mailer.two_factor_enabled.explanation' %> +<%= t 'devise.mailer.webauthn_credential.added.explanation' %> => <%= edit_user_registration_url %> diff --git a/app/views/user_mailer/webauthn_enabled.text.erb b/app/views/user_mailer/webauthn_enabled.text.erb index d4482a69bb..ca8da30a42 100644 --- a/app/views/user_mailer/webauthn_enabled.text.erb +++ b/app/views/user_mailer/webauthn_enabled.text.erb @@ -1,7 +1,7 @@ -<%= t 'devise.mailer.webauthn_credential.added.title' %> +<%= t 'devise.mailer.webauthn_enabled.title' %> === -<%= t 'devise.mailer.webauthn_credential.added.explanation' %> +<%= t 'devise.mailer.webauthn_enabled.explanation' %> => <%= edit_user_registration_url %> From f140684847525ab34c3c2b8b2905da105c789232 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 5 Mar 2026 09:56:18 +0100 Subject: [PATCH 4/7] Add for searching already-known private GtS posts (#38057) --- app/services/resolve_url_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/resolve_url_service.rb b/app/services/resolve_url_service.rb index 19a94e77ad..899f586b81 100644 --- a/app/services/resolve_url_service.rb +++ b/app/services/resolve_url_service.rb @@ -4,7 +4,7 @@ class ResolveURLService < BaseService include JsonLdHelper include Authorization - USERNAME_STATUS_RE = %r{/@(?#{Account::USERNAME_RE})/(?[0-9]+)\Z} + USERNAME_STATUS_RE = %r{/@(?#{Account::USERNAME_RE})/(statuses/)?(?[0-9a-zA-Z]+)\Z} def call(url, on_behalf_of: nil) @url = url From 14f5932f49567ce0d63570b7204511f912ca6d22 Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 5 Mar 2026 15:53:00 +0100 Subject: [PATCH 5/7] Fix poll expiration notification being re-triggered on implicit updates (#38078) --- .../process_status_update_service.rb | 5 +++ .../process_status_update_service_spec.rb | 42 +++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/app/services/activitypub/process_status_update_service.rb b/app/services/activitypub/process_status_update_service.rb index e9f5aa9e3e..156966f0a6 100644 --- a/app/services/activitypub/process_status_update_service.rb +++ b/app/services/activitypub/process_status_update_service.rb @@ -326,6 +326,11 @@ class ActivityPub::ProcessStatusUpdateService < BaseService return unless poll.present? && poll.expires_at.present? && poll.votes.exists? + # If the poll had previously expired, notifications should have already been sent out (or scheduled), + # and re-scheduling them would cause duplicate notifications for people who had already dismissed them + # (see #37948) + return if @previous_expires_at&.past? + PollExpirationNotifyWorker.remove_from_scheduled(poll.id) if @previous_expires_at.present? && @previous_expires_at > poll.expires_at PollExpirationNotifyWorker.perform_at(poll.expires_at + 5.minutes, poll.id) end diff --git a/spec/services/activitypub/process_status_update_service_spec.rb b/spec/services/activitypub/process_status_update_service_spec.rb index 562b135eb3..c2cf245798 100644 --- a/spec/services/activitypub/process_status_update_service_spec.rb +++ b/spec/services/activitypub/process_status_update_service_spec.rb @@ -136,6 +136,48 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do end end + context 'with an implicit update of a poll that has already expired' do + let(:account) { Fabricate(:account, domain: 'example.com') } + let!(:expiration) { 10.days.ago.utc } + let!(:status) do + Fabricate(:status, + text: 'Hello world', + account: account, + poll_attributes: { + options: %w(Foo Bar), + account: account, + multiple: false, + hide_totals: false, + expires_at: expiration, + }) + end + + let(:payload) do + { + '@context': 'https://www.w3.org/ns/activitystreams', + id: 'https://example.com/foo', + type: 'Question', + content: 'Hello world', + endTime: expiration.iso8601, + oneOf: [ + poll_option_json('Foo', 4), + poll_option_json('Bar', 3), + ], + } + end + + before do + travel_to(expiration - 1.day) do + Fabricate(:poll_vote, poll: status.poll) + end + end + + it 'does not re-trigger notifications' do + expect { subject.call(status, json, json) } + .to_not enqueue_sidekiq_job(PollExpirationNotifyWorker) + end + end + context 'when the status changes a poll despite being not explicitly marked as updated' do let(:account) { Fabricate(:account, domain: 'example.com') } let!(:expiration) { 10.days.from_now.utc } From 94acc71729d629c8a2d5469575d0d2156a791069 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 3 Mar 2026 16:26:56 +0100 Subject: [PATCH 6/7] [Glitch] Prevent hover card from showing on touch devices Port de4ee8565c658ae14aa5cc4504aa246694f92c1e to glitch-soc Signed-off-by: Claire --- .../components/hover_card_controller.tsx | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/app/javascript/flavours/glitch/components/hover_card_controller.tsx b/app/javascript/flavours/glitch/components/hover_card_controller.tsx index d2a636e939..d232b6760c 100644 --- a/app/javascript/flavours/glitch/components/hover_card_controller.tsx +++ b/app/javascript/flavours/glitch/components/hover_card_controller.tsx @@ -23,6 +23,7 @@ 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(); @@ -60,6 +61,12 @@ export const HoverCardController: React.FC = () => { setAccountId(undefined); }; + const handleTouchStart = () => { + // Keeping track of touch events to prevent the + // hover card from being displayed on touch devices + isUsingTouchRef.current = true; + }; + const handleMouseEnter = (e: MouseEvent) => { const { target } = e; @@ -69,6 +76,11 @@ export const HoverCardController: React.FC = () => { return; } + // Bail out if a touch is active + if (isUsingTouchRef.current) { + return; + } + // We've entered an anchor if (!isScrolling && isHoverCardAnchor(target)) { cancelLeaveTimeout(); @@ -127,9 +139,16 @@ export const HoverCardController: React.FC = () => { }; const handleMouseMove = () => { + if (isUsingTouchRef.current) { + isUsingTouchRef.current = false; + } delayEnterTimeout(enterDelay); }; + document.body.addEventListener('touchstart', handleTouchStart, { + passive: true, + }); + document.body.addEventListener('mouseenter', handleMouseEnter, { passive: true, capture: true, @@ -151,6 +170,7 @@ export const HoverCardController: React.FC = () => { }); return () => { + document.body.removeEventListener('touchstart', handleTouchStart); document.body.removeEventListener('mouseenter', handleMouseEnter); document.body.removeEventListener('mousemove', handleMouseMove); document.body.removeEventListener('mouseleave', handleMouseLeave); From ad6f346f3b2f0bf938d653940dd043fb7b4ebc85 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 25 Feb 2026 15:30:01 +0100 Subject: [PATCH 7/7] [Glitch] Fix username availability check being wrongly applied on race conditions Port ea34d35b32c41cedbddef7f57737da61d984a861 to glitch-soc Signed-off-by: Claire --- .../flavours/glitch/entrypoints/public.tsx | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/app/javascript/flavours/glitch/entrypoints/public.tsx b/app/javascript/flavours/glitch/entrypoints/public.tsx index 41c0e34396..2a3b0730e6 100644 --- a/app/javascript/flavours/glitch/entrypoints/public.tsx +++ b/app/javascript/flavours/glitch/entrypoints/public.tsx @@ -182,15 +182,25 @@ function loaded() { ({ target }) => { if (!(target instanceof HTMLInputElement)) return; - if (target.value && target.value.length > 0) { + const checkedUsername = target.value; + if (checkedUsername && checkedUsername.length > 0) { axios - .get('/api/v1/accounts/lookup', { params: { acct: target.value } }) + .get('/api/v1/accounts/lookup', { + params: { acct: checkedUsername }, + }) .then(() => { - target.setCustomValidity(formatMessage(messages.usernameTaken)); + // Only update the validity if the result is for the currently-typed username + if (checkedUsername === target.value) { + target.setCustomValidity(formatMessage(messages.usernameTaken)); + } + return true; }) .catch(() => { - target.setCustomValidity(''); + // Only update the validity if the result is for the currently-typed username + if (checkedUsername === target.value) { + target.setCustomValidity(''); + } }); } else { target.setCustomValidity('');