mirror of
https://github.com/glitch-soc/mastodon.git
synced 2025-12-15 08:48:53 +00:00
Merge remote-tracking branch 'upstream/stable-4.4' into HEAD
Conflicts: - `app/views/layouts/application.html.haml`: Conflict because of glitch-soc's theming system. Updated the line as upstream did.
This commit is contained in:
@@ -102,6 +102,16 @@ module ApplicationHelper
|
||||
policy(record).public_send(:"#{action}?")
|
||||
end
|
||||
|
||||
def conditional_link_to(condition, name, options = {}, html_options = {}, &block)
|
||||
if condition && !current_page?(block_given? ? name : options)
|
||||
link_to(name, options, html_options, &block)
|
||||
elsif block_given?
|
||||
content_tag(:span, options, html_options, &block)
|
||||
else
|
||||
content_tag(:span, name, html_options)
|
||||
end
|
||||
end
|
||||
|
||||
def material_symbol(icon, attributes = {})
|
||||
safe_join(
|
||||
[
|
||||
|
||||
@@ -96,12 +96,17 @@ export const ensureComposeIsVisible = (getState) => {
|
||||
};
|
||||
|
||||
export function setComposeToStatus(status, text, spoiler_text) {
|
||||
return{
|
||||
type: COMPOSE_SET_STATUS,
|
||||
status,
|
||||
text,
|
||||
spoiler_text,
|
||||
};
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
|
||||
dispatch({
|
||||
type: COMPOSE_SET_STATUS,
|
||||
status,
|
||||
text,
|
||||
spoiler_text,
|
||||
maxOptions,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function changeCompose(text) {
|
||||
@@ -183,7 +188,7 @@ export function directCompose(account) {
|
||||
};
|
||||
}
|
||||
|
||||
export function submitCompose() {
|
||||
export function submitCompose(successCallback) {
|
||||
return function (dispatch, getState) {
|
||||
const status = getState().getIn(['compose', 'text'], '');
|
||||
const media = getState().getIn(['compose', 'media_attachments']);
|
||||
@@ -239,6 +244,9 @@ export function submitCompose() {
|
||||
|
||||
dispatch(insertIntoTagHistory(response.data.tags, status));
|
||||
dispatch(submitComposeSuccess({ ...response.data }));
|
||||
if (typeof successCallback === 'function') {
|
||||
successCallback(response.data);
|
||||
}
|
||||
|
||||
// To make the app more responsive, immediately push the status
|
||||
// into the columns
|
||||
|
||||
@@ -3,7 +3,7 @@ import { browserHistory } from 'mastodon/components/router';
|
||||
import api from '../api';
|
||||
|
||||
import { ensureComposeIsVisible, setComposeToStatus } from './compose';
|
||||
import { importFetchedStatus, importFetchedStatuses, importFetchedAccount } from './importer';
|
||||
import { importFetchedStatus, importFetchedAccount } from './importer';
|
||||
import { fetchContext } from './statuses_typed';
|
||||
import { deleteFromTimelines } from './timelines';
|
||||
|
||||
@@ -48,7 +48,18 @@ export function fetchStatusRequest(id, skipLoading) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
||||
/**
|
||||
* @param {string} id
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.forceFetch]
|
||||
* @param {boolean} [options.alsoFetchContext]
|
||||
* @param {string | null | undefined} [options.parentQuotePostId]
|
||||
*/
|
||||
export function fetchStatus(id, {
|
||||
forceFetch = false,
|
||||
alsoFetchContext = true,
|
||||
parentQuotePostId,
|
||||
} = {}) {
|
||||
return (dispatch, getState) => {
|
||||
const skipLoading = !forceFetch && getState().getIn(['statuses', id], null) !== null;
|
||||
|
||||
@@ -66,7 +77,7 @@ export function fetchStatus(id, forceFetch = false, alsoFetchContext = true) {
|
||||
dispatch(importFetchedStatus(response.data));
|
||||
dispatch(fetchStatusSuccess(skipLoading));
|
||||
}).catch(error => {
|
||||
dispatch(fetchStatusFail(id, error, skipLoading));
|
||||
dispatch(fetchStatusFail(id, error, skipLoading, parentQuotePostId));
|
||||
});
|
||||
};
|
||||
}
|
||||
@@ -78,21 +89,27 @@ export function fetchStatusSuccess(skipLoading) {
|
||||
};
|
||||
}
|
||||
|
||||
export function fetchStatusFail(id, error, skipLoading) {
|
||||
export function fetchStatusFail(id, error, skipLoading, parentQuotePostId) {
|
||||
return {
|
||||
type: STATUS_FETCH_FAIL,
|
||||
id,
|
||||
error,
|
||||
parentQuotePostId,
|
||||
skipLoading,
|
||||
skipAlert: true,
|
||||
};
|
||||
}
|
||||
|
||||
export function redraft(status, raw_text) {
|
||||
return {
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
return (dispatch, getState) => {
|
||||
const maxOptions = getState().server.getIn(['server', 'configuration', 'polls', 'max_options']);
|
||||
|
||||
dispatch({
|
||||
type: REDRAFT,
|
||||
status,
|
||||
raw_text,
|
||||
maxOptions,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { apiRequestPost } from 'mastodon/api';
|
||||
import type { Status, StatusVisibility } from 'mastodon/models/status';
|
||||
import type { ApiStatusJSON } from 'mastodon/api_types/statuses';
|
||||
import type { StatusVisibility } from 'mastodon/models/status';
|
||||
|
||||
export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
|
||||
apiRequestPost<{ reblog: Status }>(`v1/statuses/${statusId}/reblog`, {
|
||||
apiRequestPost<{ reblog: ApiStatusJSON }>(`v1/statuses/${statusId}/reblog`, {
|
||||
visibility,
|
||||
});
|
||||
|
||||
export const apiUnreblog = (statusId: string) =>
|
||||
apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
|
||||
apiRequestPost<ApiStatusJSON>(`v1/statuses/${statusId}/unreblog`);
|
||||
|
||||
@@ -37,9 +37,7 @@ const QuoteWrapper: React.FC<{
|
||||
);
|
||||
};
|
||||
|
||||
const NestedQuoteLink: React.FC<{
|
||||
status: Status;
|
||||
}> = ({ status }) => {
|
||||
const NestedQuoteLink: React.FC<{ status: Status }> = ({ status }) => {
|
||||
const accountId = status.get('account') as string;
|
||||
const account = useAppSelector((state) =>
|
||||
accountId ? state.accounts.get(accountId) : undefined,
|
||||
@@ -78,21 +76,40 @@ type GetStatusSelector = (
|
||||
export const QuotedStatus: React.FC<{
|
||||
quote: QuoteMap;
|
||||
contextType?: string;
|
||||
parentQuotePostId?: string | null;
|
||||
variant?: 'full' | 'link';
|
||||
nestingLevel?: number;
|
||||
}> = ({ quote, contextType, nestingLevel = 1, variant = 'full' }) => {
|
||||
}> = ({
|
||||
quote,
|
||||
contextType,
|
||||
parentQuotePostId,
|
||||
nestingLevel = 1,
|
||||
variant = 'full',
|
||||
}) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const quoteState = useAppSelector((state) =>
|
||||
parentQuotePostId
|
||||
? state.statuses.getIn([parentQuotePostId, 'quote', 'state'])
|
||||
: quote.get('state'),
|
||||
);
|
||||
|
||||
const quotedStatusId = quote.get('quoted_status');
|
||||
const quoteState = quote.get('state');
|
||||
const status = useAppSelector((state) =>
|
||||
quotedStatusId ? state.statuses.get(quotedStatusId) : undefined,
|
||||
);
|
||||
|
||||
const shouldLoadQuote = !status?.get('isLoading') && quoteState !== 'deleted';
|
||||
|
||||
useEffect(() => {
|
||||
if (!status && quotedStatusId) {
|
||||
dispatch(fetchStatus(quotedStatusId));
|
||||
if (shouldLoadQuote && quotedStatusId) {
|
||||
dispatch(
|
||||
fetchStatus(quotedStatusId, {
|
||||
parentQuotePostId,
|
||||
alsoFetchContext: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
}, [status, quotedStatusId, dispatch]);
|
||||
}, [shouldLoadQuote, quotedStatusId, parentQuotePostId, dispatch]);
|
||||
|
||||
// In order to find out whether the quoted post should be completely hidden
|
||||
// due to a matching filter, we run it through the selector used by `status_container`.
|
||||
@@ -173,6 +190,7 @@ export const QuotedStatus: React.FC<{
|
||||
{canRenderChildQuote && (
|
||||
<QuotedStatus
|
||||
quote={childQuote}
|
||||
parentQuotePostId={quotedStatusId}
|
||||
contextType={contextType}
|
||||
variant={
|
||||
nestingLevel === MAX_QUOTE_POSTS_NESTING_LEVEL ? 'link' : 'full'
|
||||
@@ -208,7 +226,11 @@ export const StatusQuoteManager = (props: StatusQuoteManagerProps) => {
|
||||
if (quote) {
|
||||
return (
|
||||
<StatusContainer {...props}>
|
||||
<QuotedStatus quote={quote} contextType={props.contextType} />
|
||||
<QuotedStatus
|
||||
quote={quote}
|
||||
parentQuotePostId={status?.get('id') as string}
|
||||
contextType={props.contextType}
|
||||
/>
|
||||
</StatusContainer>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -73,6 +73,7 @@ class ComposeForm extends ImmutablePureComponent {
|
||||
singleColumn: PropTypes.bool,
|
||||
lang: PropTypes.string,
|
||||
maxChars: PropTypes.number,
|
||||
redirectOnSuccess: PropTypes.bool,
|
||||
};
|
||||
|
||||
static defaultProps = {
|
||||
|
||||
@@ -34,7 +34,7 @@ const mapStateToProps = state => ({
|
||||
maxChars: state.getIn(['server', 'server', 'configuration', 'statuses', 'max_characters'], 500),
|
||||
});
|
||||
|
||||
const mapDispatchToProps = (dispatch) => ({
|
||||
const mapDispatchToProps = (dispatch, props) => ({
|
||||
|
||||
onChange (text) {
|
||||
dispatch(changeCompose(text));
|
||||
@@ -47,7 +47,11 @@ const mapDispatchToProps = (dispatch) => ({
|
||||
modalProps: {},
|
||||
}));
|
||||
} else {
|
||||
dispatch(submitCompose());
|
||||
dispatch(submitCompose((status) => {
|
||||
if (props.redirectOnSuccess) {
|
||||
window.location.assign(status.url);
|
||||
}
|
||||
}));
|
||||
}
|
||||
},
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import ModalContainer from 'mastodon/features/ui/containers/modal_container';
|
||||
|
||||
const Compose = () => (
|
||||
<>
|
||||
<ComposeFormContainer autoFocus withoutNavigation />
|
||||
<ComposeFormContainer autoFocus withoutNavigation redirectOnSuccess />
|
||||
<AlertsController />
|
||||
<ModalContainer />
|
||||
<LoadingBarContainer className='loading-bar' />
|
||||
|
||||
@@ -32,7 +32,7 @@ const Embed: React.FC<{ id: string }> = ({ id }) => {
|
||||
const dispatchRenderSignal = useRenderSignal();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(fetchStatus(id, false, false));
|
||||
dispatch(fetchStatus(id, { alsoFetchContext: false }));
|
||||
}, [dispatch, id]);
|
||||
|
||||
const handleToggleHidden = useCallback(() => {
|
||||
|
||||
@@ -381,7 +381,10 @@ export const DetailedStatus: React.FC<{
|
||||
{hashtagBar}
|
||||
|
||||
{status.get('quote') && (
|
||||
<QuotedStatus quote={status.get('quote')} />
|
||||
<QuotedStatus
|
||||
quote={status.get('quote')}
|
||||
parentQuotePostId={status.get('id')}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
|
||||
@@ -38,7 +38,7 @@ class FilterModal extends ImmutablePureComponent {
|
||||
|
||||
handleSuccess = () => {
|
||||
const { dispatch, statusId } = this.props;
|
||||
dispatch(fetchStatus(statusId, true));
|
||||
dispatch(fetchStatus(statusId, {forceFetch: true}));
|
||||
this.setState({ isSubmitting: false, isSubmitted: true, step: 'submitted' });
|
||||
};
|
||||
|
||||
|
||||
@@ -501,8 +501,13 @@ export const composeReducer = (state = initialState, action) => {
|
||||
}
|
||||
|
||||
if (action.status.get('poll')) {
|
||||
let options = ImmutableList(action.status.get('poll').options.map(x => x.title));
|
||||
if (options.size < action.maxOptions) {
|
||||
options = options.push('');
|
||||
}
|
||||
|
||||
map.set('poll', ImmutableMap({
|
||||
options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
|
||||
options: options,
|
||||
multiple: action.status.get('poll').multiple,
|
||||
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
|
||||
}));
|
||||
@@ -530,8 +535,13 @@ export const composeReducer = (state = initialState, action) => {
|
||||
}
|
||||
|
||||
if (action.status.get('poll')) {
|
||||
let options = ImmutableList(action.status.get('poll').options.map(x => x.title));
|
||||
if (options.size < action.maxOptions) {
|
||||
options = options.push('');
|
||||
}
|
||||
|
||||
map.set('poll', ImmutableMap({
|
||||
options: ImmutableList(action.status.get('poll').options.map(x => x.title)),
|
||||
options: options,
|
||||
multiple: action.status.get('poll').multiple,
|
||||
expires_in: expiresInFromExpiresAt(action.status.get('poll').expires_at),
|
||||
}));
|
||||
|
||||
@@ -73,8 +73,15 @@ export default function statuses(state = initialState, action) {
|
||||
switch(action.type) {
|
||||
case STATUS_FETCH_REQUEST:
|
||||
return state.setIn([action.id, 'isLoading'], true);
|
||||
case STATUS_FETCH_FAIL:
|
||||
return state.delete(action.id);
|
||||
case STATUS_FETCH_FAIL: {
|
||||
if (action.parentQuotePostId && action.error.status === 404) {
|
||||
return state
|
||||
.delete(action.id)
|
||||
.setIn([action.parentQuotePostId, 'quote', 'state'], 'deleted')
|
||||
} else {
|
||||
return state.delete(action.id);
|
||||
}
|
||||
}
|
||||
case STATUS_IMPORT:
|
||||
return importStatus(state, action.status);
|
||||
case STATUSES_IMPORT:
|
||||
|
||||
@@ -1888,7 +1888,7 @@ a.sparkline {
|
||||
font-size: 15px;
|
||||
line-height: 22px;
|
||||
|
||||
li {
|
||||
> li {
|
||||
counter-increment: step 1;
|
||||
padding-inline-start: 2.5rem;
|
||||
padding-bottom: 8px;
|
||||
|
||||
@@ -2333,6 +2333,7 @@ a .account__avatar {
|
||||
.detailed-status__display-name,
|
||||
.detailed-status__datetime,
|
||||
.detailed-status__application,
|
||||
.detailed-status__link,
|
||||
.account__display-name {
|
||||
text-decoration: none;
|
||||
}
|
||||
@@ -2365,7 +2366,8 @@ a.account__display-name {
|
||||
}
|
||||
|
||||
.detailed-status__application,
|
||||
.detailed-status__datetime {
|
||||
.detailed-status__datetime,
|
||||
.detailed-status__link {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
@@ -2551,8 +2553,9 @@ a.account__display-name {
|
||||
}
|
||||
|
||||
.status__relative-time,
|
||||
.detailed-status__datetime {
|
||||
&:hover {
|
||||
.detailed-status__datetime,
|
||||
.detailed-status__link {
|
||||
&:is(a):hover {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -101,7 +101,8 @@
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
&.editable {
|
||||
&.editable,
|
||||
&.disabled {
|
||||
align-items: center;
|
||||
overflow: visible;
|
||||
}
|
||||
@@ -159,7 +160,8 @@
|
||||
}
|
||||
}
|
||||
|
||||
&__option.editable &__input {
|
||||
&__option.editable &__input,
|
||||
&__option.disabled &__input {
|
||||
&:active,
|
||||
&:focus,
|
||||
&:hover {
|
||||
|
||||
@@ -61,6 +61,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity
|
||||
|
||||
ActivityPub::Forwarder.new(@account, @json, @quote.status).forward!
|
||||
@quote.reject!
|
||||
DistributionWorker.perform_async(@quote.status_id, { 'update' => true })
|
||||
end
|
||||
|
||||
def forwarder
|
||||
|
||||
@@ -57,8 +57,16 @@ class Antispam
|
||||
end
|
||||
|
||||
def report_if_needed!(account)
|
||||
return if Report.unresolved.exists?(account: Account.representative, target_account: account)
|
||||
return if system_reports.unresolved.exists?(target_account: account)
|
||||
|
||||
Report.create!(account: Account.representative, target_account: account, category: :spam, comment: 'Account automatically reported for posting a banned URL')
|
||||
system_reports.create!(
|
||||
category: :spam,
|
||||
comment: 'Account automatically reported for posting a banned URL',
|
||||
target_account: account
|
||||
)
|
||||
end
|
||||
|
||||
def system_reports
|
||||
Account.representative.reports
|
||||
end
|
||||
end
|
||||
|
||||
@@ -116,7 +116,7 @@ class Account < ApplicationRecord
|
||||
|
||||
# Local user validations
|
||||
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: USERNAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_username? && !actor_type_application? }
|
||||
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? }
|
||||
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? && !user&.bypass_registration_checks }
|
||||
validates :display_name, length: { maximum: DISPLAY_NAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_display_name? }
|
||||
validates :note, note_length: { maximum: NOTE_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_note? }
|
||||
validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? }
|
||||
|
||||
@@ -44,12 +44,36 @@ class StatusEdit < ApplicationRecord
|
||||
scope :ordered, -> { order(id: :asc) }
|
||||
|
||||
delegate :local?, :application, :edited?, :edited_at,
|
||||
:discarded?, :visibility, :language, to: :status
|
||||
:discarded?, :reply?, :visibility, :language, to: :status
|
||||
|
||||
def with_media?
|
||||
ordered_media_attachments.any?
|
||||
end
|
||||
|
||||
def with_poll?
|
||||
poll_options.present?
|
||||
end
|
||||
|
||||
def poll
|
||||
return @poll if defined?(@poll)
|
||||
return @poll = nil if poll_options.blank?
|
||||
|
||||
@poll = Poll.new({
|
||||
options: poll_options,
|
||||
account_id: account_id,
|
||||
status_id: status_id,
|
||||
})
|
||||
end
|
||||
|
||||
alias preloadable_poll poll
|
||||
|
||||
def emojis
|
||||
return @emojis if defined?(@emojis)
|
||||
|
||||
@emojis = CustomEmoji.from_text([spoiler_text, text].join(' '), status.account.domain)
|
||||
fields = [spoiler_text, text]
|
||||
fields += preloadable_poll.options unless preloadable_poll.nil?
|
||||
|
||||
@emojis = CustomEmoji.from_text(fields.join(' '), status.account.domain)
|
||||
end
|
||||
|
||||
def ordered_media_attachments
|
||||
|
||||
@@ -142,7 +142,9 @@ class User < ApplicationRecord
|
||||
delegate :can?, to: :role
|
||||
|
||||
attr_reader :invite_code, :date_of_birth
|
||||
attr_writer :external, :bypass_registration_checks, :current_account
|
||||
attr_writer :external, :current_account
|
||||
|
||||
attribute :bypass_registration_checks, :boolean, default: false
|
||||
|
||||
def self.those_who_can(*any_of_privileges)
|
||||
matching_role_ids = UserRole.that_can(*any_of_privileges).map(&:id)
|
||||
@@ -505,10 +507,6 @@ class User < ApplicationRecord
|
||||
!!@external
|
||||
end
|
||||
|
||||
def bypass_registration_checks?
|
||||
@bypass_registration_checks
|
||||
end
|
||||
|
||||
def sanitize_role
|
||||
self.role = nil if role.present? && role.everyone?
|
||||
end
|
||||
|
||||
@@ -74,7 +74,7 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
end
|
||||
|
||||
def update_interaction_policies!
|
||||
@status.quote_approval_policy = @status_parser.quote_policy
|
||||
@status.update(quote_approval_policy: @status_parser.quote_policy)
|
||||
end
|
||||
|
||||
def update_media_attachments!
|
||||
@@ -112,6 +112,8 @@ class ActivityPub::ProcessStatusUpdateService < BaseService
|
||||
@status.ordered_media_attachment_ids = @next_media_attachments.map(&:id)
|
||||
|
||||
@media_attachments_changed = true if @status.ordered_media_attachment_ids != previous_media_attachments_ids
|
||||
|
||||
@status.media_attachments.reload if @media_attachments_changed
|
||||
end
|
||||
|
||||
def download_media_files!
|
||||
|
||||
@@ -55,6 +55,7 @@ class BackupService < BaseService
|
||||
def build_archive!
|
||||
tmp_file = Tempfile.new(%w(archive .zip))
|
||||
|
||||
Zip.write_zip64_support = true
|
||||
Zip::File.open(tmp_file, create: true) do |zipfile|
|
||||
dump_outbox!(zipfile)
|
||||
dump_media_attachments!(zipfile)
|
||||
|
||||
@@ -1,6 +0,0 @@
|
||||
- if status.ordered_media_attachments.first.video?
|
||||
= render_video_component(status, visible: false)
|
||||
- elsif status.ordered_media_attachments.first.audio?
|
||||
= render_audio_component(status)
|
||||
- else
|
||||
= render_media_gallery_component(status, visible: false)
|
||||
@@ -1,53 +0,0 @@
|
||||
.batch-table__row
|
||||
%label.batch-table__row__select.batch-checkbox
|
||||
= f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
|
||||
.batch-table__row__content
|
||||
.status__card
|
||||
- if status.reblog?
|
||||
.status__prepend
|
||||
= material_symbol('repeat')
|
||||
= t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id)))
|
||||
- elsif status.reply? && status.in_reply_to_id.present?
|
||||
.status__prepend
|
||||
= material_symbol('reply')
|
||||
= t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil))
|
||||
.status__content><
|
||||
- if status.proper.spoiler_text.blank?
|
||||
= prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis)
|
||||
- else
|
||||
%details<
|
||||
%summary><
|
||||
%strong> Content warning: #{prerender_custom_emojis(h(status.proper.spoiler_text), status.proper.emojis)}
|
||||
= prerender_custom_emojis(status_content_format(status.proper), status.proper.emojis)
|
||||
|
||||
- unless status.proper.ordered_media_attachments.empty?
|
||||
= render partial: 'admin/reports/media_attachments', locals: { status: status.proper }
|
||||
|
||||
.detailed-status__meta
|
||||
- if status.application
|
||||
= status.application.name
|
||||
·
|
||||
|
||||
= link_to admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do
|
||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }= l(status.created_at)
|
||||
- if status.edited?
|
||||
·
|
||||
= link_to t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'formatted')),
|
||||
admin_account_status_path(status.account_id, status),
|
||||
class: 'detailed-status__datetime'
|
||||
- if status.discarded?
|
||||
·
|
||||
%span.negative-hint= t('admin.statuses.deleted')
|
||||
·
|
||||
|
||||
= material_symbol visibility_icon(status)
|
||||
= t("statuses.visibilities.#{status.visibility}")
|
||||
·
|
||||
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', rel: 'noopener' do
|
||||
= t('admin.statuses.view_publicly')
|
||||
|
||||
- if status.proper.sensitive?
|
||||
·
|
||||
= material_symbol('visibility_off')
|
||||
= t('stream_entries.sensitive_content')
|
||||
@@ -57,7 +57,7 @@
|
||||
- if @statuses.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
- else
|
||||
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
|
||||
= render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f }
|
||||
|
||||
- if @report.unresolved?
|
||||
%hr.spacer/
|
||||
|
||||
40
app/views/admin/shared/_status.html.haml
Normal file
40
app/views/admin/shared/_status.html.haml
Normal file
@@ -0,0 +1,40 @@
|
||||
-# locals: (status:)
|
||||
|
||||
.status__card><
|
||||
- if status.reblog?
|
||||
.status__prepend
|
||||
= material_symbol('repeat')
|
||||
= t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(status.proper.account, path: admin_account_status_path(status.proper.account.id, status.proper.id)))
|
||||
- elsif status.reply? && status.in_reply_to_id.present?
|
||||
.status__prepend
|
||||
= material_symbol('reply')
|
||||
= t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(status.in_reply_to_account, path: status.thread.present? ? admin_account_status_path(status.thread.account_id, status.in_reply_to_id) : nil))
|
||||
|
||||
= render partial: 'admin/shared/status_content', locals: { status: status.proper }
|
||||
|
||||
.detailed-status__meta
|
||||
- if status.application
|
||||
= status.application.name
|
||||
·
|
||||
= conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status), class: 'detailed-status__datetime' do
|
||||
%time.formatted{ datetime: status.created_at.iso8601, title: l(status.created_at) }><= l(status.created_at)
|
||||
- if status.edited?
|
||||
·
|
||||
= conditional_link_to can?(:show, status), admin_account_status_path(status.account.id, status, { anchor: 'history' }), class: 'detailed-status__datetime' do
|
||||
%span><= t('statuses.edited_at_html', date: content_tag(:time, l(status.edited_at), datetime: status.edited_at.iso8601, title: l(status.edited_at), class: 'relative-formatted'))
|
||||
- if status.discarded?
|
||||
·
|
||||
%span.negative-hint= t('admin.statuses.deleted')
|
||||
- unless status.reblog?
|
||||
·
|
||||
%span<
|
||||
= material_symbol(visibility_icon(status))
|
||||
= t("statuses.visibilities.#{status.visibility}")
|
||||
- if status.proper.sensitive?
|
||||
·
|
||||
= material_symbol('visibility_off')
|
||||
= t('stream_entries.sensitive_content')
|
||||
- unless status.direct_visibility?
|
||||
·
|
||||
= link_to ActivityPub::TagManager.instance.url_for(status.proper), class: 'detailed-status__link', target: 'blank', rel: 'noopener' do
|
||||
= t('admin.statuses.view_publicly')
|
||||
22
app/views/admin/shared/_status_attachments.html.haml
Normal file
22
app/views/admin/shared/_status_attachments.html.haml
Normal file
@@ -0,0 +1,22 @@
|
||||
- if status.with_poll?
|
||||
.poll
|
||||
%ul
|
||||
- status.preloadable_poll.options.each_with_index do |option, _index|
|
||||
%li
|
||||
%label.poll__option.disabled<>
|
||||
- if status.preloadable_poll.multiple?
|
||||
%span.poll__input.checkbox{ role: 'checkbox', 'aria-label': option }
|
||||
- else
|
||||
%span.poll__input{ role: 'radio', 'aria-label': option }
|
||||
%span.poll__option__text
|
||||
= prerender_custom_emojis(html_aware_format(option, status.local?, multiline: false), status.emojis)
|
||||
%button.button.button-secondary{ disabled: true }
|
||||
= t('polls.vote')
|
||||
|
||||
- if status.with_media?
|
||||
- if status.ordered_media_attachments.first.video?
|
||||
= render_video_component(status, visible: false)
|
||||
- elsif status.ordered_media_attachments.first.audio?
|
||||
= render_audio_component(status)
|
||||
- else
|
||||
= render_media_gallery_component(status, visible: false)
|
||||
5
app/views/admin/shared/_status_batch_row.html.haml
Normal file
5
app/views/admin/shared/_status_batch_row.html.haml
Normal file
@@ -0,0 +1,5 @@
|
||||
.batch-table__row
|
||||
%label.batch-table__row__select.batch-checkbox
|
||||
= f.check_box :status_ids, { multiple: true, include_hidden: false }, status.id
|
||||
.batch-table__row__content
|
||||
= render partial: 'admin/shared/status', object: status
|
||||
10
app/views/admin/shared/_status_content.html.haml
Normal file
10
app/views/admin/shared/_status_content.html.haml
Normal file
@@ -0,0 +1,10 @@
|
||||
.status__content><
|
||||
- if status.spoiler_text.present?
|
||||
%details<
|
||||
%summary><
|
||||
%strong> Content warning: #{prerender_custom_emojis(h(status.spoiler_text), status.emojis)}
|
||||
= prerender_custom_emojis(status_content_format(status), status.emojis)
|
||||
= render partial: 'admin/shared/status_attachments', locals: { status: status.proper }
|
||||
- else
|
||||
= prerender_custom_emojis(status_content_format(status), status.emojis)
|
||||
= render partial: 'admin/shared/status_attachments', locals: { status: status.proper }
|
||||
@@ -9,17 +9,7 @@
|
||||
%time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at)
|
||||
|
||||
.status
|
||||
.status__content><
|
||||
- if status_edit.spoiler_text.blank?
|
||||
= prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis)
|
||||
- else
|
||||
%details<
|
||||
%summary><
|
||||
%strong> Content warning: #{prerender_custom_emojis(h(status_edit.spoiler_text), status_edit.emojis)}
|
||||
= prerender_custom_emojis(status_content_format(status_edit), status_edit.emojis)
|
||||
|
||||
- unless status_edit.ordered_media_attachments.empty?
|
||||
= render partial: 'admin/reports/media_attachments', locals: { status: status_edit }
|
||||
= render partial: 'admin/shared/status_content', locals: { status: status_edit }
|
||||
|
||||
.detailed-status__meta
|
||||
%time.formatted{ datetime: status_edit.created_at.iso8601, title: l(status_edit.created_at) }= l(status_edit.created_at)
|
||||
|
||||
@@ -47,6 +47,6 @@
|
||||
- if @statuses.empty?
|
||||
= nothing_here 'nothing-here--under-tabs'
|
||||
- else
|
||||
= render partial: 'admin/reports/status', collection: @statuses, locals: { f: f }
|
||||
= render partial: 'admin/shared/status_batch_row', collection: @statuses, as: :status, locals: { f: f }
|
||||
|
||||
= paginate @statuses
|
||||
|
||||
@@ -53,52 +53,11 @@
|
||||
|
||||
%h3= t('admin.statuses.contents')
|
||||
|
||||
.status__card
|
||||
- if @status.reblog?
|
||||
.status__prepend
|
||||
= material_symbol('repeat')
|
||||
= t('statuses.boosted_from_html', acct_link: admin_account_inline_link_to(@status.proper.account, path: admin_account_status_path(@status.proper.account.id, @status.proper.id)))
|
||||
- elsif @status.reply? && @status.in_reply_to_id.present?
|
||||
.status__prepend
|
||||
= material_symbol('reply')
|
||||
= t('admin.statuses.replied_to_html', acct_link: admin_account_inline_link_to(@status.in_reply_to_account, path: @status.thread.present? ? admin_account_status_path(@status.thread.account_id, @status.in_reply_to_id) : nil))
|
||||
.status__content><
|
||||
- if @status.proper.spoiler_text.blank?
|
||||
= prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis)
|
||||
- else
|
||||
%details<
|
||||
%summary><
|
||||
%strong> Content warning: #{prerender_custom_emojis(h(@status.proper.spoiler_text), @status.proper.emojis)}
|
||||
= prerender_custom_emojis(status_content_format(@status.proper), @status.proper.emojis)
|
||||
|
||||
- unless @status.proper.ordered_media_attachments.empty?
|
||||
= render partial: 'admin/reports/media_attachments', locals: { status: @status.proper }
|
||||
|
||||
.detailed-status__meta
|
||||
- if @status.application
|
||||
= @status.application.name
|
||||
·
|
||||
%span.detailed-status__datetime
|
||||
%time.formatted{ datetime: @status.created_at.iso8601, title: l(@status.created_at) }= l(@status.created_at)
|
||||
- if @status.edited?
|
||||
·
|
||||
%span.detailed-status__datetime
|
||||
= t('statuses.edited_at_html', date: content_tag(:time, l(@status.edited_at), datetime: @status.edited_at.iso8601, title: l(@status.edited_at), class: 'formatted'))
|
||||
- if @status.discarded?
|
||||
·
|
||||
%span.negative-hint= t('admin.statuses.deleted')
|
||||
- unless @status.reblog?
|
||||
·
|
||||
= material_symbol(visibility_icon(@status))
|
||||
= t("statuses.visibilities.#{@status.visibility}")
|
||||
- if @status.proper.sensitive?
|
||||
·
|
||||
= material_symbol('visibility_off')
|
||||
= t('stream_entries.sensitive_content')
|
||||
= render partial: 'admin/shared/status', object: @status
|
||||
|
||||
%hr.spacer/
|
||||
|
||||
%h3= t('admin.statuses.history')
|
||||
%h3#history= t('admin.statuses.history')
|
||||
- if @status.edits.empty?
|
||||
%p= t('admin.statuses.no_history')
|
||||
- else
|
||||
|
||||
@@ -30,7 +30,7 @@
|
||||
= vite_react_refresh_tag
|
||||
= vite_polyfills_tag
|
||||
-# Needed for the wicg-inert polyfill. It needs to be on it's own <style> tag, with this `id`
|
||||
= vite_stylesheet_tag 'styles/entrypoints/inert.scss', media: 'all', id: 'inert-style' # TODO: flavour
|
||||
= vite_stylesheet_tag 'styles/entrypoints/inert.scss', media: 'all', id: 'inert-style', crossorigin: 'anonymous' # TODO: flavour
|
||||
= flavoured_vite_typescript_tag 'common.ts', crossorigin: 'anonymous'
|
||||
|
||||
= vite_preload_file_tag "mastodon/locales/#{I18n.locale}.json" # TODO: fix preload for flavour
|
||||
|
||||
@@ -21,8 +21,9 @@ class Scheduler::SelfDestructScheduler
|
||||
|
||||
def sidekiq_overwhelmed?
|
||||
redis_mem_info = Sidekiq.default_configuration.redis_info
|
||||
maxmemory = [redis_mem_info['maxmemory'].to_f, redis_mem_info['total_system_memory'].to_f].filter(&:positive?).min
|
||||
|
||||
Sidekiq::Stats.new.enqueued > MAX_ENQUEUED || redis_mem_info['used_memory'].to_f > redis_mem_info['total_system_memory'].to_f * MAX_REDIS_MEM_USAGE
|
||||
Sidekiq::Stats.new.enqueued > MAX_ENQUEUED || redis_mem_info['used_memory'].to_f > maxmemory * MAX_REDIS_MEM_USAGE
|
||||
end
|
||||
|
||||
def delete_accounts!
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
# Disable httplog in production unless log_level is `debug`
|
||||
if !Rails.env.production? || Rails.configuration.log_level == :debug
|
||||
# Disable in production unless log level is `debug`
|
||||
if Rails.env.local? || Rails.logger.debug?
|
||||
require 'httplog'
|
||||
|
||||
HttpLog.configure do |config|
|
||||
|
||||
@@ -1707,6 +1707,7 @@ en:
|
||||
self_vote: You cannot vote in your own polls
|
||||
too_few_options: must have more than one item
|
||||
too_many_options: can't contain more than %{max} items
|
||||
vote: Vote
|
||||
preferences:
|
||||
other: Other
|
||||
posting_defaults: Posting defaults
|
||||
@@ -1876,12 +1877,12 @@ en:
|
||||
ownership: Someone else's post cannot be pinned
|
||||
reblog: A boost cannot be pinned
|
||||
quote_policies:
|
||||
followers: Only your followers
|
||||
nobody: Nobody
|
||||
public: Everyone
|
||||
followers: Followers only
|
||||
nobody: Just me
|
||||
public: Anyone
|
||||
title: '%{name}: "%{quote}"'
|
||||
visibilities:
|
||||
direct: Direct
|
||||
direct: Private mention
|
||||
private: Followers-only
|
||||
private_long: Only show to followers
|
||||
public: Public
|
||||
|
||||
@@ -363,10 +363,6 @@ namespace :api, format: false do
|
||||
namespace :web do
|
||||
resource :settings, only: [:update]
|
||||
resources :embeds, only: [:show]
|
||||
resources :push_subscriptions, only: [:create, :destroy] do
|
||||
member do
|
||||
put :update
|
||||
end
|
||||
end
|
||||
resources :push_subscriptions, only: [:create, :destroy, :update]
|
||||
end
|
||||
end
|
||||
|
||||
@@ -12,27 +12,22 @@ RSpec.describe ActivityPub::Activity::Delete do
|
||||
id: 'foo',
|
||||
type: 'Delete',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(status),
|
||||
object: object_json,
|
||||
signature: 'foo',
|
||||
}.with_indifferent_access
|
||||
}.deep_stringify_keys
|
||||
end
|
||||
|
||||
let(:object_json) { ActivityPub::TagManager.instance.uri_for(status) }
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
before do
|
||||
subject.perform
|
||||
end
|
||||
|
||||
it 'deletes sender\'s status' do
|
||||
subject.perform
|
||||
expect(Status.find_by(id: status.id)).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status has been reblogged' do
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
context 'when the status has been reblogged' do
|
||||
let!(:reblogger) { Fabricate(:account) }
|
||||
let!(:follower) { Fabricate(:account, username: 'follower', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
|
||||
let!(:reblog) { Fabricate(:status, account: reblogger, reblog: status) }
|
||||
@@ -55,12 +50,8 @@ RSpec.describe ActivityPub::Activity::Delete do
|
||||
expect { reblog.reload }.to raise_error(ActiveRecord::RecordNotFound)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the status has been reported' do
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
context 'when the status has been reported' do
|
||||
let!(:reporter) { Fabricate(:account) }
|
||||
|
||||
before do
|
||||
@@ -76,23 +67,9 @@ RSpec.describe ActivityPub::Activity::Delete do
|
||||
expect(Status.with_discarded.find_by(id: status.id)).to_not be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the deleted object is an account' do
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Delete',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
signature: 'foo',
|
||||
}.with_indifferent_access
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
|
||||
context 'when the deleted object is an account' do
|
||||
let(:object_json) { ActivityPub::TagManager.instance.uri_for(sender) }
|
||||
let(:service) { instance_double(DeleteAccountService, call: true) }
|
||||
|
||||
before do
|
||||
@@ -106,27 +83,36 @@ RSpec.describe ActivityPub::Activity::Delete do
|
||||
.to have_received(:call).with(sender, { reserve_username: false, skip_activitypub: true })
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the deleted object is a quote authorization' do
|
||||
let(:quoter) { Fabricate(:account, domain: 'b.example.com') }
|
||||
let(:status) { Fabricate(:status, account: quoter) }
|
||||
let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') }
|
||||
let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) }
|
||||
context 'when the deleted object is a quote authorization' do
|
||||
let(:quoter) { Fabricate(:account, domain: 'b.example.com') }
|
||||
let(:status) { Fabricate(:status, account: quoter) }
|
||||
let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') }
|
||||
let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) }
|
||||
|
||||
let(:json) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Delete',
|
||||
actor: ActivityPub::TagManager.instance.uri_for(sender),
|
||||
object: quote.approval_uri,
|
||||
signature: 'foo',
|
||||
}.with_indifferent_access
|
||||
let(:object_json) { quote.approval_uri }
|
||||
|
||||
it 'revokes the authorization' do
|
||||
expect { subject.perform }
|
||||
.to change { quote.reload.state }.to('revoked')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#perform' do
|
||||
subject { described_class.new(json, sender) }
|
||||
context 'when the deleted object is an inlined quote authorization' do
|
||||
let(:quoter) { Fabricate(:account, domain: 'b.example.com') }
|
||||
let(:status) { Fabricate(:status, account: quoter) }
|
||||
let(:quoted_status) { Fabricate(:status, account: sender, uri: 'https://example.com/statuses/1234') }
|
||||
let!(:quote) { Fabricate(:quote, approval_uri: 'https://example.com/approvals/1234', state: :accepted, status: status, quoted_status: quoted_status) }
|
||||
|
||||
let(:object_json) do
|
||||
{
|
||||
type: 'QuoteAuthorization',
|
||||
id: quote.approval_uri,
|
||||
attributedTo: ActivityPub::TagManager.instance.uri_for(quoted_status.account),
|
||||
interactionTarget: ActivityPub::TagManager.instance.uri_for(quoted_status),
|
||||
interactingObject: ActivityPub::TagManager.instance.uri_for(status),
|
||||
}.deep_stringify_keys
|
||||
end
|
||||
|
||||
it 'revokes the authorization' do
|
||||
expect { subject.perform }
|
||||
|
||||
47
spec/lib/antispam_spec.rb
Normal file
47
spec/lib/antispam_spec.rb
Normal file
@@ -0,0 +1,47 @@
|
||||
# frozen_string_literal: true
|
||||
|
||||
require 'rails_helper'
|
||||
|
||||
RSpec.describe Antispam do
|
||||
describe '#local_preflight_check!' do
|
||||
subject { described_class.new.local_preflight_check!(status) }
|
||||
|
||||
let(:status) { Fabricate :status }
|
||||
|
||||
context 'when there is no spammy text registered' do
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
|
||||
context 'with spammy text' do
|
||||
before { redis.sadd 'antispam:spammy_texts', 'https://banned.example' }
|
||||
|
||||
context 'when status matches' do
|
||||
let(:status) { Fabricate :status, text: 'I use https://banned.example urls in my text' }
|
||||
|
||||
it 'raises error and reports' do
|
||||
expect { subject }
|
||||
.to raise_error(described_class::SilentlyDrop)
|
||||
.and change(spam_reports, :count).by(1)
|
||||
end
|
||||
|
||||
context 'when report already exists' do
|
||||
before { Fabricate :report, account: Account.representative, target_account: status.account }
|
||||
|
||||
it 'raises error and does not report' do
|
||||
expect { subject }
|
||||
.to raise_error(described_class::SilentlyDrop)
|
||||
.and not_change(spam_reports, :count)
|
||||
end
|
||||
end
|
||||
|
||||
def spam_reports
|
||||
Account.representative.reports.where(target_account: status.account).spam
|
||||
end
|
||||
end
|
||||
|
||||
context 'when status does not match' do
|
||||
it { is_expected.to be_nil }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -32,6 +32,7 @@ RSpec.describe Mastodon::CLI::Accounts do
|
||||
|
||||
describe '#create' do
|
||||
let(:action) { :create }
|
||||
let(:username) { 'tootctl_username' }
|
||||
|
||||
shared_examples 'a new user with given email address and username' do
|
||||
it 'creates user and accounts from options and displays success message' do
|
||||
@@ -48,18 +49,24 @@ RSpec.describe Mastodon::CLI::Accounts do
|
||||
end
|
||||
|
||||
def account_from_options
|
||||
Account.find_local('tootctl_username')
|
||||
Account.find_local(username)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when required USERNAME and --email are provided' do
|
||||
let(:arguments) { ['tootctl_username'] }
|
||||
let(:arguments) { [username] }
|
||||
|
||||
context 'with USERNAME and --email only' do
|
||||
let(:options) { { email: 'tootctl@example.com' } }
|
||||
|
||||
it_behaves_like 'a new user with given email address and username'
|
||||
|
||||
context 'with a reserved username' do
|
||||
let(:username) { 'security' }
|
||||
|
||||
it_behaves_like 'a new user with given email address and username'
|
||||
end
|
||||
|
||||
context 'with invalid --email value' do
|
||||
let(:options) { { email: 'invalid' } }
|
||||
|
||||
|
||||
@@ -64,7 +64,7 @@ RSpec.describe 'API Web Push Subscriptions' do
|
||||
end
|
||||
end
|
||||
|
||||
describe 'PUT /api/web/push_subscriptions' do
|
||||
describe 'PUT /api/web/push_subscriptions/:id' do
|
||||
before { sign_in Fabricate :user }
|
||||
|
||||
let(:subscription) { Fabricate :web_push_subscription }
|
||||
|
||||
@@ -316,6 +316,23 @@ RSpec.describe ActivityPub::FetchRemoteStatusService do
|
||||
expect(existing_status.edits).to_not be_empty
|
||||
end
|
||||
end
|
||||
|
||||
context 'with an implicit update to quoting policy' do
|
||||
let(:object) do
|
||||
note.merge({
|
||||
'content' => existing_status.text,
|
||||
'interactionPolicy' => {
|
||||
'canQuote' => {
|
||||
'automaticApproval' => ['https://www.w3.org/ns/activitystreams#Public'],
|
||||
},
|
||||
},
|
||||
})
|
||||
end
|
||||
|
||||
it 'updates status' do
|
||||
expect(existing_status.reload.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
@@ -343,6 +343,42 @@ RSpec.describe ActivityPub::ProcessStatusUpdateService do
|
||||
end
|
||||
end
|
||||
|
||||
context 'when originally without media attachments and text is removed' do
|
||||
before do
|
||||
stub_request(:get, 'https://example.com/foo.png').to_return(body: attachment_fixture('emojo.png'))
|
||||
end
|
||||
|
||||
let(:payload) do
|
||||
{
|
||||
'@context': 'https://www.w3.org/ns/activitystreams',
|
||||
id: 'foo',
|
||||
type: 'Note',
|
||||
content: '',
|
||||
updated: '2021-09-08T22:39:25Z',
|
||||
attachment: [
|
||||
{ type: 'Image', mediaType: 'image/png', url: 'https://example.com/foo.png' },
|
||||
],
|
||||
}
|
||||
end
|
||||
|
||||
it 'updates media attachments, fetches attachment, records media and text removal in edit' do
|
||||
subject.call(status, json, json)
|
||||
|
||||
expect(status.reload.ordered_media_attachments.first)
|
||||
.to be_present
|
||||
.and(have_attributes(remote_url: 'https://example.com/foo.png'))
|
||||
|
||||
expect(a_request(:get, 'https://example.com/foo.png'))
|
||||
.to have_been_made
|
||||
|
||||
expect(status.edits.reload.last.ordered_media_attachment_ids)
|
||||
.to_not be_empty
|
||||
|
||||
expect(status.edits.reload.last.text)
|
||||
.to_not be_present
|
||||
end
|
||||
end
|
||||
|
||||
context 'when originally with media attachments' do
|
||||
let(:media_attachments) { [Fabricate(:media_attachment, remote_url: 'https://example.com/foo.png'), Fabricate(:media_attachment, remote_url: 'https://example.com/unused.png')] }
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ RSpec.describe 'Share page', :js, :streaming do
|
||||
fill_in_form
|
||||
|
||||
expect(page)
|
||||
.to have_css('.notification-bar-message', text: frontend_translations('compose.published.body'))
|
||||
.to have_current_path(%r{/@bob/[0-9]+})
|
||||
end
|
||||
|
||||
def fill_in_form
|
||||
|
||||
Reference in New Issue
Block a user