Merge branch 'main' into feature/require-mfa-by-admin

This commit is contained in:
FredysFonseca 2025-08-06 23:03:46 -04:00 committed by GitHub
commit 1f6f3bf01a
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
55 changed files with 421 additions and 48 deletions

View File

@ -1,6 +1,6 @@
# This configuration was generated by
# `rubocop --auto-gen-config --auto-gen-only-exclude --no-offense-counts --no-auto-gen-timestamp`
# using RuboCop version 1.79.0.
# using RuboCop version 1.79.2.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new

View File

@ -90,7 +90,7 @@ GEM
public_suffix (>= 2.0.2, < 7.0)
aes_key_wrap (1.1.0)
android_key_attestation (0.3.0)
annotaterb (4.17.0)
annotaterb (4.18.0)
activerecord (>= 6.0.0)
activesupport (>= 6.0.0)
ast (2.4.3)
@ -765,7 +765,7 @@ GEM
rspec-mocks (~> 3.0)
sidekiq (>= 5, < 9)
rspec-support (3.13.4)
rubocop (1.79.1)
rubocop (1.79.2)
json (~> 2.3)
language_server-protocol (~> 3.17.0.2)
lint_roller (~> 1.1.0)

View File

@ -82,6 +82,7 @@ class Api::V1::StatusesController < Api::BaseController
text: status_params[:status],
thread: @thread,
quoted_status: @quoted_status,
quote_approval_policy: quote_approval_policy,
media_ids: status_params[:media_ids],
sensitive: status_params[:sensitive],
spoiler_text: status_params[:spoiler_text],
@ -113,7 +114,8 @@ class Api::V1::StatusesController < Api::BaseController
sensitive: status_params[:sensitive],
language: status_params[:language],
spoiler_text: status_params[:spoiler_text],
poll: status_params[:poll]
poll: status_params[:poll],
quote_approval_policy: quote_approval_policy
)
render json: @status, serializer: REST::StatusSerializer
@ -180,6 +182,7 @@ class Api::V1::StatusesController < Api::BaseController
:status,
:in_reply_to_id,
:quoted_status_id,
:quote_approval_policy,
:sensitive,
:spoiler_text,
:visibility,
@ -202,6 +205,23 @@ class Api::V1::StatusesController < Api::BaseController
)
end
def quote_approval_policy
# TODO: handle `nil` separately
return nil unless Mastodon::Feature.outgoing_quotes_enabled? && status_params[:quote_approval_policy].present?
case status_params[:quote_approval_policy]
when 'public'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16
when 'followers'
Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16
when 'nobody'
0
else
# TODO: raise more useful message
raise ActiveRecord::RecordInvalid
end
end
def serializer_for_status
@status.is_a?(ScheduledStatus) ? REST::ScheduledStatusSerializer : REST::StatusSerializer
end

View File

@ -1 +1,3 @@
Images in this folder are based on [Tabler.io icons](https://tabler.io/icons).
Seems to be 1.5 width icons scaled to 64×64px and centered above a blue square with round corners (24px).

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

View File

@ -1,4 +1,8 @@
import { apiReblog, apiUnreblog } from 'mastodon/api/interactions';
import {
apiReblog,
apiUnreblog,
apiRevokeQuote,
} from 'mastodon/api/interactions';
import type { StatusVisibility } from 'mastodon/models/status';
import { createDataLoadingThunk } from 'mastodon/store/typed_functions';
@ -33,3 +37,19 @@ export const unreblog = createDataLoadingThunk(
return discardLoadData;
},
);
export const revokeQuote = createDataLoadingThunk(
'status/revoke_quote',
({
statusId,
quotedStatusId,
}: {
statusId: string;
quotedStatusId: string;
}) => apiRevokeQuote(quotedStatusId, statusId),
(data, { dispatch, discardLoadData }) => {
dispatch(importFetchedStatus(data));
return discardLoadData;
},
);

View File

@ -31,7 +31,9 @@ import { NOTIFICATIONS_FILTER_SET } from './notifications';
import { saveSettings } from './settings';
function excludeAllTypesExcept(filter: string) {
return allNotificationTypes.filter((item) => item !== filter);
return allNotificationTypes.filter(
(item) => item !== filter && !(item === 'quote' && filter === 'mention'),
);
}
function getExcludedTypes(state: RootState) {
@ -156,7 +158,8 @@ export const processNewNotificationForGroups = createAppAsyncThunk(
const showInColumn =
activeFilter === 'all'
? notificationShows[notification.type] !== false
: activeFilter === notification.type;
: activeFilter === notification.type ||
(activeFilter === 'mention' && notification.type === 'quote');
if (!showInColumn) return;

View File

@ -8,3 +8,8 @@ export const apiReblog = (statusId: string, visibility: StatusVisibility) =>
export const apiUnreblog = (statusId: string) =>
apiRequestPost<Status>(`v1/statuses/${statusId}/unreblog`);
export const apiRevokeQuote = (quotedStatusId: string, statusId: string) =>
apiRequestPost<Status>(
`v1/statuses/${quotedStatusId}/quotes/${statusId}/revoke`,
);

View File

@ -67,21 +67,28 @@ const messages = defineMessages({
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
filter: { id: 'status.filter', defaultMessage: 'Filter this post' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}s post' },
});
const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});
const mapStateToProps = (state, { status }) => {
const quotedStatusId = status.getIn(['quote', 'quoted_status']);
return ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null,
});
};
class StatusActionBar extends ImmutablePureComponent {
static propTypes = {
identity: identityContextPropShape,
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.record,
quotedAccountId: ImmutablePropTypes.string,
onReply: PropTypes.func,
onFavourite: PropTypes.func,
onReblog: PropTypes.func,
onDelete: PropTypes.func,
onRevokeQuote: PropTypes.func,
onDirect: PropTypes.func,
onMention: PropTypes.func,
onMute: PropTypes.func,
@ -110,6 +117,7 @@ class StatusActionBar extends ImmutablePureComponent {
updateOnProps = [
'status',
'relationship',
'quotedAccountId',
'withDismiss',
];
@ -190,6 +198,10 @@ class StatusActionBar extends ImmutablePureComponent {
}
};
handleRevokeQuoteClick = () => {
this.props.onRevokeQuote(this.props.status);
}
handleBlockClick = () => {
const { status, relationship, onBlock, onUnblock } = this.props;
const account = status.get('account');
@ -241,7 +253,7 @@ class StatusActionBar extends ImmutablePureComponent {
};
render () {
const { status, relationship, intl, withDismiss, withCounters, scrollKey } = this.props;
const { status, relationship, quotedAccountId, intl, withDismiss, withCounters, scrollKey } = this.props;
const { signedIn, permissions } = this.props.identity;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@ -291,6 +303,10 @@ class StatusActionBar extends ImmutablePureComponent {
menu.push({ text: intl.formatMessage(messages.direct, { name: account.get('username') }), action: this.handleDirectClick });
menu.push(null);
if (quotedAccountId === me) {
menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true });
}
if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {

View File

@ -111,6 +111,10 @@ const mapDispatchToProps = (dispatch, { contextType }) => ({
}
},
onRevokeQuote (status) {
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
},
onEdit (status) {
dispatch((_, getState) => {
let state = getState();

View File

@ -61,22 +61,29 @@ const messages = defineMessages({
unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' },
unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' },
openOriginalPage: { id: 'account.open_original_page', defaultMessage: 'Open original page' },
revokeQuote: { id: 'status.revoke_quote', defaultMessage: 'Remove my post from @{name}s post' },
});
const mapStateToProps = (state, { status }) => ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
});
const mapStateToProps = (state, { status }) => {
const quotedStatusId = status.getIn(['quote', 'quoted_status']);
return ({
relationship: state.getIn(['relationships', status.getIn(['account', 'id'])]),
quotedAccountId: quotedStatusId ? state.getIn(['statuses', quotedStatusId, 'account']) : null,
});
};
class ActionBar extends PureComponent {
static propTypes = {
identity: identityContextPropShape,
status: ImmutablePropTypes.map.isRequired,
relationship: ImmutablePropTypes.record,
quotedAccountId: ImmutablePropTypes.string,
onReply: PropTypes.func.isRequired,
onReblog: PropTypes.func.isRequired,
onFavourite: PropTypes.func.isRequired,
onBookmark: PropTypes.func.isRequired,
onDelete: PropTypes.func.isRequired,
onRevokeQuote: PropTypes.func,
onEdit: PropTypes.func.isRequired,
onDirect: PropTypes.func.isRequired,
onMention: PropTypes.func.isRequired,
@ -113,6 +120,10 @@ class ActionBar extends PureComponent {
this.props.onDelete(this.props.status);
};
handleRevokeQuoteClick = () => {
this.props.onRevokeQuote(this.props.status);
}
handleRedraftClick = () => {
this.props.onDelete(this.props.status, true);
};
@ -193,7 +204,7 @@ class ActionBar extends PureComponent {
};
render () {
const { status, relationship, intl } = this.props;
const { status, relationship, quotedAccountId, intl } = this.props;
const { signedIn, permissions } = this.props.identity;
const publicStatus = ['public', 'unlisted'].includes(status.get('visibility'));
@ -237,6 +248,10 @@ class ActionBar extends PureComponent {
menu.push({ text: intl.formatMessage(messages.mention, { name: status.getIn(['account', 'username']) }), action: this.handleMentionClick });
menu.push(null);
if (quotedAccountId === me) {
menu.push({ text: intl.formatMessage(messages.revokeQuote, { name: account.get('username') }), action: this.handleRevokeQuoteClick, dangerous: true });
}
if (relationship && relationship.get('muting')) {
menu.push({ text: intl.formatMessage(messages.unmute, { name: account.get('username') }), action: this.handleMuteClick });
} else {

View File

@ -259,6 +259,12 @@ class Status extends ImmutablePureComponent {
}
};
handleRevokeQuoteClick = (status) => {
const { dispatch } = this.props;
dispatch(openModal({ modalType: 'CONFIRM_REVOKE_QUOTE', modalProps: { statusId: status.get('id'), quotedStatusId: status.getIn(['quote', 'quoted_status']) }}));
};
handleEditClick = (status) => {
const { dispatch, askReplyConfirmation } = this.props;
@ -635,6 +641,7 @@ class Status extends ImmutablePureComponent {
onReblog={this.handleReblogClick}
onBookmark={this.handleBookmarkClick}
onDelete={this.handleDeleteClick}
onRevokeQuote={this.handleRevokeQuoteClick}
onEdit={this.handleEditClick}
onDirect={this.handleDirectClick}
onMention={this.handleMentionClick}

View File

@ -10,3 +10,4 @@ export { ConfirmClearNotificationsModal } from './clear_notifications';
export { ConfirmLogOutModal } from './log_out';
export { ConfirmFollowToListModal } from './follow_to_list';
export { ConfirmMissingAltTextModal } from './missing_alt_text';
export { ConfirmRevokeQuoteModal } from './revoke_quote';

View File

@ -0,0 +1,48 @@
import { useCallback } from 'react';
import { defineMessages, useIntl } from 'react-intl';
import { revokeQuote } from 'mastodon/actions/interactions_typed';
import { useAppDispatch } from 'mastodon/store';
import type { BaseConfirmationModalProps } from './confirmation_modal';
import { ConfirmationModal } from './confirmation_modal';
const messages = defineMessages({
revokeQuoteTitle: {
id: 'confirmations.revoke_quote.title',
defaultMessage: 'Remove post?',
},
revokeQuoteMessage: {
id: 'confirmations.revoke_quote.message',
defaultMessage: 'This action cannot be undone.',
},
revokeQuoteConfirm: {
id: 'confirmations.revoke_quote.confirm',
defaultMessage: 'Remove post',
},
});
export const ConfirmRevokeQuoteModal: React.FC<
{
statusId: string;
quotedStatusId: string;
} & BaseConfirmationModalProps
> = ({ statusId, quotedStatusId, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
const onConfirm = useCallback(() => {
void dispatch(revokeQuote({ quotedStatusId, statusId }));
}, [dispatch, statusId, quotedStatusId]);
return (
<ConfirmationModal
title={intl.formatMessage(messages.revokeQuoteTitle)}
message={intl.formatMessage(messages.revokeQuoteMessage)}
confirm={intl.formatMessage(messages.revokeQuoteConfirm)}
onConfirm={onConfirm}
onClose={onClose}
/>
);
};

View File

@ -37,6 +37,7 @@ import {
ConfirmLogOutModal,
ConfirmFollowToListModal,
ConfirmMissingAltTextModal,
ConfirmRevokeQuoteModal,
} from './confirmation_modals';
import { ImageModal } from './image_modal';
import MediaModal from './media_modal';
@ -59,6 +60,7 @@ export const MODAL_COMPONENTS = {
'CONFIRM_LOG_OUT': () => Promise.resolve({ default: ConfirmLogOutModal }),
'CONFIRM_FOLLOW_TO_LIST': () => Promise.resolve({ default: ConfirmFollowToListModal }),
'CONFIRM_MISSING_ALT_TEXT': () => Promise.resolve({ default: ConfirmMissingAltTextModal }),
'CONFIRM_REVOKE_QUOTE': () => Promise.resolve({ default: ConfirmRevokeQuoteModal }),
'MUTE': MuteModal,
'BLOCK': BlockModal,
'DOMAIN_BLOCK': DomainBlockModal,

View File

@ -600,6 +600,7 @@
"notification.label.mention": "Zmínka",
"notification.label.private_mention": "Soukromá zmínka",
"notification.label.private_reply": "Privátní odpověď",
"notification.label.quote": "{name} citovali váš příspěvek",
"notification.label.reply": "Odpověď",
"notification.mention": "Zmínka",
"notification.mentioned_you": "{name} vás zmínil",
@ -657,6 +658,7 @@
"notifications.column_settings.mention": "Zmínky:",
"notifications.column_settings.poll": "Výsledky anket:",
"notifications.column_settings.push": "Push oznámení",
"notifications.column_settings.quote": "Citace:",
"notifications.column_settings.reblog": "Boosty:",
"notifications.column_settings.show": "Zobrazit ve sloupci",
"notifications.column_settings.sound": "Přehrát zvuk",

View File

@ -658,6 +658,7 @@
"notifications.column_settings.mention": "Omtaler:",
"notifications.column_settings.poll": "Afstemningsresultater:",
"notifications.column_settings.push": "Push-notifikationer",
"notifications.column_settings.quote": "Citater:",
"notifications.column_settings.reblog": "Fremhævelser:",
"notifications.column_settings.show": "Vis i kolonne",
"notifications.column_settings.sound": "Afspil lyd",

View File

@ -600,6 +600,7 @@
"notification.label.mention": "Erwähnung",
"notification.label.private_mention": "Private Erwähnung",
"notification.label.private_reply": "Private Antwort",
"notification.label.quote": "{name} zitierte deinen Beitrag",
"notification.label.reply": "Antwort",
"notification.mention": "Erwähnung",
"notification.mentioned_you": "{name} erwähnte dich",
@ -657,6 +658,7 @@
"notifications.column_settings.mention": "Erwähnungen:",
"notifications.column_settings.poll": "Umfrageergebnisse:",
"notifications.column_settings.push": "Push-Benachrichtigungen",
"notifications.column_settings.quote": "Zitate:",
"notifications.column_settings.reblog": "Geteilte Beiträge:",
"notifications.column_settings.show": "In dieser Spalte anzeigen",
"notifications.column_settings.sound": "Ton abspielen",

View File

@ -658,6 +658,7 @@
"notifications.column_settings.mention": "Επισημάνσεις:",
"notifications.column_settings.poll": "Αποτελέσματα δημοσκόπησης:",
"notifications.column_settings.push": "Ειδοποιήσεις Push",
"notifications.column_settings.quote": "Παραθέσεις:",
"notifications.column_settings.reblog": "Ενισχύσεις:",
"notifications.column_settings.show": "Εμφάνισε σε στήλη",
"notifications.column_settings.sound": "Αναπαραγωγή ήχου",

View File

@ -245,6 +245,9 @@
"confirmations.remove_from_followers.confirm": "Remove follower",
"confirmations.remove_from_followers.message": "{name} will stop following you. Are you sure you want to proceed?",
"confirmations.remove_from_followers.title": "Remove follower?",
"confirmations.revoke_quote.confirm": "Remove post",
"confirmations.revoke_quote.message": "This action cannot be undone.",
"confirmations.revoke_quote.title": "Remove post?",
"confirmations.unfollow.confirm": "Unfollow",
"confirmations.unfollow.message": "Are you sure you want to unfollow {name}?",
"confirmations.unfollow.title": "Unfollow user?",
@ -896,6 +899,7 @@
"status.reply": "Reply",
"status.replyAll": "Reply to thread",
"status.report": "Report @{name}",
"status.revoke_quote": "Remove my post from @{name}s post",
"status.sensitive_warning": "Sensitive content",
"status.share": "Share",
"status.show_less_all": "Show less for all",

View File

@ -658,6 +658,7 @@
"notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.poll": "Resultados de la encuesta:",
"notifications.column_settings.push": "Notificaciones push",
"notifications.column_settings.quote": "Citas:",
"notifications.column_settings.reblog": "Adhesiones:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",

View File

@ -658,6 +658,7 @@
"notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.poll": "Resultados de la votación:",
"notifications.column_settings.push": "Notificaciones push",
"notifications.column_settings.quote": "Citas:",
"notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",

View File

@ -658,6 +658,7 @@
"notifications.column_settings.mention": "Menciones:",
"notifications.column_settings.poll": "Resultados de la votación:",
"notifications.column_settings.push": "Notificaciones push",
"notifications.column_settings.quote": "Citas:",
"notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostrar en columna",
"notifications.column_settings.sound": "Reproducir sonido",

View File

@ -600,6 +600,7 @@
"notification.label.mention": "Maininta",
"notification.label.private_mention": "Yksityismaininta",
"notification.label.private_reply": "Yksityinen vastaus",
"notification.label.quote": "{name} lainasi julkaisuasi",
"notification.label.reply": "Vastaus",
"notification.mention": "Maininta",
"notification.mentioned_you": "{name} mainitsi sinut",
@ -657,6 +658,7 @@
"notifications.column_settings.mention": "Maininnat:",
"notifications.column_settings.poll": "Äänestystulokset:",
"notifications.column_settings.push": "Puskuilmoitukset",
"notifications.column_settings.quote": "Lainaukset:",
"notifications.column_settings.reblog": "Tehostukset:",
"notifications.column_settings.show": "Näytä sarakkeessa",
"notifications.column_settings.sound": "Äänimerkki",

View File

@ -600,6 +600,7 @@
"notification.label.mention": "Mención",
"notification.label.private_mention": "Mención privada",
"notification.label.private_reply": "Resposta privada",
"notification.label.quote": "{name} citou a túa publicación",
"notification.label.reply": "Resposta",
"notification.mention": "Mención",
"notification.mentioned_you": "{name} mencionoute",

View File

@ -600,6 +600,7 @@
"notification.label.mention": "אזכור",
"notification.label.private_mention": "אזכור פרטי",
"notification.label.private_reply": "תשובה בפרטי",
"notification.label.quote": "{name} ציטט.ה את הודעתך",
"notification.label.reply": "תשובה",
"notification.mention": "אזכור",
"notification.mentioned_you": "אוזכרת על ידי {name}",

View File

@ -600,6 +600,7 @@
"notification.label.mention": "Minnst á",
"notification.label.private_mention": "Einkaspjall",
"notification.label.private_reply": "Einkasvar",
"notification.label.quote": "{name} vitnaði í færsluna þína",
"notification.label.reply": "Svara",
"notification.mention": "Minnst á",
"notification.mentioned_you": "{name} minntist á þig",
@ -657,6 +658,7 @@
"notifications.column_settings.mention": "Tilvísanir:",
"notifications.column_settings.poll": "Niðurstöður könnunar:",
"notifications.column_settings.push": "Ýti-tilkynningar",
"notifications.column_settings.quote": "Tilvitnanir:",
"notifications.column_settings.reblog": "Endurbirtingar:",
"notifications.column_settings.show": "Sýna í dálki",
"notifications.column_settings.sound": "Spila hljóð",

View File

@ -658,6 +658,7 @@
"notifications.column_settings.mention": "Menzioni:",
"notifications.column_settings.poll": "Risultati del sondaggio:",
"notifications.column_settings.push": "Notifiche push",
"notifications.column_settings.quote": "Citazioni:",
"notifications.column_settings.reblog": "Reblog:",
"notifications.column_settings.show": "Mostra nella colonna",
"notifications.column_settings.sound": "Riproduci suono",

View File

@ -34,6 +34,7 @@
"account.followers": "Imeḍfaren",
"account.followers.empty": "Ar tura, ulac yiwen i yeṭṭafaṛen amseqdac-agi.",
"account.followers_counter": "{count, plural, one {{counter} n umḍfar} other {{counter} n yimeḍfaren}}",
"account.followers_you_know_counter": "Tessneḍ {counter}",
"account.following": "Yeṭṭafaṛ",
"account.following_counter": "{count, plural, one {{counter} yettwaḍfaren} other {{counter} yettwaḍfaren}}",
"account.follows.empty": "Ar tura, amseqdac-agi ur yeṭṭafaṛ yiwen.",
@ -51,10 +52,12 @@
"account.mute_notifications_short": "Susem ilɣa",
"account.mute_short": "Sgugem",
"account.muted": "Yettwasgugem",
"account.mutual": "Temmeḍfaṛem",
"account.no_bio": "Ulac aglam i d-yettunefken.",
"account.open_original_page": "Ldi asebter anasli",
"account.posts": "Tisuffaɣ",
"account.posts_with_replies": "Tisuffaɣ d tririyin",
"account.remove_from_followers": "Kkes {name} seg ineḍfaren",
"account.report": "Cetki ɣef @{name}",
"account.requested": "Di laɛḍil ad yettwaqbel. Ssit i wakken ad yefsex usuter n uḍfar",
"account.requested_follow": "{name} yessuter ad k·m-yeḍfer",
@ -85,6 +88,7 @@
"annual_report.summary.most_used_app.most_used_app": "asnas yettwasqedcen s waṭas",
"annual_report.summary.most_used_hashtag.none": "Ula yiwen",
"annual_report.summary.new_posts.new_posts": "tisuffaɣ timaynutin",
"annual_report.summary.thanks": "Tanemmirt imi i tettekkiḍ deg Mastodon!",
"audio.hide": "Ffer amesli",
"block_modal.show_less": "Ssken-d drus",
"block_modal.show_more": "Ssken-d ugar",
@ -352,6 +356,8 @@
"keyboard_shortcuts.toot": "i wakken attebdud tajewwaqt tamaynut",
"keyboard_shortcuts.unfocus": "to un-focus compose textarea/search",
"keyboard_shortcuts.up": "i tulin ɣer d asawen n tebdart",
"learn_more_link.got_it": "Gziɣ-t",
"learn_more_link.learn_more": "Issin ugar",
"lightbox.close": "Mdel",
"lightbox.next": "Ɣer zdat",
"lightbox.previous": "Ɣer deffir",
@ -417,6 +423,8 @@
"notification.admin.sign_up": "Ijerred {name}",
"notification.annual_report.view": "Wali #Wrapstodon",
"notification.favourite": "{name} yesmenyaf addad-ik·im",
"notification.favourite_pm": "{name} yesmenyef abdar-ik·im uslig",
"notification.favourite_pm.name_and_others_with_link": "{name} akked <a>{count, plural, one {# nnayeḍ} other {# nniḍen}}</a> rnan abdar-ik·im uslig ar ismenyafen-nsen",
"notification.follow": "iṭṭafar-ik·em-id {name}",
"notification.follow.name_and_others": "{name} akked <a>{count, plural, one {# nniḍen} other {# nniḍen}}</a> iḍfeṛ-k·m-id",
"notification.follow_request": "{name} yessuter-d ad k·m-yeḍfeṛ",

View File

@ -658,6 +658,7 @@
"notifications.column_settings.mention": "Vermeldingen:",
"notifications.column_settings.poll": "Peilingresultaten:",
"notifications.column_settings.push": "Pushmeldingen",
"notifications.column_settings.quote": "Citaten:",
"notifications.column_settings.reblog": "Boosts:",
"notifications.column_settings.show": "In kolom tonen",
"notifications.column_settings.sound": "Geluid afspelen",

View File

@ -498,6 +498,8 @@
"keyboard_shortcuts.translate": "traduzir uma publicação",
"keyboard_shortcuts.unfocus": "remover o foco da área de texto / pesquisa",
"keyboard_shortcuts.up": "mover para cima na lista",
"learn_more_link.got_it": "Entendido",
"learn_more_link.learn_more": "Saber mais",
"lightbox.close": "Fechar",
"lightbox.next": "Próximo",
"lightbox.previous": "Anterior",
@ -598,6 +600,7 @@
"notification.label.mention": "Menção",
"notification.label.private_mention": "Menção privada",
"notification.label.private_reply": "Resposta privada",
"notification.label.quote": "{name} citou a sua publicação",
"notification.label.reply": "Resposta",
"notification.mention": "Menção",
"notification.mentioned_you": "{name} mencionou-te",
@ -655,6 +658,7 @@
"notifications.column_settings.mention": "Menções:",
"notifications.column_settings.poll": "Resultados da sondagem:",
"notifications.column_settings.push": "Notificações \"push\"",
"notifications.column_settings.quote": "Citações:",
"notifications.column_settings.reblog": "Impulsos:",
"notifications.column_settings.show": "Mostrar na coluna",
"notifications.column_settings.sound": "Reproduzir som",
@ -873,6 +877,11 @@
"status.open": "Expandir esta publicação",
"status.pin": "Afixar no perfil",
"status.quote_error.filtered": "Oculto devido a um dos seus filtros",
"status.quote_error.not_available": "Publicação indisponível",
"status.quote_error.pending_approval": "Publicação pendente",
"status.quote_error.pending_approval_popout.body": "As citações partilhadas no Fediverso podem demorar algum tempo a ser exibidas, uma vez que diferentes servidores têm protocolos diferentes.",
"status.quote_error.pending_approval_popout.title": "Citação pendente? Mantenha a calma",
"status.quote_post_author": "Citou uma publicação de @{name}",
"status.read_more": "Ler mais",
"status.reblog": "Impulsionar",
"status.reblog_private": "Impulsionar com a visibilidade original",

View File

@ -321,6 +321,7 @@
"explore.trending_links": "Новини",
"explore.trending_statuses": "Дописи",
"explore.trending_tags": "Хештеґи",
"featured_carousel.next": "Далі",
"filter_modal.added.context_mismatch_explanation": "Ця категорія фільтра не застосовується до контексту, в якому ви отримали доступ до цього допису. Якщо ви хочете, щоб дописи також фільтрувалися за цим контекстом, вам доведеться редагувати фільтр.",
"filter_modal.added.context_mismatch_title": "Невідповідність контексту!",
"filter_modal.added.expired_explanation": "Категорія цього фільтра застаріла, Вам потрібно змінити дату закінчення терміну дії, щоб застосувати її.",

View File

@ -658,6 +658,7 @@
"notifications.column_settings.mention": "Lượt nhắc đến:",
"notifications.column_settings.poll": "Kết quả vốt:",
"notifications.column_settings.push": "Thông báo đẩy",
"notifications.column_settings.quote": "Trích dẫn:",
"notifications.column_settings.reblog": "Lượt đăng lại:",
"notifications.column_settings.show": "Báo trên thanh bên",
"notifications.column_settings.sound": "Kèm âm báo",

View File

@ -658,6 +658,7 @@
"notifications.column_settings.mention": "提及:",
"notifications.column_settings.poll": "投票結果:",
"notifications.column_settings.push": "推播通知",
"notifications.column_settings.quote": "引用嘟文:",
"notifications.column_settings.reblog": "轉嘟:",
"notifications.column_settings.show": "於欄位中顯示",
"notifications.column_settings.sound": "播放音效",

View File

@ -26,7 +26,10 @@ const filterNotificationsByAllowedTypes = (
);
}
return notifications.filter(
(item) => item.type === 'gap' || allowedType === item.type,
(item) =>
item.type === 'gap' ||
allowedType === item.type ||
(allowedType === 'mention' && item.type === 'quote'),
);
};

View File

@ -45,9 +45,7 @@ class EmojiFormatter
i += 1
if inside_shortname && text[i] == ':'
# https://github.com/rubocop/rubocop/issues/14383
# False positive in line below, remove disable when resolved
inside_shortname = false # rubocop:disable Lint/UselessAssignment
inside_shortname = false
shortcode = text[(shortname_start_index + 1)..(i - 1)]
char_after = text[i + 1]

View File

@ -3,7 +3,7 @@
class ActivityPub::NoteSerializer < ActivityPub::Serializer
include FormattingHelper
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes
context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes, :interaction_policies
attributes :id, :type, :summary,
:in_reply_to, :published, :url,
@ -35,6 +35,8 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
attribute :quote, key: :quote_uri, if: :quote?
attribute :quote_authorization, if: :quote_authorization?
attribute :interaction_policy, if: -> { Mastodon::Feature.outgoing_quotes_enabled? }
def id
ActivityPub::TagManager.instance.uri_for(object)
end
@ -216,6 +218,23 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer
ActivityPub::TagManager.instance.approval_uri_for(object.quote)
end
def interaction_policy
approved_uris = []
# On outgoing posts, only automatic approval is supported
policy = object.quote_approval_policy >> 16
approved_uris << ActivityPub::TagManager::COLLECTIONS[:public] if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public])
approved_uris << ActivityPub::TagManager.instance.followers_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers])
approved_uris << ActivityPub::TagManager.instance.following_uri_for(object.account) if policy.anybits?(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followed])
approved_uris << ActivityPub::TagManager.instance.uri_for(object.account) if approved_uris.empty?
{
canQuote: {
automaticApproval: approved_uris,
},
}
end
class MediaAttachmentSerializer < ActivityPub::Serializer
context_extensions :blurhash, :focal_point

View File

@ -19,6 +19,7 @@ class PostStatusService < BaseService
# @option [String] :text Message
# @option [Status] :thread Optional status to reply to
# @option [Status] :quoted_status Optional status to quote
# @option [String] :quote_approval_policy Approval policy for quotes, one of `public`, `followers` or `nobody`
# @option [Boolean] :sensitive
# @option [String] :visibility
# @option [String] :spoiler_text
@ -215,6 +216,7 @@ class PostStatusService < BaseService
language: valid_locale_cascade(@options[:language], @account.user&.preferred_posting_language, I18n.default_locale),
application: @options[:application],
rate_limit: @options[:with_rate_limit],
quote_approval_policy: @options[:quote_approval_policy],
}.compact
end

View File

@ -115,6 +115,7 @@ class UpdateStatusService < BaseService
@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)
@status.quote_approval_policy = @options[:quote_approval_policy] if @options[:quote_approval_policy].present?
# We raise here to rollback the entire transaction
raise NoChangesSubmittedError unless significant_changes?

View File

@ -1,6 +1,6 @@
= content_for :heading do
= render 'application/mailer/heading',
image_url: frontend_asset_url('images/mailer-new/heading/boost.png'),
image_url: frontend_asset_url('images/mailer-new/heading/quote.png'),
subtitle: t('notification_mailer.quote.body', name: @status.account.pretty_acct),
title: t('notification_mailer.quote.title')
%table.email-w-full{ cellspacing: 0, cellpadding: 0, border: 0, role: 'presentation' }

View File

@ -1428,7 +1428,7 @@ da:
keywords: Nøgleord
statuses: Individuelle indlæg
statuses_hint_html: Dette filter gælder for udvalgte, individuelle indlæg, uanset om de matcher nøgleordene nedenfor. <a href="%{path}">Gennemgå eller fjern indlæg fra filteret</a>.
title: Redigere filter
title: Rediger filter
errors:
deprecated_api_multiple_keywords: Disse parametre kan ikke ændres fra denne applikation, da de gælder for flere end ét filternøgleord. Brug en nyere applikation eller webgrænsefladen.
invalid_context: Ingen eller ugyldig kontekst angivet
@ -1748,7 +1748,7 @@ da:
privacy: Privatliv
privacy_hint_html: Styr, hvor meget du vil afsløre til gavn for andre. Folk opdager interessante profiler og apps ved at gennemse andres følgere og se, hvilke apps de sender fra, men du foretrækker måske at holde det skjult.
reach: Rækkevidde
reach_hint_html: Indstil om du vil blive opdaget og fulgt af nye mennesker. Ønsker du, at dine indlæg skal vises på Udforsk-siden? Ønsker du, at andre skal se dig i deres følg-anbefalinger? Ønsker du at acceptere alle nye følgere automatisk, eller vil du have detaljeret kontrol over hver og en?
reach_hint_html: Indstil, om du vil opdages og følges af nye personer. Vil du have, at dine indlæg skal vises på Udforsk-siden? Vil du have, at andre skal kunne se dig i deres følg-anbefalinger? Vil du acceptere alle nye følgere automatisk eller have detaljeret kontrol over hver enkelt?
search: Søgning
search_hint_html: Indstil hvordan du vil findes. Ønsker du, at folk skal finde dig gennem hvad du har skrevet offentligt? Vil du have folk udenfor Mastodon til at finde din profil, når de søger på nettet? Vær opmærksom på, at det ikke kan garanteres at dine offentlige indlæg er udelukket fra alle søgemaskiner.
title: Fortrolighed og rækkevidde

View File

@ -485,12 +485,20 @@ kab:
title: Ihacṭagen inezzaɣ
trending_rank: 'Anezzuɣ #%{rank}'
trending: Inezzaɣ
username_blocks:
add_new: Rnu amaynut
comparison:
contains: Igber
new:
create: Rnu alugen
title: Rnu alugen n useqdac amaynut
warning_presets:
add_new: Rnu amaynut
delete: Kkes
webhooks:
delete: Kkes
enable: Rmed
enabled: D urmid
admin_mailer:
new_report:
body: "%{reporter} yettwazen ɣef %{target}"
@ -623,6 +631,7 @@ kab:
other: Ayen nniḍen
emoji_styles:
auto: Awurman
twemoji: Twemoji
errors:
'404': Asebter i tettnadiḍ ulac-it da.
'500':
@ -858,6 +867,8 @@ kab:
other: "%{count} n tbidyutin"
edited_at_html: Tettwaẓreg ass n %{date}
quote_policies:
followers: Ala wid i k·m-yeṭṭafaṛen
nobody: Ula yiwen
public: Yal yiwen
title: '%{name}: "%{quote}"'
visibilities:
@ -904,6 +915,8 @@ kab:
user_mailer:
appeal_approved:
action: Iɣewwaṛen n umiḍan
suspicious_sign_in:
change_password: snifel awal-ik·im n uɛeddi
terms_of_service_changed:
sign_off: Agraw n %{domain}
warning:

View File

@ -190,6 +190,7 @@ pt-PT:
create_relay: Criar retransmissor
create_unavailable_domain: Criar domínio indisponível
create_user_role: Criar função
create_username_block: Criar Regra de Nome de Utilizador
demote_user: Despromover utilizador
destroy_announcement: Eliminar mensagem de manutenção
destroy_canonical_email_block: Eliminar bloqueio de e-mail
@ -203,6 +204,7 @@ pt-PT:
destroy_status: Eliminar publicação
destroy_unavailable_domain: Eliminar domínio indisponível
destroy_user_role: Eliminar função
destroy_username_block: Eliminar Regra de Nome de Utilizador
disable_2fa_user: Desativar 2FA
disable_custom_emoji: Desativar emoji personalizado
disable_relay: Desativar retransmissor
@ -237,6 +239,7 @@ pt-PT:
update_report: Atualizar denúncia
update_status: Atualizar publicação
update_user_role: Atualizar função
update_username_block: Atualizar Regra de Nome de Utilizador
actions:
approve_appeal_html: "%{name} aprovou a contestação da decisão de moderação de %{target}"
approve_user_html: "%{name} aprovou a inscrição de %{target}"
@ -255,6 +258,7 @@ pt-PT:
create_relay_html: "%{name} criou o retransmissor %{target}"
create_unavailable_domain_html: "%{name} parou as entregas ao domínio %{target}"
create_user_role_html: "%{name} criou a função %{target}"
create_username_block_html: "%{name} adicionou regra para nomes de utilizadores que contêm %{target}"
demote_user_html: "%{name} despromoveu o utilizador %{target}"
destroy_announcement_html: "%{name} eliminou a mensagem de manutenção %{target}"
destroy_canonical_email_block_html: "%{name} desbloqueou o e-mail com a hash %{target}"
@ -268,6 +272,7 @@ pt-PT:
destroy_status_html: "%{name} removeu a publicação de %{target}"
destroy_unavailable_domain_html: "%{name} retomou as entregas ao domínio %{target}"
destroy_user_role_html: "%{name} eliminou a função %{target}"
destroy_username_block_html: "%{name} removeu regra para nomes de utilizadores que contêm %{target}"
disable_2fa_user_html: "%{name} desativou o requerimento de autenticação em dois passos para o utilizador %{target}"
disable_custom_emoji_html: "%{name} desativou o emoji %{target}"
disable_relay_html: "%{name} desativou o retransmissor %{target}"
@ -302,6 +307,7 @@ pt-PT:
update_report_html: "%{name} atualizou a denúncia %{target}"
update_status_html: "%{name} atualizou a publicação de %{target}"
update_user_role_html: "%{name} alterou a função %{target}"
update_username_block_html: "%{name} atualizou regra para nomes de utilizadores que contêm %{target}"
deleted_account: conta eliminada
empty: Não foram encontrados registos.
filter_by_action: Filtrar por ação
@ -1085,6 +1091,25 @@ pt-PT:
other: Utilizada por %{count} pessoas na última semana
title: Recomendações e destaques
trending: Em destaque
username_blocks:
add_new: Adicionar novo
block_registrations: Bloquear inscrições
comparison:
contains: Contém
equals: Igual a
contains_html: Contém %{string}
created_msg: Regra de nome de utilizador criada com sucesso
delete: Eliminar
edit:
title: Editar regra de nome de utilizador
matches_exactly_html: Igual a %{string}
new:
create: Criar regra
title: Criar nova regra de utilizador
no_username_block_selected: Não foi alterada nenhuma regra de nome de utilizador, pois nenhuma foi selecionada
not_permitted: Não permitido
title: Regras de nome de utilizador
updated_msg: Regra de nome de utilizador atualizada com sucesso
warning_presets:
add_new: Adicionar novo
delete: Eliminar
@ -1662,6 +1687,10 @@ pt-PT:
title: Nova menção
poll:
subject: A sondagem de %{name} terminou
quote:
body: 'A sua publicação foi citada por %{name}:'
subject: "%{name} citou a sua publicação"
title: Nova citação
reblog:
body: 'A tua publicação foi impulsionada por %{name}:'
subject: "%{name} impulsionou a tua publicação"
@ -1880,6 +1909,8 @@ pt-PT:
ownership: Não podem ser fixadas publicações de outras pessoas
reblog: Não é possível fixar um impulso
quote_policies:
followers: Apenas os seus seguidores
nobody: Ninguém
public: Todos
title: '%{name}: "%{quote}"'
visibilities:

View File

@ -1922,24 +1922,29 @@ ru:
other: 'содержались запрещённые хэштеги: %{tags}'
edited_at_html: 'Дата последнего изменения: %{date}'
errors:
in_reply_not_found: Пост, на который вы пытаетесь ответить, не существует или удалён.
over_character_limit: превышен лимит символов (%{max})
in_reply_not_found: Пост, на который вы собирались ответить, был удалён или не существует.
quoted_status_not_found: Пост, который вы собирались процитировать, был удалён или не существует.
over_character_limit: превышает максимально допустимую длину (%{max} символов)
pin_errors:
direct: Сообщения, видимые только упомянутым пользователям, не могут быть закреплены
limit: Вы закрепили максимально возможное число постов
direct: Нельзя закрепить пост, который доступен только тем, кто был упомянут в нём
limit: Вы достигли максимального количества постов, которые можно закрепить в профиле
ownership: Нельзя закрепить чужой пост
reblog: Нельзя закрепить продвинутый пост
title: '%{name}: "%{quote}"'
reblog: Нельзя закрепить продвижение
quote_policies:
followers: Только ваши подписчики
nobody: Никто
public: Кто угодно
title: "%{name}: «%{quote}»"
visibilities:
direct: Адресованный
private: Для подписчиков
private_long: Показывать только подписчикам
public: Для всех
public_long: Показывать всем
unlisted: Скрывать из лент
unlisted_long: Показывать всем, но не отображать в публичных лентах
direct: Личное упоминание
private: Только для подписчиков
private_long: Доступен только вашим подписчикам
public: Публичный
public_long: Доступен кому угодно
unlisted: Скрытый
unlisted_long: Доступен кому угодно, но не отображается в публичных лентах
statuses_cleanup:
enabled: Автоматически удалять устаревшие посты
enabled: Автоматически удалять старые посты
enabled_hint: Автоматически удаляет ваши посты после того, как они достигли определённого возрастного порога, за некоторыми исключениями ниже.
exceptions: Исключения
explanation: Из-за того, что удаление постов — это ресурсоёмкий процесс, оно производится медленно со временем, когда сервер менее всего загружен. По этой причине, посты могут удаляться не сразу, а спустя определённое время, по достижению возрастного порога.

View File

@ -56,6 +56,7 @@ ca:
scopes: API permeses per a accedir a l'aplicació. Si selecciones un àmbit de nivell superior, no cal que en seleccionis un d'individual.
setting_aggregate_reblogs: No mostra els nous impulsos dels tuts que ja s'han impulsat recentment (només afecta als impulsos nous rebuts)
setting_always_send_emails: Normalment, no s'enviarà cap notificació per correu electrònic mentre facis servir Mastodon
setting_default_quote_policy: Aquesta configuració només tindrà efecte en les publicacions creades amb la següent versió de Mastodon, però podeu seleccionar-la en preparació.
setting_default_sensitive: El contingut sensible està ocult per defecte i es pot mostrar fent-hi clic
setting_display_media_default: Amaga el contingut gràfic marcat com a sensible
setting_display_media_hide_all: Oculta sempre tot el contingut multimèdia

View File

@ -10,7 +10,7 @@ da:
indexable: Dine offentlige indlæg vil kunne vises i Mastodon-søgeresultater. Folk, som har interageret med dem, vil kunne finde dem uanset.
note: 'Du kan @omtale andre personer eller #hashtags.'
show_collections: Folk vil kunne se, hvem du følger, og hvem der følger dig. Personer, som du følger, vil kunne se, at du følger dem.
unlocked: Man vil kunne følges af folk uden først at godkende dem. Ønsker man at gennemgå Følg-anmodninger og individuelt acceptere/afvise nye følgere, så fjern markeringen.
unlocked: Folk vil kunne følge dig uden at anmode om godkendelse. Fjern markeringen, hvis du vil gennemgå anmodninger om at følge, og vælge, om du vil acceptere eller afvise nye følgere.
account_alias:
acct: Angiv brugernavn@domain for den konto, hvorfra du vil flytte
account_migration:
@ -329,7 +329,7 @@ da:
follow_request: Nogen anmodede om at følge dig
mention: Nogen omtalte dig
pending_account: Ny konto kræver gennemgang
quote: Nogle citerede dig
quote: Nogen citerede dig
reblog: Nogen fremhævede dit indlæg
report: Ny anmeldelse indsendt
software_updates:

View File

@ -27,6 +27,8 @@ kab:
username: Tzemreḍ ad tesqedceḍ isekkilen, uṭṭunen akked yijerriden n wadda
featured_tag:
name: 'Ha-t-an kra seg ihacṭagen i tesseqdaceḍ ussan-a ineggura maḍi :'
form_challenge:
current_password: Tkecmeḍ ɣer temnaḍt taɣellsant
imports:
data: Afaylu CSV id yusan seg uqeddac-nniḍen n Maṣṭudun
invite_request:
@ -143,7 +145,12 @@ kab:
name: Ahacṭag
terms_of_service:
text: Tiwtilin n useqdec
terms_of_service_generator:
domain: Taɣult
user:
date_of_birth_1i: Ass
date_of_birth_2i: Ayyur
date_of_birth_3i: Aseggas
role: Tamlilt
time_zone: Tamnaḍt tasragant
user_role:

View File

@ -56,6 +56,7 @@ pt-PT:
scopes: Quais as API a que a aplicação terá permissão para aceder. Se selecionar um âmbito de nível superior, não precisa de selecionar âmbitos individuais.
setting_aggregate_reblogs: Não mostrar os novos impulsos para publicações que tenham sido recentemente impulsionadas (apenas afeta os impulsos recentemente recebidos)
setting_always_send_emails: Normalmente as notificações por e-mail não serão enviadas quando estiver a utilizar ativamente o Mastodon
setting_default_quote_policy: Esta configuração só terá efeito nas publicações criadas com a próxima versão do Mastodon, mas pode desde já selecionar a sua preferência.
setting_default_sensitive: Os multimédia sensíveis são ocultados por predefinição e podem ser revelados com um clique/toque
setting_display_media_default: Esconder multimédia marcada como sensível
setting_display_media_hide_all: Esconder sempre toda a multimédia
@ -159,6 +160,10 @@ pt-PT:
name: Nome público da função, se esta estiver definida para ser apresentada com um emblema
permissions_as_keys: Utilizadores com esta função terão acesso a...
position: Funções mais altas decidem a resolução de conflitos em certas situações. Certas ações só podem ser executadas com certas funções com uma menor prioridade
username_block:
allow_with_approval: Em vez de impedir totalmente a inscrição, as inscrições correspondentes exigirão a sua aprovação
comparison: Tenha em atenção o Problema de Scunthorpe ao bloquear correspondências parciais
username: Terá correspondência independentemente das maiúsculas e minúsculas e de homógrafos comuns, como "4" para "a" ou "3" para "e"
webhook:
events: Selecione os eventos a enviar
template: Componha o seu próprio conteúdo JSON utilizando a interpolação de variáveis. Deixar em branco para o JSON predefinido.
@ -324,6 +329,7 @@ pt-PT:
follow_request: Alguém pediu para ser seu seguidor
mention: Alguém o mencionou
pending_account: Uma nova conta aguarda aprovação
quote: Alguém o citou
reblog: Alguém impulsionou uma publicação sua
report: Uma nova denúncia foi submetida
software_updates:
@ -370,6 +376,10 @@ pt-PT:
name: Nome
permissions_as_keys: Permissões
position: Prioridade
username_block:
allow_with_approval: Permitir inscrições com aprovação
comparison: Método de comparação
username: Palavra a corresponder
webhook:
events: Eventos ativados
template: Modelo de conteúdo

View File

@ -496,6 +496,15 @@ uk:
new:
title: Імпорт блокувань домену
no_file: Файл не вибрано
fasp:
debug:
callbacks:
delete: Видалити
providers:
delete: Видалити
registrations:
confirm: Підтвердити
save: Зберегти
follow_recommendations:
description_html: "<strong>Слідувати рекомендаціям та допомогти новим користувачам швидко знайти цікавий вміст</strong>. Коли користувачі не взаємодіяли з іншими людьми достатньо, щоб сформувати персоналізовані рекомендації, радимо замість цього вказувати ці облікові записи. Вони щоденно переобчислюються з масиву облікових записів з найбільшою кількістю недавніх взаємодій і найбільшою кількістю місцевих підписників розраховується для цієї мови."
language: Для мови

View File

@ -3,6 +3,36 @@
require 'rails_helper'
RSpec.describe CanonicalEmailBlock do
describe 'Associations' do
it { is_expected.to belong_to(:reference_account).class_name('Account').optional }
end
describe 'Scopes' do
describe '.matching_email' do
subject { described_class.matching_email(email) }
let!(:block) { Fabricate :canonical_email_block, email: 'test@example.com' }
context 'when email is exact match' do
let(:email) { 'test@example.com' }
it { is_expected.to contain_exactly(block) }
end
context 'when email does not match' do
let(:email) { 'test@example.ORG' }
it { is_expected.to be_empty }
end
context 'when email is different but normalizes to same hash' do
let(:email) { 'te.st+more@EXAMPLE.com' }
it { is_expected.to contain_exactly(block) }
end
end
end
describe '#email=' do
let(:target_hash) { '973dfe463ec85785f5f95af5ba3906eedb2d931c24e69824a89ea65dba4e813b' }

View File

@ -258,13 +258,13 @@ RSpec.describe Form::Import do
end
end
it_behaves_like 'on successful import', 'following', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like 'on successful import', 'following', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like 'on successful import', 'blocking', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like 'on successful import', 'blocking', 'overwrite', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like 'on successful import', 'muting', 'merge', 'imports.txt', (%w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like 'on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', (%w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } })
it_behaves_like 'on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', (%w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } })
it_behaves_like('on successful import', 'following', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like('on successful import', 'following', 'overwrite', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like('on successful import', 'blocking', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like('on successful import', 'blocking', 'overwrite', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like('on successful import', 'muting', 'merge', 'imports.txt', %w(user@example.com user@test.com).map { |acct| { 'acct' => acct } })
it_behaves_like('on successful import', 'domain_blocking', 'merge', 'domain_blocks.csv', %w(bad.domain worse.domain reject.media).map { |domain| { 'domain' => domain } })
it_behaves_like('on successful import', 'bookmarks', 'merge', 'bookmark-imports.txt', %w(https://example.com/statuses/1312 https://local.com/users/foo/statuses/42 https://unknown-remote.com/users/bar/statuses/1 https://example.com/statuses/direct).map { |uri| { 'uri' => uri } })
it_behaves_like 'on successful import', 'following', 'merge', 'following_accounts.csv', [
{ 'acct' => 'user@example.com', 'show_reblogs' => true, 'notify' => false, 'languages' => nil },

View File

@ -158,6 +158,31 @@ RSpec.describe '/api/v1/statuses' do
end
end
context 'with a quote policy', feature: :outgoing_quotes do
let(:quoted_status) { Fabricate(:status, account: user.account) }
let(:params) do
{
status: 'Hello world, this is a self-quote',
quote_approval_policy: 'followers',
}
end
it 'returns post with appropriate quote policy, as well as rate limit headers', :aggregate_failures do
subject
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body[:quote_approval]).to include({
automatic: ['followers'],
manual: [],
current_user: 'automatic',
})
expect(response.headers['X-RateLimit-Limit']).to eq RateLimiter::FAMILIES[:statuses][:limit].to_s
expect(response.headers['X-RateLimit-Remaining']).to eq (RateLimiter::FAMILIES[:statuses][:limit] - 1).to_s
end
end
context 'with a self-quote post', feature: :outgoing_quotes do
let(:quoted_status) { Fabricate(:status, account: user.account) }
let(:params) do
@ -307,9 +332,10 @@ RSpec.describe '/api/v1/statuses' do
describe 'PUT /api/v1/statuses/:id' do
subject do
put "/api/v1/statuses/#{status.id}", headers: headers, params: { status: 'I am updated' }
put "/api/v1/statuses/#{status.id}", headers: headers, params: params
end
let(:params) { { status: 'I am updated' } }
let(:scopes) { 'write:statuses' }
let(:status) { Fabricate(:status, account: user.account) }
@ -323,6 +349,19 @@ RSpec.describe '/api/v1/statuses' do
.to start_with('application/json')
expect(status.reload.text).to eq 'I am updated'
end
context 'when updating only the quote policy' do
let(:params) { { status: status.text, quote_approval_policy: 'public' } }
it 'updates the status', :aggregate_failures, feature: :outgoing_quotes do
expect { subject }
.to change { status.reload.quote_approval_policy }.to(Status::QUOTE_APPROVAL_POLICY_FLAGS[:public] << 16)
expect(response).to have_http_status(200)
expect(response.content_type)
.to start_with('application/json')
end
end
end
end

View File

@ -56,4 +56,19 @@ RSpec.describe ActivityPub::NoteSerializer do
})
end
end
context 'with a quote policy', feature: :outgoing_quotes do
let(:parent) { Fabricate(:status, quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16) }
it 'has the expected shape' do
expect(subject).to include({
'type' => 'Note',
'interactionPolicy' => a_hash_including(
'canQuote' => a_hash_including(
'automaticApproval' => [ActivityPub::TagManager.instance.followers_uri_for(parent.account)]
)
),
})
end
end
end

View File

@ -160,6 +160,12 @@ RSpec.describe PostStatusService do
expect(status.language).to eq 'en'
end
it 'creates a status with the quote approval policy set' do
status = create_status_with_options(quote_approval_policy: Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16)
expect(status.quote_approval_policy).to eq(Status::QUOTE_APPROVAL_POLICY_FLAGS[:followers] << 16)
end
it 'processes mentions' do
mention_service = instance_double(ProcessMentionsService)
allow(mention_service).to receive(:call)

View File

@ -112,7 +112,7 @@ RSpec.describe Scheduler::AccountsStatusesCleanupScheduler do
expect { subject.perform }
.to change(Status, :count).by(-subject.compute_budget) # Cleanable statuses
.and (not_change { account_bob.statuses.count }) # No cleanup policy for account
.and not_change { account_bob.statuses.count } # No cleanup policy for account
.and(not_change { account_dave.statuses.count }) # Disabled cleanup policy
end