Compare commits
21 Commits
v1.0.0
...
stable-4.5
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0348531cd | ||
|
|
a97811b056 | ||
|
|
f4baed2a69 | ||
|
|
f37dc6c59e | ||
|
|
9171fa49b6 | ||
|
|
ac91d30a5a | ||
|
|
dff7d55a6d | ||
|
|
c2244cbb67 | ||
|
|
436bf0590c | ||
|
|
c8a5c2c121 | ||
|
|
1033029a6c | ||
|
|
c14a62c3af | ||
|
|
d7059dcf1c | ||
|
|
a7bfcf7131 | ||
|
|
6fcdc05e43 | ||
|
|
a475f2ba39 | ||
|
|
a3f0a0373d | ||
|
|
ed521e91e1 | ||
|
|
ba22c3f133 | ||
|
|
f198ec7c1c | ||
|
|
58fed93bae |
@@ -18,6 +18,8 @@ class AccountsController < ApplicationController
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
expires_in(15.seconds, public: true, stale_while_revalidate: 30.seconds, stale_if_error: 1.hour) unless user_signed_in?
|
||||
|
||||
redirect_to short_account_path(@account) if account_id_param.present? && username_param.blank?
|
||||
end
|
||||
|
||||
format.rss do
|
||||
|
||||
@@ -26,6 +26,8 @@ class StatusesController < ApplicationController
|
||||
respond_to do |format|
|
||||
format.html do
|
||||
expires_in 10.seconds, public: true if current_account.nil?
|
||||
|
||||
redirect_to short_account_status_path(@account, @status) if account_id_param.present? && username_param.blank?
|
||||
end
|
||||
|
||||
format.json do
|
||||
|
||||
@@ -183,15 +183,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('');
|
||||
|
||||
@@ -129,6 +129,8 @@ export const FollowButton: React.FC<{
|
||||
: messages.follow;
|
||||
|
||||
let label;
|
||||
let disabled =
|
||||
relationship?.blocked_by || account?.suspended || !!account?.moved;
|
||||
|
||||
if (!signedIn) {
|
||||
label = intl.formatMessage(followMessage);
|
||||
@@ -138,12 +140,16 @@ export const FollowButton: React.FC<{
|
||||
label = <LoadingIndicator />;
|
||||
} else if (relationship.muting) {
|
||||
label = intl.formatMessage(messages.unmute);
|
||||
disabled = false;
|
||||
} else if (relationship.following) {
|
||||
label = intl.formatMessage(messages.unfollow);
|
||||
disabled = false;
|
||||
} else if (relationship.blocking) {
|
||||
label = intl.formatMessage(messages.unblock);
|
||||
disabled = false;
|
||||
} else if (relationship.requested) {
|
||||
label = intl.formatMessage(messages.followRequestCancel);
|
||||
disabled = false;
|
||||
} else if (relationship.followed_by && !account?.locked) {
|
||||
label = intl.formatMessage(messages.followBack);
|
||||
} else {
|
||||
@@ -168,11 +174,7 @@ export const FollowButton: React.FC<{
|
||||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
disabled={
|
||||
relationship?.blocked_by ||
|
||||
(!(relationship?.following || relationship?.requested) &&
|
||||
(account?.suspended || !!account?.moved))
|
||||
}
|
||||
disabled={disabled}
|
||||
secondary={following}
|
||||
compact={compact}
|
||||
className={classNames(className, { 'button--destructive': following })}
|
||||
|
||||
@@ -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) =>
|
||||
@@ -26,6 +30,7 @@ export const HoverCardController: React.FC = () => {
|
||||
const cardRef = useRef<HTMLDivElement | null>(null);
|
||||
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
||||
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
|
||||
const [setMoveTimeout, cancelMoveTimeout] = useTimeout();
|
||||
const [setScrollTimeout] = useTimeout();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -42,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;
|
||||
|
||||
@@ -60,6 +67,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
|
||||
isUsingTouch = true;
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: MouseEvent) => {
|
||||
const { target } = e;
|
||||
|
||||
@@ -69,8 +82,14 @@ export const HoverCardController: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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');
|
||||
@@ -85,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();
|
||||
}
|
||||
};
|
||||
@@ -127,9 +143,23 @@ export const HoverCardController: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleMouseMove = () => {
|
||||
if (isUsingTouch) {
|
||||
isUsingTouch = false;
|
||||
}
|
||||
|
||||
delayEnterTimeout(enterDelay);
|
||||
|
||||
cancelMoveTimeout();
|
||||
isActiveMouseMovement = true;
|
||||
setMoveTimeout(() => {
|
||||
isActiveMouseMovement = false;
|
||||
}, activeMovementThreshold);
|
||||
};
|
||||
|
||||
document.body.addEventListener('touchstart', handleTouchStart, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
@@ -151,6 +181,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);
|
||||
@@ -166,6 +197,8 @@ export const HoverCardController: React.FC = () => {
|
||||
setOpen,
|
||||
setAccountId,
|
||||
setAnchor,
|
||||
setMoveTimeout,
|
||||
cancelMoveTimeout,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -177,7 +177,7 @@ class StatusContent extends PureComponent {
|
||||
{children}
|
||||
</HandledLink>
|
||||
);
|
||||
} else if (element.classList.contains('quote-inline')) {
|
||||
} else if (element.classList.contains('quote-inline') && this.props.status.get('quote')) {
|
||||
return null;
|
||||
}
|
||||
return undefined;
|
||||
|
||||
@@ -183,15 +183,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('');
|
||||
|
||||
@@ -129,6 +129,8 @@ export const FollowButton: React.FC<{
|
||||
: messages.follow;
|
||||
|
||||
let label;
|
||||
let disabled =
|
||||
relationship?.blocked_by || account?.suspended || !!account?.moved;
|
||||
|
||||
if (!signedIn) {
|
||||
label = intl.formatMessage(followMessage);
|
||||
@@ -138,12 +140,16 @@ export const FollowButton: React.FC<{
|
||||
label = <LoadingIndicator />;
|
||||
} else if (relationship.muting) {
|
||||
label = intl.formatMessage(messages.unmute);
|
||||
disabled = false;
|
||||
} else if (relationship.following) {
|
||||
label = intl.formatMessage(messages.unfollow);
|
||||
disabled = false;
|
||||
} else if (relationship.blocking) {
|
||||
label = intl.formatMessage(messages.unblock);
|
||||
disabled = false;
|
||||
} else if (relationship.requested) {
|
||||
label = intl.formatMessage(messages.followRequestCancel);
|
||||
disabled = false;
|
||||
} else if (relationship.followed_by && !account?.locked) {
|
||||
label = intl.formatMessage(messages.followBack);
|
||||
} else {
|
||||
@@ -168,11 +174,7 @@ export const FollowButton: React.FC<{
|
||||
return (
|
||||
<Button
|
||||
onClick={handleClick}
|
||||
disabled={
|
||||
relationship?.blocked_by ||
|
||||
(!(relationship?.following || relationship?.requested) &&
|
||||
(account?.suspended || !!account?.moved))
|
||||
}
|
||||
disabled={disabled}
|
||||
secondary={following}
|
||||
compact={compact}
|
||||
className={classNames(className, { 'button--destructive': following })}
|
||||
|
||||
@@ -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) =>
|
||||
@@ -26,6 +30,7 @@ export const HoverCardController: React.FC = () => {
|
||||
const cardRef = useRef<HTMLDivElement | null>(null);
|
||||
const [setLeaveTimeout, cancelLeaveTimeout] = useTimeout();
|
||||
const [setEnterTimeout, cancelEnterTimeout, delayEnterTimeout] = useTimeout();
|
||||
const [setMoveTimeout, cancelMoveTimeout] = useTimeout();
|
||||
const [setScrollTimeout] = useTimeout();
|
||||
const location = useLocation();
|
||||
|
||||
@@ -42,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;
|
||||
|
||||
@@ -60,6 +67,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
|
||||
isUsingTouch = true;
|
||||
};
|
||||
|
||||
const handleMouseEnter = (e: MouseEvent) => {
|
||||
const { target } = e;
|
||||
|
||||
@@ -69,8 +82,14 @@ export const HoverCardController: React.FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
// 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');
|
||||
@@ -85,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();
|
||||
}
|
||||
};
|
||||
@@ -127,9 +143,23 @@ export const HoverCardController: React.FC = () => {
|
||||
};
|
||||
|
||||
const handleMouseMove = () => {
|
||||
if (isUsingTouch) {
|
||||
isUsingTouch = false;
|
||||
}
|
||||
|
||||
delayEnterTimeout(enterDelay);
|
||||
|
||||
cancelMoveTimeout();
|
||||
isActiveMouseMovement = true;
|
||||
setMoveTimeout(() => {
|
||||
isActiveMouseMovement = false;
|
||||
}, activeMovementThreshold);
|
||||
};
|
||||
|
||||
document.body.addEventListener('touchstart', handleTouchStart, {
|
||||
passive: true,
|
||||
});
|
||||
|
||||
document.body.addEventListener('mouseenter', handleMouseEnter, {
|
||||
passive: true,
|
||||
capture: true,
|
||||
@@ -151,6 +181,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);
|
||||
@@ -166,6 +197,8 @@ export const HoverCardController: React.FC = () => {
|
||||
setOpen,
|
||||
setAccountId,
|
||||
setAnchor,
|
||||
setMoveTimeout,
|
||||
cancelMoveTimeout,
|
||||
]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -410,6 +410,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
|
||||
|
||||
@@ -4,7 +4,7 @@ class ResolveURLService < BaseService
|
||||
include JsonLdHelper
|
||||
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)
|
||||
@url = url
|
||||
|
||||
@@ -6,16 +6,16 @@ class UnfollowService < BaseService
|
||||
include Lockable
|
||||
|
||||
# Unfollow and notify the remote user
|
||||
# @param [Account] source_account Where to unfollow from
|
||||
# @param [Account] target_account Which to unfollow
|
||||
# @param [Account] follower Where to unfollow from
|
||||
# @param [Account] followee Which to unfollow
|
||||
# @param [Hash] options
|
||||
# @option [Boolean] :skip_unmerge
|
||||
def call(source_account, target_account, options = {})
|
||||
@source_account = source_account
|
||||
@target_account = target_account
|
||||
@options = options
|
||||
def call(follower, followee, options = {})
|
||||
@follower = follower
|
||||
@followee = followee
|
||||
@options = options
|
||||
|
||||
with_redis_lock("relationship:#{[source_account.id, target_account.id].sort.join(':')}") do
|
||||
with_redis_lock("relationship:#{[follower.id, followee.id].sort.join(':')}") do
|
||||
unfollow! || undo_follow_request!
|
||||
end
|
||||
end
|
||||
@@ -23,19 +23,25 @@ class UnfollowService < BaseService
|
||||
private
|
||||
|
||||
def unfollow!
|
||||
follow = Follow.find_by(account: @source_account, target_account: @target_account)
|
||||
|
||||
follow = Follow.find_by(account: @follower, target_account: @followee)
|
||||
return unless follow
|
||||
|
||||
# List members are removed immediately with the follow relationship removal,
|
||||
# so we need to fetch the list IDs first
|
||||
list_ids = @follower.owned_lists.with_list_account(@followee).pluck(:list_id) unless @options[:skip_unmerge]
|
||||
|
||||
follow.destroy!
|
||||
|
||||
create_notification(follow) if !@target_account.local? && @target_account.activitypub?
|
||||
create_reject_notification(follow) if @target_account.local? && !@source_account.local? && @source_account.activitypub?
|
||||
if @followee.local? && @follower.remote? && @follower.activitypub?
|
||||
send_reject_follow(follow)
|
||||
elsif @followee.remote? && @followee.activitypub?
|
||||
send_undo_follow(follow)
|
||||
end
|
||||
|
||||
unless @options[:skip_unmerge]
|
||||
UnmergeWorker.perform_async(@target_account.id, @source_account.id, 'home')
|
||||
UnmergeWorker.push_bulk(@source_account.owned_lists.with_list_account(@target_account).pluck(:list_id)) do |list_id|
|
||||
[@target_account.id, list_id, 'list']
|
||||
UnmergeWorker.perform_async(@followee.id, @follower.id, 'home')
|
||||
UnmergeWorker.push_bulk(list_ids) do |list_id|
|
||||
[@followee.id, list_id, 'list']
|
||||
end
|
||||
end
|
||||
|
||||
@@ -43,22 +49,21 @@ class UnfollowService < BaseService
|
||||
end
|
||||
|
||||
def undo_follow_request!
|
||||
follow_request = FollowRequest.find_by(account: @source_account, target_account: @target_account)
|
||||
|
||||
follow_request = FollowRequest.find_by(account: @follower, target_account: @followee)
|
||||
return unless follow_request
|
||||
|
||||
follow_request.destroy!
|
||||
|
||||
create_notification(follow_request) unless @target_account.local?
|
||||
send_undo_follow(follow_request) unless @followee.local?
|
||||
|
||||
follow_request
|
||||
end
|
||||
|
||||
def create_notification(follow)
|
||||
def send_undo_follow(follow)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url)
|
||||
end
|
||||
|
||||
def create_reject_notification(follow)
|
||||
def send_reject_follow(follow)
|
||||
ActivityPub::DeliveryWorker.perform_async(build_reject_json(follow), follow.target_account_id, follow.account.inbox_url)
|
||||
end
|
||||
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
@@ -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 %>
|
||||
|
||||
17
config/initializers/fog_connection_cache.rb
Normal file
17
config/initializers/fog_connection_cache.rb
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -6,10 +6,20 @@ RSpec.describe 'Accounts show response' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
|
||||
context 'with numeric-based identifiers' do
|
||||
it 'returns http success' do
|
||||
get "/ap/users/#{account.id}"
|
||||
context 'with JSON format' do
|
||||
it 'returns http success' do
|
||||
get "/ap/users/#{account.id}", headers: { 'ACCEPT' => 'application/json' }
|
||||
|
||||
expect(response).to have_http_status(200)
|
||||
expect(response).to have_http_status(200)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with HTML format' do
|
||||
it 'redirects to success' do
|
||||
get "/ap/users/#{account.id}", as: 'html'
|
||||
|
||||
expect(response).to redirect_to("/@#{account.username}")
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -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 }
|
||||
|
||||
@@ -5,54 +5,57 @@ require 'rails_helper'
|
||||
RSpec.describe UnfollowService do
|
||||
subject { described_class.new }
|
||||
|
||||
let(:sender) { Fabricate(:account, username: 'alice') }
|
||||
let(:follower) { Fabricate(:account) }
|
||||
let(:followee) { Fabricate(:account) }
|
||||
|
||||
describe 'local' do
|
||||
let(:bob) { Fabricate(:account, username: 'bob') }
|
||||
before do
|
||||
follower.follow!(followee)
|
||||
end
|
||||
|
||||
before { sender.follow!(bob) }
|
||||
shared_examples 'when the followee is in a list' do
|
||||
let(:list) { Fabricate(:list, account: follower) }
|
||||
|
||||
it 'destroys the following relation' do
|
||||
subject.call(sender, bob)
|
||||
before do
|
||||
list.accounts << followee
|
||||
end
|
||||
|
||||
expect(sender)
|
||||
.to_not be_following(bob)
|
||||
it 'schedules removal of posts from this user from the list' do
|
||||
expect { subject.call(follower, followee) }
|
||||
.to enqueue_sidekiq_job(UnmergeWorker).with(followee.id, list.id, 'list')
|
||||
end
|
||||
end
|
||||
|
||||
describe 'remote ActivityPub', :inline_jobs do
|
||||
let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
|
||||
before do
|
||||
sender.follow!(bob)
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
describe 'a local user unfollowing another local user' do
|
||||
it 'destroys the following relation and unmerge from home' do
|
||||
expect { subject.call(follower, followee) }
|
||||
.to change { follower.following?(followee) }.from(true).to(false)
|
||||
.and enqueue_sidekiq_job(UnmergeWorker).with(followee.id, follower.id, 'home')
|
||||
end
|
||||
|
||||
it 'destroys the following relation and sends unfollow activity' do
|
||||
subject.call(sender, bob)
|
||||
|
||||
expect(sender)
|
||||
.to_not be_following(bob)
|
||||
expect(a_request(:post, 'http://example.com/inbox'))
|
||||
.to have_been_made.once
|
||||
end
|
||||
it_behaves_like 'when the followee is in a list'
|
||||
end
|
||||
|
||||
describe 'remote ActivityPub (reverse)', :inline_jobs do
|
||||
let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
describe 'a local user unfollowing a remote ActivityPub user' do
|
||||
let(:followee) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
|
||||
before do
|
||||
bob.follow!(sender)
|
||||
stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
|
||||
it 'destroys the following relation, unmerge from home and sends undo activity' do
|
||||
expect { subject.call(follower, followee) }
|
||||
.to change { follower.following?(followee) }.from(true).to(false)
|
||||
.and enqueue_sidekiq_job(UnmergeWorker).with(followee.id, follower.id, 'home')
|
||||
.and enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Undo'), follower.id, followee.inbox_url)
|
||||
end
|
||||
|
||||
it 'destroys the following relation and sends a reject activity' do
|
||||
subject.call(bob, sender)
|
||||
it_behaves_like 'when the followee is in a list'
|
||||
end
|
||||
|
||||
expect(sender)
|
||||
.to_not be_following(bob)
|
||||
expect(a_request(:post, 'http://example.com/inbox'))
|
||||
.to have_been_made.once
|
||||
describe 'a remote ActivityPub user unfollowing a local user' do
|
||||
let(:follower) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
|
||||
it 'destroys the following relation, unmerge from home and sends a reject activity' do
|
||||
expect { subject.call(follower, followee) }
|
||||
.to change { follower.following?(followee) }.from(true).to(false)
|
||||
.and enqueue_sidekiq_job(UnmergeWorker).with(followee.id, follower.id, 'home')
|
||||
.and enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Reject'), followee.id, follower.inbox_url)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user