Merge commit 'ce1680e6f979889ebd5c775353f3b82a8b2698bb' into glitch-soc/merge-upstream

Conflicts:
- `app/models/concerns/user/has_settings.rb`:
  Upstream added a new helper for a setting, where glitch-soc had extra ones.
  Not a real conflict, added upstream's new helper without removing glitch-soc's ones.
This commit is contained in:
Claire
2025-08-09 23:02:43 +02:00
45 changed files with 890 additions and 137 deletions

View File

@@ -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)

View File

@@ -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?

View File

@@ -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']),

View File

@@ -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<ApiQuotePolicy>(
'compose/setQuotePolicy',
);

View File

@@ -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<ApiQuoteState, 'accepted'>;
quoted_status: null;
}
interface ApiNestedQuoteJSON {
state: 'accepted';
quoted_status_id: string;
}
interface ApiQuoteAcceptedJSON {
state: 'accepted';
quoted_status: Omit<ApiStatusJSON, 'quote'> & {
quote: ApiNestedQuoteJSON | ApiQuoteEmptyJSON;
};
}
export type ApiQuoteJSON = ApiQuoteAcceptedJSON | ApiQuoteEmptyJSON;

View File

@@ -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 {

View File

@@ -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": "<h1>Што такое альтэрнатыўны тэкст?</h1> <p>Альтэрнатыўны тэкст апісвае відарыс людзям з парушэннем зроку, павольным злучэннем або тым, каму патрэбны дадатковы кантэкст.</p> <p>Вы можаце зрабіць відарыс больш дасяжным і зразумелым для ўсіх, напісаўшы зразумелы, сціслы і аб'ектыўны альтэрнатыўны тэкст.</p> <ul><li>Ахоплівайце важныя элементы</li> <li>Тлумачце тэкст на відарысе</li> <li>Карыстайцеся звычайнымі сказамі</li> <li>Пазбягайце залішняй інфармацыі</li> <li>Засяроджвайцеся на тэндэнцыях і ключавых высновах у цяжкіх для разумення візуальных матэрыялах (напрыклад, дыяграмах або картах)</li></ul>",
"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} і <a>{count, plural, one {# іншы} other {# іншыя}}</a> ўпадабалі Ваш допіс",
"notification.favourite_pm": "Ваша асабістае згадванне ўпадабана {name}",
"notification.favourite_pm.name_and_others_with_link": "{name} і <a>{count, plural, one {# іншы} few {# іншыя} many {# іншых} other {# іншых}}</a> ўпадабалі ваша асабістае згадванне",
"notification.follow": "{name} падпісаўся на вас",
"notification.follow.name_and_others": "{name} і <a>{count, plural, one {# іншы} other {# іншыя}}</a> падпісаліся на Вас",
"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} і <a>{count, plural, one {# іншы} many {# іншых} other {# іншых}}</a> абагулілі ваш допіс",
"notification.reblog.name_and_others_with_link": "{name} і <a>{count, plural, one {# іншы} many {# іншых} other {# іншых}}</a> пашырылі ваш допіс",
"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": "Апрацоўка…",

View File

@@ -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",

View File

@@ -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": "Δείξε λιγότερο για όλες",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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",

View File

@@ -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 <button>tɣeṛɣaṛ yemmugen s ActivityPub</button>.",
"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 <button>tɣeṛɣaṛ yemmugen s ActivityPub</button>.",
"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",

View File

@@ -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": "모두 접기",

View File

@@ -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 හි වෙනත් ඕනෑම සේවාදායකයකින් අන්තර්ගතයන් බැලීමට සහ පරිශීලකයින් සමඟ අන්තර් ක්‍රියා කිරීමට ඉඩ සලසයි. මෙම විශේෂිත සේවාදායකයේ සිදු කර ඇති ව්‍යතිරේක මේවාය.",

View File

@@ -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) {

View File

@@ -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<Arg> {
) => boolean;
}
const createBaseAsyncThunk = createAsyncThunk.withTypes<AppThunkConfig>();
// Type definitions for the sync thunks.
type AppThunk<Arg = void, Returned = void> = (
arg: Arg,
) => (dispatch: AppDispatch, getState: () => RootState) => Returned;
export function createThunk<Arg = void, Returned = void>(
type AppThunkCreator<Arg = void, Returned = void, ExtraArg = unknown> = (
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<Arg = void, Returned = void, ExtraArg = unknown>(
creator: AppThunkCreator<Arg, Returned, ExtraArg>,
extra?: ExtraArg,
): AppThunk<Arg, Returned>;
// Version that dispatches an named action with the result of the creator callback.
export function createAppThunk<Arg = void, Returned = void, ExtraArg = unknown>(
name: string,
creator: AppThunkCreator<Arg, Returned, ExtraArg>,
extra?: ExtraArg,
): AppThunk<Arg, Returned> & AppThunkActionCreator<Arg, Returned>;
/** Creates a thunk that dispatches an action. */
export function createAppThunk<Arg = void, Returned = void, ExtraArg = unknown>(
nameOrCreator: string | AppThunkCreator<Arg, Returned, ExtraArg>,
maybeCreatorOrExtra?: AppThunkCreator<Arg, Returned, ExtraArg> | ExtraArg,
maybeExtra?: ExtraArg,
) {
const isDispatcher = typeof nameOrCreator === 'string';
const name = isDispatcher ? nameOrCreator : undefined;
const creator = isDispatcher
? (maybeCreatorOrExtra as AppThunkCreator<Arg, Returned, ExtraArg>)
: nameOrCreator;
const extra = isDispatcher ? maybeExtra : (maybeCreatorOrExtra as ExtraArg);
let action: null | AppThunkActionCreator<Arg, Returned> = null;
// Creates a thunk that dispatches the action with the result of the creator.
const actionCreator: AppThunk<Arg, Returned> = (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<AppThunkConfig>();
export function createAsyncThunk<Arg = void, Returned = void>(
name: string,
creator: (arg: Arg, api: AppThunkApi) => Returned | Promise<Returned>,
options: AppThunkOptions<Arg> = {},
@@ -104,7 +190,7 @@ export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
name: string,
loadData: (args: Args) => Promise<LoadDataResult>,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, LoadDataResult>>;
): ReturnType<typeof createAsyncThunk<Args, LoadDataResult>>;
// Overload when the `onData` method returns discardLoadDataInPayload, then the payload is empty
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
@@ -114,7 +200,7 @@ export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, DiscardLoadData>,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, void>>;
): ReturnType<typeof createAsyncThunk<Args, void>>;
// Overload when the `onData` method returns nothing, then the mayload is the `onData` result
export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
@@ -124,7 +210,7 @@ export function createDataLoadingThunk<LoadDataResult, Args extends ArgsType>(
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, void>,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, LoadDataResult>>;
): ReturnType<typeof createAsyncThunk<Args, LoadDataResult>>;
// Overload when there is an `onData` method returning something
export function createDataLoadingThunk<
@@ -138,7 +224,7 @@ export function createDataLoadingThunk<
| AppThunkOptions<Args>
| OnData<Args, LoadDataResult, Returned>,
thunkOptions?: AppThunkOptions<Args>,
): ReturnType<typeof createThunk<Args, Returned>>;
): ReturnType<typeof createAsyncThunk<Args, Returned>>;
/**
* 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<Args, Returned>(
return createAsyncThunk<Args, Returned>(
name,
async (arg, { getState, dispatch }) => {
const data = await loadData(arg, {

View File

@@ -127,6 +127,10 @@ module User::HasSettings
settings['hide_followers_count']
end
def setting_default_quote_policy
settings['default_quote_policy'] || 'public'
end
def allows_report_emails?
settings['notification_emails.report']
end

View File

@@ -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

View File

@@ -71,6 +71,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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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: не існуе

View File

@@ -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

View File

@@ -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: 'Ліст будзе дасланы на электронную пошту <strong>%{display_count} карыстальнікам</strong>. У ім будзе наступнае:'
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: "<strong>Рэкамендацыі падпісак, дапамогаюць новым карыстальнікам хутка знайсці цікавы кантэнт</strong>. Калі карыстальнік недастаткова ўзаемадзейнічаў з іншымі, каб сфарміраваць персанальныя рэкамендацыі прытрымлівацца, замест гэтага рэкамендуюцца гэтыя ўліковыя запісы. Яны штодзённа пераразлічваюцца з сумесі ўліковых запісаў з самымі апошнімі ўзаемадзеяннямі і найбольшай колькасцю мясцовых падпісчыкаў для дадзенай мовы."
@@ -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: Большасць сцвярджаюць, што прачыталі ўмовы абслугоўвання і згаджаюцца з імі, але звычайна людзі не чытаюць іх да канца, пакуль не ўзнікне праблема. <strong>Таму зрабіце правілы вашага сервера простымі з першага погляду, прадставіўшы іх у выглядзе маркіраванага спісу.</strong> Старайцеся рабіць правілы кароткімі і простымі, але не разбіваць іх на шмат асобных пунктаў.
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 пратэрмінаваныя. Калі ласка, запусціце <code>tootctl search deploy --only-mapping --only=%{value}</code>
elasticsearch_health_red:
message_html: Кластар Elasticsearch нездаровы (чырвоны статус), функцыі пошуку недаступныя
elasticsearch_health_yellow:
@@ -990,9 +1023,25 @@ be:
generate: Выкарыстаць шаблон
generates:
action: Згенерыраваць
chance_to_review_html: "<strong>Аўтаматычна згенераваныя ўмовы карыстання не будуць аўтаматычна апублікаваныя.</strong> У Вас будзе магчымасць паглядзець на вынікі. Калі ласка, дайце неабходныя дэталі, каб працягнуць."
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: 'Электронны ліст будзе дасланы <strong>%{display_count} карыстальнікам</strong>, якія зарэгістраваліся да %{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: Я прачытаў і згаджаюся з <a href="%{terms_of_service_path}" target="_blank">умовамі карыстання</a> і <a href="%{privacy_policy_path}" target="_blank">палітыкай прыватнасці</a>
user_privacy_agreement_html: Я прачытаў і згаджаюся з <a href="%{privacy_policy_path}" target="_blank">палітыкай прыватнасці</a>
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: Вы збіраецеся <strong>замяніць свой спіс заблакіраваных карыстальнікаў</strong> <strong>%{count} карыстальнікамі</strong> з <strong>%{filename}</strong>.
many: Вы збіраецеся <strong>замяніць свой спіс заблакіраваных карыстальнікаў</strong> <strong>%{count} карыстальнікамі</strong> з <strong>%{filename}</strong>.
one: Вы збіраецеся <strong>замяніць свой спіс заблакіраваных карыстальнікаў</strong> <strong>%{count} карыстальнікам</strong> з <strong>%{filename}</strong>.
other: Вы збіраецеся <strong>замяніць свой спіс заблакіраваных карыстальнікаў</strong> <strong>%{count} карыстальнікамі</strong> з <strong>%{filename}</strong>.
bookmarks_html:
few: Вы збіраецеся <strong>замяніць свае закладкі</strong> <strong>%{count} допісамі</strong> з <strong>%{filename}</strong>.
many: Вы збіраецеся <strong>замяніць свае закладкі</strong> <strong>%{count} допісамі</strong> з <strong>%{filename}</strong>.
one: Вы збіраецеся <strong>замяніць свае закладкі</strong> <strong>%{count} допісам</strong> з <strong>%{filename}</strong>.
other: Вы збіраецеся <strong>замяніць свае закладкі</strong> <strong>%{count} допісамі</strong> з <strong>%{filename}</strong>.
domain_blocking_html:
few: Вы збіраецеся <strong>замяніць свой спіс заблакіраваных сервераў</strong> <strong>%{count} серверамі</strong> з <strong>%{filename}</strong>.
many: Вы збіраецеся <strong>замяніць свой спіс заблакіраваных сервераў</strong> <strong>%{count} серверамі</strong> з <strong>%{filename}</strong>.
one: Вы збіраецеся <strong>замяніць свой спіс заблакіраваных сервераў</strong> <strong>%{count} серверам</strong> з <strong>%{filename}</strong>.
other: Вы збіраецеся <strong>замяніць свой спіс заблакіраваных сервераў</strong> <strong>%{count} серверамі</strong> з <strong>%{filename}</strong>.
following_html:
few: Вы збіраецеся <strong>падпісацца</strong> на <strong>%{count} профілі</strong> з <strong>%{filename}</strong> і <strong>адпішацеся ад усіх астатніх</strong>.
many: Вы збіраецеся <strong>падпісацца</strong> на <strong>%{count} профіляў</strong> з <strong>%{filename}</strong> і <strong>адпішацеся ад усіх астатніх</strong>.
one: Вы збіраецеся <strong>падпісацца</strong> на <strong>%{count} профіль</strong> з <strong>%{filename}</strong> і <strong>адпішацеся ад усіх астатніх</strong>.
other: Вы збіраецеся <strong>падпісацца</strong> на <strong>%{count} профіляў</strong> з <strong>%{filename}</strong> і <strong>адпішацеся ад усіх астатніх</strong>.
lists_html:
few: Вы збіраецеся <strong>замяніць свае спісы</strong> змесцівам з <strong>%{filename}</strong>. <strong>%{count} профілі</strong> будуць дададзеныя ў новыя спісы.
many: Вы збіраецеся <strong>замяніць свае спісы</strong> змесцівам з <strong>%{filename}</strong>. <strong>%{count} профіляў</strong> будуць дададзеныя ў новыя спісы.
one: Вы збіраецеся <strong>замяніць свае спісы</strong> змесцівам з <strong>%{filename}</strong>. <strong>%{count} профіль</strong> будзе дададзены ў новыя спісы.
other: Вы збіраецеся <strong>замяніць свае спісы</strong> змесцівам з <strong>%{filename}</strong>. <strong>%{count} профіляў</strong> будуць дададзеныя ў новыя спісы.
muting_html:
few: Вы збіраецеся <strong>замяніць свой спіс профіляў, якія Вы ігнаруеце,</strong> <strong>%{count} профілямі</strong> з <strong>%{filename}</strong>.
many: Вы збіраецеся <strong>замяніць свой спіс профіляў, якія Вы ігнаруеце,</strong> <strong>%{count} профілямі</strong> з <strong>%{filename}</strong>.
one: Вы збіраецеся <strong>замяніць свой спіс профіляў, якія Вы ігнаруеце,</strong> <strong>%{count} профілем</strong> з <strong>%{filename}</strong>.
other: Вы збіраецеся <strong>замяніць свой спіс профіляў, якія Вы ігнаруеце,</strong> <strong>%{count} профілямі</strong> з <strong>%{filename}</strong>.
preambles:
blocking_html:
few: Вы збіраецеся <strong>заблакіраваць</strong> <strong>%{count} профілі</strong> з <strong>%{filename}</strong>.
many: Вы збіраецеся <strong>заблакіраваць</strong> <strong>%{count} профіляў</strong> з <strong>%{filename}</strong>.
one: Вы збіраецеся <strong>заблакіраваць</strong> <strong>%{count} профіль</strong> з <strong>%{filename}</strong>.
other: Вы збіраецеся <strong>заблакіраваць</strong> <strong>%{count} профіляў</strong> з <strong>%{filename}</strong>.
bookmarks_html:
few: Вы збіраецеся дадаць <strong>%{count} допісы</strong> з <strong>%{filename}</strong> у Вашыя <strong>закладкі</strong>.
many: Вы збіраецеся дадаць <strong>%{count} допісаў</strong> з <strong>%{filename}</strong> у Вашыя <strong>закладкі</strong>.
one: Вы збіраецеся дадаць <strong>%{count} допіс</strong> з <strong>%{filename}</strong> у Вашыя <strong>закладкі</strong>.
other: Вы збіраецеся дадаць <strong>%{count} допісаў</strong> з <strong>%{filename}</strong> у Вашыя <strong>закладкі</strong>.
domain_blocking_html:
few: Вы збіраецеся <strong>заблакіраваць</strong> <strong>%{count} серверы</strong> з <strong>%{filename}</strong>.
many: Вы збіраецеся <strong>заблакіраваць</strong> <strong>%{count} сервераў</strong> з <strong>%{filename}</strong>.
one: Вы збіраецеся <strong>заблакіраваць</strong> <strong>%{count} сервер</strong> з <strong>%{filename}</strong>.
other: Вы збіраецеся <strong>заблакіраваць</strong> <strong>%{count} сервераў</strong> з <strong>%{filename}</strong>.
following_html:
few: Вы збіраецеся <strong>падпісацца</strong> на <strong>%{count} профілі</strong> з <strong>%{filename}</strong>.
many: Вы збіраецеся <strong>падпісацца</strong> на <strong>%{count} профіляў</strong> з <strong>%{filename}</strong>.
one: Вы збіраецеся <strong>падпісацца</strong> на <strong>%{count} профіль</strong> з <strong>%{filename}</strong>.
other: Вы збіраецеся <strong>падпісацца</strong> на <strong>%{count} профіляў</strong> з <strong>%{filename}</strong>.
lists_html:
few: Вы збіраецеся дадаць <strong>%{count} профілі</strong> з <strong>%{filename}</strong> у Вашыя <strong>спісы</strong>. Калі спісаў няма, то будуць створаны новыя.
many: Вы збіраецеся дадаць <strong>%{count} профіляў</strong> з <strong>%{filename}</strong> у Вашыя <strong>спісы</strong>. Калі спісаў няма, то будуць створаны новыя.
one: Вы збіраецеся дадаць <strong>%{count} профіль</strong> з <strong>%{filename}</strong> у Вашыя <strong>спісы</strong>. Калі спісаў няма, то будуць створаны новыя.
other: Вы збіраецеся дадаць <strong>%{count} профіляў</strong> з <strong>%{filename}</strong> у Вашыя <strong>спісы</strong>. Калі спісаў няма, то будуць створаны новыя.
muting_html:
few: Вы збіраецеся пачаць <strong>ігнараваць</strong> <strong>%{count} профілі</strong> з <strong>%{filename}</strong>.
many: Вы збіраецеся пачаць <strong>ігнараваць</strong> <strong>%{count} профіляў</strong> з <strong>%{filename}</strong>.
one: Вы збіраецеся пачаць <strong>ігнараваць</strong> <strong>%{count} профіль</strong> з <strong>%{filename}</strong>.
other: Вы збіраецеся пачаць <strong>ігнараваць</strong> <strong>%{count} профіляў</strong> з <strong>%{filename}</strong>.
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: Мы ўносім праўкі ў нашыя ўмовы карыстання, якія пачнуць дзейнічаць <strong>%{date}</strong>. Мы раім Вам азнаёміцца з абноўленымі ўмовамі.
past_preamble_html: Пасля Вашага апошняга наведвання мы ўнеслі праўкі ў нашыя ўмовы карыстання. Мы раім Вам азнаёміцца з абноўленымі ўмовамі.
review_link: Прачытаць умовы карыстання
title: На %{domain} змяняюцца ўмовы карыстання
themes:
contrast: Mastodon (высокі кантраст)
default: Mastodon (цёмная)
@@ -1927,6 +2087,10 @@ be:
recovery_instructions_html: Калі раптам вы страціце доступ да свайго тэлефона, вы можаце скарыстаць адзін з кодаў аднаўлення ніжэй каб аднавіць доступ да свайго ўліковага запісу. <strong>Захоўвайце іх у бяспечным месцы</strong>. Напрыклад, вы можаце раздрукаваць іх і захоўваць разам з іншымі важнымі дакументамі.
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}. Гэтыя абнаўленні ўступяць у сілу <strong>%{date}</strong>. Мы раім Вам цалкам азнаёміцца з <a href="%{path}" target="_blank">абноўленымі ўмовамі тут</a>.
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:

View File

@@ -60,6 +60,7 @@ be:
error:
title: Узнікла памылка
new:
prompt_html: "%{client_name} хоча атрымаць дазвол на доступ да Вашага профілю. <strong>Ухваляйце гэты запыт толькі калі Вы ведаеце гэту крыніцу і давяраеце ёй.</strong>"
review_permissions: Прагледзець дазволы
title: Патрабуецца аўтарызацыя
show:

View File

@@ -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 <strong>%{domain}</strong> 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

View File

@@ -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

View File

@@ -27,6 +27,8 @@ kab:
username: Tzemreḍ ad tesqedceḍ isekkilen, uṭṭunen akked yijerriden n wadda
featured_tag:
name: 'Ha-t-an kra seg ihacṭagen i tesseqdaceḍ ussan-a ineggura maḍi :'
form_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

View File

@@ -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

View File

@@ -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
@@ -1486,53 +1487,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,
@@ -1583,4 +1537,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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -6024,9 +6024,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