From 8ee4b3f906c52bad2e2a899e17047235376a8d83 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 8 Aug 2025 10:44:05 +0200 Subject: [PATCH 1/6] Update Redux to handle quote posts (#35715) --- app/javascript/mastodon/actions/compose.js | 2 + .../mastodon/actions/compose_typed.ts | 34 +++++- app/javascript/mastodon/api_types/quotes.ts | 23 ++++ app/javascript/mastodon/api_types/statuses.ts | 2 + app/javascript/mastodon/reducers/compose.js | 23 +++- .../mastodon/store/typed_functions.ts | 106 ++++++++++++++++-- app/models/concerns/user/has_settings.rb | 4 + app/serializers/initial_state_serializer.rb | 1 + 8 files changed, 183 insertions(+), 12 deletions(-) create mode 100644 app/javascript/mastodon/api_types/quotes.ts diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index d70834cec6..28c90381e0 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -228,6 +228,8 @@ export function submitCompose() { visibility: getState().getIn(['compose', 'privacy']), poll: getState().getIn(['compose', 'poll'], null), language: getState().getIn(['compose', 'language']), + quoted_status_id: getState().getIn(['compose', 'quoted_status_id']), + quote_approval_policy: getState().getIn(['compose', 'quote_policy']), }, headers: { 'Idempotency-Key': getState().getIn(['compose', 'idempotencyKey']), diff --git a/app/javascript/mastodon/actions/compose_typed.ts b/app/javascript/mastodon/actions/compose_typed.ts index 97f0d68c51..7b1f5e688c 100644 --- a/app/javascript/mastodon/actions/compose_typed.ts +++ b/app/javascript/mastodon/actions/compose_typed.ts @@ -1,9 +1,18 @@ +import { createAction } from '@reduxjs/toolkit'; import type { List as ImmutableList, Map as ImmutableMap } from 'immutable'; import { apiUpdateMedia } from 'mastodon/api/compose'; import type { ApiMediaAttachmentJSON } from 'mastodon/api_types/media_attachments'; import type { MediaAttachment } from 'mastodon/models/media_attachment'; -import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; +import { + createDataLoadingThunk, + createAppThunk, +} from 'mastodon/store/typed_functions'; + +import type { ApiQuotePolicy } from '../api_types/quotes'; +import type { Status } from '../models/status'; + +import { ensureComposeIsVisible } from './compose'; type SimulatedMediaAttachmentJSON = ApiMediaAttachmentJSON & { unattached?: boolean; @@ -68,3 +77,26 @@ export const changeUploadCompose = createDataLoadingThunk( useLoadingBar: false, }, ); + +export const quoteComposeByStatus = createAppThunk( + 'compose/quoteComposeStatus', + (status: Status, { getState }) => { + ensureComposeIsVisible(getState); + return status; + }, +); + +export const quoteComposeById = createAppThunk( + (statusId: string, { dispatch, getState }) => { + const status = getState().statuses.get(statusId); + if (status) { + dispatch(quoteComposeByStatus(status)); + } + }, +); + +export const quoteComposeCancel = createAction('compose/quoteComposeCancel'); + +export const setQuotePolicy = createAction( + 'compose/setQuotePolicy', +); diff --git a/app/javascript/mastodon/api_types/quotes.ts b/app/javascript/mastodon/api_types/quotes.ts new file mode 100644 index 0000000000..8c0ea10fc3 --- /dev/null +++ b/app/javascript/mastodon/api_types/quotes.ts @@ -0,0 +1,23 @@ +import type { ApiStatusJSON } from './statuses'; + +export type ApiQuoteState = 'accepted' | 'pending' | 'revoked' | 'unauthorized'; +export type ApiQuotePolicy = 'public' | 'followers' | 'nobody'; + +interface ApiQuoteEmptyJSON { + state: Exclude; + quoted_status: null; +} + +interface ApiNestedQuoteJSON { + state: 'accepted'; + quoted_status_id: string; +} + +interface ApiQuoteAcceptedJSON { + state: 'accepted'; + quoted_status: Omit & { + quote: ApiNestedQuoteJSON | ApiQuoteEmptyJSON; + }; +} + +export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON; diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts index 09bd2349b3..cd0b1001ac 100644 --- a/app/javascript/mastodon/api_types/statuses.ts +++ b/app/javascript/mastodon/api_types/statuses.ts @@ -4,6 +4,7 @@ import type { ApiAccountJSON } from './accounts'; import type { ApiCustomEmojiJSON } from './custom_emoji'; import type { ApiMediaAttachmentJSON } from './media_attachments'; import type { ApiPollJSON } from './polls'; +import type { ApiQuoteJSON } from './quotes'; // See app/modals/status.rb export type StatusVisibility = @@ -118,6 +119,7 @@ export interface ApiStatusJSON { card?: ApiPreviewCardJSON; poll?: ApiPollJSON; + quote?: ApiQuoteJSON; } export interface ApiContextJSON { diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 6b799a46e8..c5b3c22ec1 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -1,6 +1,11 @@ import { Map as ImmutableMap, List as ImmutableList, OrderedSet as ImmutableOrderedSet, fromJS } from 'immutable'; -import { changeUploadCompose } from 'mastodon/actions/compose_typed'; +import { + changeUploadCompose, + quoteComposeByStatus, + quoteComposeCancel, + setQuotePolicy, +} from 'mastodon/actions/compose_typed'; import { timelineDelete } from 'mastodon/actions/timelines_typed'; import { @@ -83,6 +88,11 @@ const initialState = ImmutableMap({ resetFileKey: Math.floor((Math.random() * 0x10000)), idempotencyKey: null, tagHistory: ImmutableList(), + + // Quotes + quoted_status_id: null, + quote_policy: 'public', + default_quote_policy: 'public', // Set in hydration. }); const initialPoll = ImmutableMap({ @@ -117,6 +127,8 @@ function clearAll(state) { map.set('progress', 0); map.set('poll', null); map.set('idempotencyKey', uuid()); + map.set('quoted_status_id', null); + map.set('quote_policy', state.get('default_quote_policy')); }); } @@ -317,6 +329,15 @@ export const composeReducer = (state = initialState, action) => { return state.set('is_changing_upload', true); } else if (changeUploadCompose.rejected.match(action)) { return state.set('is_changing_upload', false); + } else if (quoteComposeByStatus.match(action)) { + const status = action.payload; + if (status.getIn(['quote_approval', 'current_user']) === 'automatic') { + return state.set('quoted_status_id', status.get('id')); + } + } else if (quoteComposeCancel.match(action)) { + return state.set('quoted_status_id', null); + } else if (setQuotePolicy.match(action)) { + return state.set('quote_policy', action.payload); } switch(action.type) { diff --git a/app/javascript/mastodon/store/typed_functions.ts b/app/javascript/mastodon/store/typed_functions.ts index f0a18a0681..69f6028be2 100644 --- a/app/javascript/mastodon/store/typed_functions.ts +++ b/app/javascript/mastodon/store/typed_functions.ts @@ -1,5 +1,12 @@ -import type { GetThunkAPI } from '@reduxjs/toolkit'; -import { createAsyncThunk, createSelector } from '@reduxjs/toolkit'; +import type { + ActionCreatorWithPreparedPayload, + GetThunkAPI, +} from '@reduxjs/toolkit'; +import { + createAsyncThunk as rtkCreateAsyncThunk, + createSelector, + createAction, +} from '@reduxjs/toolkit'; // eslint-disable-next-line @typescript-eslint/no-restricted-imports import { useDispatch, useSelector } from 'react-redux'; @@ -18,7 +25,7 @@ interface AppMeta { useLoadingBar?: boolean; } -export const createAppAsyncThunk = createAsyncThunk.withTypes<{ +export const createAppAsyncThunk = rtkCreateAsyncThunk.withTypes<{ state: RootState; dispatch: AppDispatch; rejectValue: AsyncThunkRejectValue; @@ -43,9 +50,88 @@ interface AppThunkOptions { ) => boolean; } -const createBaseAsyncThunk = createAsyncThunk.withTypes(); +// Type definitions for the sync thunks. +type AppThunk = ( + arg: Arg, +) => (dispatch: AppDispatch, getState: () => RootState) => Returned; -export function createThunk( +type AppThunkCreator = ( + arg: Arg, + api: AppThunkApi, + extra?: ExtraArg, +) => Returned; + +type AppThunkActionCreator< + Arg = void, + Returned = void, +> = ActionCreatorWithPreparedPayload< + [Returned, Arg], + Returned, + string, + never, + { arg: Arg } +>; + +// Version that does not dispatch it's own action. +export function createAppThunk( + creator: AppThunkCreator, + extra?: ExtraArg, +): AppThunk; + +// Version that dispatches an named action with the result of the creator callback. +export function createAppThunk( + name: string, + creator: AppThunkCreator, + extra?: ExtraArg, +): AppThunk & AppThunkActionCreator; + +/** Creates a thunk that dispatches an action. */ +export function createAppThunk( + nameOrCreator: string | AppThunkCreator, + maybeCreatorOrExtra?: AppThunkCreator | ExtraArg, + maybeExtra?: ExtraArg, +) { + const isDispatcher = typeof nameOrCreator === 'string'; + const name = isDispatcher ? nameOrCreator : undefined; + const creator = isDispatcher + ? (maybeCreatorOrExtra as AppThunkCreator) + : nameOrCreator; + const extra = isDispatcher ? maybeExtra : (maybeCreatorOrExtra as ExtraArg); + let action: null | AppThunkActionCreator = null; + + // Creates a thunk that dispatches the action with the result of the creator. + const actionCreator: AppThunk = (arg) => { + return (dispatch, getState) => { + const result = creator(arg, { dispatch, getState }, extra); + if (action) { + // Dispatches the action with the result. + const actionObj = action(result, arg); + dispatch(actionObj); + } + return result; + }; + }; + + // No action name provided, return the thunk directly. + if (!name) { + return actionCreator; + } + + // Create the action and assign the action creator to it in order + // to have things like `toString` and `match` available. + action = createAction(name, (payload: Returned, arg: Arg) => ({ + payload, + meta: { + arg, + }, + })); + + return Object.assign({}, action, actionCreator); +} + +const createBaseAsyncThunk = rtkCreateAsyncThunk.withTypes(); + +export function createAsyncThunk( name: string, creator: (arg: Arg, api: AppThunkApi) => Returned | Promise, options: AppThunkOptions = {}, @@ -104,7 +190,7 @@ export function createDataLoadingThunk( name: string, loadData: (args: Args) => Promise, thunkOptions?: AppThunkOptions, -): ReturnType>; +): ReturnType>; // Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty export function createDataLoadingThunk( @@ -114,7 +200,7 @@ export function createDataLoadingThunk( | AppThunkOptions | OnData, thunkOptions?: AppThunkOptions, -): ReturnType>; +): ReturnType>; // Overload when the `onData` method returns nothing, then the mayload is the `onData` result export function createDataLoadingThunk( @@ -124,7 +210,7 @@ export function createDataLoadingThunk( | AppThunkOptions | OnData, thunkOptions?: AppThunkOptions, -): ReturnType>; +): ReturnType>; // Overload when there is an `onData` method returning something export function createDataLoadingThunk< @@ -138,7 +224,7 @@ export function createDataLoadingThunk< | AppThunkOptions | OnData, thunkOptions?: AppThunkOptions, -): ReturnType>; +): ReturnType>; /** * This function creates a Redux Thunk that handles loading data asynchronously (usually from the API), dispatching `pending`, `fullfilled` and `rejected` actions. @@ -189,7 +275,7 @@ export function createDataLoadingThunk< thunkOptions = maybeThunkOptions; } - return createThunk( + return createAsyncThunk( name, async (arg, { getState, dispatch }) => { const data = await loadData(arg, { diff --git a/app/models/concerns/user/has_settings.rb b/app/models/concerns/user/has_settings.rb index 14d2f22c24..04ad524c5a 100644 --- a/app/models/concerns/user/has_settings.rb +++ b/app/models/concerns/user/has_settings.rb @@ -107,6 +107,10 @@ module User::HasSettings settings['default_privacy'] || (account.locked? ? 'private' : 'public') end + def setting_default_quote_policy + settings['default_quote_policy'] || 'public' + end + def allows_report_emails? settings['notification_emails.report'] end diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index cc95d8e754..7926cc54be 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -54,6 +54,7 @@ class InitialStateSerializer < ActiveModel::Serializer store[:default_privacy] = object.visibility || object_account_user.setting_default_privacy store[:default_sensitive] = object_account_user.setting_default_sensitive store[:default_language] = object_account_user.preferred_posting_language + store[:default_quote_policy] = object_account_user.setting_default_quote_policy end store[:text] = object.text if object.text From 1fd147bf2b7ee9d996a04d94b52ed0842c604a94 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 11:00:51 +0200 Subject: [PATCH 2/6] New Crowdin Translations (automated) (#35720) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/be.json | 87 +++++++++++- app/javascript/mastodon/locales/da.json | 2 +- app/javascript/mastodon/locales/el.json | 4 + app/javascript/mastodon/locales/fi.json | 4 + app/javascript/mastodon/locales/ga.json | 4 + app/javascript/mastodon/locales/hu.json | 4 + app/javascript/mastodon/locales/kab.json | 20 ++- app/javascript/mastodon/locales/ko.json | 4 + app/javascript/mastodon/locales/si.json | 1 + config/locales/activerecord.be.yml | 10 ++ config/locales/activerecord.kab.yml | 2 + config/locales/be.yml | 173 ++++++++++++++++++++++- config/locales/doorkeeper.be.yml | 1 + config/locales/kab.yml | 4 + config/locales/simple_form.da.yml | 2 +- config/locales/simple_form.kab.yml | 13 +- 16 files changed, 322 insertions(+), 13 deletions(-) diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index 706e746988..2d0842727a 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -43,7 +43,7 @@ "account.followers": "Падпісчыкі", "account.followers.empty": "Ніхто пакуль не падпісаны на гэтага карыстальніка.", "account.followers_counter": "{count, plural, one {{counter} падпісчык} few {{counter} падпісчыкі} many {{counter} падпісчыкаў} other {{counter} падпісчыка}}", - "account.followers_you_know_counter": "{count, one {{counter}, знаёмы вам} other {{counter}, знаёмых вам}}", + "account.followers_you_know_counter": "{count, plural, one {{counter}, знаёмы вам} other {{counter}, знаёмых вам}}", "account.following": "Падпіскі", "account.following_counter": "{count, plural, one {{counter} падпіска} few {{counter} падпіскі} many {{counter} падпісак} other {{counter} падпіскі}}", "account.follows.empty": "Карыстальнік ні на каго не падпісаны.", @@ -62,6 +62,7 @@ "account.mute_notifications_short": "Не апавяшчаць", "account.mute_short": "Ігнараваць", "account.muted": "Ігнаруецца", + "account.muting": "Ігнараванне", "account.mutual": "Вы падпісаны адно на аднаго", "account.no_bio": "Апісанне адсутнічае.", "account.open_original_page": "Адкрыць арыгінальную старонку", @@ -103,6 +104,8 @@ "alt_text_modal.add_text_from_image": "Дадаць тэкст з відарыса", "alt_text_modal.cancel": "Скасаваць", "alt_text_modal.change_thumbnail": "Змяніць мініяцюру", + "alt_text_modal.describe_for_people_with_hearing_impairments": "Апішыце гэта людзям з праблемамі са слыхам…", + "alt_text_modal.describe_for_people_with_visual_impairments": "Апішыце гэта людзям з праблемамі са зрокам…", "alt_text_modal.done": "Гатова", "announcement.announcement": "Аб'ява", "annual_report.summary.archetype.booster": "Паляўнічы на трэнды", @@ -216,7 +219,13 @@ "confirmations.delete_list.confirm": "Выдаліць", "confirmations.delete_list.message": "Вы ўпэўненыя, што хочаце беззваротна выдаліць гэты чарнавік?", "confirmations.delete_list.title": "Выдаліць спіс?", + "confirmations.discard_draft.confirm": "Адмовіцца і працягнуць", "confirmations.discard_draft.edit.cancel": "Працягнуць рэдагаванне", + "confirmations.discard_draft.edit.message": "Калі працягнуць, то ўсе змены, што Вы зрабілі ў гэтым допісе, будуць адмененыя.", + "confirmations.discard_draft.edit.title": "Адмовіцца ад змен у Вашым допісе?", + "confirmations.discard_draft.post.cancel": "Працягнуць чарнавік", + "confirmations.discard_draft.post.message": "Калі працягнуць, то допіс, які Вы зараз пішаце, не будзе апублікаваны.", + "confirmations.discard_draft.post.title": "Выдаліць чарнавік?", "confirmations.discard_edit_media.confirm": "Адмяніць", "confirmations.discard_edit_media.message": "У вас ёсць незахаваныя змены ў апісанні або прэв'ю, усе роўна скасаваць іх?", "confirmations.follow_to_list.confirm": "Падпісацца і дадаць у спіс", @@ -226,6 +235,7 @@ "confirmations.logout.message": "Вы ўпэўненыя, што хочаце выйсці?", "confirmations.logout.title": "Выйсці?", "confirmations.missing_alt_text.confirm": "Дадаць альтэрнатыўны тэкст", + "confirmations.missing_alt_text.message": "У Вашым допісе ёсць медыя без альтэрнатыўнага тэксту. Дадаванне апісання дапамагае зрабіць Ваш допіс даступным для большай колькасці людзей.", "confirmations.missing_alt_text.secondary": "Усё адно апублікаваць", "confirmations.missing_alt_text.title": "Дадаць альтэрнатыўны тэкст?", "confirmations.mute.confirm": "Ігнараваць", @@ -233,7 +243,11 @@ "confirmations.redraft.message": "Вы ўпэўнены, што хочаце выдаліць допіс і перапісаць яго? Упадабанні і пашырэнні згубяцца, а адказы да арыгінальнага допісу асірацеюць.", "confirmations.redraft.title": "Выдаліць і перапісаць допіс?", "confirmations.remove_from_followers.confirm": "Выдаліць падпісчыка", + "confirmations.remove_from_followers.message": "Карыстальнік {name} больш не будзе падпісаны на Вас. Упэўненыя, што хочаце працягнуць?", "confirmations.remove_from_followers.title": "Выдаліць падпісчыка?", + "confirmations.revoke_quote.confirm": "Выдаліць допіс", + "confirmations.revoke_quote.message": "Гэтае дзеянне немагчыма адмяніць.", + "confirmations.revoke_quote.title": "Выдаліць допіс?", "confirmations.unfollow.confirm": "Адпісацца", "confirmations.unfollow.message": "Вы ўпэўненыя, што хочаце адпісацца ад {name}?", "confirmations.unfollow.title": "Адпісацца ад карыстальніка?", @@ -295,6 +309,9 @@ "emoji_button.search_results": "Вынікі пошуку", "emoji_button.symbols": "Сімвалы", "emoji_button.travel": "Падарожжы і месцы", + "empty_column.account_featured.me": "Вы яшчэ нічога не паказалі. Ці ведалі Вы, што ў сваім профілі Вы можаце паказаць свае хэштэгі, якім найбольш карыстаецеся, і нават профілі сваіх сяброў?", + "empty_column.account_featured.other": "{acct} яшчэ нічога не паказаў. Ці ведалі Вы, што ў сваім профілі Вы можаце паказаць свае хэштэгі, якім найбольш карыстаецеся, і нават профілі сваіх сяброў?", + "empty_column.account_featured_other.unknown": "Гэты профіль яшчэ нічога не паказаў.", "empty_column.account_hides_collections": "Гэты карыстальнік вырашыў схаваць гэтую інфармацыю", "empty_column.account_suspended": "Уліковы запіс прыпынены", "empty_column.account_timeline": "Тут няма допісаў!", @@ -327,6 +344,7 @@ "explore.trending_links": "Навіны", "explore.trending_statuses": "Допісы", "explore.trending_tags": "Хэштэгі", + "featured_carousel.header": "{count, plural,one {Замацаваны допіс} other {Замацаваныя допісы}}", "featured_carousel.next": "Далей", "featured_carousel.post": "Допіс", "featured_carousel.previous": "Назад", @@ -383,6 +401,8 @@ "generic.saved": "Захавана", "getting_started.heading": "Пачатак працы", "hashtag.admin_moderation": "Адкрыць інтэрфейс мадэратара для #{name}", + "hashtag.browse": "Праглядзець допісы ў #{hashtag}", + "hashtag.browse_from_account": "Праглядзець допісы ад @{name} у #{hashtag}", "hashtag.column_header.tag_mode.all": "і {additional}", "hashtag.column_header.tag_mode.any": "або {additional}", "hashtag.column_header.tag_mode.none": "без {additional}", @@ -395,7 +415,10 @@ "hashtag.counter_by_accounts": "{count, plural, one {{counter} удзельнік} few {{counter} удзельніка} many {{counter} удзельнікаў} other {{counter} удзельніка}}", "hashtag.counter_by_uses": "{count, plural, one {{counter} допіс} few {{counter} допісы} many {{counter} допісаў} other {{counter} допісу}}", "hashtag.counter_by_uses_today": "{count, plural, one {{counter} допіс} few {{counter} допісы} many {{counter} допісаў} other {{counter} допісу}} за сёння", + "hashtag.feature": "Паказваць у профілі", "hashtag.follow": "Падпісацца на хэштэг", + "hashtag.mute": "Ігнараваць #{hashtag}", + "hashtag.unfeature": "Не паказваць у профілі", "hashtag.unfollow": "Адпісацца ад хэштэга", "hashtags.and_other": "…і яшчэ {count, plural, other {#}}", "hints.profiles.followers_may_be_missing": "Падпісчыкі гэтага профілю могуць адсутнічаць.", @@ -424,8 +447,12 @@ "ignore_notifications_modal.not_following_title": "Ігнараваць апавяшчэнні ад людзей на якіх вы не падпісаны?", "ignore_notifications_modal.private_mentions_title": "Ігнараваць апавяшчэнні пра непажаданыя асабістыя згадванні?", "info_button.label": "Даведка", + "info_button.what_is_alt_text": "

Што такое альтэрнатыўны тэкст?

Альтэрнатыўны тэкст апісвае відарыс людзям з парушэннем зроку, павольным злучэннем або тым, каму патрэбны дадатковы кантэкст.

Вы можаце зрабіць відарыс больш дасяжным і зразумелым для ўсіх, напісаўшы зразумелы, сціслы і аб'ектыўны альтэрнатыўны тэкст.

  • Ахоплівайце важныя элементы
  • Тлумачце тэкст на відарысе
  • Карыстайцеся звычайнымі сказамі
  • Пазбягайце залішняй інфармацыі
  • Засяроджвайцеся на тэндэнцыях і ключавых высновах у цяжкіх для разумення візуальных матэрыялах (напрыклад, дыяграмах або картах)
", "interaction_modal.action.favourite": "Каб працягнуць, вы мусіце ўпадабаць нешта са свайго ўліковага запісу.", "interaction_modal.action.follow": "Каб працягнуць, вы мусіце падпісацца на некага са свайго ўліковага запісу.", + "interaction_modal.action.reblog": "Каб працягнуць, Вам трэба пашырыць допіс са свайго профілю.", + "interaction_modal.action.reply": "Каб працягнуць, Вам трэба адказаць са свайго профілю.", + "interaction_modal.action.vote": "Каб працягнуць, Вам трэба прагаласаваць са свайго профілю.", "interaction_modal.go": "Перайсці", "interaction_modal.no_account_yet": "Не маеце ўліковага запісу?", "interaction_modal.on_another_server": "На іншым серверы", @@ -434,6 +461,7 @@ "interaction_modal.title.follow": "Падпісацца на {name}", "interaction_modal.title.reblog": "Пашырыць допіс ад {name}", "interaction_modal.title.reply": "Адказаць на допіс {name}", + "interaction_modal.title.vote": "Прагаласуйце ў апытанні {name}", "interaction_modal.username_prompt": "Напр., {example}", "intervals.full.days": "{number, plural, one {# дзень} few {# дні} many {# дзён} other {# дня}}", "intervals.full.hours": "{number, plural, one {# гадзіна} few {# гадзіны} many {# гадзін} other {# гадзіны}}", @@ -473,6 +501,8 @@ "keyboard_shortcuts.translate": "каб перакласці допіс", "keyboard_shortcuts.unfocus": "Расфакусаваць тэкставую вобласць/пошукавы радок", "keyboard_shortcuts.up": "Перамясціцца ўверх па спісе", + "learn_more_link.got_it": "Зразумеў(-ла)", + "learn_more_link.learn_more": "Падрабязней", "lightbox.close": "Закрыць", "lightbox.next": "Далей", "lightbox.previous": "Назад", @@ -487,10 +517,15 @@ "lists.add_to_list": "Дадаць у спіс", "lists.add_to_lists": "Дадаць {name} у спісы", "lists.create": "Стварыць", + "lists.create_a_list_to_organize": "Стварыце новы спіс, каб арганізаваць сваю Галоўную старонку", "lists.create_list": "Стварыць спіс", "lists.delete": "Выдаліць спіс", "lists.done": "Гатова", "lists.edit": "Рэдагаваць спіс", + "lists.exclusive": "Схаваць карыстальнікаў на Галоўнай старонцы", + "lists.exclusive_hint": "Калі ў гэтым спісе нехта ёсць, схавайце яго на сваёй Галоўнай старонцы, каб не бачыць яго допісы двойчы.", + "lists.find_users_to_add": "Знайсці карыстальнікаў, каб дадаць", + "lists.list_members_count": "{count, plural,one {# карыстальнік}other {# карыстальнікі}}", "lists.list_name": "Назва спіса", "lists.new_list_name": "Назва новага спіса", "lists.no_lists_yet": "Пакуль няма спісаў.", @@ -502,6 +537,7 @@ "lists.replies_policy.none": "Нікога", "lists.save": "Захаваць", "lists.search": "Пошук", + "lists.show_replies_to": "Уключыць адказы ад карыстальнікаў са спіса", "load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}", "loading_indicator.label": "Ідзе загрузка…", "media_gallery.hide": "Схаваць", @@ -544,6 +580,8 @@ "navigation_bar.search_trends": "Пошук / Трэндавае", "navigation_panel.collapse_followed_tags": "Згарнуць меню падпісак на хэштэгі", "navigation_panel.collapse_lists": "Згарнуць меню спісаў", + "navigation_panel.expand_followed_tags": "Разгарнуць меню падпісак на хэштэгі", + "navigation_panel.expand_lists": "Разгарнуць меню спіса", "not_signed_in_indicator.not_signed_in": "Вам трэба ўвайсці каб атрымаць доступ да гэтага рэсурсу.", "notification.admin.report": "{name} паскардзіўся на {target}", "notification.admin.report_account": "{name} паскардзіўся на {count, plural, one {# допіс} many {# допісаў} other {# допіса}} ад {target} з прычыны {category}", @@ -551,16 +589,21 @@ "notification.admin.report_statuses": "{name} паскардзіўся на {target} з прычыны {category}", "notification.admin.report_statuses_other": "{name} паскардзіўся на {target}", "notification.admin.sign_up": "{name} зарэгістраваўся", + "notification.admin.sign_up.name_and_others": "{name} і {count, plural, one {# іншы} other {# іншых}} зарэгістраваліся", + "notification.annual_report.message": "Вас чакае Ваш #Wrapstodon нумар {year}! Падзяліцеся сваімі галоўнымі падзеямі і запамінальнымі момантамі ў Mastodon!", "notification.annual_report.view": "Перайсці да #Wrapstodon", - "notification.favourite": "Ваш допіс упадабаны {name}", + "notification.favourite": "Карыстальнік {name} упадабаў Ваш допіс", + "notification.favourite.name_and_others_with_link": "{name} і {count, plural, one {# іншы} other {# іншыя}} ўпадабалі Ваш допіс", "notification.favourite_pm": "Ваша асабістае згадванне ўпадабана {name}", "notification.favourite_pm.name_and_others_with_link": "{name} і {count, plural, one {# іншы} few {# іншыя} many {# іншых} other {# іншых}} ўпадабалі ваша асабістае згадванне", "notification.follow": "{name} падпісаўся на вас", + "notification.follow.name_and_others": "{name} і {count, plural, one {# іншы} other {# іншыя}} падпісаліся на Вас", "notification.follow_request": "{name} адправіў запыт на падпіску", "notification.follow_request.name_and_others": "{name} і {count, plural, one {# іншы} many {# іншых} other {# іншых}} запыталіся падпісацца на вас", "notification.label.mention": "Згадванне", "notification.label.private_mention": "Асабістае згадванне", "notification.label.private_reply": "Асабісты адказ", + "notification.label.quote": "Карыстальнік {name} цытаваў Ваш допіс", "notification.label.reply": "Адказ", "notification.mention": "Згадванне", "notification.mentioned_you": "{name} згадаў вас", @@ -576,7 +619,7 @@ "notification.own_poll": "Ваша апытанне скончылася", "notification.poll": "Апытанне, дзе вы прынялі ўдзел, скончылася", "notification.reblog": "{name} пашырыў ваш допіс", - "notification.reblog.name_and_others_with_link": "{name} і {count, plural, one {# іншы} many {# іншых} other {# іншых}} абагулілі ваш допіс", + "notification.reblog.name_and_others_with_link": "{name} і {count, plural, one {# іншы} many {# іншых} other {# іншых}} пашырылі ваш допіс", "notification.relationships_severance_event": "Страціў сувязь з {name}", "notification.relationships_severance_event.account_suspension": "Адміністратар з {from} прыпыніў працу {target}, што азначае, што вы больш не можаце атрымліваць ад іх абнаўлення ці ўзаемадзейнічаць з імі.", "notification.relationships_severance_event.domain_block": "Адміністратар з {from} заблакіраваў {target}, у тым ліку {followersCount} вашых падпісчыка(-аў) і {followingCount, plural, one {# уліковы запіс} few {# уліковыя запісы} many {# уліковых запісаў} other {# уліковых запісаў}}.", @@ -585,11 +628,19 @@ "notification.status": "Новы допіс ад {name}", "notification.update": "Допіс {name} адрэдагаваны", "notification_requests.accept": "Прыняць", + "notification_requests.accept_multiple": "{count, plural,one {Прыняць # запыт…} other {Прыняць # запытаў…}}", + "notification_requests.confirm_accept_multiple.button": "{count, plural,one {Прыняць запыт} other {Прыняць запыты}}", + "notification_requests.confirm_accept_multiple.message": "Вы збіраецеся прыняць {count, plural, one {адзін запыт на апавяшчэнне} other {# запытаў на апавяшчэнне}}. Упэўненыя, што хочаце працягнуць?", "notification_requests.confirm_accept_multiple.title": "Прыняць запыты на апавяшчэнні?", + "notification_requests.confirm_dismiss_multiple.button": "{count, plural,one {Адмовіцца ад запыту} other {Адмовіцца ад запытаў}}", + "notification_requests.confirm_dismiss_multiple.message": "Вы збіраецеся адмовіцца ад {count, plural, one {аднаго запыту на апавяшчэнне} other {# запытаў на апавяшчэнне}}. Вы не зможаце зноў лёгка атрымаць доступ да {count, plural, one {яго} other {іх}}. Упэўненыя, што хочаце працягнуць?", "notification_requests.confirm_dismiss_multiple.title": "Адхіліць запыты на апавяшчэнні?", "notification_requests.dismiss": "Адхіліць", + "notification_requests.dismiss_multiple": "{count, plural,one {Адмовіцца ад запыту…} other {Адмовіцца ад запытаў…}}", "notification_requests.edit_selection": "Рэдагаваць", "notification_requests.exit_selection": "Гатова", + "notification_requests.explainer_for_limited_account": "Апавяшчэнне з гэтага профілю было адфільтраванае, бо гэты профіль абмежаваў мадэратар.", + "notification_requests.explainer_for_limited_remote_account": "Апавяшчэнні з гэтага профілю былі адфільтраваныя, бо гэты профіль абмежаваў мадэратар.", "notification_requests.maximize": "Разгарнуць", "notification_requests.minimize_banner": "Згарнуць банер адфільтраваных апавяшчэнняў", "notification_requests.notifications_from": "Апавяшчэнні ад {name}", @@ -610,6 +661,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": "Прайграваць гук", @@ -633,7 +685,10 @@ "notifications.policy.accept": "Прыняць", "notifications.policy.accept_hint": "Паказваць у апавяшчэннях", "notifications.policy.drop": "Iгнараваць", + "notifications.policy.drop_hint": "Адправіць у бездань, адкуль больш ніколі не ўбачыце", "notifications.policy.filter": "Фільтраваць", + "notifications.policy.filter_hint": "Адправіць у скрыню адфільтраваных апавяшчэнняў", + "notifications.policy.filter_limited_accounts_hint": "Абмежавана мадэратарамі сервера", "notifications.policy.filter_limited_accounts_title": "Уліковыя запісы пад мадэрацыяй", "notifications.policy.filter_new_accounts.hint": "Створаныя на працягу {days, plural, one {апошняга # дня} few {апошніх # дзён} many {апошніх # дзён} other {апошняй # дня}}", "notifications.policy.filter_new_accounts_title": "Новыя ўліковыя запісы", @@ -755,6 +810,7 @@ "report_notification.categories.violation": "Парушэнне правілаў", "report_notification.categories.violation_sentence": "парушэнне правілаў", "report_notification.open": "Адкрыць скаргу", + "search.clear": "Ачысціць пошук", "search.no_recent_searches": "Гісторыя пошуку пустая", "search.placeholder": "Пошук", "search.quick_action.account_search": "Супадзенне профіляў {x}", @@ -796,6 +852,8 @@ "status.bookmark": "Дадаць закладку", "status.cancel_reblog_private": "Прыбраць", "status.cannot_reblog": "Гэты пост нельга пашырыць", + "status.context.load_new_replies": "Даступныя новыя адказы", + "status.context.loading": "Правяраюцца новыя адказы", "status.continued_thread": "Працяг ланцужка", "status.copy": "Скапіраваць спасылку на допіс", "status.delete": "Выдаліць", @@ -807,7 +865,7 @@ "status.edited_x_times": "Рэдагавана {count, plural, one {{count} раз} few {{count} разы} many {{count} разоў} other {{count} разу}}", "status.embed": "Атрымаць убудаваны код", "status.favourite": "Упадабанае", - "status.favourites": "{count, plural, one {# упадабанае} few {# упадабаныя} many {# упадабаных} other {# упадабанага}}", + "status.favourites": "{count, plural, one {упадабанне} few {упадабанні} other {упадабанняў}}", "status.filter": "Фільтраваць гэты допіс", "status.history.created": "Створана {name} {date}", "status.history.edited": "Адрэдагавана {name} {date}", @@ -821,19 +879,27 @@ "status.mute_conversation": "Ігнараваць размову", "status.open": "Разгарнуць гэты допіс", "status.pin": "Замацаваць у профілі", + "status.quote_error.filtered": "Схавана адным з Вашых фільтраў", + "status.quote_error.not_available": "Допіс недаступны", + "status.quote_error.pending_approval": "Допіс чакае пацвярджэння", + "status.quote_error.pending_approval_popout.body": "Допісы, якія былі цытаваныя паміж серверамі Fediverse, могуць доўга загружацца, паколькі розныя серверы маюць розныя пратаколы.", + "status.quote_error.pending_approval_popout.title": "Цытаваны допіс чакае пацвярджэння? Захоўвайце спакой", + "status.quote_post_author": "Цытаваў допіс @{name}", "status.read_more": "Чытаць болей", "status.reblog": "Пашырыць", "status.reblog_private": "Пашырыць з першапачатковай бачнасцю", - "status.reblogged_by": "{name} пашырыў(-ла)", - "status.reblogs": "{count, plural, one {# пашырэнне} few {# пашырэнні} many {# пашырэнняў} other {# пашырэння}}", + "status.reblogged_by": "Карыстальнік {name} пашырыў", + "status.reblogs": "{count, plural, one {пашырэнне} few {пашырэнні} many {пашырэнняў} other {пашырэння}}", "status.reblogs.empty": "Гэты допіс яшчэ ніхто не пашырыў. Калі гэта адбудзецца, гэтых людзей будзе бачна тут.", - "status.redraft": "Выдаліць і паправіць", + "status.redraft": "Выдаліць і перапісаць", "status.remove_bookmark": "Выдаліць закладку", + "status.remove_favourite": "Выдаліць з упадабаных", "status.replied_in_thread": "Адказаў у ланцужку", "status.replied_to": "Адказаў {name}", "status.reply": "Адказаць", "status.replyAll": "Адказаць у ланцугу", "status.report": "Паскардзіцца на @{name}", + "status.revoke_quote": "Выдаліць мой допіс з допісу @{name}", "status.sensitive_warning": "Уражвальны змест", "status.share": "Абагуліць", "status.show_less_all": "Згарнуць усё", @@ -853,7 +919,9 @@ "tabs_bar.notifications": "Апавяшчэнні", "tabs_bar.publish": "Новы допіс", "tabs_bar.search": "Пошук", + "terms_of_service.effective_as_of": "Дзейнічае да {date}", "terms_of_service.title": "Умовы выкарыстання", + "terms_of_service.upcoming_changes_on": "Змены, якія адбудуцца {date}", "time_remaining.days": "{number, plural, one {застаўся # дзень} few {засталося # дні} many {засталося # дзён} other {засталося # дня}}", "time_remaining.hours": "{number, plural, one {засталася # гадзіна} few {засталося # гадзіны} many {засталося # гадзін} other {засталося # гадзіны}}", "time_remaining.minutes": "{number, plural, one {засталася # хвіліна} few {засталося # хвіліны} many {засталося # хвілін} other {засталося # хвіліны}}", @@ -869,6 +937,11 @@ "upload_button.label": "Дадаць выяву, відэа- ці аўдыяфайл", "upload_error.limit": "Перавышана колькасць файлаў.", "upload_error.poll": "Немагчыма прымацаваць файл да апытання.", + "upload_form.drag_and_drop.instructions": "Каб абраць медыя далучэнне, націсніце прабел ці Enter. Падчас перасоўвання выкарыстоўвайце кнопкі са стрэлкамі, каб пасунуць медыя далучэнне ў любым напрамку. Націсніце прабел ці Enter зноў, каб перасунуць медыя далучэнне ў новае месца, або Escape для адмены.", + "upload_form.drag_and_drop.on_drag_cancel": "Перасоўванне адмененае. Медыя ўлажэнне {item} на месцы.", + "upload_form.drag_and_drop.on_drag_end": "Медыя ўлажэнне {item} на месцы.", + "upload_form.drag_and_drop.on_drag_over": "Медыя ўлажэнне {item} перасунутае.", + "upload_form.drag_and_drop.on_drag_start": "Абранае медыя ўлажэнне {item}.", "upload_form.edit": "Рэдагаваць", "upload_progress.label": "Запампоўванне...", "upload_progress.processing": "Апрацоўка…", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 96a6eecf8c..597990435b 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -899,7 +899,7 @@ "status.reply": "Besvar", "status.replyAll": "Svar alle", "status.report": "Anmeld @{name}", - "status.revoke_quote": "Fjern mit indlæg fra @{name}'s indlæg", + "status.revoke_quote": "Fjern eget indlæg fra @{name}s indlæg", "status.sensitive_warning": "Følsomt indhold", "status.share": "Del", "status.show_less_all": "Vis mindre for alle", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 58de44bce3..5632def3ea 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Αφαίρεση ακολούθου", "confirmations.remove_from_followers.message": "Ο χρήστης {name} θα σταματήσει να σε ακολουθεί. Σίγουρα θες να συνεχίσεις;", "confirmations.remove_from_followers.title": "Αφαίρεση ακολούθου;", + "confirmations.revoke_quote.confirm": "Αφαίρεση ανάρτησης", + "confirmations.revoke_quote.message": "Αυτή η ενέργεια δεν μπορεί να αναιρεθεί.", + "confirmations.revoke_quote.title": "Αφαίρεση ανάρτησης;", "confirmations.unfollow.confirm": "Άρση ακολούθησης", "confirmations.unfollow.message": "Σίγουρα θες να πάψεις να ακολουθείς τον/την {name};", "confirmations.unfollow.title": "Άρση ακολούθησης;", @@ -896,6 +899,7 @@ "status.reply": "Απάντησε", "status.replyAll": "Απάντησε στο νήμα συζήτησης", "status.report": "Αναφορά @{name}", + "status.revoke_quote": "Αφαίρεση της ανάρτησης μου από την ανάρτηση του/της @{name}", "status.sensitive_warning": "Ευαίσθητο περιεχόμενο", "status.share": "Κοινοποίηση", "status.show_less_all": "Δείξε λιγότερο για όλες", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index 2f7c13bfaf..e1b8d49e11 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Poista seuraaja", "confirmations.remove_from_followers.message": "{name} lakkaa seuraamasta sinua. Haluatko varmasti jatkaa?", "confirmations.remove_from_followers.title": "Poistetaanko seuraaja?", + "confirmations.revoke_quote.confirm": "Poista julkaisu", + "confirmations.revoke_quote.message": "Tätä toimea ei voi peruuttaa.", + "confirmations.revoke_quote.title": "Poistetaanko julkaisu?", "confirmations.unfollow.confirm": "Lopeta seuraaminen", "confirmations.unfollow.message": "Haluatko varmasti lopettaa profiilin {name} seuraamisen?", "confirmations.unfollow.title": "Lopetetaanko käyttäjän seuraaminen?", @@ -896,6 +899,7 @@ "status.reply": "Vastaa", "status.replyAll": "Vastaa ketjuun", "status.report": "Raportoi @{name}", + "status.revoke_quote": "Poista julkaisuni käyttäjän @{name} julkaisusta", "status.sensitive_warning": "Arkaluonteista sisältöä", "status.share": "Jaa", "status.show_less_all": "Näytä kaikista vähemmän", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 1f7c3845e1..4760b64626 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Bain leantóir", "confirmations.remove_from_followers.message": "Scoirfidh {name} de bheith ag leanúint leat. An bhfuil tú cinnte gur mian leat leanúint ar aghaidh?", "confirmations.remove_from_followers.title": "Bain an leantóir?", + "confirmations.revoke_quote.confirm": "Bain postáil", + "confirmations.revoke_quote.message": "Ní féidir an gníomh seo a chealú.", + "confirmations.revoke_quote.title": "Bain postáil?", "confirmations.unfollow.confirm": "Ná lean", "confirmations.unfollow.message": "An bhfuil tú cinnte gur mhaith leat {name} a dhíleanúint?", "confirmations.unfollow.title": "Dílean ​​an t-úsáideoir?", @@ -896,6 +899,7 @@ "status.reply": "Freagair", "status.replyAll": "Freagair le snáithe", "status.report": "Tuairiscigh @{name}", + "status.revoke_quote": "Bain mo phost ó phost @{name}", "status.sensitive_warning": "Ábhar íogair", "status.share": "Comhroinn", "status.show_less_all": "Taispeáin níos lú d'uile", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 60740438e1..5ba44f9c75 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "Követő eltávolítása", "confirmations.remove_from_followers.message": "{name} követ téged. Biztos, hogy folytatod?", "confirmations.remove_from_followers.title": "Követő eltávolítása?", + "confirmations.revoke_quote.confirm": "Bejegyzés eltávolítása", + "confirmations.revoke_quote.message": "Ez a művelet nem vonható vissza.", + "confirmations.revoke_quote.title": "Bejegyzés eltávolítása?", "confirmations.unfollow.confirm": "Követés visszavonása", "confirmations.unfollow.message": "Biztos, hogy vissza szeretnéd vonni {name} követését?", "confirmations.unfollow.title": "Megszünteted a felhasználó követését?", @@ -896,6 +899,7 @@ "status.reply": "Válasz", "status.replyAll": "Válasz a beszélgetésre", "status.report": "@{name} bejelentése", + "status.revoke_quote": "Saját bejegyzés eltávolítása @{name} bejegyzéséből", "status.sensitive_warning": "Kényes tartalom", "status.share": "Megosztás", "status.show_less_all": "Kevesebbet mindenhol", diff --git a/app/javascript/mastodon/locales/kab.json b/app/javascript/mastodon/locales/kab.json index b0bee442cf..e6e1bf565f 100644 --- a/app/javascript/mastodon/locales/kab.json +++ b/app/javascript/mastodon/locales/kab.json @@ -85,9 +85,11 @@ "alt_text_modal.cancel": "Semmet", "alt_text_modal.done": "Immed", "announcement.announcement": "Ulɣu", + "annual_report.summary.followers.followers": "imeḍfaṛen", "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.percentile.we_wont_tell_bernie": "Ur as-neqqar i yiwen.", "annual_report.summary.thanks": "Tanemmirt imi i tettekkiḍ deg Mastodon!", "audio.hide": "Ffer amesli", "block_modal.show_less": "Ssken-d drus", @@ -123,6 +125,7 @@ "column.firehose": "Isuddam usriden", "column.follow_requests": "Isuturen n teḍfeṛt", "column.home": "Agejdan", + "column.list_members": "Sefrek iεeggalen n tebdart", "column.lists": "Tibdarin", "column.mutes": "Imiḍanen yettwasgugmen", "column.notifications": "Ilɣa", @@ -160,6 +163,7 @@ "compose_form.save_changes": "Leqqem", "compose_form.spoiler.marked": "Kkes aḍris yettwaffren deffir n walɣu", "compose_form.spoiler.unmarked": "Rnu aḍris yettwaffren deffir n walɣu", + "compose_form.spoiler_placeholder": "Alɣu n ugbur (afrayan)", "confirmation_modal.cancel": "Sefsex", "confirmations.block.confirm": "Sewḥel", "confirmations.delete.confirm": "Kkes", @@ -168,8 +172,10 @@ "confirmations.delete_list.confirm": "Kkes", "confirmations.delete_list.message": "Tebɣiḍ s tidet ad tekkseḍ umuɣ-agi i lebda?", "confirmations.delete_list.title": "Tukksa n tebdart?", + "confirmations.discard_draft.confirm": "Ttu-t u kemmel", "confirmations.discard_edit_media.confirm": "Sefsex", "confirmations.follow_to_list.confirm": "Ḍfeṛ-it sakin rnu-t ɣer tebdart", + "confirmations.follow_to_list.title": "Ḍfer aseqdac?", "confirmations.logout.confirm": "Ffeɣ", "confirmations.logout.message": "D tidet tebɣiḍ ad teffɣeḍ?", "confirmations.logout.title": "Tebɣiḍ ad teffɣeḍ ssya?", @@ -178,6 +184,8 @@ "confirmations.missing_alt_text.title": "Rnu aḍris amlellay?", "confirmations.mute.confirm": "Sgugem", "confirmations.redraft.confirm": "Kkes sakin ɛiwed tira", + "confirmations.remove_from_followers.confirm": "Kkes aneḍfar", + "confirmations.revoke_quote.confirm": "Kkes tasuffeɣt", "confirmations.unfollow.confirm": "Ur ḍḍafaṛ ara", "confirmations.unfollow.message": "Tetḥeqqeḍ belli tebɣiḍ ur teṭafaṛeḍ ara {name}?", "content_warning.hide": "Ffer tasuffeɣt", @@ -203,7 +211,12 @@ "domain_block_modal.you_wont_see_posts": "Ur tettuɣaleḍ ara ttwaliḍ tisuffaɣ neɣ ulɣuten n iseqdacen n uqeddac-a.", "domain_pill.activitypub_like_language": "ActivityPub am tutlayt yettmeslay Mastodon d izeḍwan inmettiyen nniḍen.", "domain_pill.server": "Aqeddac", + "domain_pill.their_handle": "Asulay-is:", "domain_pill.username": "Isem n useqdac", + "domain_pill.whats_in_a_handle": "D acu i yellan deg usulay?", + "domain_pill.who_they_are": "Imi isulayen qqaren-d anwa i d yiwen d wanda yella, tzemreḍ ad temyigweḍ d yemdanen deg web anmetti yebnan s .", + "domain_pill.who_you_are": "Imi isulay-ik·im yeqqar-d anwa i d kečč·kemmi d wanda i telliḍ, zemren medden ad myigwen yid-k·m deg web anmetti yebnan s .", + "domain_pill.your_handle": "Asulay-ik·im:", "domain_pill.your_server": "D axxam-inek·inem umḍin, anda i zedɣent akk tsuffaɣ-ik·im. Ur k·m-yeεǧib ara wa? Ssenfel-d iqeddacen melmi i ak·m-yehwa, awi-d daɣen ineḍfaren-ik·im yid-k·m.", "embed.instructions": "Ẓẓu addad-agi deg usmel-inek·inem s wenɣal n tangalt yellan sdaw-agi.", "embed.preview": "Akka ara d-iban:", @@ -264,6 +277,7 @@ "firehose.remote": "Iqeddacen nniḍen", "follow_request.authorize": "Ssireg", "follow_request.reject": "Agi", + "follow_suggestions.curated_suggestion": "Yettwafren sɣur tarbaɛt", "follow_suggestions.dismiss": "Dayen ur t-id-skan ara", "follow_suggestions.featured_longer": "Yettwafraned s ufus sɣur agraw n {domain}", "follow_suggestions.friends_of_friends_longer": "D aɣeṛfan ar wid i teṭṭafareḍ", @@ -301,6 +315,7 @@ "hashtag.follow": "Ḍfeṛ ahacṭag", "hashtag.mute": "Sgugem #{hashtag}", "hashtags.and_other": "…d {count, plural, one {}other {# nniḍen}}", + "home.column_settings.show_quotes": "Sken-d tibdarin", "home.column_settings.show_reblogs": "Ssken-d beṭṭu", "home.column_settings.show_replies": "Ssken-d tiririyin", "home.hide_announcements": "Ffer ulɣuyen", @@ -354,6 +369,7 @@ "keyboard_shortcuts.toggle_hidden": "i uskan/tuffra n uḍris deffir CW", "keyboard_shortcuts.toggle_sensitivity": "i teskent/tuffra n yimidyaten", "keyboard_shortcuts.toot": "i wakken attebdud tajewwaqt tamaynut", + "keyboard_shortcuts.translate": "i usuqel n tsuffeɣt", "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", @@ -458,6 +474,7 @@ "notifications.column_settings.mention": "Abdar:", "notifications.column_settings.poll": "Igemmaḍ n usenqed:", "notifications.column_settings.push": "Ilɣa yettudemmren", + "notifications.column_settings.quote": "Yebder-d:", "notifications.column_settings.reblog": "Seǧhed:", "notifications.column_settings.show": "Ssken-d tilɣa deg ujgu", "notifications.column_settings.sound": "Rmed imesli", @@ -631,6 +648,7 @@ "status.mute_conversation": "Sgugem adiwenni", "status.open": "Semɣeṛ tasuffeɣt-ayi", "status.pin": "Senteḍ-itt deg umaɣnu", + "status.quote_post_author": "Yebder-d tasuffeɣt sɣur @{name}", "status.read_more": "Issin ugar", "status.reblog": "Bḍu", "status.reblogged_by": "Yebḍa-tt {name}", @@ -650,7 +668,7 @@ "status.show_original": "Sken aɣbalu", "status.title.with_attachments": "{user} posted {attachmentCount, plural, one {an attachment} other {# attachments}}", "status.translate": "Suqel", - "status.translated_from_with": "Yettwasuqel seg {lang} s {provider}", + "status.translated_from_with": "Tettwasuqel seg {lang} s {provider}", "status.uncached_media_warning": "Ulac taskant", "status.unmute_conversation": "Kkes asgugem n udiwenni", "status.unpin": "Kkes asenteḍ seg umaɣnu", diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json index e6cdc4e780..92e3d7cafd 100644 --- a/app/javascript/mastodon/locales/ko.json +++ b/app/javascript/mastodon/locales/ko.json @@ -245,6 +245,9 @@ "confirmations.remove_from_followers.confirm": "팔로워 제거", "confirmations.remove_from_followers.message": "{name} 님이 나를 팔로우하지 않게 됩니다. 계속할까요?", "confirmations.remove_from_followers.title": "팔로워를 제거할까요?", + "confirmations.revoke_quote.confirm": "게시물 삭제", + "confirmations.revoke_quote.message": "이 작업은 되돌릴 수 없습니다.", + "confirmations.revoke_quote.title": "게시물을 지울까요?", "confirmations.unfollow.confirm": "팔로우 해제", "confirmations.unfollow.message": "정말로 {name} 님을 팔로우 해제하시겠습니까?", "confirmations.unfollow.title": "사용자를 언팔로우 할까요?", @@ -885,6 +888,7 @@ "status.reply": "답장", "status.replyAll": "글타래에 답장", "status.report": "@{name} 신고하기", + "status.revoke_quote": "내 게시물을 @{name}의 게시물에서 삭제", "status.sensitive_warning": "민감한 내용", "status.share": "공유", "status.show_less_all": "모두 접기", diff --git a/app/javascript/mastodon/locales/si.json b/app/javascript/mastodon/locales/si.json index 438ca0b735..3de45108ee 100644 --- a/app/javascript/mastodon/locales/si.json +++ b/app/javascript/mastodon/locales/si.json @@ -1,6 +1,7 @@ { "about.blocks": "මැදිහත්කරණ සේවාදායක", "about.contact": "සබඳතාව:", + "about.default_locale": "Default", "about.disclaimer": "මාස්ටඩන් යනු නිදහස් විවෘත මූලාශ්‍ර මෘදුකාංගයකි. එය මාස්ටඩන් gGmbH හි වෙළඳ නාමයකි.", "about.domain_blocks.no_reason_available": "හේතුව ලබා ගත නොහැක.", "about.domain_blocks.preamble": "Mastodon සාමාන්‍යයෙන් ඔබට fediverse හි වෙනත් ඕනෑම සේවාදායකයකින් අන්තර්ගතයන් බැලීමට සහ පරිශීලකයින් සමඟ අන්තර් ක්‍රියා කිරීමට ඉඩ සලසයි. මෙම විශේෂිත සේවාදායකයේ සිදු කර ඇති ව්‍යතිරේක මේවාය.", diff --git a/config/locales/activerecord.be.yml b/config/locales/activerecord.be.yml index 73f6f67f01..975bc1a704 100644 --- a/config/locales/activerecord.be.yml +++ b/config/locales/activerecord.be.yml @@ -18,9 +18,13 @@ be: attributes: domain: invalid: не з’яўляецца сапраўдным даменным імем + messages: + invalid_domain_on_line: "%{value} не пасуе для карэктнай назвы сервера" models: account: attributes: + fields: + fields_with_values_missing_labels: утрымлівае значэнні без апісанняў username: invalid: павінна змяшчаць толькі літары, лічбы і ніжнія падкрэсліванні reserved: зарэзервавана @@ -45,8 +49,14 @@ be: attributes: reblog: taken: гэтага допісу ўжо існуе + terms_of_service: + attributes: + effective_date: + too_soon: занадта рана, мусіць быць пасля %{date} user: attributes: + date_of_birth: + below_limit: ніжэй за дазволены ўзрост email: blocked: выкарыстоўвае забароненую крыніцу электроннай пошты unreachable: не існуе diff --git a/config/locales/activerecord.kab.yml b/config/locales/activerecord.kab.yml index 8cdc6501cb..8eb8ff24d0 100644 --- a/config/locales/activerecord.kab.yml +++ b/config/locales/activerecord.kab.yml @@ -39,6 +39,8 @@ kab: taken: n iddaden yellan yakan user: attributes: + date_of_birth: + below_limit: ddaw n talast n leɛmeṛ email: blocked: isseqdac asaǧǧaw n yimayl ur yettusirgen ara unreachable: ur d-ttban ara d akken yella diff --git a/config/locales/be.yml b/config/locales/be.yml index 2855230b15..3cd6effdc8 100644 --- a/config/locales/be.yml +++ b/config/locales/be.yml @@ -25,6 +25,7 @@ be: one: Допіс other: Допісы posts_tab_heading: Допісы + self_follow_error: Нельга падпісацца на свой профіль admin: account_actions: action: Выканаць дзеянне @@ -51,6 +52,7 @@ be: title: Змяніць адрас эл. пошты для %{username} change_role: changed_msg: Роля паспяхова зменена! + edit_roles: Наладзіць ролі карыстальнікаў label: Змяніць ролю no_role: Няма ролі title: Змяніць ролю для %{username} @@ -194,6 +196,7 @@ be: create_relay: Стварыць рэтранслятар create_unavailable_domain: Стварыць недаступны Дамен create_user_role: Стварыць ролю + create_username_block: Стварыць правіла імя карыстальніка demote_user: Панізіць карыстальніка destroy_announcement: Выдаліць аб'яву destroy_canonical_email_block: Выдаліць блакіроўку электроннай пошты @@ -207,6 +210,7 @@ be: destroy_status: Выдаліць допіс destroy_unavailable_domain: Выдаліць недаступны дамен destroy_user_role: Выдаліць ролю + destroy_username_block: Выдаліць правіла імя карыстальніка disable_2fa_user: Адключыць двухэтапнае спраўджанне disable_custom_emoji: Адключыць адвольныя эмодзі disable_relay: Выключыць рэтранслятар @@ -241,6 +245,7 @@ be: update_report: Абнавіць скаргу update_status: Абнавіць допіс update_user_role: Абнавіць ролю + update_username_block: Абнавіць правіла імя карыстальніка actions: approve_appeal_html: Карыстальнік %{name} ухваліў запыт на мадэрацыю %{target} approve_user_html: "%{name} пацвердзіў рэгістрацыю ад %{target}" @@ -259,6 +264,7 @@ be: create_relay_html: "%{name} стварыў(-ла) рэтранслятар %{target}" create_unavailable_domain_html: "%{name} прыпыніў дастаўку да дамена %{target}" create_user_role_html: "%{name} зрабіў ролю %{target}" + create_username_block_html: Адміністратар %{name} дадаў правіла для імён карыстальнікаў, у якіх %{target} demote_user_html: "%{name} прыбраў карыстальніка %{target}" destroy_announcement_html: "%{name} выдаліў аб'яву %{target}" destroy_canonical_email_block_html: "%{name} разблакіраваў эл. пошту з хэшам %{target}" @@ -272,6 +278,7 @@ be: destroy_status_html: "%{name} выдаліў допіс %{target}" destroy_unavailable_domain_html: "%{name} дазволіў працягнуць адпраўку на дамен %{target}" destroy_user_role_html: "%{name} выдаліў ролю %{target}" + destroy_username_block_html: Адміністратар %{name} прыбраў правіла для імён карыстальнікаў, у якіх %{target} disable_2fa_user_html: "%{name} амяніў абавязковую двухфактарную верыфікацыю для карыстальніка %{target}" disable_custom_emoji_html: "%{name} заблакіраваў эмодзі %{target}" disable_relay_html: "%{name} выключыў(-ла) рэтранслятар %{target}" @@ -306,6 +313,7 @@ be: update_report_html: "%{name} абнавіў скаргу %{target}" update_status_html: "%{name} абнавіў допіс %{target}" update_user_role_html: "%{name} змяніў ролю %{target}" + update_username_block_html: Адміністратар %{name} змяніў правіла для імён карыстальнікаў, у якіх %{target} deleted_account: выдалены ўліковы запіс empty: Логі не знойдзены. filter_by_action: Фільтраваць па дзеянню @@ -313,6 +321,7 @@ be: title: Аўдыт unavailable_instance: "(імя дамена недаступнае)" announcements: + back: Вярнуцца да аб'яў destroyed_msg: Аб’ява выдалена! edit: title: Рэдагаваць абвестку @@ -321,6 +330,10 @@ be: new: create: Стварыць аб'яву title: Новая аб'ява + preview: + disclaimer: Паколькі карыстальнікі не могуць адмовіцца ад іх, апавяшчэнні па электроннай пошце мусяць выкарыстоўвацца толькі для важных аб'яў, накшталт уцечкі асабістых дадзеных ці зачынення сервера. + explanation_html: 'Ліст будзе дасланы на электронную пошту %{display_count} карыстальнікам. У ім будзе наступнае:' + title: Перадпрагляд аб'явы апавяшчэння publish: Апублікаваць published_msg: Аб'ява паспяхова апублікавана! scheduled_for: Запланавана на %{time} @@ -492,22 +505,32 @@ be: fasp: debug: callbacks: + created_at: Створана delete: Выдаліць ip: IP-адрас + request_body: Запытаць цела + title: Зрабіць дэбаг зваротных выклікаў providers: active: Актыўны base_url: Базавы URL-адрас + callback: Зваротны выклік delete: Выдаліць edit: Рэдагаваць пастаўшчыка finish_registration: Завяршыць рэгістрацыю name: Назва providers: Пастаўшчыкі public_key_fingerprint: Лічбавы адбітак публічнага ключа + registration_requested: Патрабуюцца рэгістрацыя registrations: confirm: Пацвердзіць + description: Вы запыталі рэгістрацыю ад FASP. Адмоўцеся, калі Вы не рабілі гэтага. Калі ж Вы гэта зрабілі, то ўважліва параўнайце імя і ключ перад пацвярджэннем рэгістрацыі. reject: Адхіліць + title: Пацвердзіць рэгістрацыю ў FASP save: Захаваць + select_capabilities: Выбраць здольнасці sign_in: Увайсці + status: Допіс + title: Дапаможныя серверы Fediverse title: FASP follow_recommendations: description_html: "Рэкамендацыі падпісак, дапамогаюць новым карыстальнікам хутка знайсці цікавы кантэнт. Калі карыстальнік недастаткова ўзаемадзейнічаў з іншымі, каб сфарміраваць персанальныя рэкамендацыі прытрымлівацца, замест гэтага рэкамендуюцца гэтыя ўліковыя запісы. Яны штодзённа пераразлічваюцца з сумесі ўліковых запісаў з самымі апошнімі ўзаемадзеяннямі і найбольшай колькасцю мясцовых падпісчыкаў для дадзенай мовы." @@ -586,7 +609,9 @@ be: moderation_notes: create: Дадаць нататку мадэратара created_msg: Нататка мадэратара для экзэмпляра сервера створана! + description_html: Паглядзіце і пакіньце нататкі іншым мадэратарам або сабе ў будучыні destroyed_msg: Нататка мадэратара экзэмпляра сервера выдалена! + placeholder: Інфармацыя пра гэты выпадак, прынятыя меры ці нешта яшчэ, што дапаможа Вам разабрацца з гэтым у будучыні. title: Нататкі мадэратараў private_comment: Прыватны каментарый public_comment: Публічны каментарый @@ -807,15 +832,21 @@ be: description_html: Большасць сцвярджаюць, што прачыталі ўмовы абслугоўвання і згаджаюцца з імі, але звычайна людзі не чытаюць іх да канца, пакуль не ўзнікне праблема. Таму зрабіце правілы вашага сервера простымі з першага погляду, прадставіўшы іх у выглядзе маркіраванага спісу. Старайцеся рабіць правілы кароткімі і простымі, але не разбіваць іх на шмат асобных пунктаў. edit: Рэдагаваць правіла empty: Правілы сервера яшчэ не вызначаны. + move_down: Перасунуць уніз + move_up: Перасунуць уверх title: Правілы сервера translation: Пераклад translations: Пераклады + translations_explanation: Вы можаце па жаданні дадаваць пераклады Вашых правіл. Калі перакладу няма, то пакажацца арыгінальная версія. Калі ласка, заўсёды правярайце, каб пераклад быў такім жа актуальным, як і арыгінал. settings: about: manage_rules: Кіраваць правіламі сервера preamble: Дайце падрабязную інфармацыю аб тым, як сервер працуе, мадэруецца, фінансуецца. rules_hint: Існуе спецыяльная вобласць для правілаў, якіх вашы карыстальнікі павінны прытрымлівацца. title: Пра нас + allow_referrer_origin: + desc: Калі Вашыя карыстальнікі націскаюць спасылкі на знешнія сайты, іх браўзер можа дасылаць адрас Вашага сервера Mastodon як крыніцу спасылкі. Адключыце гэту функцыю, калі яна ўнікальна ідэнтыфікуе Вашых карыстальнікаў (напрыклад, калі гэта персанальны сервер Mastodon). + title: Дазволіць знешнім сайтам бачыць Ваш сервер Mastodon як крыніцу трафіка appearance: preamble: Наладзьце вэб-інтэрфейс Mastodon. title: Выгляд @@ -922,6 +953,8 @@ be: system_checks: database_schema_check: message_html: Ёсць незавершаныя міграцыі базы даных. Запусціце іх, каб пераканацца, што праграма паводзіць сябе належным чынам + elasticsearch_analysis_index_mismatch: + message_html: Налады аналізатара індэксаў Elasticsearch пратэрмінаваныя. Калі ласка, запусціце tootctl search deploy --only-mapping --only=%{value} elasticsearch_health_red: message_html: Кластар Elasticsearch нездаровы (чырвоны статус), функцыі пошуку недаступныя elasticsearch_health_yellow: @@ -990,9 +1023,25 @@ be: generate: Выкарыстаць шаблон generates: action: Згенерыраваць + chance_to_review_html: "Аўтаматычна згенераваныя ўмовы карыстання не будуць аўтаматычна апублікаваныя. У Вас будзе магчымасць паглядзець на вынікі. Калі ласка, дайце неабходныя дэталі, каб працягнуць." + explanation_html: Узор умоў карыстання прадстаўлены выключна з інфармацыйнай мэтай і не павінен успрымацца як юрыдычная кансультацыя ні ў якім пытанні. Калі ласка, правядзіце размову з Вашым уласным юрыдычным кансультантам па Вашай сітуацыі і Вашых канкрэтных юрыдычных пытаннях. + title: Стварэнне ўмоў карыстання + going_live_on_html: Уступяць у сілу %{date} history: Гісторыя live: Дзейнічае + no_history: Пакуль не заўважана ніякіх змен ва ўмовах пагаднення. + no_terms_of_service_html: У Вас пакуль няма ніякіх умоў карыстання. Умовы карыстання ствараюцца для яснасці і абароны ад патэнцыяльных абавязкаў у спрэчках з Вашымі карыстальнікамі. + notified_on_html: Карыстальнікам паведамяць %{date} notify_users: Апавясціць карыстальнікаў + preview: + explanation_html: 'Электронны ліст будзе дасланы %{display_count} карыстальнікам, якія зарэгістраваліся да %{date}. У лісце будзе наступны тэкст:' + send_preview: Адправіць на %{email} для перадпрагляду + send_to_all: + few: Адправіць %{display_count} электронныя лісты + many: Адправіць %{display_count} электронных лістоў + one: Адправіць %{display_count} электронны ліст + other: Адправіць %{display_count} электронных лістоў + title: Перадпрагляд апавяшчэння пра ўмовы карыстання publish: Апублікаваць published_on_html: Апублікавана %{date} save_draft: Захаваць чарнавік @@ -1002,10 +1051,15 @@ be: allow: Дазволіць approved: Пацверджаны confirm_allow: Вы ўпэўнены, што хочаце дазволіць выбраныя тэгі? + confirm_disallow: Вы ўпэўненыя, што хочаце забараніць абраныя хэштэгі? disallow: Забараніць links: allow: Дазволіць спасылка allow_provider: Дазволіць выдаўца + confirm_allow: Вы ўпэўненыя, што хочаце дазволіць абраныя спасылкі? + confirm_allow_provider: Вы ўпэўненыя, што хочаце дазволіць абраныя спасылкі? + confirm_disallow: Вы ўпэўненыя, што хочаце забараніць абраныя спасылкі? + confirm_disallow_provider: Вы ўпэўненыя, што хочаце забараніць абраныя серверы? description_html: Гэта спасылкі, якія зараз часта распаўсюджваюцца ўліковымі запісамі, з якіх ваш сервер бачыць паведамленні. Гэта можа дапамагчы вашым карыстальнікам даведацца, што адбываецца ў свеце. Ніякія спасылкі не будуць паказвацца публічна, пакуль вы не зацвердзіце аўтара. Вы таксама можаце дазволіць або адхіліць асобныя спасылкі. disallow: Забараніць спасылку disallow_provider: Дазволіць выдаўца @@ -1031,6 +1085,10 @@ be: statuses: allow: Дазволіць допіс allow_account: Дазволіць аўтара + confirm_allow: Вы ўпэўненыя, што хочаце дазволіць абраныя допісы? + confirm_allow_account: Вы ўпэўненыя, што хочаце дазволіць абраныя профілі? + confirm_disallow: Вы ўпэўненыя, што хочаце забараніць абраныя допісы? + confirm_disallow_account: Вы ўпэўненыя, што хочаце забараніць абраныя профілі? description_html: Гэта допісы, пра якія ведае ваш сервер, што на дадзены момант часта абагульваюцца і падабаюцца людзям. Гэта можа дапамагчы вашым новым і пастаянным карыстальнікам знайсці больш людзей, на якіх можна падпісацца. Ніякія допісы не будуць паказвацца публічна, пакуль вы не зацвердзіце аўтара, а аўтар не дазволіць прапанаваць свой уліковы запіс іншым. Вы таксама можаце дазволіць або адхіліць асобныя допісы. disallow: Забараніць допіс disallow_account: Забараніць аўтара @@ -1069,6 +1127,25 @@ be: other: Выкарысталі %{count} чалавек за апошні тыдзень title: Рэкамендацыі і трэнды trending: Трэндавае + username_blocks: + add_new: Дадаць новае + block_registrations: Заблакіраваць рэгістрацыю + comparison: + contains: Мае + equals: Такое ж, як + contains_html: Мае %{string} + created_msg: Правіла імя карыстальніка паспяхова створанае + delete: Выдаліць + edit: + title: Змяніць правіла імя карыстальніка + matches_exactly_html: Такое ж, як %{string} + new: + create: Стварыць правіла + title: Стварыць новае правіла імя карыстальніка + no_username_block_selected: Аніводнае з правіл імён карыстальніка не было змененае, бо аніводнае не было абранае + not_permitted: Забаронена + title: Правілы імені карыстальніка + updated_msg: Правіла імя карыстальніка паспяхова абноўленае warning_presets: add_new: Дадаць новы delete: Выдаліць @@ -1227,6 +1304,7 @@ be: set_new_password: Прызначыць новы пароль setup: email_below_hint_html: Праверце папку са спамам або зрабіце новы запыт. Вы можаце выправіць свой email, калі ён няправільны. + email_settings_hint_html: Націсніце на спасылку, якую мы адправілі на %{email}, каб пачаць карыстацца Mastodon. Мы пакуль пачакаем тут. link_not_received: Не атрымалі спасылку? new_confirmation_instructions_sent: Праз некалькі хвілін вы атрымаеце новы ліст на email са спасылкай для пацверджання! title: Праверце вашу пошту @@ -1235,6 +1313,7 @@ be: title: Уваход у %{domain} sign_up: manual_review: Рэгістрацыі на %{domain} праходзяць ручную праверку нашымі мадэратарамі. Каб дапамагчы нам апрацаваць вашу рэгістрацыю, напішыце крыху пра сябе і чаму вы хочаце мець уліковы запіс на %{domain}. + preamble: З профілем на серверы Mastodon Вы зможаце падпісацца на любога чалавека ў fediverse, незалежна ад таго, на якім серверы знаходзіцца іх профіль. title: Наладзьма вас на %{domain}. status: account_status: Стан уліковага запісу @@ -1246,9 +1325,15 @@ be: view_strikes: Праглядзець мінулыя папярэджанні для вашага ўліковага запісу too_fast: Форма адпраўлена занадта хутка, паспрабуйце яшчэ раз. use_security_key: Выкарыстаеце ключ бяспекі + user_agreement_html: Я прачытаў і згаджаюся з умовамі карыстання і палітыкай прыватнасці + user_privacy_agreement_html: Я прачытаў і згаджаюся з палітыкай прыватнасці author_attribution: example_title: Прыклад тэксту + hint_html: Вы пішаце навіны ці артыкулы ў блогу па-за Mastodon? Кантралюйце, як пазначаецца Вашае аўтарства, калі імі дзеляцца ў Mastodon. + instructions: 'Упэўніцеся, што гэты код прысутнічае ў HTML-кодзе Вашага артыкула:' + more_from_html: Больш ад %{name} s_blog: Блог %{name} + then_instructions: Пасля, дадайце назву сайта публікацыі ў полі знізу. title: Пазначэнне аўтарства challenge: confirm: Працягнуць @@ -1464,6 +1549,68 @@ be: merge_long: Захаваць існуючыя запісы і дадаць новыя overwrite: Перазапісаць overwrite_long: Замяніць бягучыя запісы на новыя + overwrite_preambles: + blocking_html: + few: Вы збіраецеся замяніць свой спіс заблакіраваных карыстальнікаў %{count} карыстальнікамі з %{filename}. + many: Вы збіраецеся замяніць свой спіс заблакіраваных карыстальнікаў %{count} карыстальнікамі з %{filename}. + one: Вы збіраецеся замяніць свой спіс заблакіраваных карыстальнікаў %{count} карыстальнікам з %{filename}. + other: Вы збіраецеся замяніць свой спіс заблакіраваных карыстальнікаў %{count} карыстальнікамі з %{filename}. + bookmarks_html: + few: Вы збіраецеся замяніць свае закладкі %{count} допісамі з %{filename}. + many: Вы збіраецеся замяніць свае закладкі %{count} допісамі з %{filename}. + one: Вы збіраецеся замяніць свае закладкі %{count} допісам з %{filename}. + other: Вы збіраецеся замяніць свае закладкі %{count} допісамі з %{filename}. + domain_blocking_html: + few: Вы збіраецеся замяніць свой спіс заблакіраваных сервераў %{count} серверамі з %{filename}. + many: Вы збіраецеся замяніць свой спіс заблакіраваных сервераў %{count} серверамі з %{filename}. + one: Вы збіраецеся замяніць свой спіс заблакіраваных сервераў %{count} серверам з %{filename}. + other: Вы збіраецеся замяніць свой спіс заблакіраваных сервераў %{count} серверамі з %{filename}. + following_html: + few: Вы збіраецеся падпісацца на %{count} профілі з %{filename} і адпішацеся ад усіх астатніх. + many: Вы збіраецеся падпісацца на %{count} профіляў з %{filename} і адпішацеся ад усіх астатніх. + one: Вы збіраецеся падпісацца на %{count} профіль з %{filename} і адпішацеся ад усіх астатніх. + other: Вы збіраецеся падпісацца на %{count} профіляў з %{filename} і адпішацеся ад усіх астатніх. + lists_html: + few: Вы збіраецеся замяніць свае спісы змесцівам з %{filename}. %{count} профілі будуць дададзеныя ў новыя спісы. + many: Вы збіраецеся замяніць свае спісы змесцівам з %{filename}. %{count} профіляў будуць дададзеныя ў новыя спісы. + one: Вы збіраецеся замяніць свае спісы змесцівам з %{filename}. %{count} профіль будзе дададзены ў новыя спісы. + other: Вы збіраецеся замяніць свае спісы змесцівам з %{filename}. %{count} профіляў будуць дададзеныя ў новыя спісы. + muting_html: + few: Вы збіраецеся замяніць свой спіс профіляў, якія Вы ігнаруеце, %{count} профілямі з %{filename}. + many: Вы збіраецеся замяніць свой спіс профіляў, якія Вы ігнаруеце, %{count} профілямі з %{filename}. + one: Вы збіраецеся замяніць свой спіс профіляў, якія Вы ігнаруеце, %{count} профілем з %{filename}. + other: Вы збіраецеся замяніць свой спіс профіляў, якія Вы ігнаруеце, %{count} профілямі з %{filename}. + preambles: + blocking_html: + few: Вы збіраецеся заблакіраваць %{count} профілі з %{filename}. + many: Вы збіраецеся заблакіраваць %{count} профіляў з %{filename}. + one: Вы збіраецеся заблакіраваць %{count} профіль з %{filename}. + other: Вы збіраецеся заблакіраваць %{count} профіляў з %{filename}. + bookmarks_html: + few: Вы збіраецеся дадаць %{count} допісы з %{filename} у Вашыя закладкі. + many: Вы збіраецеся дадаць %{count} допісаў з %{filename} у Вашыя закладкі. + one: Вы збіраецеся дадаць %{count} допіс з %{filename} у Вашыя закладкі. + other: Вы збіраецеся дадаць %{count} допісаў з %{filename} у Вашыя закладкі. + domain_blocking_html: + few: Вы збіраецеся заблакіраваць %{count} серверы з %{filename}. + many: Вы збіраецеся заблакіраваць %{count} сервераў з %{filename}. + one: Вы збіраецеся заблакіраваць %{count} сервер з %{filename}. + other: Вы збіраецеся заблакіраваць %{count} сервераў з %{filename}. + following_html: + few: Вы збіраецеся падпісацца на %{count} профілі з %{filename}. + many: Вы збіраецеся падпісацца на %{count} профіляў з %{filename}. + one: Вы збіраецеся падпісацца на %{count} профіль з %{filename}. + other: Вы збіраецеся падпісацца на %{count} профіляў з %{filename}. + lists_html: + few: Вы збіраецеся дадаць %{count} профілі з %{filename} у Вашыя спісы. Калі спісаў няма, то будуць створаны новыя. + many: Вы збіраецеся дадаць %{count} профіляў з %{filename} у Вашыя спісы. Калі спісаў няма, то будуць створаны новыя. + one: Вы збіраецеся дадаць %{count} профіль з %{filename} у Вашыя спісы. Калі спісаў няма, то будуць створаны новыя. + other: Вы збіраецеся дадаць %{count} профіляў з %{filename} у Вашыя спісы. Калі спісаў няма, то будуць створаны новыя. + muting_html: + few: Вы збіраецеся пачаць ігнараваць %{count} профілі з %{filename}. + many: Вы збіраецеся пачаць ігнараваць %{count} профіляў з %{filename}. + one: Вы збіраецеся пачаць ігнараваць %{count} профіль з %{filename}. + other: Вы збіраецеся пачаць ігнараваць %{count} профіляў з %{filename}. preface: Вы можаце імпартаваць даныя, экспартаваныя вамі з іншага сервера, напрыклад, спіс людзей, на якіх вы падпісаны або якіх блакуеце. recent_imports: Нядаўнія імпарты states: @@ -1550,6 +1697,7 @@ be: media_attachments: validations: images_and_video: Немагчыма далучыць відэа да допісу, які ўжо змяшчае выявы + not_found: Файл %{ids} не знойдзены або ўжо далучаны да іншага допісу not_ready: Няможна далучыць файлы, апрацоўка якіх яшчэ не скончылася. Паспрабуйце яшчэ раз праз хвілінку! too_many: Немагчыма далучыць больш за 4 файлы migrations: @@ -1617,6 +1765,10 @@ be: title: Новае згадванне poll: subject: Апытанне ад %{name} скончылася + quote: + body: 'Ваш допіс працытаваў карыстальнік %{name}:' + subject: Карыстальнік %{name} працытаваў Ваш допіс + title: Цытаваць reblog: body: "%{name} пашырыў ваш пост:" subject: "%{name} пашырыў ваш допіс" @@ -1696,7 +1848,7 @@ be: follow_failure: Вы не можаце падпісацца на некаторыя акаўнты. follow_selected_followers: Падпісацца на выбраных падпісчыкаў followers: Падпісчыкі - following: Падпісаны + following: Падпіскі invited: Запрошаны last_active: Апошняя актыўнасць most_recent: Даўнасць @@ -1835,6 +1987,7 @@ be: edited_at_html: Адрэдагавана %{date} errors: in_reply_not_found: Здаецца, допіс, на які вы спрабуеце адказаць, не існуе. + quoted_status_not_found: Выглядае, што допісу, які Вы спрабуеце цытаваць, не існуе. over_character_limit: перавышаная колькасць сімвалаў у %{max} pin_errors: direct: Допісы, бачныя толькі згаданым карыстальнікам, нельга замацаваць @@ -1842,6 +1995,8 @@ be: ownership: Немагчыма замацаваць чужы допіс reblog: Немагчыма замацаваць пашырэнне quote_policies: + followers: Толькі Вашыя падпісчыкі + nobody: Ніхто public: Усе title: '%{name}: "%{quote}"' visibilities: @@ -1896,6 +2051,11 @@ be: does_not_match_previous_name: не супадае з папярэднім імям terms_of_service: title: Умовы выкарыстання + terms_of_service_interstitial: + future_preamble_html: Мы ўносім праўкі ў нашыя ўмовы карыстання, якія пачнуць дзейнічаць %{date}. Мы раім Вам азнаёміцца з абноўленымі ўмовамі. + past_preamble_html: Пасля Вашага апошняга наведвання мы ўнеслі праўкі ў нашыя ўмовы карыстання. Мы раім Вам азнаёміцца з абноўленымі ўмовамі. + review_link: Прачытаць умовы карыстання + title: На %{domain} змяняюцца ўмовы карыстання themes: contrast: Mastodon (высокі кантраст) default: Mastodon (цёмная) @@ -1927,6 +2087,10 @@ be: recovery_instructions_html: Калі раптам вы страціце доступ да свайго тэлефона, вы можаце скарыстаць адзін з кодаў аднаўлення ніжэй каб аднавіць доступ да свайго ўліковага запісу. Захоўвайце іх у бяспечным месцы. Напрыклад, вы можаце раздрукаваць іх і захоўваць разам з іншымі важнымі дакументамі. webauthn: Ключы бяспекі user_mailer: + announcement_published: + description: 'Аб''ява ад адміністратараў %{domain}:' + subject: Аб'ява сэрвісу + title: Аб'ява сэрвісу %{domain} appeal_approved: action: Налады ўліковага запісу explanation: Апеляцыя на папярэджанне супраць вашага ўліковага запісу ад %{strike_date}, якую вы падалі %{appeal_date}, была ўхвалена. Ваш уліковы запіс зноў на добрым рахунку. @@ -1957,7 +2121,13 @@ be: subject: У вас уліковы запіс зайшлі з новага IP-адрасу title: Новы ўваход terms_of_service_changed: + agreement: Працягваючы карыстацца %{domain}, Вы пагаджаецеся з гэтымі ўмовамі. Калі Вы не згодныя з абноўленымі ўмовамі, то можаце ў любы момант адмовіцца ад пагаднення з %{domain}, выдаліўшы свой профіль. + changelog: 'Коратка пра тое, што значыць гэтае абнаўленне:' + description: 'Вы атрымалі дадзены ліст, бо мы ўносім праўкі ў нашыя ўмовы карыстання на %{domain}. Гэтыя абнаўленні ўступяць у сілу %{date}. Мы раім Вам цалкам азнаёміцца з абноўленымі ўмовамі тут:' + description_html: Вы атрымалі дадзены ліст, бо мы ўносім праўкі ў нашыя ўмовы карыстання на %{domain}. Гэтыя абнаўленні ўступяць у сілу %{date}. Мы раім Вам цалкам азнаёміцца з абноўленымі ўмовамі тут. sign_off: Каманда %{domain} + subject: Абнаўленні ў нашых умовах карыстання + subtitle: Змяняюцца ўмовы карыстання на %{domain} title: Важнае абнаўленне warning: appeal: Падаць апеляцыю @@ -2047,6 +2217,7 @@ be: instructions_html: Скапіруйце прыведзены ніжэй код і ўстаўце ў HTML вашага сайта. Затым дадайце адрас вашага сайта ў адно з дадатковых палёў вашага профілю на ўкладцы «рэдагаваць профіль» і захавайце змены. verification: Верыфікацыя verified_links: Вашыя правераныя спасылкі + website_verification: Пацвярджэнне сайта webauthn_credentials: add: Дадаць новы ключ бяспекі create: diff --git a/config/locales/doorkeeper.be.yml b/config/locales/doorkeeper.be.yml index b01f476a20..7f1dadc4cf 100644 --- a/config/locales/doorkeeper.be.yml +++ b/config/locales/doorkeeper.be.yml @@ -60,6 +60,7 @@ be: error: title: Узнікла памылка new: + prompt_html: "%{client_name} хоча атрымаць дазвол на доступ да Вашага профілю. Ухваляйце гэты запыт толькі калі Вы ведаеце гэту крыніцу і давяраеце ёй." review_permissions: Прагледзець дазволы title: Патрабуецца аўтарызацыя show: diff --git a/config/locales/kab.yml b/config/locales/kab.yml index 4685887a96..a50b2d108a 100644 --- a/config/locales/kab.yml +++ b/config/locales/kab.yml @@ -567,6 +567,7 @@ kab: accept: Qbel back: Tuɣalin invited_by: 'Tzemreḍ ad tkecmeḍ ɣer %{domain} s tanemmirt i tinnubga i d-teṭṭfeḍ sɣur :' + preamble: Tiyi ttwasemmant-d yerna ttwaḍemnent sɣur imḍebbren n %{domain}. preamble_invited: Uqbel ad tkemmleḍ, ttxil-k·m ẓer ilugan i d-sbedden yimkariyen n %{domain}. title: Kra n yilugan igejdanen. title_invited: Tettwaɛerḍeḍ. @@ -578,6 +579,7 @@ kab: preamble_html: Kcem ar %{domain} s inekcam-inek n tuqqna. Ma yella yezga-d umiḍan-ik deg uqeddac-nniḍen, ur tezmireḍ ara ad tkecmeḍ sya. title: Akeččum ɣer %{domain} sign_up: + preamble: S umiḍan yellan deg uqeddac-a n Mastodon, ad tizimreḍ ad t-ḍefreḍ yal yiwen nniḍen i yellan deg fediverse, akken yebɣu yili wanda i yella umiḍan-nsen. title: Iyya ad d-nessewjed tiɣawsiwin i %{domain}. status: account_status: Addad n umiḍan @@ -744,6 +746,8 @@ kab: action: Err body: 'Yuder-ik·ikem-id %{name} deg:' subject: Yuder-ik·ikem-id %{name} + quote: + title: Tabdert tamaynut reblog: subject: "%{name} yesselha addad-ik·im" title: Azuzer amaynut diff --git a/config/locales/simple_form.da.yml b/config/locales/simple_form.da.yml index 296ce49131..e85dd32556 100644 --- a/config/locales/simple_form.da.yml +++ b/config/locales/simple_form.da.yml @@ -56,7 +56,7 @@ da: scopes: De API'er, som applikationen vil kunne tilgå. Vælges en topniveaudstrækning, vil detailvalg være unødvendige. setting_aggregate_reblogs: Vis ikke nye fremhævelser for nyligt fremhævede indlæg (påvirker kun nyligt modtagne fremhævelser) setting_always_send_emails: Normalt sendes ingen e-mailnotifikationer under aktivt brug af Mastodon - setting_default_quote_policy: Denne indstilling træder kun i kraft for indlæg oprettet med den næste Mastodon-version, men egne præference kan vælges som forberedelse. + setting_default_quote_policy: Denne indstilling træder kun i kraft for indlæg oprettet med den næste Mastodon-version, men egen præference kan vælges som forberedelse. setting_default_sensitive: Sensitive medier er som standard skjult og kan vises med et klik setting_display_media_default: Skjul medier med sensitiv-markering setting_display_media_hide_all: Skjul altid medier diff --git a/config/locales/simple_form.kab.yml b/config/locales/simple_form.kab.yml index d1a288e213..16c425186b 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_admin_settings: + min_age: Ad ttwasutren yiseqdacen ad sentemen azemz-nsen n tlalit deg ujerred form_challenge: current_password: Tkecmeḍ ɣer temnaḍt taɣellsant imports: @@ -37,15 +39,19 @@ kab: comment: D afrayan. Cfu ɣef wayɣer i terniḍ alugen-a. severities: no_access: Sewḥel anekcu ɣer akk tiɣbula + user: + date_of_birth: + one: Ilaq ad neḍmen belli tesɛiḍ ma ulac %{count} akken ad tesqedceḍ %{domain}. Ur neḥrez ara aya. + other: Ilaq ad neḍmen belli tesɛiḍ ma ulac %{count} akken ad tesqedceḍ %{domain}. Ur neḥrez ara aya. labels: account: fields: name: Tabzimt value: Agbur account_alias: - acct: Tansa n umiḍan aqbur + acct: Asulay n umiḍan aqbur account_migration: - acct: Tansa n umiḍan amaynut + acct: Asulay n umiḍan amaynut account_warning_preset: title: Azwel admin_account_action: @@ -90,12 +96,14 @@ kab: setting_always_send_emails: Dima ttazen-d ilɣa s yimayl setting_default_language: Tutlayt n usuffeɣ setting_default_privacy: Tabaḍnit n usuffeɣ + setting_default_quote_policy: Anwa i izemren ad d-yebder setting_display_media: Askanay n imidyaten setting_display_media_default: Akk-a kan setting_display_media_hide_all: Ffer-iten akk setting_display_media_show_all: Sken-iten-id akk setting_hide_network: Ffer azetta-k·m inmetti setting_theme: Asental n wesmel + setting_trends: Sken-d inezzaɣ n wass-a setting_use_pending_items: Askar aleɣwayan sign_in_token_attempt: Tangalt n tɣellist title: Azwel @@ -116,6 +124,7 @@ kab: status_page_url: URL n uusebter n waddaden theme: Asentel amezwer thumbnail: Tanfult n uqeddac + trends: Rmed inezzaɣ interactions: must_be_follower: Ssewḥel ilɣa sɣur wid akk d tid ur yellin ara d imeḍfaren-ik·im must_be_following: Ssewḥel ilɣa sɣur wid akked tid ur tettḍafareḍ ara From 868c46bc7687a8c361060078ad7d33f1e00941a6 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 8 Aug 2025 11:46:09 +0200 Subject: [PATCH 3/6] Add delivery failure handling to FASP jobs (#35723) --- app/models/fasp/provider.rb | 17 ++++ app/workers/fasp/account_search_worker.rb | 16 +-- ...announce_account_lifecycle_event_worker.rb | 10 +- ...announce_content_lifecycle_event_worker.rb | 10 +- app/workers/fasp/announce_trend_worker.rb | 10 +- app/workers/fasp/backfill_worker.rb | 12 +-- app/workers/fasp/base_worker.rb | 19 ++++ .../fasp/follow_recommendation_worker.rb | 20 ++-- ...livery_last_failed_at_to_fasp_providers.rb | 7 ++ db/schema.rb | 97 +++++++++--------- spec/models/fasp/provider_spec.rb | 98 +++++++++++++++++++ .../examples/workers/fasp/delivery_failure.rb | 57 +++++++++++ .../fasp/account_search_worker_spec.rb | 18 +++- ...nce_account_lifecycle_event_worker_spec.rb | 16 ++- ...nce_content_lifecycle_event_worker_spec.rb | 16 ++- .../fasp/announce_trend_worker_spec.rb | 18 +++- spec/workers/fasp/backfill_worker_spec.rb | 16 ++- .../fasp/follow_recommendation_worker_spec.rb | 22 +++-- 18 files changed, 373 insertions(+), 106 deletions(-) create mode 100644 app/workers/fasp/base_worker.rb create mode 100644 db/migrate/20250805075010_add_delivery_last_failed_at_to_fasp_providers.rb create mode 100644 spec/support/examples/workers/fasp/delivery_failure.rb diff --git a/app/models/fasp/provider.rb b/app/models/fasp/provider.rb index 9f7be482fe..9c9b187cca 100644 --- a/app/models/fasp/provider.rb +++ b/app/models/fasp/provider.rb @@ -9,6 +9,7 @@ # capabilities :jsonb not null # confirmed :boolean default(FALSE), not null # contact_email :string +# delivery_last_failed_at :datetime # fediverse_account :string # name :string not null # privacy_policy :jsonb @@ -22,6 +23,8 @@ class Fasp::Provider < ApplicationRecord include DebugConcern + RETRY_INTERVAL = 1.hour + has_many :fasp_backfill_requests, inverse_of: :fasp_provider, class_name: 'Fasp::BackfillRequest', dependent: :delete_all has_many :fasp_debug_callbacks, inverse_of: :fasp_provider, class_name: 'Fasp::DebugCallback', dependent: :delete_all has_many :fasp_subscriptions, inverse_of: :fasp_provider, class_name: 'Fasp::Subscription', dependent: :delete_all @@ -122,6 +125,16 @@ class Fasp::Provider < ApplicationRecord @delivery_failure_tracker ||= DeliveryFailureTracker.new(base_url, resolution: :minutes) end + def available? + delivery_failure_tracker.available? || retry_worthwile? + end + + def update_availability! + self.delivery_last_failed_at = (Time.current unless delivery_failure_tracker.available?) + + save! + end + private def create_keypair @@ -148,4 +161,8 @@ class Fasp::Provider < ApplicationRecord Fasp::Request.new(self).delete(path) end end + + def retry_worthwile? + delivery_last_failed_at && delivery_last_failed_at < RETRY_INTERVAL.ago + end end diff --git a/app/workers/fasp/account_search_worker.rb b/app/workers/fasp/account_search_worker.rb index 745285c44d..12b000fa86 100644 --- a/app/workers/fasp/account_search_worker.rb +++ b/app/workers/fasp/account_search_worker.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -class Fasp::AccountSearchWorker - include Sidekiq::Worker - - sidekiq_options queue: 'fasp', retry: 0 +class Fasp::AccountSearchWorker < Fasp::BaseWorker + sidekiq_options retry: 0 def perform(query) return unless Mastodon::Feature.fasp_enabled? @@ -17,11 +15,13 @@ class Fasp::AccountSearchWorker fetch_service = ActivityPub::FetchRemoteActorService.new account_search_providers.each do |provider| - Fasp::Request.new(provider).get("/account_search/v0/search?#{params}").each do |uri| - next if Account.where(uri:).any? + with_provider(provider) do + Fasp::Request.new(provider).get("/account_search/v0/search?#{params}").each do |uri| + next if Account.where(uri:).any? - account = fetch_service.call(uri) - async_refresh.increment_result_count(by: 1) if account.present? + account = fetch_service.call(uri) + async_refresh.increment_result_count(by: 1) if account.present? + end end end ensure diff --git a/app/workers/fasp/announce_account_lifecycle_event_worker.rb b/app/workers/fasp/announce_account_lifecycle_event_worker.rb index ea8544c24d..fc7fb235ea 100644 --- a/app/workers/fasp/announce_account_lifecycle_event_worker.rb +++ b/app/workers/fasp/announce_account_lifecycle_event_worker.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -class Fasp::AnnounceAccountLifecycleEventWorker - include Sidekiq::Worker - - sidekiq_options queue: 'fasp', retry: 5 +class Fasp::AnnounceAccountLifecycleEventWorker < Fasp::BaseWorker + sidekiq_options retry: 5 def perform(uri, event_type) Fasp::Subscription.includes(:fasp_provider).category_account.lifecycle.each do |subscription| - announce(subscription, uri, event_type) + with_provider(subscription.fasp_provider) do + announce(subscription, uri, event_type) + end end end diff --git a/app/workers/fasp/announce_content_lifecycle_event_worker.rb b/app/workers/fasp/announce_content_lifecycle_event_worker.rb index 744528f2d3..d4450a8aec 100644 --- a/app/workers/fasp/announce_content_lifecycle_event_worker.rb +++ b/app/workers/fasp/announce_content_lifecycle_event_worker.rb @@ -1,13 +1,13 @@ # frozen_string_literal: true -class Fasp::AnnounceContentLifecycleEventWorker - include Sidekiq::Worker - - sidekiq_options queue: 'fasp', retry: 5 +class Fasp::AnnounceContentLifecycleEventWorker < Fasp::BaseWorker + sidekiq_options retry: 5 def perform(uri, event_type) Fasp::Subscription.includes(:fasp_provider).category_content.lifecycle.each do |subscription| - announce(subscription, uri, event_type) + with_provider(subscription.fasp_provider) do + announce(subscription, uri, event_type) + end end end diff --git a/app/workers/fasp/announce_trend_worker.rb b/app/workers/fasp/announce_trend_worker.rb index ae93c3d9f6..dc1d94a271 100644 --- a/app/workers/fasp/announce_trend_worker.rb +++ b/app/workers/fasp/announce_trend_worker.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -class Fasp::AnnounceTrendWorker - include Sidekiq::Worker - - sidekiq_options queue: 'fasp', retry: 5 +class Fasp::AnnounceTrendWorker < Fasp::BaseWorker + sidekiq_options retry: 5 def perform(status_id, trend_source) status = ::Status.includes(:account).find(status_id) return unless status.account.indexable? Fasp::Subscription.includes(:fasp_provider).category_content.trends.each do |subscription| - announce(subscription, status.uri) if trending?(subscription, status, trend_source) + with_provider(subscription.fasp_provider) do + announce(subscription, status.uri) if trending?(subscription, status, trend_source) + end end rescue ActiveRecord::RecordNotFound # status might not exist anymore, in which case there is nothing to do diff --git a/app/workers/fasp/backfill_worker.rb b/app/workers/fasp/backfill_worker.rb index 4e30b71a7d..228dcbc1d2 100644 --- a/app/workers/fasp/backfill_worker.rb +++ b/app/workers/fasp/backfill_worker.rb @@ -1,16 +1,16 @@ # frozen_string_literal: true -class Fasp::BackfillWorker - include Sidekiq::Worker - - sidekiq_options queue: 'fasp', retry: 5 +class Fasp::BackfillWorker < Fasp::BaseWorker + sidekiq_options retry: 5 def perform(backfill_request_id) backfill_request = Fasp::BackfillRequest.find(backfill_request_id) - announce(backfill_request) + with_provider(backfill_request.fasp_provider) do + announce(backfill_request) - backfill_request.advance! + backfill_request.advance! + end rescue ActiveRecord::RecordNotFound # ignore missing backfill requests end diff --git a/app/workers/fasp/base_worker.rb b/app/workers/fasp/base_worker.rb new file mode 100644 index 0000000000..fe7f0b0c00 --- /dev/null +++ b/app/workers/fasp/base_worker.rb @@ -0,0 +1,19 @@ +# frozen_string_literal: true + +class Fasp::BaseWorker + include Sidekiq::Worker + + sidekiq_options queue: 'fasp' + + private + + def with_provider(provider) + return unless provider.available? + + yield + rescue *Mastodon::HTTP_CONNECTION_ERRORS + raise if provider.available? + ensure + provider.update_availability! + end +end diff --git a/app/workers/fasp/follow_recommendation_worker.rb b/app/workers/fasp/follow_recommendation_worker.rb index 5e760491bf..b0eb4e38bf 100644 --- a/app/workers/fasp/follow_recommendation_worker.rb +++ b/app/workers/fasp/follow_recommendation_worker.rb @@ -1,9 +1,7 @@ # frozen_string_literal: true -class Fasp::FollowRecommendationWorker - include Sidekiq::Worker - - sidekiq_options queue: 'fasp', retry: 0 +class Fasp::FollowRecommendationWorker < Fasp::BaseWorker + sidekiq_options retry: 0 def perform(account_id) return unless Mastodon::Feature.fasp_enabled? @@ -20,14 +18,16 @@ class Fasp::FollowRecommendationWorker fetch_service = ActivityPub::FetchRemoteActorService.new follow_recommendation_providers.each do |provider| - Fasp::Request.new(provider).get("/follow_recommendation/v0/accounts?#{params}").each do |uri| - next if Account.where(uri:).any? + with_provider(provider) do + Fasp::Request.new(provider).get("/follow_recommendation/v0/accounts?#{params}").each do |uri| + next if Account.where(uri:).any? - new_account = fetch_service.call(uri) + new_account = fetch_service.call(uri) - if new_account.present? - Fasp::FollowRecommendation.find_or_create_by(requesting_account: account, recommended_account: new_account) - async_refresh.increment_result_count(by: 1) + if new_account.present? + Fasp::FollowRecommendation.find_or_create_by(requesting_account: account, recommended_account: new_account) + async_refresh.increment_result_count(by: 1) + end end end end diff --git a/db/migrate/20250805075010_add_delivery_last_failed_at_to_fasp_providers.rb b/db/migrate/20250805075010_add_delivery_last_failed_at_to_fasp_providers.rb new file mode 100644 index 0000000000..f8af1f3337 --- /dev/null +++ b/db/migrate/20250805075010_add_delivery_last_failed_at_to_fasp_providers.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddDeliveryLastFailedAtToFaspProviders < ActiveRecord::Migration[8.0] + def change + add_column :fasp_providers, :delivery_last_failed_at, :datetime + end +end diff --git a/db/schema.rb b/db/schema.rb index 272d6fac18..cf8c74c8e2 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do +ActiveRecord::Schema[8.0].define(version: 2025_08_05_075010) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -488,6 +488,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do t.string "fediverse_account" t.datetime "created_at", null: false t.datetime "updated_at", null: false + t.datetime "delivery_last_failed_at" t.index ["base_url"], name: "index_fasp_providers_on_base_url", unique: true end @@ -1483,53 +1484,6 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do add_foreign_key "web_settings", "users", name: "fk_11910667b2", on_delete: :cascade add_foreign_key "webauthn_credentials", "users", on_delete: :cascade - create_view "instances", materialized: true, sql_definition: <<-SQL - WITH domain_counts(domain, accounts_count) AS ( - SELECT accounts.domain, - count(*) AS accounts_count - FROM accounts - WHERE (accounts.domain IS NOT NULL) - GROUP BY accounts.domain - ) - SELECT domain_counts.domain, - domain_counts.accounts_count - FROM domain_counts - UNION - SELECT domain_blocks.domain, - COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count - FROM (domain_blocks - LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_blocks.domain)::text))) - UNION - SELECT domain_allows.domain, - COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count - FROM (domain_allows - LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_allows.domain)::text))); - SQL - add_index "instances", "reverse(('.'::text || (domain)::text)), domain", name: "index_instances_on_reverse_domain" - add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true - - create_view "user_ips", sql_definition: <<-SQL - SELECT user_id, - ip, - max(used_at) AS used_at - FROM ( SELECT users.id AS user_id, - users.sign_up_ip AS ip, - users.created_at AS used_at - FROM users - WHERE (users.sign_up_ip IS NOT NULL) - UNION ALL - SELECT session_activations.user_id, - session_activations.ip, - session_activations.updated_at - FROM session_activations - UNION ALL - SELECT login_activities.user_id, - login_activities.ip, - login_activities.created_at - FROM login_activities - WHERE (login_activities.success = true)) t0 - GROUP BY user_id, ip; - SQL create_view "account_summaries", materialized: true, sql_definition: <<-SQL SELECT accounts.id AS account_id, mode() WITHIN GROUP (ORDER BY t0.language) AS language, @@ -1580,4 +1534,51 @@ ActiveRecord::Schema[8.0].define(version: 2025_07_17_003848) do SQL add_index "global_follow_recommendations", ["account_id"], name: "index_global_follow_recommendations_on_account_id", unique: true + create_view "instances", materialized: true, sql_definition: <<-SQL + WITH domain_counts(domain, accounts_count) AS ( + SELECT accounts.domain, + count(*) AS accounts_count + FROM accounts + WHERE (accounts.domain IS NOT NULL) + GROUP BY accounts.domain + ) + SELECT domain_counts.domain, + domain_counts.accounts_count + FROM domain_counts + UNION + SELECT domain_blocks.domain, + COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count + FROM (domain_blocks + LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_blocks.domain)::text))) + UNION + SELECT domain_allows.domain, + COALESCE(domain_counts.accounts_count, (0)::bigint) AS accounts_count + FROM (domain_allows + LEFT JOIN domain_counts ON (((domain_counts.domain)::text = (domain_allows.domain)::text))); + SQL + add_index "instances", "reverse(('.'::text || (domain)::text)), domain", name: "index_instances_on_reverse_domain" + add_index "instances", ["domain"], name: "index_instances_on_domain", unique: true + + create_view "user_ips", sql_definition: <<-SQL + SELECT user_id, + ip, + max(used_at) AS used_at + FROM ( SELECT users.id AS user_id, + users.sign_up_ip AS ip, + users.created_at AS used_at + FROM users + WHERE (users.sign_up_ip IS NOT NULL) + UNION ALL + SELECT session_activations.user_id, + session_activations.ip, + session_activations.updated_at + FROM session_activations + UNION ALL + SELECT login_activities.user_id, + login_activities.ip, + login_activities.created_at + FROM login_activities + WHERE (login_activities.success = true)) t0 + GROUP BY user_id, ip; + SQL end diff --git a/spec/models/fasp/provider_spec.rb b/spec/models/fasp/provider_spec.rb index 9fd2c4c234..c0e6ae255a 100644 --- a/spec/models/fasp/provider_spec.rb +++ b/spec/models/fasp/provider_spec.rb @@ -214,4 +214,102 @@ RSpec.describe Fasp::Provider do expect(subject.delivery_failure_tracker).to be_a(DeliveryFailureTracker) end end + + describe '#available?' do + subject { Fabricate(:fasp_provider, delivery_last_failed_at:) } + + let(:delivery_last_failed_at) { nil } + + before do + allow(subject.delivery_failure_tracker).to receive(:available?).and_return(available) + end + + context 'when the delivery failure tracker reports it is available' do + let(:available) { true } + + it 'returns true' do + expect(subject.available?).to be true + end + end + + context 'when the delivery failure tracker reports it is unavailable' do + let(:available) { false } + + context 'when the last failure was more than one hour ago' do + let(:delivery_last_failed_at) { 61.minutes.ago } + + it 'returns true' do + expect(subject.available?).to be true + end + end + + context 'when the last failure is very recent' do + let(:delivery_last_failed_at) { 5.minutes.ago } + + it 'returns false' do + expect(subject.available?).to be false + end + end + end + end + + describe '#update_availability!' do + subject { Fabricate(:fasp_provider, delivery_last_failed_at:) } + + before do + allow(subject.delivery_failure_tracker).to receive(:available?).and_return(available) + end + + context 'when `delivery_last_failed_at` is `nil`' do + let(:delivery_last_failed_at) { nil } + + context 'when the delivery failure tracker reports it is available' do + let(:available) { true } + + it 'does not update the provider' do + subject.update_availability! + + expect(subject.saved_changes?).to be false + end + end + + context 'when the delivery failure tracker reports it is unavailable' do + let(:available) { false } + + it 'sets `delivery_last_failed_at` to the current time' do + freeze_time + + subject.update_availability! + + expect(subject.delivery_last_failed_at).to eq Time.zone.now + end + end + end + + context 'when `delivery_last_failed_at` is present' do + context 'when the delivery failure tracker reports it is available' do + let(:available) { true } + let(:delivery_last_failed_at) { 5.minutes.ago } + + it 'sets `delivery_last_failed_at` to `nil`' do + subject.update_availability! + + expect(subject.delivery_last_failed_at).to be_nil + end + end + + context 'when the delivery failure tracker reports it is unavailable' do + let(:available) { false } + let(:delivery_last_failed_at) { 5.minutes.ago } + + it 'updates `delivery_last_failed_at` to the current time' do + freeze_time + + subject.update_availability! + + expect(subject.delivery_last_failed_at).to eq Time.zone.now + end + end + end + end end diff --git a/spec/support/examples/workers/fasp/delivery_failure.rb b/spec/support/examples/workers/fasp/delivery_failure.rb new file mode 100644 index 0000000000..006d93e80d --- /dev/null +++ b/spec/support/examples/workers/fasp/delivery_failure.rb @@ -0,0 +1,57 @@ +# frozen_string_literal: true + +RSpec.shared_examples 'worker handling fasp delivery failures' do + context 'when provider is not available' do + before do + provider.update(delivery_last_failed_at: 1.minute.ago) + domain = Addressable::URI.parse(provider.base_url).normalized_host + UnavailableDomain.create!(domain:) + end + + it 'does not attempt connecting and does not fail the job' do + expect { subject }.to_not raise_error + expect(stubbed_request).to_not have_been_made + end + end + + context 'when connection to provider fails' do + before do + base_stubbed_request + .to_raise(HTTP::ConnectionError) + end + + context 'when provider becomes unavailable' do + before do + travel_to 5.minutes.ago + 4.times do + provider.delivery_failure_tracker.track_failure! + travel_to 1.minute.since + end + end + + it 'updates the provider and does not fail the job, so it will not be retried' do + expect { subject }.to_not raise_error + expect(provider.reload.delivery_last_failed_at).to eq Time.current + end + end + + context 'when provider is still marked as available' do + it 'fails the job so it can be retried' do + expect { subject }.to raise_error(HTTP::ConnectionError) + end + end + end + + context 'when connection to a previously unavailable provider succeeds' do + before do + provider.update(delivery_last_failed_at: 2.hours.ago) + domain = Addressable::URI.parse(provider.base_url).normalized_host + UnavailableDomain.create!(domain:) + end + + it 'marks the provider as being available again' do + expect { subject }.to_not raise_error + expect(provider).to be_available + end + end +end diff --git a/spec/workers/fasp/account_search_worker_spec.rb b/spec/workers/fasp/account_search_worker_spec.rb index a96ba0c23b..1c543b62ae 100644 --- a/spec/workers/fasp/account_search_worker_spec.rb +++ b/spec/workers/fasp/account_search_worker_spec.rb @@ -5,12 +5,14 @@ require 'rails_helper' RSpec.describe Fasp::AccountSearchWorker, feature: :fasp do include ProviderRequestHelper + subject { described_class.new.perform('cats') } + let(:provider) { Fabricate(:account_search_fasp) } let(:account) { Fabricate(:account) } let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService, call: true) } + let(:path) { '/account_search/v0/search?term=cats&limit=10' } let!(:stubbed_request) do - path = '/account_search/v0/search?term=cats&limit=10' stub_provider_request(provider, method: :get, path:, @@ -25,7 +27,7 @@ RSpec.describe Fasp::AccountSearchWorker, feature: :fasp do end it 'requests search results and fetches received account uris' do - described_class.new.perform('cats') + subject expect(stubbed_request).to have_been_made expect(fetch_service).to have_received(:call).with('https://fedi.example.com/accounts/2') @@ -35,7 +37,7 @@ RSpec.describe Fasp::AccountSearchWorker, feature: :fasp do it 'marks a running async refresh as finished' do async_refresh = AsyncRefresh.create("fasp:account_search:#{Digest::MD5.base64digest('cats')}", count_results: true) - described_class.new.perform('cats') + subject expect(async_refresh.reload).to be_finished end @@ -43,8 +45,16 @@ RSpec.describe Fasp::AccountSearchWorker, feature: :fasp do it 'tracks the number of fetched accounts in the async refresh' do async_refresh = AsyncRefresh.create("fasp:account_search:#{Digest::MD5.base64digest('cats')}", count_results: true) - described_class.new.perform('cats') + subject expect(async_refresh.reload.result_count).to eq 2 end + + describe 'provider delivery failure handling' do + let(:base_stubbed_request) do + stub_request(:get, provider.url(path)) + end + + it_behaves_like('worker handling fasp delivery failures') + end end diff --git a/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb b/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb index 0d4a870875..b89d4fe658 100644 --- a/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb +++ b/spec/workers/fasp/announce_account_lifecycle_event_worker_spec.rb @@ -5,15 +5,19 @@ require 'rails_helper' RSpec.describe Fasp::AnnounceAccountLifecycleEventWorker do include ProviderRequestHelper + subject { described_class.new.perform(account_uri, 'new') } + let(:account_uri) { 'https://masto.example.com/accounts/1' } let(:subscription) do Fabricate(:fasp_subscription, category: 'account') end let(:provider) { subscription.fasp_provider } + let(:path) { '/data_sharing/v0/announcements' } + let!(:stubbed_request) do stub_provider_request(provider, method: :post, - path: '/data_sharing/v0/announcements', + path:, response_body: { source: { subscription: { @@ -27,8 +31,16 @@ RSpec.describe Fasp::AnnounceAccountLifecycleEventWorker do end it 'sends the account uri to subscribed providers' do - described_class.new.perform(account_uri, 'new') + subject expect(stubbed_request).to have_been_made end + + describe 'provider delivery failure handling' do + let(:base_stubbed_request) do + stub_request(:post, provider.url(path)) + end + + it_behaves_like('worker handling fasp delivery failures') + end end diff --git a/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb b/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb index 60618607c9..6ff5a9f771 100644 --- a/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb +++ b/spec/workers/fasp/announce_content_lifecycle_event_worker_spec.rb @@ -5,15 +5,19 @@ require 'rails_helper' RSpec.describe Fasp::AnnounceContentLifecycleEventWorker do include ProviderRequestHelper + subject { described_class.new.perform(status_uri, 'new') } + let(:status_uri) { 'https://masto.example.com/status/1' } let(:subscription) do Fabricate(:fasp_subscription) end let(:provider) { subscription.fasp_provider } + let(:path) { '/data_sharing/v0/announcements' } + let!(:stubbed_request) do stub_provider_request(provider, method: :post, - path: '/data_sharing/v0/announcements', + path:, response_body: { source: { subscription: { @@ -27,8 +31,16 @@ RSpec.describe Fasp::AnnounceContentLifecycleEventWorker do end it 'sends the status uri to subscribed providers' do - described_class.new.perform(status_uri, 'new') + subject expect(stubbed_request).to have_been_made end + + describe 'provider delivery failure handling' do + let(:base_stubbed_request) do + stub_request(:post, provider.url(path)) + end + + it_behaves_like('worker handling fasp delivery failures') + end end diff --git a/spec/workers/fasp/announce_trend_worker_spec.rb b/spec/workers/fasp/announce_trend_worker_spec.rb index 799d8a8f48..369c2f1267 100644 --- a/spec/workers/fasp/announce_trend_worker_spec.rb +++ b/spec/workers/fasp/announce_trend_worker_spec.rb @@ -5,6 +5,8 @@ require 'rails_helper' RSpec.describe Fasp::AnnounceTrendWorker do include ProviderRequestHelper + subject { described_class.new.perform(status.id, 'favourite') } + let(:status) { Fabricate(:status) } let(:subscription) do Fabricate(:fasp_subscription, @@ -14,10 +16,12 @@ RSpec.describe Fasp::AnnounceTrendWorker do threshold_likes: 2) end let(:provider) { subscription.fasp_provider } + let(:path) { '/data_sharing/v0/announcements' } + let!(:stubbed_request) do stub_provider_request(provider, method: :post, - path: '/data_sharing/v0/announcements', + path:, response_body: { source: { subscription: { @@ -36,15 +40,23 @@ RSpec.describe Fasp::AnnounceTrendWorker do end it 'sends the account uri to subscribed providers' do - described_class.new.perform(status.id, 'favourite') + subject expect(stubbed_request).to have_been_made end + + describe 'provider delivery failure handling' do + let(:base_stubbed_request) do + stub_request(:post, provider.url(path)) + end + + it_behaves_like('worker handling fasp delivery failures') + end end context 'when the configured threshold is not met' do it 'does not notify any provider' do - described_class.new.perform(status.id, 'favourite') + subject expect(stubbed_request).to_not have_been_made end diff --git a/spec/workers/fasp/backfill_worker_spec.rb b/spec/workers/fasp/backfill_worker_spec.rb index 43734e02ba..e15493ea5d 100644 --- a/spec/workers/fasp/backfill_worker_spec.rb +++ b/spec/workers/fasp/backfill_worker_spec.rb @@ -5,13 +5,17 @@ require 'rails_helper' RSpec.describe Fasp::BackfillWorker do include ProviderRequestHelper + subject { described_class.new.perform(backfill_request.id) } + let(:backfill_request) { Fabricate(:fasp_backfill_request) } let(:provider) { backfill_request.fasp_provider } let(:status) { Fabricate(:status) } + let(:path) { '/data_sharing/v0/announcements' } + let!(:stubbed_request) do stub_provider_request(provider, method: :post, - path: '/data_sharing/v0/announcements', + path:, response_body: { source: { backfillRequest: { @@ -25,8 +29,16 @@ RSpec.describe Fasp::BackfillWorker do end it 'sends status uri to provider that requested backfill' do - described_class.new.perform(backfill_request.id) + subject expect(stubbed_request).to have_been_made end + + describe 'provider delivery failure handling' do + let(:base_stubbed_request) do + stub_request(:post, provider.url(path)) + end + + it_behaves_like('worker handling fasp delivery failures') + end end diff --git a/spec/workers/fasp/follow_recommendation_worker_spec.rb b/spec/workers/fasp/follow_recommendation_worker_spec.rb index baa647aa06..895175f3f3 100644 --- a/spec/workers/fasp/follow_recommendation_worker_spec.rb +++ b/spec/workers/fasp/follow_recommendation_worker_spec.rb @@ -5,13 +5,15 @@ require 'rails_helper' RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do include ProviderRequestHelper + subject { described_class.new.perform(account.id) } + let(:provider) { Fabricate(:follow_recommendation_fasp) } let(:account) { Fabricate(:account) } + let(:account_uri) { ActivityPub::TagManager.instance.uri_for(account) } let(:fetch_service) { instance_double(ActivityPub::FetchRemoteActorService) } + let(:path) { "/follow_recommendation/v0/accounts?accountUri=#{URI.encode_uri_component(account_uri)}" } let!(:stubbed_request) do - account_uri = ActivityPub::TagManager.instance.uri_for(account) - path = "/follow_recommendation/v0/accounts?accountUri=#{URI.encode_uri_component(account_uri)}" stub_provider_request(provider, method: :get, path:, @@ -28,7 +30,7 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do end it "sends the requesting account's uri to provider and fetches received account uris" do - described_class.new.perform(account.id) + subject expect(stubbed_request).to have_been_made expect(fetch_service).to have_received(:call).with('https://fedi.example.com/accounts/1') @@ -38,7 +40,7 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do it 'marks a running async refresh as finished' do async_refresh = AsyncRefresh.create("fasp:follow_recommendation:#{account.id}", count_results: true) - described_class.new.perform(account.id) + subject expect(async_refresh.reload).to be_finished end @@ -46,14 +48,22 @@ RSpec.describe Fasp::FollowRecommendationWorker, feature: :fasp do it 'tracks the number of fetched accounts in the async refresh' do async_refresh = AsyncRefresh.create("fasp:follow_recommendation:#{account.id}", count_results: true) - described_class.new.perform(account.id) + subject expect(async_refresh.reload.result_count).to eq 2 end it 'persists the results' do expect do - described_class.new.perform(account.id) + subject end.to change(Fasp::FollowRecommendation, :count).by(2) end + + describe 'provider delivery failure handling' do + let(:base_stubbed_request) do + stub_request(:get, provider.url(path)) + end + + it_behaves_like('worker handling fasp delivery failures') + end end From 5d934c283525527959537be561cc0ba86955bf46 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 12:06:23 +0200 Subject: [PATCH 4/6] Update dependency httplog to v1.7.3 (#35721) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index 943334b03d..5f9259edea 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -315,7 +315,7 @@ GEM http_accept_language (2.1.1) httpclient (2.9.0) mutex_m - httplog (1.7.2) + httplog (1.7.3) rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.14.7) From b8982cb881bf96ea5cb8f6ce48cf057f0ee4e520 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Fri, 8 Aug 2025 11:31:50 -0400 Subject: [PATCH 5/6] Use `around_action` to preserve stored location in `auth/sessions#destroy` (#35716) --- app/controllers/auth/sessions_controller.rb | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/app/controllers/auth/sessions_controller.rb b/app/controllers/auth/sessions_controller.rb index c52bda67b0..182f242ae5 100644 --- a/app/controllers/auth/sessions_controller.rb +++ b/app/controllers/auth/sessions_controller.rb @@ -12,6 +12,8 @@ class Auth::SessionsController < Devise::SessionsController skip_before_action :require_functional! skip_before_action :update_user_sign_in + around_action :preserve_stored_location, only: :destroy, if: :continue_after? + prepend_before_action :check_suspicious!, only: [:create] include Auth::TwoFactorAuthenticationConcern @@ -31,11 +33,9 @@ class Auth::SessionsController < Devise::SessionsController end def destroy - tmp_stored_location = stored_location_for(:user) super session.delete(:challenge_passed_at) flash.delete(:notice) - store_location_for(:user, tmp_stored_location) if continue_after? end def webauthn_options @@ -96,6 +96,12 @@ class Auth::SessionsController < Devise::SessionsController private + def preserve_stored_location + original_stored_location = stored_location_for(:user) + yield + store_location_for(:user, original_stored_location) + end + def check_suspicious! user = find_user @login_is_suspicious = suspicious_sign_in?(user) unless user.nil? From ce1680e6f979889ebd5c775353f3b82a8b2698bb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 8 Aug 2025 17:32:15 +0200 Subject: [PATCH 6/6] Update dependency core-js to v3.45.0 (#35667) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 26f7cd409e..b525ed6d67 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6015,9 +6015,9 @@ __metadata: linkType: hard "core-js@npm:^3.30.2, core-js@npm:^3.41.0": - version: 3.44.0 - resolution: "core-js@npm:3.44.0" - checksum: 10c0/759bf3dc5f75068e9425dddf895fd5531c38794a11ea1c2b65e5ef7c527fe3652d59e8c287e574a211af9bab3c057c5c3fa6f6a773f4e142af895106efad38a4 + version: 3.45.0 + resolution: "core-js@npm:3.45.0" + checksum: 10c0/118350f9f1d81f42a1276590d6c217dca04c789fdb8074c82e53056b1a784948769a62b16b98493fd73e8a988545432f302bca798571e56ad881b9c039a5a83c languageName: node linkType: hard