diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml index b52400441d..048c2ccbc1 100644 --- a/.rubocop_todo.yml +++ b/.rubocop_todo.yml @@ -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 diff --git a/Gemfile.lock b/Gemfile.lock index 475172934a..943334b03d 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -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) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index 222beab9ce..cf2d415570 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -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], @@ -115,6 +116,7 @@ class Api::V1::StatusesController < Api::BaseController language: status_params[:language], spoiler_text: status_params[:spoiler_text], poll: status_params[:poll], + quote_approval_policy: quote_approval_policy, content_type: status_params[:content_type] ) @@ -182,6 +184,7 @@ class Api::V1::StatusesController < Api::BaseController :status, :in_reply_to_id, :quoted_status_id, + :quote_approval_policy, :sensitive, :spoiler_text, :visibility, @@ -205,6 +208,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 diff --git a/app/javascript/images/mailer-new/heading/README.md b/app/javascript/images/mailer-new/heading/README.md index ecd4b949e7..9fb6841f14 100644 --- a/app/javascript/images/mailer-new/heading/README.md +++ b/app/javascript/images/mailer-new/heading/README.md @@ -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). diff --git a/app/javascript/images/mailer-new/heading/quote.png b/app/javascript/images/mailer-new/heading/quote.png new file mode 100644 index 0000000000..c2af73282f Binary files /dev/null and b/app/javascript/images/mailer-new/heading/quote.png differ diff --git a/app/javascript/mastodon/actions/interactions_typed.ts b/app/javascript/mastodon/actions/interactions_typed.ts index f58faffa86..832ea18910 100644 --- a/app/javascript/mastodon/actions/interactions_typed.ts +++ b/app/javascript/mastodon/actions/interactions_typed.ts @@ -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; + }, +); diff --git a/app/javascript/mastodon/actions/notification_groups.ts b/app/javascript/mastodon/actions/notification_groups.ts index 4386325481..7e162e5e51 100644 --- a/app/javascript/mastodon/actions/notification_groups.ts +++ b/app/javascript/mastodon/actions/notification_groups.ts @@ -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,12 +158,15 @@ 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; if ( - (notification.type === 'mention' || notification.type === 'update') && + (notification.type === 'mention' || + notification.type === 'update' || + notification.type === 'quote') && notification.status?.filtered ) { const filters = notification.status.filtered.filter((result) => diff --git a/app/javascript/mastodon/actions/notifications.js b/app/javascript/mastodon/actions/notifications.js index 2499b8da1d..cbfddc750f 100644 --- a/app/javascript/mastodon/actions/notifications.js +++ b/app/javascript/mastodon/actions/notifications.js @@ -31,7 +31,7 @@ export function updateNotifications(notification, intlMessages, intlLocale) { let filtered = false; - if (['mention', 'status'].includes(notification.type) && notification.status.filtered) { + if (['mention', 'status', 'quote'].includes(notification.type) && notification.status.filtered) { const filters = notification.status.filtered.filter(result => result.filter.context.includes('notifications')); if (filters.some(result => result.filter.filter_action === 'hide')) { diff --git a/app/javascript/mastodon/api/interactions.ts b/app/javascript/mastodon/api/interactions.ts index 118b5f06d2..5ffa5d1507 100644 --- a/app/javascript/mastodon/api/interactions.ts +++ b/app/javascript/mastodon/api/interactions.ts @@ -8,3 +8,8 @@ export const apiReblog = (statusId: string, visibility: StatusVisibility) => export const apiUnreblog = (statusId: string) => apiRequestPost(`v1/statuses/${statusId}/unreblog`); + +export const apiRevokeQuote = (quotedStatusId: string, statusId: string) => + apiRequestPost( + `v1/statuses/${quotedStatusId}/quotes/${statusId}/revoke`, + ); diff --git a/app/javascript/mastodon/components/status_action_bar.jsx b/app/javascript/mastodon/components/status_action_bar.jsx index ec5a9780cb..663fc53407 100644 --- a/app/javascript/mastodon/components/status_action_bar.jsx +++ b/app/javascript/mastodon/components/status_action_bar.jsx @@ -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 { diff --git a/app/javascript/mastodon/containers/status_container.jsx b/app/javascript/mastodon/containers/status_container.jsx index 0fb5f25560..f3bc8fe5ff 100644 --- a/app/javascript/mastodon/containers/status_container.jsx +++ b/app/javascript/mastodon/containers/status_container.jsx @@ -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(); diff --git a/app/javascript/mastodon/features/notifications/components/column_settings.jsx b/app/javascript/mastodon/features/notifications/components/column_settings.jsx index 9616adcb93..b1f4e59818 100644 --- a/app/javascript/mastodon/features/notifications/components/column_settings.jsx +++ b/app/javascript/mastodon/features/notifications/components/column_settings.jsx @@ -143,6 +143,17 @@ class ColumnSettings extends PureComponent { +
+

+ +
+ + {showPushSettings && } + + +
+
+

diff --git a/app/javascript/mastodon/features/status/components/action_bar.jsx b/app/javascript/mastodon/features/status/components/action_bar.jsx index 81f8163fff..5d6625fc10 100644 --- a/app/javascript/mastodon/features/status/components/action_bar.jsx +++ b/app/javascript/mastodon/features/status/components/action_bar.jsx @@ -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 { diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index 4a714fa2c5..7bdcdb8971 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -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} diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts index 25ffb3b629..139b6f8ba2 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/index.ts @@ -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'; diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx new file mode 100644 index 0000000000..83964aa5fe --- /dev/null +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/revoke_quote.tsx @@ -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 ( + + ); +}; diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 4a98de0a31..3b7a24faaf 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -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, diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 17fb3eb2b8..690a35990e 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -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", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 645808695f..b401c13d32 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -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", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 9025144284..0ada8ba8c1 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -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", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index a2dfbb8bd3..58de44bce3 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -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": "Αναπαραγωγή ήχου", diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index c91516695b..6b796f9f81 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -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?", @@ -658,6 +661,7 @@ "notifications.column_settings.mention": "Mentions:", "notifications.column_settings.poll": "Poll results:", "notifications.column_settings.push": "Push notifications", + "notifications.column_settings.quote": "Quotes:", "notifications.column_settings.reblog": "Boosts:", "notifications.column_settings.show": "Show in column", "notifications.column_settings.sound": "Play sound", @@ -895,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", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 75460017da..41051d493a 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -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", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index d0c2290717..80a0138d93 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -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", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 62c296ad52..18ba22e95a 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -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", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 9e52dcb069..2f7c13bfaf 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -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", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 4cd1f9718f..c1da87693e 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -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", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index e9a755fab7..725130e75b 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -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}", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 54dcc70a09..b27db1c57a 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -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óð", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 9edc9ae567..072ee445b5 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -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", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index 52305fec2b..b0bee442cf 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -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 {count, plural, one {# nnayeḍ} other {# nniḍen}} rnan abdar-ik·im uslig ar ismenyafen-nsen", "notification.follow": "iṭṭafar-ik·em-id {name}", "notification.follow.name_and_others": "{name} akked {count, plural, one {# nniḍen} other {# nniḍen}} iḍfeṛ-k·m-id", "notification.follow_request": "{name} yessuter-d ad k·m-yeḍfeṛ", diff --git a/app/javascript/mastodon/locales/nl.json b/app/javascript/mastodon/locales/nl.json index 22cae4b455..3ae1fd3f24 100644 --- a/app/javascript/mastodon/locales/nl.json +++ b/app/javascript/mastodon/locales/nl.json @@ -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", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 8b0f2cbddf..cf2e828a45 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -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", diff --git a/app/javascript/mastodon/locales/uk.json b/app/javascript/mastodon/locales/uk.json index 38401f18fa..f33558e6e8 100644 --- a/app/javascript/mastodon/locales/uk.json +++ b/app/javascript/mastodon/locales/uk.json @@ -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": "Категорія цього фільтра застаріла, Вам потрібно змінити дату закінчення терміну дії, щоб застосувати її.", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index fc67e1d7ae..2d7b6bbf31 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -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", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index cafd061818..f917ad623c 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -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": "播放音效", diff --git a/app/javascript/mastodon/reducers/settings.js b/app/javascript/mastodon/reducers/settings.js index 9e2ee5f55a..cea8949f23 100644 --- a/app/javascript/mastodon/reducers/settings.js +++ b/app/javascript/mastodon/reducers/settings.js @@ -36,6 +36,7 @@ const initialState = ImmutableMap({ follow_request: false, favourite: false, reblog: false, + quote: false, mention: false, poll: false, status: false, @@ -59,6 +60,7 @@ const initialState = ImmutableMap({ follow_request: false, favourite: true, reblog: true, + quote: true, mention: true, poll: true, status: true, @@ -72,6 +74,7 @@ const initialState = ImmutableMap({ follow_request: false, favourite: true, reblog: true, + quote: true, mention: true, poll: true, status: true, diff --git a/app/javascript/mastodon/selectors/notifications.ts b/app/javascript/mastodon/selectors/notifications.ts index ea640406ea..8c808a2dff 100644 --- a/app/javascript/mastodon/selectors/notifications.ts +++ b/app/javascript/mastodon/selectors/notifications.ts @@ -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'), ); }; diff --git a/app/lib/emoji_formatter.rb b/app/lib/emoji_formatter.rb index a5f5103fff..c193df9bb6 100644 --- a/app/lib/emoji_formatter.rb +++ b/app/lib/emoji_formatter.rb @@ -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] diff --git a/app/models/username_block.rb b/app/models/username_block.rb index 18db4ca34e..33169a47f8 100644 --- a/app/models/username_block.rb +++ b/app/models/username_block.rb @@ -29,10 +29,12 @@ class UsernameBlock < ApplicationRecord validates :username, presence: true, uniqueness: true scope :matches_exactly, ->(str) { where(exact: true).where(normalized_username: str) } - scope :matches_partially, ->(str) { where(exact: false).where(Arel::Nodes.build_quoted(str).matches(Arel::Nodes.build_quoted('%').concat(arel_table[:normalized_username]).concat(Arel::Nodes.build_quoted('%')))) } + scope :matches_partially, ->(str) { where(exact: false).where(Arel::Nodes.build_quoted(normalize_value_for(:normalized_username, str)).matches(Arel::Nodes.build_quoted('%').concat(arel_table[:normalized_username]).concat(Arel::Nodes.build_quoted('%')))) } before_save :set_normalized_username + normalizes :normalized_username, with: ->(value) { value.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS) } + def comparison exact? ? 'equals' : 'contains' end @@ -42,8 +44,7 @@ class UsernameBlock < ApplicationRecord end def self.matches?(str, allow_with_approval: false) - normalized_str = str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS) - matches_exactly(normalized_str).or(matches_partially(normalized_str)).where(allow_with_approval: allow_with_approval).any? + matches_exactly(str).or(matches_partially(str)).where(allow_with_approval: allow_with_approval).any? end def to_log_human_identifier @@ -53,10 +54,6 @@ class UsernameBlock < ApplicationRecord private def set_normalized_username - self.normalized_username = normalize(username) - end - - def normalize(str) - str.downcase.gsub(Regexp.union(HOMOGLYPHS.keys), HOMOGLYPHS) + self.normalized_username = username end end diff --git a/app/serializers/activitypub/note_serializer.rb b/app/serializers/activitypub/note_serializer.rb index 42bc86540f..332875e1ea 100644 --- a/app/serializers/activitypub/note_serializer.rb +++ b/app/serializers/activitypub/note_serializer.rb @@ -3,7 +3,7 @@ class ActivityPub::NoteSerializer < ActivityPub::Serializer include FormattingHelper - context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes, :direct_message + context_extensions :atom_uri, :conversation, :sensitive, :voters_count, :quotes, :interaction_policies, :direct_message attributes :id, :type, :summary, :in_reply_to, :published, :url, @@ -37,6 +37,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 raise Mastodon::NotPermittedError, 'Local-only statuses should not be serialized' if object.local_only? && !instance_options[:allow_local_only] @@ -228,6 +230,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 diff --git a/app/services/post_status_service.rb b/app/services/post_status_service.rb index a448f05993..53c3f871f7 100644 --- a/app/services/post_status_service.rb +++ b/app/services/post_status_service.rb @@ -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 @@ -232,6 +233,7 @@ class PostStatusService < BaseService application: @options[:application], content_type: @options[:content_type] || @account.user&.setting_default_content_type, rate_limit: @options[:with_rate_limit], + quote_approval_policy: @options[:quote_approval_policy], }.compact end diff --git a/app/services/update_status_service.rb b/app/services/update_status_service.rb index 5ae53751e9..1ebabf5fbe 100644 --- a/app/services/update_status_service.rb +++ b/app/services/update_status_service.rb @@ -117,6 +117,7 @@ class UpdateStatusService < BaseService @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.content_type = @options[:content_type] || @status.content_type + @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? diff --git a/app/views/notification_mailer/quote.html.haml b/app/views/notification_mailer/quote.html.haml index 1a2d8f8c76..139f4b2e8d 100644 --- a/app/views/notification_mailer/quote.html.haml +++ b/app/views/notification_mailer/quote.html.haml @@ -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' } diff --git a/config/locales/da.yml b/config/locales/da.yml index 85de82443e..20e3173c5a 100644 --- a/config/locales/da.yml +++ b/config/locales/da.yml @@ -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. Gennemgå eller fjern indlæg fra filteret. - 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 diff --git a/config/locales/kab.yml b/config/locales/kab.yml index 96f69dcbdc..4685887a96 100644 --- a/config/locales/kab.yml +++ b/config/locales/kab.yml @@ -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: diff --git a/config/locales/pt-PT.yml b/config/locales/pt-PT.yml index e8c357ee0f..3be630dbcd 100644 --- a/config/locales/pt-PT.yml +++ b/config/locales/pt-PT.yml @@ -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: diff --git a/config/locales/ru.yml b/config/locales/ru.yml index 17ba89bef0..a25ac88c54 100644 --- a/config/locales/ru.yml +++ b/config/locales/ru.yml @@ -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: Из-за того, что удаление постов — это ресурсоёмкий процесс, оно производится медленно со временем, когда сервер менее всего загружен. По этой причине, посты могут удаляться не сразу, а спустя определённое время, по достижению возрастного порога. diff --git a/config/locales/simple_form.ca.yml b/config/locales/simple_form.ca.yml index f979e68323..ee24975b5d 100644 --- a/config/locales/simple_form.ca.yml +++ b/config/locales/simple_form.ca.yml @@ -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 diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml index 58f6a129bb..296ce49131 100644 --- a/config/locales/simple_form.da.yml +++ b/config/locales/simple_form.da.yml @@ -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: diff --git a/config/locales/simple_form.kab.yml b/config/locales/simple_form.kab.yml index c0ff7e598e..d1a288e213 100644 --- a/config/locales/simple_form.kab.yml +++ b/config/locales/simple_form.kab.yml @@ -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: diff --git a/config/locales/simple_form.pt-PT.yml b/config/locales/simple_form.pt-PT.yml index 0e65d82fd8..adc58a7aed 100644 --- a/config/locales/simple_form.pt-PT.yml +++ b/config/locales/simple_form.pt-PT.yml @@ -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 diff --git a/config/locales/uk.yml b/config/locales/uk.yml index ed0ce8d7b5..721655d9a4 100644 --- a/config/locales/uk.yml +++ b/config/locales/uk.yml @@ -496,6 +496,15 @@ uk: new: title: Імпорт блокувань домену no_file: Файл не вибрано + fasp: + debug: + callbacks: + delete: Видалити + providers: + delete: Видалити + registrations: + confirm: Підтвердити + save: Зберегти follow_recommendations: description_html: "Слідувати рекомендаціям та допомогти новим користувачам швидко знайти цікавий вміст. Коли користувачі не взаємодіяли з іншими людьми достатньо, щоб сформувати персоналізовані рекомендації, радимо замість цього вказувати ці облікові записи. Вони щоденно переобчислюються з масиву облікових записів з найбільшою кількістю недавніх взаємодій і найбільшою кількістю місцевих підписників розраховується для цієї мови." language: Для мови diff --git a/spec/models/canonical_email_block_spec.rb b/spec/models/canonical_email_block_spec.rb index c63483f968..0b8dcfb9c1 100644 --- a/spec/models/canonical_email_block_spec.rb +++ b/spec/models/canonical_email_block_spec.rb @@ -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' } diff --git a/spec/models/form/import_spec.rb b/spec/models/form/import_spec.rb index dd8fd35a05..d682e13ecb 100644 --- a/spec/models/form/import_spec.rb +++ b/spec/models/form/import_spec.rb @@ -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 }, diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index ac15ae2462..ba18623302 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -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 diff --git a/spec/serializers/activitypub/note_serializer_spec.rb b/spec/serializers/activitypub/note_serializer_spec.rb index 9c898e5212..d5d02a0d49 100644 --- a/spec/serializers/activitypub/note_serializer_spec.rb +++ b/spec/serializers/activitypub/note_serializer_spec.rb @@ -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 diff --git a/spec/services/post_status_service_spec.rb b/spec/services/post_status_service_spec.rb index 7e47506a9f..64bb5e32e2 100644 --- a/spec/services/post_status_service_spec.rb +++ b/spec/services/post_status_service_spec.rb @@ -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) diff --git a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb index 55b66629e0..1cd01eb3b9 100644 --- a/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb +++ b/spec/workers/scheduler/accounts_statuses_cleanup_scheduler_spec.rb @@ -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