Merge remote-tracking branch 'parent/main' into upstream-20240117

This commit is contained in:
KMY 2025-01-17 16:29:11 +09:00
commit 5d79bd078c
150 changed files with 2982 additions and 1485 deletions

View file

@ -49,7 +49,7 @@ class ActivityPub::CollectionsController < ActivityPub::BaseController
def collection_presenter
ActivityPub::CollectionPresenter.new(
id: account_collection_url(@account, params[:id]),
id: ActivityPub::TagManager.instance.collection_uri_for(@account, params[:id]),
type: @type,
size: @size,
items: @items

View file

@ -41,12 +41,8 @@ class ActivityPub::OutboxesController < ActivityPub::BaseController
end
end
def outbox_url(**)
if params[:account_username].present?
account_outbox_url(@account, **)
else
instance_actor_outbox_url(**)
end
def outbox_url(...)
ActivityPub::TagManager.instance.outbox_uri_for(@account, ...)
end
def next_page

View file

@ -21,6 +21,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
endpoint: subscription_params[:endpoint],
key_p256dh: subscription_params[:keys][:p256dh],
key_auth: subscription_params[:keys][:auth],
standard: subscription_params[:standard] || false,
data: data_params,
user_id: current_user.id,
access_token_id: doorkeeper_token.id
@ -55,7 +56,7 @@ class Api::V1::Push::SubscriptionsController < Api::BaseController
end
def subscription_params
params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
params.require(:subscription).permit(:endpoint, :standard, keys: [:auth, :p256dh])
end
def data_params

View file

@ -80,10 +80,31 @@ class Api::V2::NotificationsController < Api::BaseController
return [] if @notifications.empty?
MastodonOTELTracer.in_span('Api::V2::NotificationsController#load_grouped_notifications') do
NotificationGroup.from_notifications(@notifications, pagination_range: (@notifications.last.id)..(@notifications.first.id), grouped_types: params[:grouped_types])
pagination_range = (@notifications.last.id)..@notifications.first.id
# If the page is incomplete, we know we are on the last page
if incomplete_page?
if paginating_up?
pagination_range = @notifications.last.id...(params[:max_id]&.to_i)
else
range_start = params[:since_id]&.to_i
range_start += 1 unless range_start.nil?
pagination_range = range_start..(@notifications.first.id)
end
end
NotificationGroup.from_notifications(@notifications, pagination_range: pagination_range, grouped_types: params[:grouped_types])
end
end
def incomplete_page?
@notifications.size < limit_param(DEFAULT_NOTIFICATIONS_LIMIT)
end
def paginating_up?
params[:min_id].present?
end
def browserable_account_notifications
current_account.notifications.without_suspended.browserable(
types: Array(browserable_params[:types]),

View file

@ -66,7 +66,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
end
def subscription_params
@subscription_params ||= params.require(:subscription).permit(:endpoint, keys: [:auth, :p256dh])
@subscription_params ||= params.require(:subscription).permit(:standard, :endpoint, keys: [:auth, :p256dh])
end
def web_push_subscription_params
@ -76,6 +76,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController
endpoint: subscription_params[:endpoint],
key_auth: subscription_params[:keys][:auth],
key_p256dh: subscription_params[:keys][:p256dh],
standard: subscription_params[:standard] || false,
user_id: active_session.user_id,
}
end

View file

@ -18,7 +18,7 @@ class Auth::SetupController < ApplicationController
if @user.update(user_params)
@user.resend_confirmation_instructions unless @user.confirmed?
redirect_to auth_setup_path, notice: I18n.t('auth.setup.new_confirmation_instructions_sent')
redirect_to auth_setup_path, notice: t('auth.setup.new_confirmation_instructions_sent')
else
render :show
end

View file

@ -46,7 +46,7 @@ class FollowerAccountsController < ApplicationController
end
def page_url(page)
account_followers_url(@account, page: page) unless page.nil?
ActivityPub::TagManager.instance.followers_uri_for(@account, page: page) unless page.nil?
end
def next_page_url

View file

@ -63,7 +63,9 @@ module ThemeHelper
end
def cached_custom_css_digest
Rails.cache.read(:setting_digest_custom_css)
Rails.cache.fetch(:setting_digest_custom_css) do
Setting.custom_css&.then { |content| Digest::SHA256.hexdigest(content) }
end
end
def theme_color_for(theme)

View file

@ -33,7 +33,7 @@ const unsubscribe = ({ registration, subscription }) =>
subscription ? subscription.unsubscribe().then(() => registration) : registration;
const sendSubscriptionToBackend = (subscription) => {
const params = { subscription };
const params = { subscription: { ...subscription.toJSON(), standard: true } };
if (me) {
const data = pushNotificationsSetting.get(me);

View file

@ -45,7 +45,7 @@ export interface BaseApiAccountJSON {
avatar_static: string;
bot: boolean;
created_at: string;
discoverable: boolean;
discoverable?: boolean;
indexable: boolean;
display_name: string;
emojis: ApiCustomEmojiJSON[];

View file

@ -335,15 +335,29 @@ class Announcement extends ImmutablePureComponent {
const endsAt = announcement.get('ends_at') && new Date(announcement.get('ends_at'));
const now = new Date();
const hasTimeRange = startsAt && endsAt;
const skipYear = hasTimeRange && startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
const skipEndDate = hasTimeRange && startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
const skipTime = announcement.get('all_day');
let timestamp = null;
if (hasTimeRange) {
const skipYear = startsAt.getFullYear() === endsAt.getFullYear() && endsAt.getFullYear() === now.getFullYear();
const skipEndDate = startsAt.getDate() === endsAt.getDate() && startsAt.getMonth() === endsAt.getMonth() && startsAt.getFullYear() === endsAt.getFullYear();
timestamp = (
<>
<FormattedDate value={startsAt} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} />
</>
);
} else {
const publishedAt = new Date(announcement.get('published_at'));
timestamp = (
<FormattedDate value={publishedAt} year={publishedAt.getFullYear() === now.getFullYear() ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} />
);
}
return (
<div className='announcements__item'>
<strong className='announcements__item__range'>
<FormattedMessage id='announcement.announcement' defaultMessage='Announcement' />
{hasTimeRange && <span> · <FormattedDate value={startsAt} year={(skipYear || startsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month='short' day='2-digit' hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /> - <FormattedDate value={endsAt} year={(skipYear || endsAt.getFullYear() === now.getFullYear()) ? undefined : 'numeric'} month={skipEndDate ? undefined : 'short'} day={skipEndDate ? undefined : '2-digit'} hour={skipTime ? undefined : '2-digit'} minute={skipTime ? undefined : '2-digit'} /></span>}
<span> · {timestamp}</span>
</strong>
<Content announcement={announcement} />

View file

@ -12,6 +12,7 @@ import AddPhotoAlternateIcon from '@/material-icons/400-24px/add_photo_alternate
import EditIcon from '@/material-icons/400-24px/edit.svg?react';
import PersonIcon from '@/material-icons/400-24px/person.svg?react';
import { updateAccount } from 'mastodon/actions/accounts';
import { closeOnboarding } from 'mastodon/actions/onboarding';
import { Button } from 'mastodon/components/button';
import { Column } from 'mastodon/components/column';
import { ColumnHeader } from 'mastodon/components/column_header';
@ -58,7 +59,9 @@ export const Profile: React.FC<{
);
const [avatar, setAvatar] = useState<File>();
const [header, setHeader] = useState<File>();
const [discoverable, setDiscoverable] = useState(true);
const [discoverable, setDiscoverable] = useState(
account?.discoverable ?? true,
);
const [isSaving, setIsSaving] = useState(false);
const [errors, setErrors] = useState<ApiAccountErrors>();
const avatarFileRef = createRef<HTMLInputElement>();
@ -132,6 +135,7 @@ export const Profile: React.FC<{
)
.then(() => {
history.push('/start/follows');
dispatch(closeOnboarding());
return '';
})
// eslint-disable-next-line @typescript-eslint/use-unknown-in-catch-callback-variable

View file

@ -110,6 +110,7 @@ const mapStateToProps = state => ({
hasMediaAttachments: state.getIn(['compose', 'media_attachments']).size > 0,
canUploadMore: !state.getIn(['compose', 'media_attachments']).some(x => ['audio', 'video'].includes(x.get('type'))) && state.getIn(['compose', 'media_attachments']).size < state.getIn(['server', 'server', 'configuration', 'statuses', 'max_media_attachments']),
firstLaunch: state.getIn(['settings', 'introductionVersion'], 0) < INTRODUCTION_VERSION,
newAccount: !state.getIn(['accounts', me, 'note']) && !state.getIn(['accounts', me, 'bot']) && state.getIn(['accounts', me, 'following_count'], 0) === 0 && state.getIn(['accounts', me, 'statuses_count'], 0) === 0,
username: state.getIn(['accounts', me, 'username']),
});
@ -154,6 +155,7 @@ class SwitchingColumnsArea extends PureComponent {
children: PropTypes.node,
location: PropTypes.object,
singleColumn: PropTypes.bool,
forceOnboarding: PropTypes.bool,
};
UNSAFE_componentWillMount () {
@ -184,14 +186,16 @@ class SwitchingColumnsArea extends PureComponent {
};
render () {
const { children, singleColumn } = this.props;
const { children, singleColumn, forceOnboarding } = this.props;
const { signedIn } = this.props.identity;
const pathName = this.props.location.pathname;
let redirect;
if (signedIn) {
if (singleColumn) {
if (forceOnboarding) {
redirect = <Redirect from='/' to='/start' exact />;
} else if (singleColumn) {
redirect = <Redirect from='/' to='/home' exact />;
} else {
redirect = <Redirect from='/' to='/deck/getting-started' exact />;
@ -320,6 +324,7 @@ class UI extends PureComponent {
intl: PropTypes.object.isRequired,
layout: PropTypes.string.isRequired,
firstLaunch: PropTypes.bool,
newAccount: PropTypes.bool,
username: PropTypes.string,
...WithRouterPropTypes,
};
@ -616,7 +621,7 @@ class UI extends PureComponent {
render () {
const { draggingOver } = this.state;
const { children, isComposing, location, layout } = this.props;
const { children, isComposing, location, layout, firstLaunch, newAccount } = this.props;
const handlers = {
help: this.handleHotkeyToggleHelp,
@ -646,7 +651,7 @@ class UI extends PureComponent {
<div className={classNames('ui', { 'is-composing': isComposing })} ref={this.setRef}>
<Header />
<SwitchingColumnsArea identity={this.props.identity} location={location} singleColumn={layout === 'mobile' || layout === 'single-column'}>
<SwitchingColumnsArea identity={this.props.identity} location={location} singleColumn={layout === 'mobile' || layout === 'single-column'} forceOnboarding={firstLaunch && newAccount}>
{children}
</SwitchingColumnsArea>

View file

@ -104,10 +104,11 @@
"annual_report.summary.most_used_hashtag.none": "Žádné",
"annual_report.summary.new_posts.new_posts": "nové příspěvky",
"annual_report.summary.percentile.text": "<topLabel>To vás umisťuje do vrcholu</topLabel><percentage></percentage><bottomLabel>{domain} uživatelů.</bottomLabel>",
"annual_report.summary.percentile.we_wont_tell_bernie": "To, že jste zdejší smetánka zůstane mezi námi ;).",
"annual_report.summary.thanks": "Děkujeme, že jste součástí Mastodonu!",
"attachments_list.unprocessed": "(nezpracováno)",
"audio.hide": "Skrýt zvuk",
"block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou stále být viditelné pro nepřihlášené uživatele.",
"block_modal.remote_users_caveat": "Požádáme server {domain}, aby respektoval vaše rozhodnutí. Úplné dodržování nastavení však není zaručeno, protože některé servery mohou řešit blokování různě. Veřejné příspěvky mohou být stále viditelné pro nepřihlášené uživatele.",
"block_modal.show_less": "Zobrazit méně",
"block_modal.show_more": "Zobrazit více",
"block_modal.they_cant_mention": "Nemůže vás zmiňovat ani sledovat.",
@ -180,6 +181,7 @@
"compose_form.poll.duration": "Doba trvání ankety",
"compose_form.poll.multiple": "Výběr z více možností",
"compose_form.poll.option_placeholder": "Volba {number}",
"compose_form.poll.single": "Jediná volba",
"compose_form.poll.switch_to_multiple": "Povolit u ankety výběr více voleb",
"compose_form.poll.switch_to_single": "Povolit u ankety výběr pouze jedné volby",
"compose_form.poll.type": "Styl",
@ -204,6 +206,7 @@
"confirmations.edit.message": "Editovat teď znamená přepsání zprávy, kterou právě tvoříte. Opravdu chcete pokračovat?",
"confirmations.edit.title": "Přepsat příspěvek?",
"confirmations.follow_to_list.confirm": "Sledovat a přidat do seznamu",
"confirmations.follow_to_list.message": "Musíte {name} sledovat, abyste je přidali do seznamu.",
"confirmations.follow_to_list.title": "Sledovat uživatele?",
"confirmations.logout.confirm": "Odhlásit se",
"confirmations.logout.message": "Opravdu se chcete odhlásit?",
@ -236,19 +239,25 @@
"disabled_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován.",
"dismissable_banner.community_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí, jejichž účty hostuje {domain}.",
"dismissable_banner.dismiss": "Zavřít",
"dismissable_banner.explore_links": "Tyto zprávy jsou dnes nejvíce sdíleny ve fediversu. Novější novinky publikované více různými lidmi jsou v pořadí vyšší.",
"dismissable_banner.explore_statuses": "Tyto příspěvky napříč fediversem dnes získávají na popularitě. Novější příspěvky s více boosty a oblíbenými jsou výše v pořadí.",
"dismissable_banner.explore_tags": "Tyto hashtagy dnes na fediversu získávají na popularitě. Hashtagy, které používá více různých lidí, jsou řazeny výše.",
"dismissable_banner.public_timeline": "Toto jsou nejnovější veřejné příspěvky od lidí na fediversu, které lidé na {domain} sledují.",
"domain_block_modal.block": "Blokovat server",
"domain_block_modal.block_account_instead": "Raději blokovat @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Lidé z tohoto serveru mohou interagovat s vašimi starými příspěvky.",
"domain_block_modal.they_cant_follow": "Nikdo z tohoto serveru vás nemůže sledovat.",
"domain_block_modal.they_wont_know": "Nebude vědět, že je zablokován*a.",
"domain_block_modal.title": "Blokovat doménu?",
"domain_block_modal.you_will_lose_num_followers": "Ztratíte {followersCount, plural, one {{followersCountDisplay} sledujícího} few {{followersCountDisplay} sledující} many {{followersCountDisplay} sledujících} other {{followersCountDisplay} sledujících}} a {followingCount, plural, one {{followingCountDisplay} sledovaného} few {{followingCountDisplay} sledované} many {{followingCountDisplay} sledovaných} other {{followingCountDisplay} sledovaných}}.",
"domain_block_modal.you_will_lose_relationships": "Ztratíte všechny sledující a lidi, které sledujete z tohoto serveru.",
"domain_block_modal.you_wont_see_posts": "Neuvidíte příspěvky ani upozornění od uživatelů z tohoto serveru.",
"domain_pill.activitypub_lets_connect": "Umožňuje vám spojit se a komunikovat s lidmi nejen na Mastodonu, ale i s dalšími sociálními aplikacemi.",
"domain_pill.activitypub_like_language": "ActivityPub je jako jazyk, kterým Mastodon mluví s jinými sociálními sítěmi.",
"domain_pill.server": "Server",
"domain_pill.their_handle": "Handle:",
"domain_pill.their_server": "Digitální domov, kde žijí všechny příspěvky.",
"domain_pill.their_username": "Jedinečný identikátor na serveru. Je možné najít uživatele se stejným uživatelským jménem na různých serverech.",
"domain_pill.their_server": "Jejich digitální domov, kde žijí jejich všechny příspěvky.",
"domain_pill.their_username": "Jejich jedinečný identikátor na jejich serveru. Je možné najít uživatele se stejným uživatelským jménem na jiných serverech.",
"domain_pill.username": "Uživatelské jméno",
"domain_pill.whats_in_a_handle": "Co obsahuje handle?",
"domain_pill.who_they_are": "Protože handle říkají kdo je kdo a také kde, je možné interagovat s lidmi napříč sociálními weby <button>platforem postavených na ActivityPub</button>.",
@ -322,6 +331,7 @@
"filter_modal.select_filter.title": "Filtrovat tento příspěvek",
"filter_modal.title.status": "Filtrovat příspěvek",
"filter_warning.matches_filter": "Odpovídá filtru “<span>{title}</span>”",
"filtered_notifications_banner.pending_requests": "Od {count, plural, =0 {nikoho, koho možná znáte} one {člověka, kterého možná znáte} few {#, které možná znáte} many {#, které možná znáte} other {#, které možná znáte}}",
"filtered_notifications_banner.title": "Filtrovaná oznámení",
"firehose.all": "Vše",
"firehose.local": "Tento server",
@ -329,14 +339,14 @@
"follow_request.authorize": "Autorizovat",
"follow_request.reject": "Zamítnout",
"follow_requests.unlocked_explanation": "Přestože váš účet není uzamčen, personál {domain} usoudil, že byste mohli chtít tyto požadavky na sledování zkontrolovat ručně.",
"follow_suggestions.curated_suggestion": "Výběr personálů",
"follow_suggestions.curated_suggestion": "Výběr personálu",
"follow_suggestions.dismiss": "Znovu nezobrazovat",
"follow_suggestions.featured_longer": "Ručně vybráno týmem {domain}",
"follow_suggestions.friends_of_friends_longer": "Populární mezi lidmi, které sledujete",
"follow_suggestions.hints.featured": "Tento profil byl ručně vybrán týmem {domain}.",
"follow_suggestions.hints.friends_of_friends": "Tento profil je populární mezi lidmi, které sledujete.",
"follow_suggestions.hints.most_followed": "Tento profil je jedním z nejvíce sledovaných na {domain}.",
"follow_suggestions.hints.most_interactions": "Tento profil nedávno dostalo velkou pozornost na {domain}.",
"follow_suggestions.hints.most_followed": "Tento profil je jedním z nejsledovanějších na {domain}.",
"follow_suggestions.hints.most_interactions": "Tomuto profilu se nedávno dostalo velké pozornosti na {domain}.",
"follow_suggestions.hints.similar_to_recently_followed": "Tento profil je podobný profilům, které jste nedávno sledovali.",
"follow_suggestions.personalized_suggestion": "Přizpůsobený návrh",
"follow_suggestions.popular_suggestion": "Populární návrh",
@ -355,6 +365,7 @@
"footer.terms_of_service": "Obchodní podmínky",
"generic.saved": "Uloženo",
"getting_started.heading": "Začínáme",
"hashtag.admin_moderation": "Otevřít moderátorské rozhraní pro #{name}",
"hashtag.column_header.tag_mode.all": "a {additional}",
"hashtag.column_header.tag_mode.any": "nebo {additional}",
"hashtag.column_header.tag_mode.none": "bez {additional}",
@ -370,9 +381,13 @@
"hashtag.follow": "Sledovat hashtag",
"hashtag.unfollow": "Přestat sledovat hashtag",
"hashtags.and_other": "…a {count, plural, one {# další} few {# další} other {# dalších}}",
"hints.profiles.followers_may_be_missing": "Sledující mohou pro tento profil chybět.",
"hints.profiles.follows_may_be_missing": "Sledování mohou pro tento profil chybět.",
"hints.profiles.posts_may_be_missing": "Některé příspěvky z tohoto profilu mohou chybět.",
"hints.profiles.see_more_followers": "Zobrazit více sledujících na {domain}",
"hints.profiles.see_more_follows": "Zobrazit další sledování na {domain}",
"hints.profiles.see_more_posts": "Zobrazit další příspěvky na {domain}",
"hints.threads.replies_may_be_missing": "Odpovědi z jiných serverů mohou chybět.",
"hints.threads.see_more": "Zobrazit další odpovědi na {domain}",
"home.column_settings.show_reblogs": "Zobrazit boosty",
"home.column_settings.show_replies": "Zobrazit odpovědi",
@ -381,7 +396,22 @@
"home.pending_critical_update.link": "Zobrazit aktualizace",
"home.pending_critical_update.title": "K dispozici je kritická bezpečnostní aktualizace!",
"home.show_announcements": "Zobrazit oznámení",
"ignore_notifications_modal.disclaimer": "Mastodon nemůže informovat uživatele, že jste ignorovali jejich oznámení. Ignorování oznámení nezabrání odesílání zpráv samotných.",
"ignore_notifications_modal.filter_instead": "Místo toho filtrovat",
"ignore_notifications_modal.filter_to_act_users": "Stále budete moci přijmout, odmítnout nebo nahlásit uživatele",
"ignore_notifications_modal.filter_to_avoid_confusion": "Filtrování pomáhá vyhnout se možným nejasnostem",
"ignore_notifications_modal.filter_to_review_separately": "Filtrovaná oznámení můžete zkontrolovat samostatně",
"ignore_notifications_modal.ignore": "Ignorovat oznámení",
"ignore_notifications_modal.limited_accounts_title": "Ignorovat oznámení z moderovaných účtů?",
"ignore_notifications_modal.new_accounts_title": "Ignorovat oznámení z nových účtů?",
"ignore_notifications_modal.not_followers_title": "Ignorovat oznámení od lidí, kteří vás nesledují?",
"ignore_notifications_modal.not_following_title": "Ignorovat oznámení od lidí, které nesledujete?",
"ignore_notifications_modal.private_mentions_title": "Ignorovat oznámení z nevyžádaných soukromých zmínek?",
"interaction_modal.action.favourite": "Chcete-li pokračovat, musíte oblíbit z vašeho účtu.",
"interaction_modal.action.follow": "Chcete-li pokračovat, musíte sledovat z vašeho účtu.",
"interaction_modal.action.reblog": "Chcete-li pokračovat, musíte dát boost z vašeho účtu.",
"interaction_modal.action.reply": "Chcete-li pokračovat, musíte odpovědět z vašeho účtu.",
"interaction_modal.action.vote": "Chcete-li pokračovat, musíte hlasovat z vašeho účtu.",
"interaction_modal.go": "Přejít",
"interaction_modal.no_account_yet": "Ještě nemáte účet?",
"interaction_modal.on_another_server": "Na jiném serveru",
@ -433,20 +463,27 @@
"lightbox.close": "Zavřít",
"lightbox.next": "Další",
"lightbox.previous": "Předchozí",
"lightbox.zoom_in": "Přiblížit na skutečnou velikost",
"lightbox.zoom_out": "Přizpůsobit velikost",
"limited_account_hint.action": "Přesto profil zobrazit",
"limited_account_hint.title": "Tento profil byl skryt moderátory {domain}.",
"link_preview.author": "Podle {name}",
"link_preview.more_from_author": "Více od {name}",
"link_preview.shares": "{count, plural, one {{counter} příspěvek} few {{counter} příspěvky} many {{counter} příspěvků} other {{counter} příspěvků}}",
"lists.add_member": "Přidat",
"lists.add_to_list": "Přidat do seznamu",
"lists.add_to_lists": "Přidat {name} do seznamů",
"lists.create": "Vytvořit",
"lists.create_a_list_to_organize": "Vytvořte nový seznam pro organizaci vašeho domovského kanálu",
"lists.create_list": "Vytvořit seznam",
"lists.delete": "Smazat seznam",
"lists.done": "Hotovo",
"lists.edit": "Upravit seznam",
"lists.exclusive": "Skrýt členy na domovském kanálu",
"lists.exclusive_hint": "Pokud je někdo na tomto seznamu, skryjte jej ve vašem domovském kanálu, abyste se vyhnuli dvojímu vidění jejich příspěvků.",
"lists.find_users_to_add": "Najít uživatele, které chcete přidat",
"lists.list_members": "Členové seznamu",
"lists.list_members_count": "{count, plural, one {# člen} few {# členové} many {# členů} other {# členů}}",
"lists.list_name": "Název seznamu",
"lists.new_list_name": "Název nového seznamu",
"lists.no_lists_yet": "Zatím žádné seznamy.",
@ -458,6 +495,7 @@
"lists.replies_policy.none": "Nikomu",
"lists.save": "Uložit",
"lists.search": "Hledat",
"lists.show_replies_to": "Zahrnout odpovědi od členů seznamu pro",
"load_pending": "{count, plural, one {# nová položka} few {# nové položky} many {# nových položek} other {# nových položek}}",
"loading_indicator.label": "Načítání…",
"media_gallery.hide": "Skrýt",
@ -500,14 +538,25 @@
"navigation_bar.security": "Zabezpečení",
"not_signed_in_indicator.not_signed_in": "Pro přístup k tomuto zdroji se musíte přihlásit.",
"notification.admin.report": "Uživatel {name} nahlásil {target}",
"notification.admin.report_account": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target} za {category}",
"notification.admin.report_account_other": "{name} nahlásil {count, plural, one {jeden příspěvek} few {# příspěvky} many {# příspěvků} other {# příspěvků}} od {target}",
"notification.admin.report_statuses": "{name} nahlásil {target} za {category}",
"notification.admin.report_statuses_other": "{name} nahlásil {target}",
"notification.admin.sign_up": "Uživatel {name} se zaregistroval",
"notification.admin.sign_up.name_and_others": "{name} a {count, plural, one {# další} few {# další} many {# dalších} other {# dalších}} se zaregistrovali",
"notification.annual_report.message": "Váš #Wrapstodon {year} na Vás čeká! Podívejte se, jak vypadal tento Váš rok na Mastodonu!",
"notification.annual_report.view": "Zobrazit #Wrapstodon",
"notification.favourite": "Uživatel {name} si oblíbil váš příspěvek",
"notification.favourite.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Váš příspěvek",
"notification.favourite_pm": "{name} si oblíbil vaši soukromou zmínku",
"notification.favourite_pm.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> si oblíbil} few {<a># další</a> si oblíbili} other {<a># dalších</a> si oblíbilo}} Vaši soukromou zmínku",
"notification.follow": "Uživatel {name} vás začal sledovat",
"notification.follow.name_and_others": "{name} a {count, plural, one {<a># další</a> Vás začal sledovat} few {<a># další</a> Vás začali sledovat} other {<a># dalších</a> Vás začalo sledovat}}",
"notification.follow_request": "Uživatel {name} požádal o povolení vás sledovat",
"notification.follow_request.name_and_others": "{name} a {count, plural, one {# další Vám poslal žádost o sledování} few {# další Vám poslali žádost o sledování} other {# dalších Vám poslalo žádost o sledování}}",
"notification.label.mention": "Zmínka",
"notification.label.private_mention": "Soukromá zmínka",
"notification.label.private_reply": "Privátní odpověď",
"notification.label.reply": "Odpověď",
"notification.mention": "Zmínka",
"notification.mentioned_you": "{name} vás zmínil",
@ -523,6 +572,7 @@
"notification.own_poll": "Vaše anketa skončila",
"notification.poll": "Anketa, ve které jste hlasovali, skončila",
"notification.reblog": "Uživatel {name} boostnul váš příspěvek",
"notification.reblog.name_and_others_with_link": "{name} a {count, plural, one {<a># další</a> boostnul} few {<a># další</a> boostnuli} other {<a># dalších</a> boostnulo}} Váš příspěvek",
"notification.relationships_severance_event": "Kontakt ztracen s {name}",
"notification.relationships_severance_event.account_suspension": "Administrátor z {from} pozastavil {target}, což znamená, že již od nich nemůžete přijímat aktualizace nebo s nimi interagovat.",
"notification.relationships_severance_event.domain_block": "Administrátor z {from} pozastavil {target}, včetně {followersCount} z vašich sledujících a {followingCount, plural, one {# účet, který sledujete} few {# účty, které sledujete} many {# účtů, které sledujete} other {# účtů, které sledujete}}.",
@ -531,10 +581,19 @@
"notification.status": "Uživatel {name} právě přidal příspěvek",
"notification.update": "Uživatel {name} upravil příspěvek",
"notification_requests.accept": "Přijmout",
"notification_requests.accept_multiple": "{count, plural, one {Schválit # požadavek…} few {Schválit # požadavky…} other {Schválit # požadavků…}}",
"notification_requests.confirm_accept_multiple.button": "{count, plural, one {Schválit požadavek} other {Schválit požadavky}}",
"notification_requests.confirm_accept_multiple.message": "Chystáte se schválit {count, plural, one {jeden požadavek} few {# požadavky} other {# požadavků}} na oznámení. Opravdu chcete pokračovat?",
"notification_requests.confirm_accept_multiple.title": "Přijmout žádosti o oznámení?",
"notification_requests.confirm_dismiss_multiple.button": "{count, plural, one {Zamítnout požadavek} other {Zamítnout požadavky}}",
"notification_requests.confirm_dismiss_multiple.message": "Chystáte se zamítnout {count, plural, one {jeden požadavek} few {# požadavky} many {# požadavků} other {# požadavků}} na oznámení. Poté k {count, plural, one {němu} other {něm}} již nebudete mít snadný přístup. Opravdu chcete pokračovat?",
"notification_requests.confirm_dismiss_multiple.title": "Zamítnout požadavky na oznámení?",
"notification_requests.dismiss": "Zamítnout",
"notification_requests.dismiss_multiple": "Zamítnout {count, plural, one {# požadavek} few {# požadavky} many {# požadavků} other {# požadavků}}…",
"notification_requests.edit_selection": "Upravit",
"notification_requests.exit_selection": "Hotovo",
"notification_requests.explainer_for_limited_account": "Oznámení z tohoto účtu byla filtrována, protože tento účet byl omezen moderátorem.",
"notification_requests.explainer_for_limited_remote_account": "Oznámení z tohoto účtu byla filtrována, protože tento účet nebo jeho server byl omezen moderátorem.",
"notification_requests.maximize": "Maximalizovat",
"notification_requests.minimize_banner": "Minimalizovat banner filtrovaných oznámení",
"notification_requests.notifications_from": "Oznámení od {name}",
@ -578,6 +637,7 @@
"notifications.policy.accept": "Přijmout",
"notifications.policy.accept_hint": "Zobrazit v oznámeních",
"notifications.policy.drop": "Ignorovat",
"notifications.policy.drop_hint": "Permanentně odstranit, aby již nikdy nespatřil světlo světa",
"notifications.policy.filter": "Filtrovat",
"notifications.policy.filter_hint": "Odeslat do filtrované schránky oznámení",
"notifications.policy.filter_limited_accounts_hint": "Omezeno moderátory serveru",

View file

@ -407,6 +407,13 @@
"ignore_notifications_modal.not_followers_title": "An dtugann tú aird ar fhógraí ó dhaoine nach leanann tú?",
"ignore_notifications_modal.not_following_title": "An ndéanann tú neamhaird de fhógraí ó dhaoine nach leanann tú?",
"ignore_notifications_modal.private_mentions_title": "An dtugann tú aird ar fhógraí ó Luaintí Príobháideacha gan iarraidh?",
"interaction_modal.action.favourite": "Chun leanúint ar aghaidh, ní mór duit an ceann is fearr leat ó do chuntas.",
"interaction_modal.action.follow": "Chun leanúint ar aghaidh, ní mór duit leanúint ó do chuntas.",
"interaction_modal.action.reblog": "Chun leanúint ar aghaidh, ní mór duit athbhlagáil ó do chuntas.",
"interaction_modal.action.reply": "Chun leanúint ar aghaidh, ní mór duit freagra a thabhairt ó do chuntas.",
"interaction_modal.action.vote": "Chun leanúint ar aghaidh, ní mór duit vótáil ó do chuntas.",
"interaction_modal.go": "Téigh",
"interaction_modal.no_account_yet": "Níl cuntas agat fós?",
"interaction_modal.on_another_server": "Ar freastalaí eile",
"interaction_modal.on_this_server": "Ar an freastalaí seo",
"interaction_modal.title.favourite": "An postáil {name} is fearr leat",
@ -414,6 +421,7 @@
"interaction_modal.title.reblog": "Mol postáil de chuid {name}",
"interaction_modal.title.reply": "Freagair postáil {name}",
"interaction_modal.title.vote": "Vótáil i vótaíocht {name}",
"interaction_modal.username_prompt": "M.sh. {example}",
"intervals.full.days": "{number, plural, one {# lá} other {# lá}}",
"intervals.full.hours": "{number, plural, one {# uair} other {# uair}}",
"intervals.full.minutes": "{number, plural, one {# nóiméad} other {# nóiméad}}",
@ -449,6 +457,7 @@
"keyboard_shortcuts.toggle_hidden": "Taispeáin/folaigh an téacs taobh thiar de CW",
"keyboard_shortcuts.toggle_sensitivity": "Taispeáin / cuir i bhfolach meáin",
"keyboard_shortcuts.toot": "Cuir tús le postáil nua",
"keyboard_shortcuts.translate": "post a aistriú",
"keyboard_shortcuts.unfocus": "Unfocus cum textarea/search",
"keyboard_shortcuts.up": "Bog suas ar an liosta",
"lightbox.close": "Dún",
@ -687,6 +696,8 @@
"privacy_policy.title": "Polasaí príobháideachais",
"recommended": "Molta",
"refresh": "Athnuaigh",
"regeneration_indicator.please_stand_by": "Fan i do sheasamh, le do thoil.",
"regeneration_indicator.preparing_your_home_feed": "Ag ullmhú do bheatha baile…",
"relative_time.days": "{number}l",
"relative_time.full.days": "{number, plural, one {# lá} other {# lá}} ó shin",
"relative_time.full.hours": "{number, plural, one {# uair} other {# uair}} ó shin",
@ -826,6 +837,7 @@
"status.reblogs.empty": "Níor mhol éinne an phostáil seo fós. Nuair a mholfaidh duine éigin í, taispeánfar anseo é sin.",
"status.redraft": "Scrios ⁊ athdhréachtaigh",
"status.remove_bookmark": "Bain leabharmharc",
"status.remove_favourite": "Bain ó cheanáin",
"status.replied_in_thread": "D'fhreagair sa snáithe",
"status.replied_to": "D'fhreagair {name}",
"status.reply": "Freagair",

View file

@ -453,10 +453,11 @@
"keyboard_shortcuts.requests": "Követési kérések listájának megnyitása",
"keyboard_shortcuts.search": "Fókuszálás a keresősávra",
"keyboard_shortcuts.spoilers": "Tartalmi figyelmeztetés mező megjelenítése/elrejtése",
"keyboard_shortcuts.start": "\"Első lépések\" oszlop megnyitása",
"keyboard_shortcuts.start": "„Első lépések” oszlop megnyitása",
"keyboard_shortcuts.toggle_hidden": "Tartalmi figyelmeztetéssel ellátott szöveg megjelenítése/elrejtése",
"keyboard_shortcuts.toggle_sensitivity": "Média megjelenítése/elrejtése",
"keyboard_shortcuts.toot": "Új bejegyzés írása",
"keyboard_shortcuts.translate": "Bejegyzés lefordítása",
"keyboard_shortcuts.unfocus": "Szerkesztés/keresés fókuszból való kivétele",
"keyboard_shortcuts.up": "Mozgás felfelé a listában",
"lightbox.close": "Bezárás",

View file

@ -457,6 +457,7 @@
"keyboard_shortcuts.toggle_hidden": "Monstrar/celar texto detra advertimento de contento",
"keyboard_shortcuts.toggle_sensitivity": "Monstrar/celar multimedia",
"keyboard_shortcuts.toot": "Initiar un nove message",
"keyboard_shortcuts.translate": "a traducer un message",
"keyboard_shortcuts.unfocus": "Disfocalisar le area de composition de texto/de recerca",
"keyboard_shortcuts.up": "Displaciar in alto in le lista",
"lightbox.close": "Clauder",
@ -802,7 +803,7 @@
"status.bookmark": "Adder al marcapaginas",
"status.cancel_reblog_private": "Disfacer impulso",
"status.cannot_reblog": "Iste message non pote esser impulsate",
"status.continued_thread": "Discussion continuate",
"status.continued_thread": "Continuation del discussion",
"status.copy": "Copiar ligamine a message",
"status.delete": "Deler",
"status.detailed_status": "Vista detaliate del conversation",
@ -836,6 +837,7 @@
"status.reblogs.empty": "Necuno ha ancora impulsate iste message. Quando alcuno lo face, le impulsos apparera hic.",
"status.redraft": "Deler e reconciper",
"status.remove_bookmark": "Remover marcapagina",
"status.remove_favourite": "Remover del favoritos",
"status.replied_in_thread": "Respondite in le discussion",
"status.replied_to": "Respondite a {name}",
"status.reply": "Responder",

View file

@ -431,11 +431,11 @@
"keyboard_shortcuts.column": "Focalizza alla colonna",
"keyboard_shortcuts.compose": "Focalizza l'area di composizione testuale",
"keyboard_shortcuts.description": "Descrizione",
"keyboard_shortcuts.direct": "per aprire la colonna menzioni private",
"keyboard_shortcuts.direct": "Apre la colonna \"menzioni private\"",
"keyboard_shortcuts.down": "Scorri in basso nell'elenco",
"keyboard_shortcuts.enter": "Apre il post",
"keyboard_shortcuts.favourite": "Contrassegna il post come preferito",
"keyboard_shortcuts.favourites": "Apri l'elenco dei preferiti",
"keyboard_shortcuts.favourites": "Apre l'elenco dei preferiti",
"keyboard_shortcuts.federated": "Apre la cronologia federata",
"keyboard_shortcuts.heading": "Scorciatoie da tastiera",
"keyboard_shortcuts.home": "Apre la cronologia domestica",
@ -457,6 +457,7 @@
"keyboard_shortcuts.toggle_hidden": "Mostra/Nasconde il testo dietro CW",
"keyboard_shortcuts.toggle_sensitivity": "Mostra/Nasconde media",
"keyboard_shortcuts.toot": "Crea un nuovo post",
"keyboard_shortcuts.translate": "Traduce un post",
"keyboard_shortcuts.unfocus": "Rimuove il focus sull'area di composizione testuale/ricerca",
"keyboard_shortcuts.up": "Scorre in su nell'elenco",
"lightbox.close": "Chiudi",

View file

@ -533,6 +533,13 @@
"ignore_notifications_modal.not_followers_title": "本当に「フォローされていないアカウントからの通知」を無視するようにしますか?",
"ignore_notifications_modal.not_following_title": "本当に「フォローしていないアカウントからの通知」を無視するようにしますか?",
"ignore_notifications_modal.private_mentions_title": "本当に「外部からの非公開の返信」を無視するようにしますか?",
"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": "別のサーバー",
"interaction_modal.on_this_server": "このサーバー",
"interaction_modal.title.favourite": "{name}さんの投稿をお気に入り登録",
@ -540,6 +547,7 @@
"interaction_modal.title.reblog": "{name}さんの投稿をブースト",
"interaction_modal.title.reply": "{name}さんの投稿にリプライ",
"interaction_modal.title.vote": "{name}さんのアンケートに投票",
"interaction_modal.username_prompt": "例: {example}",
"intervals.full.days": "{number}日",
"intervals.full.hours": "{number}時間",
"intervals.full.minutes": "{number}分",

View file

@ -345,7 +345,7 @@
"hints.profiles.see_more_followers": "Skatīt vairāk sekotāju {domain}",
"hints.profiles.see_more_follows": "Skatīt vairāk sekojumu {domain}",
"hints.profiles.see_more_posts": "Skatīt vairāk ierakstu {domain}",
"hints.threads.replies_may_be_missing": "Var trūkt atbildes no citiem serveriem.",
"hints.threads.replies_may_be_missing": "Var trūkt atbilžu no citiem serveriem.",
"hints.threads.see_more": "Skatīt vairāk atbilžu {domain}",
"home.column_settings.show_reblogs": "Rādīt pastiprinātos ierakstus",
"home.column_settings.show_replies": "Rādīt atbildes",

View file

@ -126,9 +126,35 @@
"bundle_column_error.network.title": "網路錯誤",
"bundle_column_error.retry": "Koh試",
"bundle_column_error.return": "Tńg去頭頁",
"bundle_column_error.routing.body": "Tshuē bô所要求ê頁面。Lí kám確定地址liâu-á ê URL正確",
"bundle_column_error.routing.title": "404",
"bundle_modal_error.close": "關",
"bundle_modal_error.message": "Tī載入tsit ê畫面ê時起錯誤。",
"bundle_modal_error.retry": "Koh試",
"column.create_list": "建立列單",
"column.direct": "私人ê提起",
"column.directory": "瀏覽個人資料",
"column.domain_blocks": "封鎖ê域名",
"column.edit_list": "編輯列單",
"column.favourites": "Siōng kah意",
"column.firehose": "Tsit-má ê動態",
"column.follow_requests": "跟tuè請求",
"column.home": "頭頁",
"column_header.pin": "釘",
"column_header.show_settings": "顯示設定",
"column_header.unpin": "Pak掉",
"column_search.cancel": "取消",
"column_subheading.settings": "設定",
"community.column_settings.local_only": "Kan-ta展示本地ê",
"community.column_settings.media_only": "Kan-ta展示媒體",
"community.column_settings.remote_only": "Kan-ta展示遠距離ê",
"compose.language.change": "換語言",
"compose.language.search": "Tshiau-tshuē語言……",
"compose.published.body": "成功PO文。",
"compose.published.open": "開",
"compose.saved.body": "PO文儲存ah。",
"compose_form.direct_message_warning_learn_more": "詳細資訊",
"compose_form.encryption_warning": "Mastodon ê PO文無點tuì點加密。M̄通用Mastodon分享任何敏感ê資訊。",
"confirmations.follow_to_list.confirm": "跟tuè加入kàu列單",
"notification.favourite_pm": "{name} kah意lí ê私人提起",
"notification.favourite_pm.name_and_others_with_link": "{name} kap<a>{count, plural, other {另外 # ê lâng}}</a>kah意lí ê私人提起",

View file

@ -47,11 +47,13 @@
"account.mutual": "आपसी",
"account.no_bio": "कुनै विवरण प्रदान गरिएको छैन।",
"account.posts": "पोस्टहरू",
"account.posts_with_replies": "पोस्ट र जवाफहरू",
"account.report": "@{name}लाई रिपोर्ट गर्नुहोस्",
"account.requested": "स्वीकृतिको पर्खाइमा। फलो अनुरोध रद्द गर्न क्लिक गर्नुहोस्",
"account.requested_follow": "{name} ले तपाईंलाई फलो गर्न अनुरोध गर्नुभएको छ",
"account.share": "@{name} को प्रोफाइल सेयर गर्नुहोस्",
"account.show_reblogs": "@{name} को बूस्टहरू देखाउनुहोस्",
"account.statuses_counter": "{count, plural, one {{counter} पोस्ट} other {{counter} पोस्टहरू}}",
"account.unblock": "@{name} लाई अनब्लक गर्नुहोस्",
"account.unblock_domain": "{domain} डोमेनलाई अनब्लक गर्नुहोस्",
"account.unblock_short": "अनब्लक गर्नुहोस्",
@ -67,14 +69,18 @@
"alert.unexpected.message": "एउटा अनपेक्षित त्रुटि भयो।",
"announcement.announcement": "घोषणा",
"annual_report.summary.followers.followers": "फलोअरहरु",
"annual_report.summary.highlighted_post.by_reblogs": "सबैभन्दा बढि बूस्ट गरिएको पोस्ट",
"annual_report.summary.new_posts.new_posts": "नयाँ पोस्टहरू",
"block_modal.remote_users_caveat": "हामी सर्भर {domain} लाई तपाईंको निर्णयको सम्मान गर्न सोध्नेछौं। तर, हामी अनुपालनको ग्यारेन्टी दिन सक्दैनौं किनभने केही सर्भरहरूले ब्लकहरू फरक रूपमा ह्यान्डल गर्न सक्छन्। सार्वजनिक पोस्टहरू लग इन नभएका प्रयोगकर्ताहरूले देख्न सक्छन्।",
"block_modal.show_less": "कम देखाउनुहोस्",
"block_modal.show_more": "थप देखाउनुहोस्",
"block_modal.title": "प्रयोगकर्तालाई ब्लक गर्ने हो?",
"block_modal.title": "प्रयोगकर्तालाई ब्लक गर्ने?",
"boost_modal.reblog": "पोस्ट बुस्ट गर्ने?",
"boost_modal.undo_reblog": "पोस्ट अनबुस्ट गर्ने?",
"bundle_column_error.copy_stacktrace": "त्रुटि रिपोर्ट प्रतिलिपि गर्नुहोस्",
"bundle_column_error.network.title": "नेटवर्क त्रुटि",
"bundle_column_error.retry": "पुन: प्रयास गर्नुहोस्",
"bundle_column_error.routing.title": "४०४",
"bundle_modal_error.close": "बन्द गर्नुहोस्",
"bundle_modal_error.retry": "Try again",
"closed_registrations.other_server_instructions": "Mastodon विकेन्द्रीकृत भएकोले, तपाइँ अर्को सर्भरमा खाता खोल्न सक्नुहुन्छ र पनि यो सर्भरसँग अन्तरक्रिया गर्न सक्नुहुन्छ।",
@ -82,23 +88,54 @@
"closed_registrations_modal.find_another_server": "अर्को सर्भर खोज्नुहोस्",
"closed_registrations_modal.title": "Mastodon मा साइन अप गर्दै",
"column.blocks": "ब्लक गरिएको प्रयोगकर्ताहरु",
"column.bookmarks": "बुकमार्कहरू",
"column.create_list": "सूची बनाउनुहोस्",
"column.direct": "निजी उल्लेखहरू",
"column.directory": "प्रोफाइल ब्राउज गर्नुहोस्",
"column.domain_blocks": "ब्लक गरिएको डोमेन",
"column.edit_list": "सूची सम्पादन गर्नुहोस्",
"column.follow_requests": "फलो अनुरोधहरू",
"column.home": "गृहपृष्ठ",
"column.lists": "सूचीहरू",
"column.mutes": "म्यूट गरिएका प्रयोगकर्ताहरू",
"column.notifications": "सूचनाहरू",
"column.pins": "पिन गरिएका पोस्टहरू",
"column_header.hide_settings": "सेटिङ्हरू लुकाउनुहोस्",
"column_header.pin": "पिन गर्नुहोस्",
"column_header.unpin": "अनपिन गर्नुहोस्",
"column_search.cancel": "रद्द गर्नुहोस्",
"column_subheading.settings": "सेटिङहरू",
"community.column_settings.media_only": "मिडिया मात्र",
"compose.language.change": "भाषा परिवर्तन गर्नुहोस्",
"compose.language.search": "भाषाहरू खोज्नुहोस्...",
"compose.published.body": "पोस्ट प्रकाशित भयो।",
"compose.published.open": "खोल्नुहोस्",
"compose.saved.body": "पोस्ट सेभ गरियो।",
"compose_form.direct_message_warning_learn_more": "थप जान्नुहोस्",
"compose_form.placeholder": "तपाईको मनमा के छ?",
"compose_form.publish": "पोस्ट गर्नुहोस्",
"compose_form.publish_form": "नयाँ पोस्ट",
"compose_form.reply": "जवाफ दिनुहोस्",
"compose_form.save_changes": "अपडेट गर्नुहोस्",
"confirmations.delete.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाईं यो पोष्ट मेटाउन चाहनुहुन्छ?",
"confirmations.delete.title": "पोस्ट मेटाउने?",
"confirmations.delete_list.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाईं यो सूची स्थायी रूपमा मेटाउन चाहनुहुन्छ?",
"confirmations.delete_list.title": "सूची मेटाउने?",
"confirmations.edit.confirm": "सम्पादन गर्नुहोस्",
"confirmations.edit.message": "अहिले सम्पादन गर्नाले तपाईंले हाल लेखिरहनुभएको सन्देश अधिलेखन हुनेछ। के तपाईं अगाडि बढ्न चाहनुहुन्छ?",
"confirmations.edit.title": "पोस्ट अधिलेखन गर्ने?",
"confirmations.follow_to_list.confirm": "फलो गर्नुहोस र सूचीमा थप्नुहोस्",
"confirmations.follow_to_list.message": "सूचीमा {name}लाई थप्नको लागि तपाईंले तिनीहरूलाई फलो गरेको हुनुपर्छ।",
"confirmations.follow_to_list.title": "प्रयोगकर्तालाई फलो गर्ने हो?",
"confirmations.follow_to_list.title": "प्रयोगकर्तालाई फलो गर्ने?",
"confirmations.logout.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाइँ लाई लग आउट गर्न चाहनुहुन्छ?",
"confirmations.logout.title": "लग आउट गर्ने?",
"confirmations.redraft.title": "पोस्ट मेटाएर पुन: ड्राफ्ट गर्ने?",
"confirmations.reply.message": "अहिले जवाफ दिनाले तपाईंले हाल लेखिरहनुभएको सन्देश अधिलेखन हुनेछ। के तपाईं अगाडि बढ्न चाहनुहुन्छ?",
"confirmations.reply.title": "पोस्ट अधिलेखन गर्ने?",
"confirmations.unfollow.confirm": "अनफलो गर्नुहोस्",
"confirmations.unfollow.message": "के तपाइँ पक्का हुनुहुन्छ कि तपाइँ {name}लाई अनफलो गर्न चाहनुहुन्छ?",
"confirmations.unfollow.title": "प्रयोगकर्तालाई अनफलो गर्ने हो?",
"confirmations.unfollow.title": "प्रयोगकर्तालाई अनफलो गर्ने?",
"disabled_account_banner.account_settings": "खाता सेटिङहरू",
"empty_column.follow_requests": "तपाईंले अहिलेसम्म कुनै पनि फलो अनुरोधहरू प्राप्त गर्नुभएको छैन। तपाईंले कुनै प्राप्त गरेपछि त्यो यहाँ देखिनेछ।",
"empty_column.followed_tags": "तपाईंले अहिलेसम्म कुनै पनि ह्यासट्यागहरू फलो गर्नुभएको छैन। तपाईंले ह्यासट्याग फलो गरेपछि तिनीहरू यहाँ देखिनेछन्।",
"follow_suggestions.dismiss": "फेरि नदेखाउनुहोस्",
@ -111,15 +148,35 @@
"followed_tags": "फलो गरिएका ह्यासट्यागहरू",
"hashtag.follow": "ह्यासट्याग फलो गर्नुहोस्",
"hashtag.unfollow": "ह्यासट्याग अनफलो गर्नुहोस्",
"home.column_settings.show_reblogs": "बूस्टहरू देखाउनुहोस्",
"interaction_modal.no_account_yet": "अहिलेसम्म खाता छैन?",
"interaction_modal.title.follow": "{name} लाई फलो गर्नुहोस्",
"interaction_modal.title.reblog": "{name} को पोस्ट बुस्ट गर्नुहोस्",
"keyboard_shortcuts.boost": "पोस्ट बुस्ट गर्नुहोस्",
"mute_modal.they_wont_know": "उनीहरूलाई म्यूट गरिएको बारे थाहा हुँदैन।",
"mute_modal.title": "प्रयोगकर्तालाई म्युट गर्ने हो?",
"mute_modal.title": "प्रयोगकर्तालाई म्युट गर्ने?",
"navigation_bar.blocks": "ब्लक गरिएको प्रयोगकर्ताहरु",
"navigation_bar.follow_requests": "फलो अनुरोधहरू",
"navigation_bar.followed_tags": "फलो गरिएका ह्यासट्यागहरू",
"notification.reblog": "{name} ले तपाईंको पोस्ट बूस्ट गर्नुभयो",
"notification_requests.confirm_accept_multiple.title": "सूचना अनुरोधहरू स्वीकार गर्ने?",
"notification_requests.confirm_dismiss_multiple.title": "सूचना अनुरोधहरू खारेज गर्ने?",
"notifications.clear_title": "सूचनाहरू खाली गर्ने?",
"notifications.column_settings.reblog": "बूस्टहरू:",
"notifications.filter.boosts": "बूस्टहरू",
"report.comment.title": "के हामीले थाहा पाउनुपर्ने अरू केही छ जस्तो लाग्छ?",
"report.forward_hint": "यो खाता अर्को सर्भरबाट हो। त्यहाँ पनि रिपोर्टको गुमनाम प्रतिलिपि पठाउने हो?",
"report.rules.title": "कुन नियमहरू उल्लङ्घन भइरहेका छन्?",
"report.statuses.title": "के यस रिपोर्टलाई समर्थन गर्ने कुनै पोस्टहरू छन्?",
"report.thanks.title": "यो हेर्न चाहनुहुन्न?",
"report.unfollow": "@{name} लाई अनफलो गर्नुहोस्",
"search_results.hashtags": "ह्यासट्यागहरू",
"status.cancel_reblog_private": "अनबुस्ट गर्नुहोस्",
"status.cannot_reblog": "यो पोस्टलाई बुस्ट गर्न सकिँदैन",
"status.mute": "@{name}लाई म्यूट गर्नुहोस्",
"status.mute_conversation": "कुराकानी म्यूट गर्नुहोस्",
"status.reblog": "बूस्ट गर्नुहोस्",
"status.reblogged_by": "{name} ले बूस्ट गर्नुभएको",
"status.reblogs": "{count, plural, one {बूस्ट} other {बूस्टहरू}}",
"status.unmute_conversation": "कुराकानी अनम्यूट गर्नुहोस्"
}

View file

@ -837,6 +837,7 @@
"status.reblogs.empty": "Ingen har framheva dette tutet enno. Om nokon gjer, så dukkar det opp her.",
"status.redraft": "Slett & skriv på nytt",
"status.remove_bookmark": "Fjern bokmerke",
"status.remove_favourite": "Fjern frå favorittar",
"status.replied_in_thread": "Svara i tråden",
"status.replied_to": "Svarte {name}",
"status.reply": "Svar",

View file

@ -406,6 +406,9 @@
"ignore_notifications_modal.not_followers_title": "Ignoruj powiadomienia od użytkowników którzy cię nie obserwują?",
"ignore_notifications_modal.not_following_title": "Ignoruj powiadomienia od użytkowników których nie obserwujesz?",
"ignore_notifications_modal.private_mentions_title": "Ignoruj powiadomienia o nieproszonych wzmiankach prywatnych?",
"interaction_modal.action.favourite": "Aby kontynuować, musisz dodać do ulubionych na swoim koncie.",
"interaction_modal.action.follow": "Aby kontynuować, musisz obserwować ze swojego konta.",
"interaction_modal.no_account_yet": "Nie masz jeszcze konta?",
"interaction_modal.on_another_server": "Na innym serwerze",
"interaction_modal.on_this_server": "Na tym serwerze",
"interaction_modal.title.favourite": "Polub wpis użytkownika {name}",
@ -413,6 +416,7 @@
"interaction_modal.title.reblog": "Podbij wpis {name}",
"interaction_modal.title.reply": "Odpowiedz na post {name}",
"interaction_modal.title.vote": "Weź udział w głosowaniu {name}",
"interaction_modal.username_prompt": "Np. {example}",
"intervals.full.days": "{number, plural, one {# dzień} few {# dni} many {# dni} other {# dni}}",
"intervals.full.hours": "{number, plural, one {# godzina} few {# godziny} many {# godzin} other {# godzin}}",
"intervals.full.minutes": "{number, plural, one {# minuta} few {# minuty} many {# minut} other {# minut}}",
@ -825,6 +829,7 @@
"status.reblogs.empty": "Nikt nie podbił jeszcze tego wpisu. Gdy ktoś to zrobi, pojawi się tutaj.",
"status.redraft": "Usuń i przeredaguj",
"status.remove_bookmark": "Usuń zakładkę",
"status.remove_favourite": "Usuń z ulubionych",
"status.replied_in_thread": "Odpowiedź w wątku",
"status.replied_to": "Odpowiedź do wpisu użytkownika {name}",
"status.reply": "Odpowiedz",

View file

@ -108,7 +108,7 @@
"annual_report.summary.thanks": "Obrigada por fazer parte do Mastodon!",
"attachments_list.unprocessed": "(não processado)",
"audio.hide": "Ocultar áudio",
"block_modal.remote_users_caveat": "Pediremos ao servidor {domínio} que respeite sua decisão. No entanto, a conformidade não é garantida pois alguns servidores podem lidar com os blocos de maneira diferente. As postagens públicas ainda podem estar visíveis para usuários não logados.",
"block_modal.remote_users_caveat": "Pediremos ao servidor {domain} que respeite sua decisão. No entanto, a conformidade não é garantida, já que alguns servidores podem lidar com bloqueios de maneira diferente. As postagens públicas ainda podem estar visíveis para usuários não logados.",
"block_modal.show_less": "Mostrar menos",
"block_modal.show_more": "Mostrar mais",
"block_modal.they_cant_mention": "Eles não podem mencionar ou seguir você.",
@ -243,12 +243,12 @@
"dismissable_banner.explore_statuses": "Estas publicações através do fediverse estão ganhando atenção hoje. Publicações mais recentes com mais boosts e favoritos são classificados mais altamente.",
"dismissable_banner.explore_tags": "Estas hashtags estão ganhando atenção hoje no fediverse. Hashtags usadas por muitas pessoas diferentes são classificadas mais altamente.",
"dismissable_banner.public_timeline": "Estas são as publicações mais recentes das pessoas no fediverse que as pessoas do {domain} seguem.",
"domain_block_modal.block": "Servidor de blocos.",
"domain_block_modal.block_account_instead": "Bloco @(nome)",
"domain_block_modal.block": "Bloquear servidor",
"domain_block_modal.block_account_instead": "Bloquear @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "Pessoas deste servidor podem interagir com suas publicações antigas.",
"domain_block_modal.they_cant_follow": "Ninguém deste servidor pode lhe seguir.",
"domain_block_modal.they_wont_know": "Eles não saberão que foram bloqueados.",
"domain_block_modal.title": "Dominio do bloco",
"domain_block_modal.title": "Bloquear domínio?",
"domain_block_modal.you_will_lose_num_followers": "Você perderá {followersCount, plural, one {{followersCountDisplay} seguidor} other {{followersCountDisplay} seguidores}} e {followingCount, plural, one {{followingCountDisplay} pessoa que você segue} other {{followingCountDisplay} pessoas que você segue}}.",
"domain_block_modal.you_will_lose_relationships": "Você irá perder todos os seguidores e pessoas que você segue neste servidor.",
"domain_block_modal.you_wont_see_posts": "Você não verá postagens ou notificações de usuários neste servidor.",
@ -259,9 +259,9 @@
"domain_pill.their_server": "Sua casa digital, onde ficam todas as suas postagens.",
"domain_pill.their_username": "Seu identificador exclusivo em seu servidor. É possível encontrar usuários com o mesmo nome de usuário em servidores diferentes.",
"domain_pill.username": "Nome de usuário",
"domain_pill.whats_in_a_handle": "O que há em uma alça?",
"domain_pill.who_they_are": "Como os identificadores indicam quem alguém é e onde está, você pode interagir com pessoas na web social de <button>plataformas alimentadas pelo ActivityPub</button>.",
"domain_pill.who_you_are": "Como seu identificador indica quem você é e onde está, as pessoas podem interagir com você nas redes sociais das <button>plataformas alimentadas pelo ActivityPub</button>.",
"domain_pill.whats_in_a_handle": "O que há em um identificador?",
"domain_pill.who_they_are": "Como os identificadores indicam quem alguém é e onde está, você pode interagir com pessoas na rede de <button>plataformas alimentadas pelo ActivityPub</button>.",
"domain_pill.who_you_are": "Como seu identificador indica quem você é e onde está, as pessoas podem interagir com você na rede de <button>plataformas alimentadas pelo ActivityPub</button>.",
"domain_pill.your_handle": "Seu identificador:",
"domain_pill.your_server": "Sua casa digital, onde ficam todas as suas postagens. Não gosta deste? Transfira servidores a qualquer momento e traga seus seguidores também.",
"domain_pill.your_username": "Seu identificador exclusivo neste servidor. É possível encontrar usuários com o mesmo nome de usuário em servidores diferentes.",

View file

@ -1,7 +1,7 @@
{
"about.blocks": "Servidores moderados",
"about.contact": "Contacto:",
"about.disclaimer": "O Mastodon é um software livre, de código aberto e uma marca registada do Mastodon gGmbH.",
"about.disclaimer": "O Mastodon é um software livre, de código aberto e uma marca registada de Mastodon gGmbH.",
"about.domain_blocks.no_reason_available": "Motivo não disponível",
"about.domain_blocks.preamble": "O Mastodon geralmente permite ver e interagir com o conteúdo de utilizadores de qualquer outra instância no fediverso. Estas são as exceções desta instância em específico.",
"about.domain_blocks.silenced.explanation": "Normalmente não verás perfis e conteúdos deste servidor, a não ser que os procures explicitamente ou optes por segui-los.",
@ -85,7 +85,7 @@
"alert.rate_limited.title": "Limite de tentativas",
"alert.unexpected.message": "Ocorreu um erro inesperado.",
"alert.unexpected.title": "Bolas!",
"alt_text_badge.title": "Texto alternativo",
"alt_text_badge.title": "Texto descritivo",
"announcement.announcement": "Mensagem de manutenção",
"annual_report.summary.archetype.booster": "O caçador de frescura",
"annual_report.summary.archetype.lurker": "O espreitador",
@ -244,7 +244,7 @@
"dismissable_banner.explore_tags": "Estas etiquetas estão a ganhar força no fediverso atualmente. As etiquetas que são utilizadas por mais pessoas diferentes são classificadas numa posição mais elevada.",
"dismissable_banner.public_timeline": "Estas são as publicações públicas mais recentes de pessoas no fediverso que as pessoas em {domain} seguem.",
"domain_block_modal.block": "Bloquear servidor",
"domain_block_modal.block_account_instead": "Bloquear antes @{name}",
"domain_block_modal.block_account_instead": "Em vez disso, bloquear @{name}",
"domain_block_modal.they_can_interact_with_old_posts": "As pessoas deste servidor podem interagir com as tuas publicações antigas.",
"domain_block_modal.they_cant_follow": "Ninguém deste servidor pode seguir-te.",
"domain_block_modal.they_wont_know": "Eles não saberão que foram bloqueados.",
@ -260,7 +260,7 @@
"domain_pill.their_username": "O identificador único dele no seu servidor. É possível encontrar utilizadores com o mesmo nome de utilizador em servidores diferentes.",
"domain_pill.username": "Nome de utilizador",
"domain_pill.whats_in_a_handle": "Em que consiste um identificador?",
"domain_pill.who_they_are": "Uma vez que os identificadores dizem quem é alguém e onde está, pode interagir com as pessoas através da rede social de <button>plataformas que suportam ActivityPub</button>.",
"domain_pill.who_they_are": "Uma vez que os identificadores dizem quem é alguém e onde está, podes interagir com as pessoas através da rede social de <button>plataformas que suportam ActivityPub</button>.",
"domain_pill.who_you_are": "Uma vez que o teu identificador indica quem és e onde estás, as pessoas podem interagir contigo através da rede social de <button>plataformas que suportam ActivityPub</button>.",
"domain_pill.your_handle": "O teu identificador:",
"domain_pill.your_server": "A tua casa digital, onde se encontram todas as tuas publicações. Não gostas deste? Muda de servidor a qualquer momento e leva também os teus seguidores.",
@ -642,10 +642,10 @@
"notifications.policy.filter_hint": "Enviar para a caixa de notificações filtradas",
"notifications.policy.filter_limited_accounts_hint": "Limitado pelos moderadores do servidor",
"notifications.policy.filter_limited_accounts_title": "Contas moderadas",
"notifications.policy.filter_new_accounts.hint": "Criada {days, plural, one {no último dia} other {nos últimos # dias}}",
"notifications.policy.filter_new_accounts.hint": "Criadas {days, plural, one {no último dia} other {nos últimos # dias}}",
"notifications.policy.filter_new_accounts_title": "Novas contas",
"notifications.policy.filter_not_followers_hint": "Incluindo pessoas que te seguem há menos de {days, plural, one {um dia} other {# dias}}",
"notifications.policy.filter_not_followers_title": "Pessoas não te seguem",
"notifications.policy.filter_not_followers_title": "Pessoas que não te seguem",
"notifications.policy.filter_not_following_hint": "Até que os aproves manualmente",
"notifications.policy.filter_not_following_title": "Pessoas que não segues",
"notifications.policy.filter_private_mentions_hint": "Filtrado, a não ser que seja em resposta à tua própria menção ou se seguires o remetente",
@ -723,7 +723,7 @@
"report.category.title_account": "perfil",
"report.category.title_status": "publicação",
"report.close": "Concluído",
"report.comment.title": "Há algo mais que pensa que devemos saber?",
"report.comment.title": "Há mais alguma coisa que devamos saber?",
"report.forward": "Reencaminhar para {target}",
"report.forward_hint": "A conta pertence a outro servidor. Enviar uma cópia anónima da denúncia para esse servidor também?",
"report.mute": "Ocultar",
@ -739,15 +739,15 @@
"report.reasons.spam": "É spam",
"report.reasons.spam_description": "Hiperligações maliciosas, contactos falsos ou respostas repetitivas",
"report.reasons.violation": "Viola as regras do servidor",
"report.reasons.violation_description": "Está ciente de que infringe regras específicas",
"report.rules.subtitle": "Selecione tudo o que se aplicar",
"report.reasons.violation_description": "Infringe regras específicas",
"report.rules.subtitle": "Seleciona tudo o que se aplicar",
"report.rules.title": "Que regras estão a ser violadas?",
"report.statuses.subtitle": "Selecione tudo o que se aplicar",
"report.statuses.subtitle": "Seleciona tudo o que se aplicar",
"report.statuses.title": "Existe alguma publicação que suporte esta denúncia?",
"report.submit": "Enviar",
"report.target": "A denunciar {target}",
"report.thanks.take_action": "Aqui estão as suas opções para controlar o que vê no Mastodon:",
"report.thanks.take_action_actionable": "Enquanto revemos a sua denúncia, pode tomar medidas contra @{name}:",
"report.thanks.take_action_actionable": "Enquanto revemos a tua denúncia, podes tomar medidas contra @{name}:",
"report.thanks.title": "Não quer ver isto?",
"report.thanks.title_actionable": "Obrigado por nos informares, vamos analisar a situação.",
"report.unfollow": "Deixar de seguir @{name}",
@ -758,7 +758,7 @@
"report_notification.categories.other": "Outro",
"report_notification.categories.other_sentence": "outro",
"report_notification.categories.spam": "Spam",
"report_notification.categories.spam_sentence": "spam",
"report_notification.categories.spam_sentence": "publicidade indesejada / spam",
"report_notification.categories.violation": "Violação de regra",
"report_notification.categories.violation_sentence": "violação de regra",
"report_notification.open": "Abrir denúncia",
@ -813,7 +813,7 @@
"status.edited": "Última edição em {date}",
"status.edited_x_times": "Editado {count, plural,one {{count} vez} other {{count} vezes}}",
"status.embed": "Obter código de incorporação",
"status.favourite": "Assinalar como favorito",
"status.favourite": "Adicionar aos favoritos",
"status.favourites": "{count, plural, one {favorito} other {favoritos}}",
"status.filter": "Filtrar esta publicação",
"status.history.created": "{name} criado em {date}",

View file

@ -378,6 +378,8 @@
"ignore_notifications_modal.not_followers_title": "Nevšímať si oznámenia od ľudí, ktorí ťa nenasledujú?",
"ignore_notifications_modal.not_following_title": "Nevšímať si oznámenia od ľudí, ktorých nenasleduješ?",
"ignore_notifications_modal.private_mentions_title": "Nevšímať si oznámenia o nevyžiadaných súkromných spomínaniach?",
"interaction_modal.action.favourite": "Pre pokračovanie si musíš obľúbiť zo svojho účtu.",
"interaction_modal.action.follow": "Pre pokračovanie musíš nasledovať zo svojho účtu.",
"interaction_modal.action.reply": "Pre pokračovanie musíš odpovedať s tvojho účtu.",
"interaction_modal.action.vote": "Pre pokračovanie musíš hlasovať s tvojho účtu.",
"interaction_modal.go": "Prejdi",
@ -389,6 +391,7 @@
"interaction_modal.title.reblog": "Zdieľať príspevok od {name}",
"interaction_modal.title.reply": "Odpovedať na príspevok od {name}",
"interaction_modal.title.vote": "Hlasuj v ankete od {name}",
"interaction_modal.username_prompt": "Napr. {example}",
"intervals.full.days": "{number, plural, one {# deň} few {# dni} many {# dní} other {# dní}}",
"intervals.full.hours": "{number, plural, one {# hodina} few {# hodiny} many {# hodín} other {# hodín}}",
"intervals.full.minutes": "{number, plural, one {# minúta} few {# minúty} many {# minút} other {# minút}}",
@ -517,6 +520,7 @@
"notification.moderation_warning": "Dostal/a si varovanie od moderátora",
"notification.moderation_warning.action_delete_statuses": "Niektoré z tvojich príspevkov boli odstránené.",
"notification.moderation_warning.action_disable": "Tvoj účet bol vypnutý.",
"notification.moderation_warning.action_mark_statuses_as_sensitive": "Niektoré tvoje príspevky boli označené za chúlostivé.",
"notification.moderation_warning.action_none": "Tvoj účet dostal upozornenie od moderátora.",
"notification.moderation_warning.action_sensitive": "Tvoje príspevky budú odteraz označované ako chúlostivé.",
"notification.moderation_warning.action_silence": "Tvoj účet bol obmedzený.",
@ -530,6 +534,7 @@
"notification.status": "{name} uverejňuje niečo nové",
"notification.update": "{name} upravuje príspevok",
"notification_requests.accept": "Prijať",
"notification_requests.confirm_accept_multiple.title": "Priať požiadavku o oboznámenia?",
"notification_requests.dismiss": "Zamietnuť",
"notification_requests.edit_selection": "Uprav",
"notification_requests.exit_selection": "Hotovo",
@ -574,9 +579,11 @@
"notifications.policy.accept_hint": "Ukáž v oznámeniach",
"notifications.policy.drop": "Ignoruj",
"notifications.policy.filter": "Triediť",
"notifications.policy.filter_limited_accounts_hint": "Obmedzené moderátormi servera",
"notifications.policy.filter_limited_accounts_title": "Moderované účty",
"notifications.policy.filter_new_accounts_title": "Nové účty",
"notifications.policy.filter_not_followers_title": "Ľudia, ktorí ťa nenasledujú",
"notifications.policy.filter_not_following_hint": "Pokiaľ ich ručne neschváliš",
"notifications.policy.filter_not_following_title": "Ľudia, ktorých nenasleduješ",
"notifications.policy.filter_private_mentions_title": "Nevyžiadané priame spomenutia",
"notifications.policy.title": "Spravuj oznámenia od…",
@ -624,6 +631,8 @@
"privacy_policy.title": "Pravidlá ochrany súkromia",
"recommended": "Odporúčané",
"refresh": "Obnoviť",
"regeneration_indicator.please_stand_by": "Prosím, čakajte.",
"regeneration_indicator.preparing_your_home_feed": "Pripravuje sa tvoj domáci kanál…",
"relative_time.days": "{number} dní",
"relative_time.full.days": "Pred {number, plural, one {# dňom} other {# dňami}}",
"relative_time.full.hours": "Pred {number, plural, one {# hodinou} other {# hodinami}}",
@ -714,6 +723,7 @@
"server_banner.about_active_users": "Ľudia používajúci tento server za posledných 30 dní (aktívni používatelia za mesiac)",
"server_banner.active_users": "Aktívne účty",
"server_banner.administered_by": "Správa servera:",
"server_banner.is_one_of_many": "{domain} je jeden z mnohých nezávislých Mastodon serverov, ktoré môžeš použiť na zúčastňovanie sa v rámci fediversa.",
"server_banner.server_stats": "Štatistiky servera:",
"sign_in_banner.create_account": "Vytvoriť účet",
"sign_in_banner.sign_in": "Prihlásiť sa",
@ -756,6 +766,7 @@
"status.reblogs.empty": "Nikto ešte tento príspevok nezdieľal. Keď tak niekto urobí, zobrazí sa to tu.",
"status.redraft": "Vymazať a prepísať",
"status.remove_bookmark": "Odstrániť záložku",
"status.remove_favourite": "Odstráň z obľúbených",
"status.replied_in_thread": "Odpovedal/a vo vlákne",
"status.replied_to": "Odpoveď na {name}",
"status.reply": "Odpovedať",

View file

@ -697,6 +697,7 @@
"recommended": "Rekommenderas",
"refresh": "Läs om",
"regeneration_indicator.please_stand_by": "Vänligen vänta.",
"regeneration_indicator.preparing_your_home_feed": "Förbereder ditt hemflöde…",
"relative_time.days": "{number}d",
"relative_time.full.days": "{number, plural, one {# dag} other {# dagar}} sedan",
"relative_time.full.hours": "{number, plural, one {# timme} other {# timmar}} sedan",

View file

@ -550,7 +550,7 @@
"notification.favourite.name_and_others_with_link": "{name} và <a>{count, plural, other {# người khác}}</a> đã thích tút của bạn",
"notification.favourite_pm": "{name} đã thích lượt nhắn riêng của bạn",
"notification.favourite_pm.name_and_others_with_link": "{name} và <a>{count, plural, other {# người khác}}</a> đã thích lượt nhắn riêng của bạn",
"notification.follow": "{name} theo dõi bạn",
"notification.follow": "{name} đã theo dõi bạn",
"notification.follow.name_and_others": "{name} và <a>{count, plural, other {# người khác}}</a> theo dõi bạn",
"notification.follow_request": "{name} yêu cầu theo dõi bạn",
"notification.follow_request.name_and_others": "{name} và {count, plural, other {# người khác}} đã yêu cầu theo dõi bạn",
@ -764,7 +764,7 @@
"report_notification.open": "Mở báo cáo",
"search.no_recent_searches": "Gần đây chưa tìm gì",
"search.placeholder": "Tìm kiếm",
"search.quick_action.account_search": "Người tên {x}",
"search.quick_action.account_search": "Người tên {x}",
"search.quick_action.go_to_account": "Xem trang {x}",
"search.quick_action.go_to_hashtag": "Xem hashtag {x}",
"search.quick_action.open_url": "Mở liên kết trong Mastodon",

View file

@ -7119,6 +7119,8 @@ a.status-card {
}
&--layout-3 {
min-height: calc(64px * 2 + 8px);
& > .media-gallery__item:nth-child(1) {
border-end-end-radius: 0;
border-start-end-radius: 0;
@ -7138,6 +7140,8 @@ a.status-card {
}
&--layout-4 {
min-height: calc(64px * 2 + 8px);
& > .media-gallery__item:nth-child(1) {
border-end-end-radius: 0;
border-start-end-radius: 0;

View file

@ -5,6 +5,7 @@ class ActivityPub::Activity::Announce < ActivityPub::Activity
def perform
return reject_payload! if delete_arrived_first?(@json['id']) || !related_to_local_activity?
return reject_payload! if @object.nil?
with_redis_lock("announce:#{value_or_id(@object)}") do
original_status = status_from_object

View file

@ -98,8 +98,34 @@ class ActivityPub::TagManager
account_status_shares_url(target.account, target)
end
def followers_uri_for(target)
target.local? ? account_followers_url(target) : target.followers_url.presence
def following_uri_for(target, ...)
raise ArgumentError, 'target must be a local account' unless target.local?
account_following_index_url(target, ...)
end
def followers_uri_for(target, ...)
return target.followers_url.presence unless target.local?
account_followers_url(target, ...)
end
def collection_uri_for(target, ...)
raise NotImplementedError unless target.local?
account_collection_url(target, ...)
end
def inbox_uri_for(target)
raise NotImplementedError unless target.local?
target.instance_actor? ? instance_actor_inbox_url : account_inbox_url(target)
end
def outbox_uri_for(target, ...)
raise NotImplementedError unless target.local?
target.instance_actor? ? instance_actor_outbox_url(...) : account_outbox_url(target, ...)
end
# Primary audience of a status
@ -111,9 +137,9 @@ class ActivityPub::TagManager
when 'public'
[COLLECTIONS[:public]]
when 'unlisted', 'public_unlisted', 'private'
[account_followers_url(status.account)]
[followers_uri_for(status.account)]
when 'login'
[account_followers_url(status.account), 'as:LoginOnly', 'kmyblue:LoginOnly', 'LoginUser']
[followers_uri_for(status.account), 'as:LoginOnly', 'kmyblue:LoginOnly', 'LoginUser']
when 'direct'
if status.account.silenced?
# Only notify followers if the account is locally silenced
@ -156,7 +182,7 @@ class ActivityPub::TagManager
case status.visibility
when 'public'
cc << account_followers_url(status.account)
cc << followers_uri_for(status.account)
when 'unlisted', 'public_unlisted'
cc << COLLECTIONS[:public]
end

View file

@ -46,6 +46,8 @@ class DeliveryFailureTracker
urls.reject do |url|
host = Addressable::URI.parse(url).normalized_host
unavailable_domains_map[host]
rescue Addressable::URI::InvalidURIError, IDN::Idna::IdnaError
true
end
end

View file

@ -111,16 +111,10 @@ class Request
end
begin
# If we are using a persistent connection, we have to
# read every response to be able to move forward at all.
# However, simply calling #to_s or #flush may not be safe,
# as the response body, if malicious, could be too big
# for our memory. So we use the #body_with_limit method
response.body_with_limit if http_client.persistent?
yield response if block_given?
ensure
http_client.close unless http_client.persistent?
response.truncated_body if http_client.persistent? && !response.connection.finished_request?
http_client.close unless http_client.persistent? && response.connection.finished_request?
end
end

View file

@ -2,7 +2,8 @@
class WebPushRequest
SIGNATURE_ALGORITHM = 'p256ecdsa'
AUTH_HEADER = 'WebPush'
LEGACY_AUTH_HEADER = 'WebPush'
STANDARD_AUTH_HEADER = 'vapid'
PAYLOAD_EXPIRATION = 24.hours
JWT_ALGORITHM = 'ES256'
JWT_TYPE = 'JWT'
@ -10,6 +11,7 @@ class WebPushRequest
attr_reader :web_push_subscription
delegate(
:standard,
:endpoint,
:key_auth,
:key_p256dh,
@ -24,20 +26,36 @@ class WebPushRequest
@audience ||= Addressable::URI.parse(endpoint).normalized_site
end
def authorization_header
[AUTH_HEADER, encoded_json_web_token].join(' ')
def legacy_authorization_header
[LEGACY_AUTH_HEADER, encoded_json_web_token].join(' ')
end
def crypto_key_header
[SIGNATURE_ALGORITHM, vapid_key.public_key_for_push_header].join('=')
end
def encrypt(payload)
def legacy_encrypt(payload)
Webpush::Legacy::Encryption.encrypt(payload, key_p256dh, key_auth)
end
def standard_authorization_header
[STANDARD_AUTH_HEADER, standard_vapid_value].join(' ')
end
def standard_encrypt(payload)
Webpush::Encryption.encrypt(payload, key_p256dh, key_auth)
end
def legacy
!standard
end
private
def standard_vapid_value
"t=#{encoded_json_web_token},k=#{vapid_key.public_key_for_push_header}"
end
def encoded_json_web_token
JWT.encode(
web_token_payload,

View file

@ -114,25 +114,27 @@ class Account < ApplicationRecord
validates_with UniqueUsernameValidator, if: -> { will_save_change_to_username? }
# Remote user validations, also applies to internal actors
validates :username, format: { with: USERNAME_ONLY_RE }, if: -> { (remote? || actor_type == 'Application') && will_save_change_to_username? }
validates :username, format: { with: USERNAME_ONLY_RE }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? }
# Remote user validations
validates :uri, presence: true, unless: :local?, on: :create
# Local user validations
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: USERNAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' }
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && actor_type != 'Application' }
validates :username, format: { with: /\A[a-z0-9_]+\z/i }, length: { maximum: USERNAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_username? && !actor_type_application? }
validates_with UnreservedUsernameValidator, if: -> { local? && will_save_change_to_username? && !actor_type_application? }
validates :display_name, length: { maximum: DISPLAY_NAME_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_display_name? }
validates :note, note_length: { maximum: NOTE_LENGTH_LIMIT }, if: -> { local? && will_save_change_to_note? }
validates :fields, length: { maximum: DEFAULT_FIELDS_SIZE }, if: -> { local? && will_save_change_to_fields? }
validates_with EmptyProfileFieldNamesValidator, if: -> { local? && will_save_change_to_fields? }
with_options on: :create do
validates :uri, absence: true, if: :local?
validates :inbox_url, absence: true, if: :local?
validates :shared_inbox_url, absence: true, if: :local?
validates :followers_url, absence: true, if: :local?
with_options on: :create, if: :local? do
validates :followers_url, absence: true
validates :inbox_url, absence: true
validates :shared_inbox_url, absence: true
validates :uri, absence: true
end
validates :domain, exclusion: { in: [''] }
normalizes :username, with: ->(username) { username.squish }
scope :without_internal, -> { where(id: 1...) }
@ -194,7 +196,7 @@ class Account < ApplicationRecord
end
def remote?
domain.present?
!domain.nil?
end
def moved?
@ -215,6 +217,10 @@ class Account < ApplicationRecord
self.actor_type = ActiveModel::Type::Boolean.new.cast(val) ? 'Service' : 'Person'
end
def actor_type_application?
actor_type == 'Application'
end
def group?
actor_type == 'Group'
end

View file

@ -28,6 +28,7 @@ class AccountWarning < ApplicationRecord
suspend: 4_000,
}, suffix: :action
APPEAL_WINDOW = 20.days
RECENT_PERIOD = 3.months.freeze
normalizes :text, with: ->(text) { text.to_s }, apply_to_nil: true
@ -50,6 +51,10 @@ class AccountWarning < ApplicationRecord
overruled_at.present?
end
def appeal_eligible?
created_at >= APPEAL_WINDOW.ago
end
def to_log_human_identifier
target_account.acct
end

View file

@ -16,8 +16,6 @@
# updated_at :datetime not null
#
class Appeal < ApplicationRecord
MAX_STRIKE_AGE = 20.days
TEXT_LENGTH_LIMIT = 2_000
belongs_to :account
@ -68,6 +66,6 @@ class Appeal < ApplicationRecord
private
def validate_time_frame
errors.add(:base, I18n.t('strikes.errors.too_late')) if strike.created_at < MAX_STRIKE_AGE.ago
errors.add(:base, I18n.t('strikes.errors.too_late')) unless strike.appeal_eligible?
end
end

View file

@ -0,0 +1,99 @@
# frozen_string_literal: true
module Status::Visibility
extend ActiveSupport::Concern
included do
enum :visibility,
{ public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10, login: 11 },
suffix: :visibility,
validate: true
enum :searchability,
{ public: 0, private: 1, direct: 2, limited: 3, unsupported: 4, public_unlisted: 10 },
suffix: :searchability
enum :limited_scope,
{ none: 0, mutual: 1, circle: 2, personal: 3, reply: 4 },
suffix: :limited
scope :unset_searchability, -> { where(searchability: nil, reblog_of_id: nil) }
scope :distributable_visibility, -> { where(visibility: %i(public unlisted)) }
scope :distributable_visibility_for_anonymous, -> { where(visibility: %i(public public_unlisted unlisted)) }
scope :list_eligible_visibility, -> { where(visibility: %i(public unlisted private)) }
scope :not_direct_visibility, -> { where.not(visibility: :direct) }
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
before_validation :set_visibility, unless: :visibility?
before_validation :set_searchability
end
class_methods do
def selectable_visibilities
selectable_all_visibilities - %w(mutual circle reply direct)
end
def selectable_all_visibilities
vs = %w(public public_unlisted login unlisted private mutual circle reply direct)
vs -= %w(public_unlisted) unless Setting.enable_public_unlisted_visibility
vs -= %w(public) unless Setting.enable_public_visibility
vs
end
def selectable_reblog_visibilities
%w(unset) + selectable_visibilities
end
def all_visibilities
visibilities.keys
end
def selectable_searchabilities
ss = searchabilities.keys - %w(unsupported)
ss -= %w(public_unlisted) unless Setting.enable_public_unlisted_visibility
ss
end
def selectable_searchabilities_for_search
searchabilities.keys - %w(public_unlisted unsupported)
end
def all_searchabilities
searchabilities.keys - %w(unlisted login unsupported)
end
end
def hidden?
!distributable?
end
def distributable?
public_visibility? || unlisted_visibility? || public_unlisted_visibility?
end
alias sign? distributable?
private
def set_visibility
self.visibility ||= reblog.visibility if reblog?
self.visibility ||= visibility_from_account
end
def visibility_from_account
account.locked? ? :private : :public
end
def set_searchability
return if searchability.nil?
self.searchability = if %w(public public_unlisted login unlisted).include?(visibility)
searchability
elsif ['limited', 'direct'].include?(visibility)
searchability == 'limited' ? :limited : :direct
elsif visibility == 'private'
['public', 'public_unlisted'].include?(searchability) ? :private : searchability
else
:direct
end
end
end

View file

@ -31,7 +31,7 @@ class DomainBlock < ApplicationRecord
include DomainNormalizable
include DomainMaterializable
enum :severity, { silence: 0, suspend: 1, noop: 2 }
enum :severity, { silence: 0, suspend: 1, noop: 2 }, validate: true
validates :domain, presence: true, uniqueness: true, domain: true

View file

@ -31,7 +31,7 @@ class Invite < ApplicationRecord
validates :comment, length: { maximum: COMMENT_SIZE_LIMIT }
before_validation :set_code
before_validation :set_code, on: :create
def valid_for_use?
(max_uses.nil? || uses < max_uses) && !expired? && user&.functional?

View file

@ -24,7 +24,7 @@ class IpBlock < ApplicationRecord
sign_up_requires_approval: 5000,
sign_up_block: 5500,
no_access: 9999,
}, prefix: true
}, prefix: true, validate: true
validates :ip, :severity, presence: true
validates :ip, uniqueness: true

View file

@ -89,22 +89,32 @@ class NotificationGroup < ActiveModelSerializers::Model
binds = [
account_id,
SAMPLE_ACCOUNTS_SIZE,
pagination_range.begin,
pagination_range.end,
ActiveRecord::Relation::QueryAttribute.new('group_keys', group_keys, ActiveRecord::ConnectionAdapters::PostgreSQL::OID::Array.new(ActiveModel::Type::String.new)),
pagination_range.begin || 0,
]
binds << pagination_range.end unless pagination_range.end.nil?
upper_bound_cond = begin
if pagination_range.end.nil?
''
elsif pagination_range.exclude_end?
'AND id < $5'
else
'AND id <= $5'
end
end
ActiveRecord::Base.connection.select_all(<<~SQL.squish, 'grouped_notifications', binds).cast_values.to_h { |k, *values| [k, values] }
SELECT
groups.group_key,
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1),
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT $2),
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4) AS notifications_count,
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 AND activity_type = 'EmojiReaction'),
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id >= $3 ORDER BY id ASC LIMIT 1) AS min_id,
(SELECT created_at FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id <= $4 ORDER BY id DESC LIMIT 1)
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} ORDER BY id DESC LIMIT 1),
array(SELECT from_account_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} ORDER BY id DESC LIMIT $2),
(SELECT count(*) FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond}) AS notifications_count,
array(SELECT activity_id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} AND activity_type = 'EmojiReaction'),
(SELECT id FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key AND id >= $4 ORDER BY id ASC LIMIT 1) AS min_id,
(SELECT created_at FROM notifications WHERE notifications.account_id = $1 AND notifications.group_key = groups.group_key #{upper_bound_cond} ORDER BY id DESC LIMIT 1)
FROM
unnest($5::text[]) AS groups(group_key);
unnest($3::text[]) AS groups(group_key);
SQL
else
binds = [

View file

@ -45,6 +45,7 @@ class Status < ApplicationRecord
include Status::SearchConcern
include Status::SnapshotConcern
include Status::ThreadingConcern
include Status::Visibility
include DtlHelper
MEDIA_ATTACHMENTS_LIMIT = 4
@ -62,10 +63,6 @@ class Status < ApplicationRecord
update_index('statuses', :proper)
update_index('public_statuses', :proper)
enum :visibility, { public: 0, unlisted: 1, private: 2, direct: 3, limited: 4, public_unlisted: 10, login: 11 }, suffix: :visibility, validate: true
enum :searchability, { public: 0, private: 1, direct: 2, limited: 3, unsupported: 4, public_unlisted: 10 }, suffix: :searchability
enum :limited_scope, { none: 0, mutual: 1, circle: 2, personal: 3, reply: 4 }, suffix: :limited
belongs_to :application, class_name: 'Doorkeeper::Application', optional: true
belongs_to :account, inverse_of: :statuses
@ -130,7 +127,6 @@ class Status < ApplicationRecord
validates_with StatusLengthValidator
validates_with DisallowedHashtagsValidator
validates :reblog, uniqueness: { scope: :account }, if: :reblog?
validates :visibility, exclusion: { in: %w(direct limited) }, if: :reblog?
accepts_nested_attributes_for :poll
@ -159,11 +155,6 @@ class Status < ApplicationRecord
scope :tagged_with_none, lambda { |tag_ids|
where('NOT EXISTS (SELECT * FROM statuses_tags forbidden WHERE forbidden.status_id = statuses.id AND forbidden.tag_id IN (?))', tag_ids)
}
scope :unset_searchability, -> { where(searchability: nil, reblog_of_id: nil) }
scope :distributable_visibility, -> { where(visibility: %i(public public_unlisted login unlisted)) }
scope :distributable_visibility_for_anonymous, -> { where(visibility: %i(public public_unlisted unlisted)) }
scope :list_eligible_visibility, -> { where(visibility: %i(public public_unlisted login unlisted private)) }
scope :not_direct_visibility, -> { where.not(visibility: :direct) }
after_create_commit :trigger_create_webhooks
after_update_commit :trigger_update_webhooks
@ -176,8 +167,6 @@ class Status < ApplicationRecord
before_validation :prepare_contents, if: :local?
before_validation :set_reblog
before_validation :set_visibility
before_validation :set_searchability
before_validation :set_conversation
before_validation :set_local
@ -305,16 +294,6 @@ class Status < ApplicationRecord
PreviewCardsStatus.where(status_id: id).delete_all
end
def hidden?
!distributable?
end
def distributable?
public_visibility? || unlisted_visibility? || public_unlisted_visibility?
end
alias sign? distributable?
def with_media?
media_attachments.any?
end
@ -521,39 +500,6 @@ class Status < ApplicationRecord
end
class << self
def selectable_visibilities
selectable_all_visibilities - %w(mutual circle reply direct)
end
def selectable_all_visibilities
vs = %w(public public_unlisted login unlisted private mutual circle reply direct)
vs -= %w(public_unlisted) unless Setting.enable_public_unlisted_visibility
vs -= %w(public) unless Setting.enable_public_visibility
vs
end
def selectable_reblog_visibilities
%w(unset) + selectable_visibilities
end
def all_visibilities
visibilities.keys
end
def selectable_searchabilities
ss = searchabilities.keys - %w(unsupported)
ss -= %w(public_unlisted) unless Setting.enable_public_unlisted_visibility
ss
end
def selectable_searchabilities_for_search
searchabilities.keys - %w(public_unlisted unsupported)
end
def all_searchabilities
searchabilities.keys - %w(unlisted login unsupported)
end
def favourites_map(status_ids, account_id)
Favourite.select(:status_id).where(status_id: status_ids).where(account_id: account_id).each_with_object({}) { |f, h| h[f.status_id] = true }
end
@ -660,25 +606,6 @@ class Status < ApplicationRecord
update_column(:poll_id, poll.id) if association(:poll).loaded? && poll.present?
end
def set_visibility
self.visibility = reblog.visibility if reblog? && visibility.nil?
self.visibility = (account.locked? ? :private : :public) if visibility.nil?
end
def set_searchability
return if searchability.nil?
self.searchability = if %w(public public_unlisted login unlisted).include?(visibility)
searchability
elsif visibility == 'limited' || visibility == 'direct'
searchability == 'limited' ? :limited : :direct
elsif visibility == 'private'
searchability == 'public' || searchability == 'public_unlisted' ? :private : searchability
else
:direct
end
end
def set_conversation
self.thread = thread.reblog if thread&.reblog?

View file

@ -36,11 +36,11 @@ class Tag < ApplicationRecord
has_one :trend, class_name: 'TagTrend', inverse_of: :tag, dependent: :destroy
HASHTAG_SEPARATORS = "_\u00B7\u30FB\u200c"
HASHTAG_FIRST_SEQUENCE_CHUNK_ONE = "[[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}]"
HASHTAG_FIRST_SEQUENCE_CHUNK_TWO = "[[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_]"
HASHTAG_FIRST_SEQUENCE = "(#{HASHTAG_FIRST_SEQUENCE_CHUNK_ONE}#{HASHTAG_FIRST_SEQUENCE_CHUNK_TWO})"
HASHTAG_FIRST_SEQUENCE_CHUNK_ONE = "[[:word:]_][[:word:]#{HASHTAG_SEPARATORS}]*[[:alpha:]#{HASHTAG_SEPARATORS}]".freeze
HASHTAG_FIRST_SEQUENCE_CHUNK_TWO = "[[:word:]#{HASHTAG_SEPARATORS}]*[[:word:]_]".freeze
HASHTAG_FIRST_SEQUENCE = "(#{HASHTAG_FIRST_SEQUENCE_CHUNK_ONE}#{HASHTAG_FIRST_SEQUENCE_CHUNK_TWO})".freeze
HASHTAG_LAST_SEQUENCE = '([[:word:]_]*[[:alpha:]][[:word:]_]*)'
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}"
HASHTAG_NAME_PAT = "#{HASHTAG_FIRST_SEQUENCE}|#{HASHTAG_LAST_SEQUENCE}".freeze
HASHTAG_RE = %r{(?<![=/)\p{Alnum}])#(#{HASHTAG_NAME_PAT})}
HASHTAG_NAME_RE = /\A(#{HASHTAG_NAME_PAT})\z/i

View file

@ -16,7 +16,7 @@ class Trends::Links < Trends::Base
class Query < Trends::Query
def to_arel
scope = PreviewCard.joins(:trend).reorder(score: :desc)
scope = scope.reorder(language_order_clause.desc, score: :desc) if preferred_languages.present?
scope = scope.reorder(language_order_clause, score: :desc) if preferred_languages.present?
scope = scope.merge(PreviewCardTrend.allowed) if @allowed
scope = scope.offset(@offset) if @offset.present?
scope = scope.limit(@limit) if @limit.present?
@ -25,8 +25,8 @@ class Trends::Links < Trends::Base
private
def language_order_clause
Arel::Nodes::Case.new.when(PreviewCardTrend.arel_table[:language].in(preferred_languages)).then(1).else(0)
def trend_class
PreviewCardTrend
end
end

View file

@ -94,6 +94,16 @@ class Trends::Query
to_arel.to_a
end
def language_order_clause
Arel::Nodes::Case.new.when(language_is_preferred).then(1).else(0).desc
end
def language_is_preferred
trend_class
.arel_table[:language]
.in(preferred_languages)
end
def preferred_languages
if @account&.chosen_languages.present?
@account.chosen_languages

View file

@ -15,7 +15,7 @@ class Trends::Statuses < Trends::Base
class Query < Trends::Query
def to_arel
scope = Status.joins(:trend).reorder(score: :desc)
scope = scope.reorder(language_order_clause.desc, score: :desc) if preferred_languages.present?
scope = scope.reorder(language_order_clause, score: :desc) if preferred_languages.present?
scope = scope.merge(StatusTrend.allowed) if @allowed
scope = scope.not_excluded_by_account(@account).not_domain_blocked_by_account(@account) if @account.present?
scope = scope.offset(@offset) if @offset.present?
@ -25,8 +25,8 @@ class Trends::Statuses < Trends::Base
private
def language_order_clause
Arel::Nodes::Case.new.when(StatusTrend.arel_table[:language].in(preferred_languages)).then(1).else(0)
def trend_class
StatusTrend
end
end

View file

@ -15,7 +15,8 @@ class Trends::Tags < Trends::Base
class Query < Trends::Query
def to_arel
scope = Tag.joins(:trend).reorder(language_order_clause.desc, score: :desc)
scope = Tag.joins(:trend).reorder(score: :desc)
scope = scope.reorder(language_order_clause, score: :desc) if preferred_languages.present?
scope = scope.merge(TagTrend.allowed) if @allowed
scope = scope.offset(@offset) if @offset.present?
scope = scope.limit(@limit) if @limit.present?
@ -24,8 +25,8 @@ class Trends::Tags < Trends::Base
private
def language_order_clause
Arel::Nodes::Case.new.when(TagTrend.arel_table[:language].in(preferred_languages)).then(1).else(0)
def trend_class
TagTrend
end
end

View file

@ -44,6 +44,7 @@ class UserRole < ApplicationRecord
NOBODY_POSITION = -1
POSITION_LIMIT = (2**31) - 1
CSS_COLORS = /\A#?(?:[A-F0-9]{3}){1,2}\z/i # CSS-style hex colors
module Flags
NONE = 0
@ -95,7 +96,7 @@ class UserRole < ApplicationRecord
attr_writer :current_account
validates :name, presence: true, unless: :everyone?
validates :color, format: { with: /\A#?(?:[A-F0-9]{3}){1,2}\z/i }, unless: -> { color.blank? }
validates :color, format: { with: CSS_COLORS }, if: :color?
validates :position, numericality: { in: (-POSITION_LIMIT..POSITION_LIMIT) }
validate :validate_permissions_elevation

View file

@ -5,10 +5,11 @@
# Table name: web_push_subscriptions
#
# id :bigint(8) not null, primary key
# endpoint :string not null
# key_p256dh :string not null
# key_auth :string not null
# data :json
# endpoint :string not null
# key_auth :string not null
# key_p256dh :string not null
# standard :boolean default(FALSE), not null
# created_at :datetime not null
# updated_at :datetime not null
# access_token_id :bigint(8)

View file

@ -6,7 +6,7 @@ class AccountWarningPolicy < ApplicationPolicy
end
def appeal?
target? && record.created_at >= Appeal::MAX_STRIKE_AGE.ago
target? && record.appeal_eligible?
end
private

View file

@ -1,7 +1,7 @@
# frozen_string_literal: true
class LanguagePresenter < ActiveModelSerializers::Model
attributes :code, :name, :native_name
attributes :code, :name
def initialize(code)
super()
@ -13,8 +13,4 @@ class LanguagePresenter < ActiveModelSerializers::Model
def name
@item[0]
end
def native_name
@item[1]
end
end

View file

@ -44,7 +44,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
delegate :suspended?, :instance_actor?, to: :object
def id
object.instance_actor? ? instance_actor_url : account_url(object)
ActivityPub::TagManager.instance.uri_for(object)
end
def type
@ -60,27 +60,27 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer
end
def following
account_following_index_url(object)
ActivityPub::TagManager.instance.following_uri_for(object)
end
def followers
account_followers_url(object)
ActivityPub::TagManager.instance.followers_uri_for(object)
end
def inbox
object.instance_actor? ? instance_actor_inbox_url : account_inbox_url(object)
ActivityPub::TagManager.instance.inbox_uri_for(object)
end
def outbox
object.instance_actor? ? instance_actor_outbox_url : account_outbox_url(object)
ActivityPub::TagManager.instance.outbox_uri_for(object)
end
def featured
account_collection_url(object, :featured)
ActivityPub::TagManager.instance.collection_uri_for(object, :featured)
end
def featured_tags
account_collection_url(object, :tags)
ActivityPub::TagManager.instance.collection_uri_for(object, :tags)
end
def endpoints

View file

@ -38,6 +38,6 @@ class ActivityPub::AddSerializer < ActivityPub::Serializer
end
def target
account_collection_url(object.account, :featured)
ActivityPub::TagManager.instance.collection_uri_for(object.account, :featured)
end
end

View file

@ -38,6 +38,6 @@ class ActivityPub::RemoveSerializer < ActivityPub::Serializer
end
def target
account_collection_url(object.account, :featured)
ActivityPub::TagManager.instance.collection_uri_for(object.account, :featured)
end
end

View file

@ -8,8 +8,4 @@ class REST::ScheduledStatusSerializer < ActiveModel::Serializer
def id
object.id.to_s
end
def params
object.params.without('application_id')
end
end

View file

@ -1,7 +1,9 @@
# frozen_string_literal: true
class REST::WebPushSubscriptionSerializer < ActiveModel::Serializer
attributes :id, :endpoint, :alerts, :server_key, :policy
attributes :id, :endpoint, :standard, :alerts, :server_key, :policy
delegate :standard, to: :object
def alerts
(object.data&.dig('alerts') || {}).each_with_object({}) { |(k, v), h| h[k] = ActiveModel::Type::Boolean.new.cast(v) }

View file

@ -13,7 +13,7 @@ class WebfingerSerializer < ActiveModel::Serializer
if object.instance_actor?
[instance_actor_url]
else
[short_account_url(object), account_url(object)]
[short_account_url(object), ActivityPub::TagManager.instance.uri_for(object)]
end
end
@ -43,6 +43,6 @@ class WebfingerSerializer < ActiveModel::Serializer
end
def self_href
object.instance_actor? ? instance_actor_url : account_url(object)
ActivityPub::TagManager.instance.uri_for(object)
end
end

View file

@ -11,6 +11,8 @@ class ActivityPub::ProcessAccountService < BaseService
SCAN_SEARCHABILITY_RE = /\[searchability:(public|followers|reactors|private)\]/
SCAN_SEARCHABILITY_FEDIBIRD_RE = /searchable_by_(all_users|followers_only|reacted_users_only|nobody)/
VALID_URI_SCHEMES = %w(http https).freeze
# Should be called with confirmed valid JSON
# and WebFinger-resolved username and domain
def call(username, domain, json, options = {}) # rubocop:disable Metrics/PerceivedComplexity
@ -113,16 +115,28 @@ class ActivityPub::ProcessAccountService < BaseService
end
def set_immediate_protocol_attributes!
@account.inbox_url = @json['inbox'] || ''
@account.outbox_url = @json['outbox'] || ''
@account.shared_inbox_url = (@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox']) || ''
@account.followers_url = @json['followers'] || ''
@account.inbox_url = valid_collection_uri(@json['inbox'])
@account.outbox_url = valid_collection_uri(@json['outbox'])
@account.shared_inbox_url = valid_collection_uri(@json['endpoints'].is_a?(Hash) ? @json['endpoints']['sharedInbox'] : @json['sharedInbox'])
@account.followers_url = valid_collection_uri(@json['followers'])
@account.url = url || @uri
@account.uri = @uri
@account.actor_type = actor_type
@account.created_at = @json['published'] if @json['published'].present?
end
def valid_collection_uri(uri)
uri = uri.first if uri.is_a?(Array)
uri = uri['id'] if uri.is_a?(Hash)
return '' unless uri.is_a?(String)
parsed_uri = Addressable::URI.parse(uri)
VALID_URI_SCHEMES.include?(parsed_uri.scheme) && parsed_uri.host.present? ? parsed_uri : ''
rescue Addressable::URI::InvalidURIError
''
end
def set_immediate_attributes!
@account.featured_collection_url = @json['featured'] || ''
@account.display_name = @json['name'] || ''
@ -398,10 +412,11 @@ class ActivityPub::ProcessAccountService < BaseService
end
def collection_info(type)
return [nil, nil] if @json[type].blank?
collection_uri = valid_collection_uri(@json[type])
return [nil, nil] if collection_uri.blank?
return @collections[type] if @collections.key?(type)
collection = fetch_resource_without_id_validation(@json[type])
collection = fetch_resource_without_id_validation(collection_uri)
total_items = collection.is_a?(Hash) && collection['totalItems'].present? && collection['totalItems'].is_a?(Numeric) ? collection['totalItems'] : nil
has_first_page = collection.is_a?(Hash) && collection['first'].present?

View file

@ -65,7 +65,7 @@ class DeleteAccountService < BaseService
scheduled_expiration_statuses
status_pins
tag_follows
)
).freeze
ASSOCIATIONS_ON_DESTROY = %w(
reports

View file

@ -19,7 +19,19 @@ class Web::PushNotificationWorker
# in the meantime, so we have to double-check before proceeding
return unless @notification.activity.present? && @subscription.pushable?(@notification)
payload = web_push_request.encrypt(push_notification_json)
if web_push_request.legacy
perform_legacy_request
else
perform_standard_request
end
rescue ActiveRecord::RecordNotFound
true
end
private
def perform_legacy_request
payload = web_push_request.legacy_encrypt(push_notification_json)
request_pool.with(web_push_request.audience) do |http_client|
request = Request.new(:post, web_push_request.endpoint, body: payload.fetch(:ciphertext), http_client: http_client)
@ -31,28 +43,48 @@ class Web::PushNotificationWorker
'Content-Encoding' => 'aesgcm',
'Encryption' => "salt=#{Webpush.encode64(payload.fetch(:salt)).delete('=')}",
'Crypto-Key' => "dh=#{Webpush.encode64(payload.fetch(:server_public_key)).delete('=')};#{web_push_request.crypto_key_header}",
'Authorization' => web_push_request.authorization_header,
'Authorization' => web_push_request.legacy_authorization_header,
'Unsubscribe-URL' => subscription_url
)
request.perform do |response|
# If the server responds with an error in the 4xx range
# that isn't about rate-limiting or timeouts, we can
# assume that the subscription is invalid or expired
# and must be removed
if (400..499).cover?(response.code) && ![408, 429].include?(response.code)
@subscription.destroy!
elsif !(200...300).cover?(response.code)
raise Mastodon::UnexpectedResponseError, response
end
end
send(request)
end
rescue ActiveRecord::RecordNotFound
true
end
private
def perform_standard_request
payload = web_push_request.standard_encrypt(push_notification_json)
request_pool.with(web_push_request.audience) do |http_client|
request = Request.new(:post, web_push_request.endpoint, body: payload, http_client: http_client)
request.add_headers(
'Content-Type' => 'application/octet-stream',
'Ttl' => TTL.to_s,
'Urgency' => URGENCY,
'Content-Encoding' => 'aes128gcm',
'Authorization' => web_push_request.standard_authorization_header,
'Unsubscribe-URL' => subscription_url,
'Content-Length' => payload.length.to_s
)
send(request)
end
end
def send(request)
request.perform do |response|
# If the server responds with an error in the 4xx range
# that isn't about rate-limiting or timeouts, we can
# assume that the subscription is invalid or expired
# and must be removed
if (400..499).cover?(response.code) && ![408, 429].include?(response.code)
@subscription.destroy!
elsif !(200...300).cover?(response.code)
raise Mastodon::UnexpectedResponseError, response
end
end
end
def web_push_request
@web_push_request || WebPushRequest.new(@subscription)