Merge pull request #3435 from ClearlyClaire/glitch-soc/merge-4.3
Merge upstream changes up to 14f5932f49 into stable-4.3
This commit is contained in:
@@ -182,15 +182,25 @@ function loaded() {
|
|||||||
({ target }) => {
|
({ target }) => {
|
||||||
if (!(target instanceof HTMLInputElement)) return;
|
if (!(target instanceof HTMLInputElement)) return;
|
||||||
|
|
||||||
if (target.value && target.value.length > 0) {
|
const checkedUsername = target.value;
|
||||||
|
if (checkedUsername && checkedUsername.length > 0) {
|
||||||
axios
|
axios
|
||||||
.get('/api/v1/accounts/lookup', { params: { acct: target.value } })
|
.get('/api/v1/accounts/lookup', {
|
||||||
|
params: { acct: checkedUsername },
|
||||||
|
})
|
||||||
.then(() => {
|
.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;
|
return true;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
target.setCustomValidity('');
|
// Only update the validity if the result is for the currently-typed username
|
||||||
|
if (checkedUsername === target.value) {
|
||||||
|
target.setCustomValidity('');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
target.setCustomValidity('');
|
target.setCustomValidity('');
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ 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();
|
||||||
@@ -60,6 +61,12 @@ export const HoverCardController: React.FC = () => {
|
|||||||
setAccountId(undefined);
|
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 handleMouseEnter = (e: MouseEvent) => {
|
||||||
const { target } = e;
|
const { target } = e;
|
||||||
|
|
||||||
@@ -69,6 +76,11 @@ export const HoverCardController: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bail out if a touch is active
|
||||||
|
if (isUsingTouchRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We've entered an anchor
|
// We've entered an anchor
|
||||||
if (!isScrolling && isHoverCardAnchor(target)) {
|
if (!isScrolling && isHoverCardAnchor(target)) {
|
||||||
cancelLeaveTimeout();
|
cancelLeaveTimeout();
|
||||||
@@ -127,9 +139,16 @@ export const HoverCardController: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = () => {
|
const handleMouseMove = () => {
|
||||||
|
if (isUsingTouchRef.current) {
|
||||||
|
isUsingTouchRef.current = false;
|
||||||
|
}
|
||||||
delayEnterTimeout(enterDelay);
|
delayEnterTimeout(enterDelay);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('touchstart', handleTouchStart, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
|
||||||
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
||||||
passive: true,
|
passive: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
@@ -151,6 +170,7 @@ export const HoverCardController: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
document.body.removeEventListener('touchstart', handleTouchStart);
|
||||||
document.body.removeEventListener('mouseenter', handleMouseEnter);
|
document.body.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
document.body.removeEventListener('mousemove', handleMouseMove);
|
document.body.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.body.removeEventListener('mouseleave', handleMouseLeave);
|
document.body.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
|||||||
@@ -182,15 +182,25 @@ function loaded() {
|
|||||||
({ target }) => {
|
({ target }) => {
|
||||||
if (!(target instanceof HTMLInputElement)) return;
|
if (!(target instanceof HTMLInputElement)) return;
|
||||||
|
|
||||||
if (target.value && target.value.length > 0) {
|
const checkedUsername = target.value;
|
||||||
|
if (checkedUsername && checkedUsername.length > 0) {
|
||||||
axios
|
axios
|
||||||
.get('/api/v1/accounts/lookup', { params: { acct: target.value } })
|
.get('/api/v1/accounts/lookup', {
|
||||||
|
params: { acct: checkedUsername },
|
||||||
|
})
|
||||||
.then(() => {
|
.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;
|
return true;
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
target.setCustomValidity('');
|
// Only update the validity if the result is for the currently-typed username
|
||||||
|
if (checkedUsername === target.value) {
|
||||||
|
target.setCustomValidity('');
|
||||||
|
}
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
target.setCustomValidity('');
|
target.setCustomValidity('');
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ 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();
|
||||||
@@ -60,6 +61,12 @@ export const HoverCardController: React.FC = () => {
|
|||||||
setAccountId(undefined);
|
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 handleMouseEnter = (e: MouseEvent) => {
|
||||||
const { target } = e;
|
const { target } = e;
|
||||||
|
|
||||||
@@ -69,6 +76,11 @@ export const HoverCardController: React.FC = () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Bail out if a touch is active
|
||||||
|
if (isUsingTouchRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// We've entered an anchor
|
// We've entered an anchor
|
||||||
if (!isScrolling && isHoverCardAnchor(target)) {
|
if (!isScrolling && isHoverCardAnchor(target)) {
|
||||||
cancelLeaveTimeout();
|
cancelLeaveTimeout();
|
||||||
@@ -127,9 +139,16 @@ export const HoverCardController: React.FC = () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handleMouseMove = () => {
|
const handleMouseMove = () => {
|
||||||
|
if (isUsingTouchRef.current) {
|
||||||
|
isUsingTouchRef.current = false;
|
||||||
|
}
|
||||||
delayEnterTimeout(enterDelay);
|
delayEnterTimeout(enterDelay);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
document.body.addEventListener('touchstart', handleTouchStart, {
|
||||||
|
passive: true,
|
||||||
|
});
|
||||||
|
|
||||||
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
||||||
passive: true,
|
passive: true,
|
||||||
capture: true,
|
capture: true,
|
||||||
@@ -151,6 +170,7 @@ export const HoverCardController: React.FC = () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
document.body.removeEventListener('touchstart', handleTouchStart);
|
||||||
document.body.removeEventListener('mouseenter', handleMouseEnter);
|
document.body.removeEventListener('mouseenter', handleMouseEnter);
|
||||||
document.body.removeEventListener('mousemove', handleMouseMove);
|
document.body.removeEventListener('mousemove', handleMouseMove);
|
||||||
document.body.removeEventListener('mouseleave', handleMouseLeave);
|
document.body.removeEventListener('mouseleave', handleMouseLeave);
|
||||||
|
|||||||
@@ -326,6 +326,11 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
|||||||
|
|
||||||
return unless poll.present? && poll.expires_at.present? && poll.votes.exists?
|
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.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)
|
PollExpirationNotifyWorker.perform_at(poll.expires_at + 5.minutes, poll.id)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ class ResolveURLService < BaseService
|
|||||||
include JsonLdHelper
|
include JsonLdHelper
|
||||||
include Authorization
|
include Authorization
|
||||||
|
|
||||||
USERNAME_STATUS_RE = %r{/@(?<username>#{Account::USERNAME_RE})/(?<status_id>[0-9]+)\Z}
|
USERNAME_STATUS_RE = %r{/@(?<username>#{Account::USERNAME_RE})/(statuses/)?(?<status_id>[0-9a-zA-Z]+)\Z}
|
||||||
|
|
||||||
def call(url, on_behalf_of: nil)
|
def call(url, on_behalf_of: nil)
|
||||||
@url = url
|
@url = url
|
||||||
|
|||||||
@@ -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 %>
|
=> <%= edit_user_registration_url %>
|
||||||
|
|||||||
@@ -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 %>
|
=> <%= edit_user_registration_url %>
|
||||||
|
|||||||
@@ -136,6 +136,48 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
|||||||
end
|
end
|
||||||
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
|
context 'when the status changes a poll despite being not explicitly marked as updated' do
|
||||||
let(:account) { Fabricate(:account, domain: 'example.com') }
|
let(:account) { Fabricate(:account, domain: 'example.com') }
|
||||||
let!(:expiration) { 10.days.from_now.utc }
|
let!(:expiration) { 10.days.from_now.utc }
|
||||||
|
|||||||
Reference in New Issue
Block a user