Merge commit 'd7bb36a64a842dfac52fb1cfb0fcd6646981fb38' into glitch-soc/merge-4.6
This commit is contained in:
commit
1285172f2a
17 files changed
+132
-36
No files matched your search
+2
-2
@@ -353,7 +353,7 @@ GEM
|
||||
azure-blob (~> 0.5.2)
|
||||
hashie (~> 5.0)
|
||||
jmespath (1.6.2)
|
||||
json (2.19.8)
|
||||
json (2.19.9)
|
||||
json-canonicalization (1.0.0)
|
||||
json-jwt (1.17.1)
|
||||
activesupport (>= 4.2)
|
||||
@@ -931,7 +931,7 @@ GEM
|
||||
crack (>= 0.3.2)
|
||||
hashdiff (>= 0.4.0, < 2.0.0)
|
||||
webrick (1.9.2)
|
||||
websocket-driver (0.8.1)
|
||||
websocket-driver (0.8.2)
|
||||
base64
|
||||
websocket-extensions (>= 0.1.0)
|
||||
websocket-extensions (0.1.5)
|
||||
|
||||
@@ -40,7 +40,7 @@ module Admin
|
||||
|
||||
def resource_params
|
||||
params
|
||||
.expect(invite: [:max_uses, :expires_in])
|
||||
.expect(invite: [:max_uses, :expires_in, :autofollow])
|
||||
end
|
||||
|
||||
def filtered_invites
|
||||
|
||||
@@ -99,7 +99,7 @@
|
||||
margin: 12px 16px;
|
||||
padding: 8px;
|
||||
transition: border-color 0.2s ease-in-out;
|
||||
width: calc(100% - 48px);
|
||||
width: calc(100% - 32px);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
|
||||
@@ -72,7 +72,7 @@ const DirectTimeline = ({ columnId, multiColumn }) => {
|
||||
scrollKey={`direct_timeline-${columnId}`}
|
||||
emptyMessage={<FormattedMessage id='empty_column.direct' defaultMessage="You don't have any private mentions yet. When you send or receive one, it will show up here." />}
|
||||
bindToDocument={!multiColumn}
|
||||
prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='/terms' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
|
||||
prepend={<div className='follow_requests-unlocked_explanation'><span><FormattedMessage id='compose_form.encryption_warning' defaultMessage='Posts on Mastodon are not end-to-end encrypted. Do not share any dangerous information over Mastodon.' /> <a href='https://docs.joinmastodon.org/user/posting/#private' rel='noreferrer' target='_blank'><FormattedMessage id='compose_form.direct_message_warning_learn_more' defaultMessage='Learn more' /></a></span></div>}
|
||||
alwaysPrepend
|
||||
/>
|
||||
|
||||
|
||||
@@ -15,16 +15,21 @@ import { IconButton } from 'mastodon/components/icon_button';
|
||||
import { injectIntl } from '@/mastodon/components/intl';
|
||||
import Option from 'mastodon/features/report/components/option';
|
||||
import { languages as preloadedLanguages } from 'mastodon/initial_state';
|
||||
import { selectTimelinesByAccount } from '@/mastodon/selectors/timelines';
|
||||
|
||||
const messages = defineMessages({
|
||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||
});
|
||||
|
||||
const getAccountLanguages = createSelector([
|
||||
(state, accountId) => state.getIn(['timelines', `account:${accountId}`, 'items'], ImmutableList()),
|
||||
state => state.get('statuses'),
|
||||
], (statusIds, statuses) =>
|
||||
ImmutableSet(statusIds.map(statusId => statuses.get(statusId)).filter(status => !status.get('reblog')).map(status => status.get('language'))));
|
||||
const getAccountLanguages = createSelector(
|
||||
[selectTimelinesByAccount, (state) => state.get('statuses')],
|
||||
(timelines, statuses) => ImmutableSet(
|
||||
timelines
|
||||
.reduce((statusIds, timeline) => statusIds.concat(timeline.get('items')), ImmutableList())
|
||||
.map(statusId => statuses.get(statusId))
|
||||
.filter(status => !status.get('reblog'))
|
||||
.map(status => status.get('language'))
|
||||
));
|
||||
|
||||
const mapStateToProps = (state, { accountId }) => ({
|
||||
acct: state.getIn(['accounts', accountId, 'acct']),
|
||||
@@ -37,7 +42,6 @@ const mapDispatchToProps = (dispatch, { accountId }) => ({
|
||||
onSubmit (languages) {
|
||||
dispatch(followAccount(accountId, { languages }));
|
||||
},
|
||||
|
||||
});
|
||||
|
||||
class SubscribedLanguagesModal extends ImmutablePureComponent {
|
||||
|
||||
+12
-8
@@ -52,13 +52,18 @@ export const ConfirmationModal: React.FC<
|
||||
noCloseOnConfirm = false,
|
||||
noFocusButton = false,
|
||||
}) => {
|
||||
const handleClick = useCallback(() => {
|
||||
if (!noCloseOnConfirm) {
|
||||
onClose();
|
||||
}
|
||||
const handleSubmit = useCallback(
|
||||
(e: React.FormEvent) => {
|
||||
e.preventDefault();
|
||||
|
||||
void onConfirm();
|
||||
}, [onClose, onConfirm, noCloseOnConfirm]);
|
||||
if (!noCloseOnConfirm) {
|
||||
onClose();
|
||||
}
|
||||
|
||||
void onConfirm();
|
||||
},
|
||||
[onClose, onConfirm, noCloseOnConfirm],
|
||||
);
|
||||
|
||||
const handleSecondary = useCallback(() => {
|
||||
onClose();
|
||||
@@ -66,7 +71,7 @@ export const ConfirmationModal: React.FC<
|
||||
}, [onClose, onSecondary]);
|
||||
|
||||
return (
|
||||
<ModalShell onSubmit={handleClick}>
|
||||
<ModalShell onSubmit={handleSubmit}>
|
||||
<ModalShellBody className={className}>
|
||||
{noFocusButton ? (
|
||||
<NavigationFocusTarget as='h1' id={titleId}>
|
||||
@@ -107,7 +112,6 @@ export const ConfirmationModal: React.FC<
|
||||
{/* eslint-disable jsx-a11y/no-autofocus -- we are in a modal and thus autofocusing is justified */}
|
||||
<Button
|
||||
type='submit'
|
||||
onClick={handleClick}
|
||||
loading={updating}
|
||||
disabled={disabled}
|
||||
autoFocus={!noFocusButton}
|
||||
|
||||
@@ -32,6 +32,12 @@ export const selectTimelineByParams = createAppSelector(
|
||||
(key, timelines) => toTypedTimeline(timelines.get(key)),
|
||||
);
|
||||
|
||||
export const selectTimelinesByAccount = createAppSelector(
|
||||
[(state) => state.timelines, (_, accountId: string) => accountId],
|
||||
(timelines, accountId) =>
|
||||
timelines.filter((_, key) => key.startsWith(`account:${accountId}:`)),
|
||||
);
|
||||
|
||||
export function toTypedTimeline(timeline?: ImmutableMap<string, unknown>) {
|
||||
if (!timeline) {
|
||||
return null;
|
||||
|
||||
@@ -9,13 +9,15 @@ describe('relativeTimeParts', () => {
|
||||
|
||||
// Past
|
||||
[-30 * SECOND, { value: -30, unit: 'second' }],
|
||||
[-90 * SECOND, { value: -2, unit: 'minute' }],
|
||||
[-60 * SECOND, { value: -1, unit: 'minute' }],
|
||||
[-90 * SECOND, { value: -1, unit: 'minute' }],
|
||||
[-30 * MINUTE, { value: -30, unit: 'minute' }],
|
||||
[-90 * MINUTE, { value: -2, unit: 'hour' }],
|
||||
[-60 * MINUTE, { value: -1, unit: 'hour' }],
|
||||
[-90 * MINUTE, { value: -1, unit: 'hour' }],
|
||||
[-5 * HOUR, { value: -5, unit: 'hour' }],
|
||||
[-24 * HOUR, { value: -1, unit: 'day' }],
|
||||
[-36 * HOUR, { value: -1, unit: 'day' }],
|
||||
[-47 * HOUR, { value: -2, unit: 'day' }],
|
||||
[-47 * HOUR, { value: -1, unit: 'day' }],
|
||||
[-3 * DAY, { value: -3, unit: 'day' }],
|
||||
|
||||
// Future
|
||||
|
||||
@@ -16,21 +16,29 @@ export function relativeTimeParts(
|
||||
): { value: number; unit: TimeUnit; delta: number } {
|
||||
const delta = ts - now;
|
||||
const absDelta = Math.abs(delta);
|
||||
const sign = Math.sign(delta);
|
||||
|
||||
if (absDelta < MINUTE) {
|
||||
return { value: Math.floor(delta / SECOND), unit: 'second', delta };
|
||||
return {
|
||||
value: sign * Math.floor(absDelta / SECOND),
|
||||
unit: 'second',
|
||||
delta,
|
||||
};
|
||||
}
|
||||
|
||||
if (absDelta < HOUR) {
|
||||
return { value: Math.floor(delta / MINUTE), unit: 'minute', delta };
|
||||
return {
|
||||
value: sign * Math.floor(absDelta / MINUTE),
|
||||
unit: 'minute',
|
||||
delta,
|
||||
};
|
||||
}
|
||||
|
||||
if (absDelta < DAY) {
|
||||
return { value: Math.floor(delta / HOUR), unit: 'hour', delta };
|
||||
return { value: sign * Math.floor(absDelta / HOUR), unit: 'hour', delta };
|
||||
}
|
||||
|
||||
// Round instead of use floor as days are big enough that the value is usually off by a few hours.
|
||||
return { value: Math.round(delta / DAY), unit: 'day', delta };
|
||||
return { value: sign * Math.floor(absDelta / DAY), unit: 'day', delta };
|
||||
}
|
||||
|
||||
export function isToday(ts: number, now = Date.now()): boolean {
|
||||
|
||||
@@ -7255,6 +7255,7 @@ a.status-card {
|
||||
border-radius: 8px;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
max-width: 100%;
|
||||
min-height: 64px;
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
@@ -8573,7 +8574,7 @@ noscript {
|
||||
}
|
||||
|
||||
&__pagination {
|
||||
padding: 15px;
|
||||
padding: 0 12px 15px 0;
|
||||
color: var(--color-text-secondary);
|
||||
position: absolute;
|
||||
bottom: 3px;
|
||||
@@ -8598,7 +8599,10 @@ noscript {
|
||||
align-items: center;
|
||||
margin-top: 15px;
|
||||
margin-inline-start: -2px;
|
||||
width: calc(100% - (90px - 33px));
|
||||
|
||||
--pagination-width: 70px;
|
||||
|
||||
width: calc(100% - var(--pagination-width));
|
||||
|
||||
&__item {
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -37,7 +37,7 @@ class ActivityPub::Activity::Reject < ActivityPub::Activity
|
||||
quote = quote_from_request_json(@object)
|
||||
return unless quote.present? && quote.status.local?
|
||||
|
||||
reject_quote!(quoting_status.quote)
|
||||
reject_quote!(quote)
|
||||
end
|
||||
|
||||
def reject_quote!(quote)
|
||||
|
||||
@@ -66,8 +66,8 @@ class ActivityPub::ProcessFeaturedItemService
|
||||
return false if local_actor_uri?
|
||||
return false if Account.exists?(uri: @actor_uri)
|
||||
|
||||
object_json = fetch_resource(@actor_uri, true)
|
||||
(Array(object_json['type']) & ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES).empty?
|
||||
object_json = fetch_resource(@actor_uri, true, raise_on_error: :temporary)
|
||||
object_json.nil? || (as_array(object_json['fetch']) & ActivityPub::FetchRemoteActorService::SUPPORTED_TYPES).empty?
|
||||
end
|
||||
|
||||
def verify_authorization!
|
||||
|
||||
@@ -3,13 +3,14 @@
|
||||
class DeleteAccountService < BaseService
|
||||
include Payloadable
|
||||
|
||||
ASSOCIATIONS_ON_SUSPEND = %w(
|
||||
ASSOCIATIONS_ON_PURGE = %w(
|
||||
account_notes
|
||||
account_pins
|
||||
active_relationships
|
||||
aliases
|
||||
block_relationships
|
||||
blocked_by_relationships
|
||||
collections
|
||||
conversation_mutes
|
||||
conversations
|
||||
custom_filters
|
||||
@@ -26,6 +27,7 @@ class DeleteAccountService < BaseService
|
||||
report_notes
|
||||
scheduled_statuses
|
||||
status_pins
|
||||
tag_follows
|
||||
).freeze
|
||||
|
||||
# The following associations have no important side-effects
|
||||
@@ -302,9 +304,9 @@ class DeleteAccountService < BaseService
|
||||
|
||||
def associations_for_destruction
|
||||
if keep_account_record?
|
||||
ASSOCIATIONS_ON_SUSPEND
|
||||
ASSOCIATIONS_ON_PURGE
|
||||
else
|
||||
ASSOCIATIONS_ON_SUSPEND + ASSOCIATIONS_ON_DESTROY
|
||||
ASSOCIATIONS_ON_PURGE + ASSOCIATIONS_ON_DESTROY
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -112,7 +112,10 @@ class UpdateStatusService < BaseService
|
||||
end
|
||||
|
||||
def update_immediate_attributes!
|
||||
@status.text = @options[:text].presence || @options.delete(:spoiler_text) || '' if @options.key?(:text)
|
||||
if @options.key?(:text)
|
||||
@status.text = @options[:text].presence || ''
|
||||
@status.text = @options.delete(:spoiler_text) || '' if @status.text.blank? && @status.quote.blank?
|
||||
end
|
||||
@status.spoiler_text = @options[:spoiler_text] || '' if @options.key?(:spoiler_text)
|
||||
@status.sensitive = @options[:sensitive] || @options[:spoiler_text].present? if @options.key?(:sensitive) || @options.key?(:spoiler_text)
|
||||
@status.language = valid_locale_cascade(@options[:language], @status.language, @status.account.user&.preferred_posting_language, I18n.default_locale)
|
||||
|
||||
@@ -143,6 +143,28 @@ RSpec.describe ActivityPub::ProcessFeaturedItemService do
|
||||
end.to_not change(CollectionItem, :count)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when featured object cannot be fetched' do
|
||||
let(:hashtag_json) do
|
||||
{
|
||||
'id' => 'https://example.com/hashtags/people',
|
||||
'type' => 'Hashtag',
|
||||
'name' => '#people',
|
||||
}
|
||||
end
|
||||
let(:featured_object_uri) { hashtag_json['id'] }
|
||||
|
||||
before do
|
||||
stub_request(:get, featured_object_uri)
|
||||
.to_return(status: 404)
|
||||
end
|
||||
|
||||
it 'does not create a collection item and returns `nil`' do
|
||||
expect do
|
||||
expect(subject.call(collection, object, position:)).to be_nil
|
||||
end.to_not change(CollectionItem, :count)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when only the id of the collection item is given' do
|
||||
|
||||
@@ -47,6 +47,7 @@ RSpec.describe DeleteAccountService do
|
||||
expect { poll.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { poll_vote.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { account_note.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
expect { collection.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
|
||||
def expect_deletion_of_associated_target_records
|
||||
@@ -62,6 +63,12 @@ RSpec.describe DeleteAccountService do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'ASSOCIATIONS_WITHOUT_SIDE_EFFECTS' do
|
||||
it 'is a subset of ASSOCIATIONS_ON_PURGE' do
|
||||
expect(described_class::ASSOCIATIONS_WITHOUT_SIDE_EFFECTS - described_class::ASSOCIATIONS_ON_PURGE).to eq []
|
||||
end
|
||||
end
|
||||
|
||||
describe '#call on local account', :inline_jobs do
|
||||
before do
|
||||
stub_request(:post, remote_alice.inbox_url).to_return(status: 201)
|
||||
@@ -74,6 +81,7 @@ RSpec.describe DeleteAccountService do
|
||||
it_behaves_like 'common behavior' do
|
||||
let(:account) { Fabricate(:account) }
|
||||
let(:local_follower) { Fabricate(:account) }
|
||||
let!(:collection) { Fabricate(:collection, account:) } # rubocop:disable RSpec/LetSetup
|
||||
|
||||
it 'sends a delete actor activity to all known inboxes' do
|
||||
subject
|
||||
@@ -92,6 +100,8 @@ RSpec.describe DeleteAccountService do
|
||||
let(:account) { Fabricate(:account, inbox_url: 'https://bob.com/inbox', protocol: :activitypub, domain: 'bob.com') }
|
||||
let(:local_follower) { Fabricate(:account) }
|
||||
|
||||
let!(:collection) { Fabricate(:remote_collection, account:) } # rubocop:disable RSpec/LetSetup
|
||||
|
||||
it 'sends expected activities to followed and follower inboxes' do
|
||||
subject
|
||||
|
||||
|
||||
@@ -40,6 +40,37 @@ RSpec.describe UpdateStatusService do
|
||||
)
|
||||
expect(status.edits.ordered.pluck(:text)).to eq %w(Foo Bar)
|
||||
end
|
||||
|
||||
context 'when the status has a quote' do
|
||||
before { Fabricate(:quote, status: status) }
|
||||
|
||||
it 'updates text, resets card, saves edit history' do
|
||||
subject.call(status, status.account_id, text: 'Bar')
|
||||
|
||||
expect(status.reload)
|
||||
.to have_attributes(
|
||||
text: 'Bar',
|
||||
preview_card: be_nil
|
||||
)
|
||||
expect(status.edits.ordered.pluck(:text)).to eq %w(Foo Bar)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status has a quote and has a spoiler' do
|
||||
before { Fabricate(:quote, status: status) }
|
||||
|
||||
it 'updates text, resets card, saves edit history' do
|
||||
subject.call(status, status.account_id, spoiler_text: 'Bar', text: '')
|
||||
|
||||
expect(status.reload)
|
||||
.to have_attributes(
|
||||
text: '',
|
||||
spoiler_text: 'Bar',
|
||||
preview_card: be_nil
|
||||
)
|
||||
expect(status.edits.ordered.pluck(:text)).to eq ['Foo', '']
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when content warning changes' do
|
||||
|
||||
Reference in new issue
Block a user