Merge commit '366856f3bcdc2ff008b04e493a5de317ab83d5d0' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2025-11-19 21:49:09 +01:00
45 changed files with 374 additions and 202 deletions

View File

@@ -24,7 +24,7 @@ gem 'ruby-vips', '~> 2.2', require: false
gem 'active_model_serializers', '~> 0.10'
gem 'addressable', '~> 2.8'
gem 'bootsnap', '~> 1.18.0', require: false
gem 'bootsnap', '~> 1.19.0', require: false
gem 'browser'
gem 'charlock_holmes', '~> 0.7.7'
gem 'chewy', '~> 7.3'

View File

@@ -129,7 +129,7 @@ GEM
binding_of_caller (1.0.1)
debug_inspector (>= 1.2.0)
blurhash (0.1.8)
bootsnap (1.18.6)
bootsnap (1.19.0)
msgpack (~> 1.2)
brakeman (7.1.1)
racc
@@ -349,7 +349,7 @@ GEM
azure-blob (~> 0.5.2)
hashie (~> 5.0)
jmespath (1.6.2)
json (2.15.2)
json (2.16.0)
json-canonicalization (1.0.0)
json-jwt (1.17.0)
activesupport (>= 4.2)
@@ -446,7 +446,7 @@ GEM
mime-types-data (3.2025.0924)
mini_mime (1.1.5)
mini_portile2 (2.8.9)
minitest (5.26.0)
minitest (5.26.1)
msgpack (1.8.0)
multi_json (1.17.0)
mutex_m (0.3.0)
@@ -759,7 +759,7 @@ GEM
rubocop-ast (>= 1.47.1, < 2.0)
ruby-progressbar (~> 1.7)
unicode-display_width (>= 2.4.0, < 4.0)
rubocop-ast (1.47.1)
rubocop-ast (1.48.0)
parser (>= 3.3.7.2)
prism (~> 1.4)
rubocop-capybara (2.22.1)
@@ -778,10 +778,10 @@ GEM
rack (>= 1.1)
rubocop (>= 1.75.0, < 2.0)
rubocop-ast (>= 1.44.0, < 2.0)
rubocop-rspec (3.7.0)
rubocop-rspec (3.8.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec_rails (2.31.0)
rubocop (~> 1.81)
rubocop-rspec_rails (2.32.0)
lint_roller (~> 1.1)
rubocop (~> 1.72, >= 1.72.1)
rubocop-rspec (~> 3.5)
@@ -941,7 +941,7 @@ DEPENDENCIES
better_errors (~> 2.9)
binding_of_caller (~> 1.0)
blurhash (~> 0.1)
bootsnap (~> 1.18.0)
bootsnap (~> 1.19.0)
brakeman (~> 7.0)
browser
bundler-audit (~> 0.9)

View File

@@ -9,7 +9,7 @@ module Admin
@site_upload.destroy!
redirect_back fallback_location: admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
redirect_back_or_to admin_settings_path, notice: I18n.t('admin.site_uploads.destroyed_msg')
end
private

View File

@@ -180,15 +180,15 @@ export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
if (shouldHandleEvent) {
const matchCandidates: {
handler: (event: KeyboardEvent) => void;
// A candidate will be have an undefined handler if it's matched,
// but handled in a parent component rather than this one.
handler: ((event: KeyboardEvent) => void) | undefined;
priority: number;
}[] = [];
(Object.keys(hotkeyMatcherMap) as HotkeyName[]).forEach(
(handlerName) => {
const handler = handlersRef.current[handlerName];
if (handler) {
const hotkeyMatcher = hotkeyMatcherMap[handlerName];
const { isMatch, priority } = hotkeyMatcher(
@@ -199,7 +199,6 @@ export function useHotkeys<T extends HTMLElement>(handlers: HandlerMap) {
if (isMatch) {
matchCandidates.push({ handler, priority });
}
}
},
);

View File

@@ -38,7 +38,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
return (
<Link
className={classNames('mention hashtag', className)}
to={`/tags/${hashtag}`}
to={`/tags/${encodeURIComponent(hashtag)}`}
rel='tag'
data-menu-hashtag={hashtagAccountId}
>
@@ -71,7 +71,7 @@ export const HandledLink: FC<HandledLinkProps & ComponentProps<'a'>> = ({
return (
<a
{...props}
href={encodeURI(href)}
href={href}
title={href}
className={classNames('unhandled-link', className)}
target='_blank'

View File

@@ -13,6 +13,7 @@ import {
import { pasteLinkCompose } from 'mastodon/actions/compose_typed';
import { openModal } from 'mastodon/actions/modal';
import { PRIVATE_QUOTE_MODAL_ID } from 'mastodon/features/ui/components/confirmation_modals/private_quote_notify';
import { me } from 'mastodon/initial_state';
import ComposeForm from '../components/compose_form';
@@ -53,6 +54,7 @@ const mapStateToProps = state => ({
quoteToPrivate:
!!state.getIn(['compose', 'quoted_status_id'])
&& state.getIn(['compose', 'privacy']) === 'private'
&& state.getIn(['statuses', state.getIn(['compose', 'quoted_status_id']), 'account']) !== me
&& !state.getIn(['settings', 'dismissed_banners', PRIVATE_QUOTE_MODAL_ID]),
isInReply: state.getIn(['compose', 'in_reply_to']) !== null,
lang: state.getIn(['compose', 'language']),

View File

@@ -1,6 +1,7 @@
import { initialState } from '@/mastodon/initial_state';
import { toSupportedLocale } from './locale';
import type { LocaleOrCustom } from './types';
import { emojiLogger } from './utils';
// eslint-disable-next-line import/default -- Importing via worker loader.
import EmojiWorker from './worker?worker&inline';
@@ -24,19 +25,17 @@ export function initializeEmoji() {
}
if (worker) {
// Assign worker to const to make TS happy inside the event listener.
const thisWorker = worker;
const timeoutId = setTimeout(() => {
log('worker is not ready after timeout');
worker = null;
void fallbackLoad();
}, WORKER_TIMEOUT);
thisWorker.addEventListener('message', (event: MessageEvent<string>) => {
worker.addEventListener('message', (event: MessageEvent<string>) => {
const { data: message } = event;
if (message === 'ready') {
log('worker ready, loading data');
clearTimeout(timeoutId);
thisWorker.postMessage('custom');
messageWorker('custom');
void loadEmojiLocale(userLocale);
// Load English locale as well, because people are still used to
// using it from before we supported other locales.
@@ -55,20 +54,35 @@ export function initializeEmoji() {
async function fallbackLoad() {
log('falling back to main thread for loading');
const { importCustomEmojiData } = await import('./loader');
await importCustomEmojiData();
const emojis = await importCustomEmojiData();
if (emojis) {
log('loaded %d custom emojis', emojis.length);
}
await loadEmojiLocale(userLocale);
if (userLocale !== 'en') {
await loadEmojiLocale('en');
}
}
export async function loadEmojiLocale(localeString: string) {
async function loadEmojiLocale(localeString: string) {
const locale = toSupportedLocale(localeString);
const { importEmojiData, localeToPath } = await import('./loader');
if (worker) {
worker.postMessage(locale);
const path = await localeToPath(locale);
log('asking worker to load locale %s from %s', locale, path);
messageWorker(locale, path);
} else {
const { importEmojiData } = await import('./loader');
await importEmojiData(locale);
const emojis = await importEmojiData(locale);
if (emojis) {
log('loaded %d emojis to locale %s', emojis.length, locale);
}
}
}
function messageWorker(locale: LocaleOrCustom, path?: string) {
if (!worker) {
return;
}
worker.postMessage({ locale, path });
}

View File

@@ -8,44 +8,64 @@ import {
putLatestEtag,
} from './database';
import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale';
import type { CustomEmojiData, LocaleOrCustom } from './types';
import { emojiLogger } from './utils';
import type { CustomEmojiData } from './types';
const log = emojiLogger('loader');
export async function importEmojiData(localeString: string) {
export async function importEmojiData(localeString: string, path?: string) {
const locale = toSupportedLocale(localeString);
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale);
// Validate the provided path.
if (path && !/^[/a-z]*\/packs\/assets\/compact-\w+\.json$/.test(path)) {
throw new Error('Invalid path for emoji data');
} else {
// Otherwise get the path if not provided.
path ??= await localeToPath(locale);
}
const emojis = await fetchAndCheckEtag<CompactEmoji[]>(locale, path);
if (!emojis) {
return;
}
const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData(emojis);
log('loaded %d for %s locale', flattenedEmojis.length, locale);
await putEmojiData(flattenedEmojis, locale);
return flattenedEmojis;
}
export async function importCustomEmojiData() {
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>('custom');
const emojis = await fetchAndCheckEtag<CustomEmojiData[]>(
'custom',
'/api/v1/custom_emojis',
);
if (!emojis) {
return;
}
log('loaded %d custom emojis', emojis.length);
await putCustomEmojiData(emojis);
return emojis;
}
async function fetchAndCheckEtag<ResultType extends object[]>(
localeOrCustom: LocaleOrCustom,
const modules = import.meta.glob<string>(
'../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
export function localeToPath(locale: Locale) {
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}
export async function fetchAndCheckEtag<ResultType extends object[]>(
localeString: string,
path: string,
): Promise<ResultType | null> {
const locale = toSupportedLocaleOrCustom(localeOrCustom);
const locale = toSupportedLocaleOrCustom(localeString);
// Use location.origin as this script may be loaded from a CDN domain.
const url = new URL(location.origin);
if (locale === 'custom') {
url.pathname = '/api/v1/custom_emojis';
} else {
const modulePath = await localeToPath(locale);
url.pathname = modulePath;
}
const url = new URL(path, location.origin);
const oldEtag = await loadLatestEtag(locale);
const response = await fetch(url, {
@@ -60,38 +80,20 @@ async function fetchAndCheckEtag<ResultType extends object[]>(
}
if (!response.ok) {
throw new Error(
`Failed to fetch emoji data for ${localeOrCustom}: ${response.statusText}`,
`Failed to fetch emoji data for ${locale}: ${response.statusText}`,
);
}
const data = (await response.json()) as ResultType;
if (!Array.isArray(data)) {
throw new Error(
`Unexpected data format for ${localeOrCustom}: expected an array`,
);
throw new Error(`Unexpected data format for ${locale}: expected an array`);
}
// Store the ETag for future requests
const etag = response.headers.get('ETag');
if (etag) {
await putLatestEtag(etag, localeOrCustom);
await putLatestEtag(etag, localeString);
}
return data;
}
const modules = import.meta.glob<string>(
'../../../../../node_modules/emojibase-data/**/compact.json',
{
query: '?url',
import: 'default',
},
);
function localeToPath(locale: Locale) {
const key = `../../../../../node_modules/emojibase-data/${locale}/compact.json`;
if (!modules[key] || typeof modules[key] !== 'function') {
throw new Error(`Unsupported locale: ${locale}`);
}
return modules[key]();
}

View File

@@ -162,7 +162,7 @@ describe('loadEmojiDataToState', () => {
const dbCall = vi
.spyOn(db, 'loadEmojiByHexcode')
.mockRejectedValue(new db.LocaleNotLoadedError('en'));
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce();
vi.spyOn(loader, 'importEmojiData').mockResolvedValueOnce(undefined);
const consoleCall = vi
.spyOn(console, 'warn')
.mockImplementationOnce(() => null);

View File

@@ -1,18 +1,25 @@
import { importEmojiData, importCustomEmojiData } from './loader';
import { importCustomEmojiData, importEmojiData } from './loader';
addEventListener('message', handleMessage);
self.postMessage('ready'); // After the worker is ready, notify the main thread
function handleMessage(event: MessageEvent<string>) {
const { data: locale } = event;
void loadData(locale);
function handleMessage(event: MessageEvent<{ locale: string; path?: string }>) {
const {
data: { locale, path },
} = event;
void loadData(locale, path);
}
async function loadData(locale: string) {
if (locale !== 'custom') {
await importEmojiData(locale);
async function loadData(locale: string, path?: string) {
let importCount: number | undefined;
if (locale === 'custom') {
importCount = (await importCustomEmojiData())?.length;
} else if (path) {
importCount = (await importEmojiData(locale, path))?.length;
} else {
await importCustomEmojiData();
throw new Error('Path is required for loading locale emoji data');
}
if (importCount) {
self.postMessage(`loaded ${importCount} emojis into ${locale}`);
}
self.postMessage(`loaded ${locale}`);
}

View File

@@ -4,7 +4,7 @@
@typescript-eslint/no-unsafe-assignment */
import type { CSSProperties } from 'react';
import { useState, useRef, useCallback } from 'react';
import { useState, useRef, useCallback, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
@@ -55,6 +55,8 @@ export const DetailedStatus: React.FC<{
pictureInPicture: any;
onToggleHidden?: (status: any) => void;
onToggleMediaVisibility?: () => void;
ancestors?: number;
multiColumn?: boolean;
}> = ({
status,
onOpenMedia,
@@ -69,6 +71,8 @@ export const DetailedStatus: React.FC<{
pictureInPicture,
onToggleMediaVisibility,
onToggleHidden,
ancestors = 0,
multiColumn = false,
}) => {
const properStatus = status?.get('reblog') ?? status;
const [height, setHeight] = useState(0);
@@ -123,6 +127,30 @@ export const DetailedStatus: React.FC<{
if (onTranslate) onTranslate(status);
}, [onTranslate, status]);
// The component is managed and will change if the status changes
// Ancestors can increase when loading a thread, in which case we want to scroll,
// or decrease if a post is deleted, in which case we don't want to mess with it
const previousAncestors = useRef(-1);
useEffect(() => {
if (nodeRef.current && previousAncestors.current < ancestors) {
nodeRef.current.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header, so compensate for that.
if (!multiColumn) {
const offset = document
.querySelector('.column-header__wrapper')
?.getBoundingClientRect().bottom;
if (offset) {
const scrollingElement = document.scrollingElement ?? document.body;
scrollingElement.scrollBy(0, -offset);
}
}
}
previousAncestors.current = ancestors;
}, [ancestors, multiColumn]);
if (!properStatus) {
return null;
}

View File

@@ -164,8 +164,6 @@ class Status extends ImmutablePureComponent {
componentDidMount () {
attachFullscreenListener(this.onFullScreenChange);
this._scrollStatusIntoView();
}
UNSAFE_componentWillReceiveProps (nextProps) {
@@ -487,35 +485,11 @@ class Status extends ImmutablePureComponent {
this.statusNode = c;
};
_scrollStatusIntoView () {
const { status, multiColumn } = this.props;
if (status) {
requestIdleCallback(() => {
this.statusNode?.scrollIntoView(true);
// In the single-column interface, `scrollIntoView` will put the post behind the header,
// so compensate for that.
if (!multiColumn) {
const offset = document.querySelector('.column-header__wrapper')?.getBoundingClientRect()?.bottom;
if (offset) {
const scrollingElement = document.scrollingElement || document.body;
scrollingElement.scrollBy(0, -offset);
}
}
});
}
}
componentDidUpdate (prevProps) {
const { status, ancestorsIds, descendantsIds } = this.props;
const { status, descendantsIds } = this.props;
const isSameStatus = status && (prevProps.status?.get('id') === status.get('id'));
if (status && (ancestorsIds.length > prevProps.ancestorsIds.length || !isSameStatus)) {
this._scrollStatusIntoView();
}
// Only highlight replies after the initial load
if (prevProps.descendantsIds.length && isSameStatus) {
const newRepliesIds = difference(descendantsIds, prevProps.descendantsIds);
@@ -619,6 +593,8 @@ class Status extends ImmutablePureComponent {
showMedia={this.state.showMedia}
onToggleMediaVisibility={this.handleToggleMediaVisibility}
pictureInPicture={pictureInPicture}
ancestors={this.props.ancestorsIds.length}
multiColumn={multiColumn}
/>
<ActionBar

View File

@@ -903,6 +903,7 @@
"status.edited_x_times": "Upraveno {count, plural, one {{count}krát} few {{count}krát} many {{count}krát} other {{count}krát}}",
"status.embed": "Získejte kód pro vložení",
"status.favourite": "Oblíbit",
"status.favourites_count": "{count, plural, one {{counter} oblíbený} few {{counter} oblíbené} many {{counter} oblíbených} other {{counter} oblíbených}}",
"status.filter": "Filtrovat tento příspěvek",
"status.history.created": "Uživatel {name} vytvořil {date}",
"status.history.edited": "Uživatel {name} upravil {date}",
@@ -937,12 +938,14 @@
"status.quotes.empty": "Tento příspěvek zatím nikdo necitoval. Pokud tak někdo učiní, uvidíte to zde.",
"status.quotes.local_other_disclaimer": "Citace zamítnuté autorem nebudou zobrazeny.",
"status.quotes.remote_other_disclaimer": "Pouze citace z {domain} zde budou zaručeně ukázány. Citace zamítnuté autorem nebudou zobrazeny.",
"status.quotes_count": "{count, plural, one {{counter} citace} few {{counter} citace} many {{counter} citací} other {{counter} citací}}",
"status.read_more": "Číst více",
"status.reblog": "Boostnout",
"status.reblog_or_quote": "Boostnout nebo citovat",
"status.reblog_private": "Sdílejte znovu se svými sledujícími",
"status.reblogged_by": "Uživatel {name} boostnul",
"status.reblogs.empty": "Tento příspěvek ještě nikdo neboostnul. Pokud to někdo udělá, zobrazí se zde.",
"status.reblogs_count": "{count, plural, one {{counter} boost} few {{counter} boosty} many {{counter} boostů} other {{counter} boostů}}",
"status.redraft": "Smazat a přepsat",
"status.remove_bookmark": "Odstranit ze záložek",
"status.remove_favourite": "Odebrat z oblíbených",

View File

@@ -903,6 +903,7 @@
"status.edited_x_times": "Golygwyd {count, plural, one {{count} gwaith} other {{count} gwaith}}",
"status.embed": "Cael y cod mewnblannu",
"status.favourite": "Ffafrio",
"status.favourites_count": "{count, plural, one {{counter} ffefryn} other {{counter} ffefryn}}",
"status.filter": "Hidlo'r postiad hwn",
"status.history.created": "Crëwyd gan {name} {date}",
"status.history.edited": "Golygwyd gan {name} {date}",
@@ -937,12 +938,14 @@
"status.quotes.empty": "Does neb wedi dyfynnu'r postiad hwn eto. Pan fydd rhywun yn gwneud hynny, bydd yn ymddangos yma.",
"status.quotes.local_other_disclaimer": "Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.",
"status.quotes.remote_other_disclaimer": "Dim ond dyfyniadau o {domain} sy'n siŵr o gael eu dangos yma. Bydd dyfyniadau wedi'u gwrthod gan yr awdur ddim yn cael eu dangos.",
"status.quotes_count": "{count, plural, one {{counter} dyfyniad} other {{counter} dyfyniad}}",
"status.read_more": "Darllen rhagor",
"status.reblog": "Hybu",
"status.reblog_or_quote": "Hybu neu ddyfynnu",
"status.reblog_private": "Rhannwch eto gyda'ch dilynwyr",
"status.reblogged_by": "Hybodd {name}",
"status.reblogs.empty": "Does neb wedi hybio'r post yma eto. Pan y bydd rhywun yn gwneud, byddent yn ymddangos yma.",
"status.reblogs_count": "{count, plural, one {{counter} hwb} other {{counter} hwb}}",
"status.redraft": "Dileu ac ail lunio",
"status.remove_bookmark": "Tynnu nod tudalen",
"status.remove_favourite": "Tynnu o'r ffefrynnau",

View File

@@ -231,7 +231,7 @@
"confirmations.delete_list.title": "Slet liste?",
"confirmations.discard_draft.confirm": "Kassér og fortsæt",
"confirmations.discard_draft.edit.cancel": "Fortsæt redigering",
"confirmations.discard_draft.edit.message": "Hvis du fortsætter, kasseres alle ændringer, du har foretaget i det indlæg, du er i gang med at redigere.",
"confirmations.discard_draft.edit.message": "Hvis du fortsætter, vil alle ændringer, du har foretaget i det indlæg, du er er ved at redigere, blive slettet.",
"confirmations.discard_draft.edit.title": "Kassér ændringer til dit indlæg?",
"confirmations.discard_draft.post.cancel": "Genoptag udkast",
"confirmations.discard_draft.post.message": "Hvis du fortsætter, kasseres det indlæg, du er i gang med at udforme.",
@@ -507,7 +507,7 @@
"keyboard_shortcuts.pinned": "Åbn liste over fastgjorte indlæg",
"keyboard_shortcuts.profile": "Åbn forfatters profil",
"keyboard_shortcuts.quote": "Citér indlæg",
"keyboard_shortcuts.reply": "Besvar indlægget",
"keyboard_shortcuts.reply": "Besvar indlæg",
"keyboard_shortcuts.requests": "Åbn liste over følgeanmodninger",
"keyboard_shortcuts.search": "Fokusér søgebjælke",
"keyboard_shortcuts.spoilers": "Vis/skjul indholdsadvarsel-felt",
@@ -675,7 +675,7 @@
"notifications.column_settings.filter_bar.category": "Hurtigfiltreringsbjælke",
"notifications.column_settings.follow": "Nye følgere:",
"notifications.column_settings.follow_request": "Nye følgeanmodninger:",
"notifications.column_settings.group": "Gruppere",
"notifications.column_settings.group": "Gruppér",
"notifications.column_settings.mention": "Omtaler:",
"notifications.column_settings.poll": "Afstemningsresultater:",
"notifications.column_settings.push": "Push-notifikationer",
@@ -764,7 +764,7 @@
"privacy_policy.last_updated": "Senest opdateret {date}",
"privacy_policy.title": "Privatlivspolitik",
"quote_error.edit": "Citater kan ikke tilføjes ved redigering af et indlæg.",
"quote_error.poll": "Citering ikke tilladt i afstemninger.",
"quote_error.poll": "Citering er ikke tilladt med afstemninger.",
"quote_error.private_mentions": "Citering er ikke tilladt med direkte omtaler.",
"quote_error.quote": "Kun ét citat ad gangen er tilladt.",
"quote_error.unauthorized": "Du har ikke tilladelse til at citere dette indlæg.",
@@ -867,7 +867,7 @@
"search_results.title": "Søg efter \"{q}\"",
"server_banner.about_active_users": "Personer, som brugte denne server de seneste 30 dage (månedlige aktive brugere)",
"server_banner.active_users": "aktive brugere",
"server_banner.administered_by": "Håndteres af:",
"server_banner.administered_by": "Administreret af:",
"server_banner.is_one_of_many": "{domain} er en af de mange uafhængige Mastodon-servere, du kan bruge for at deltage i fediverset.",
"server_banner.server_stats": "Serverstatstik:",
"sign_in_banner.create_account": "Opret konto",
@@ -902,7 +902,7 @@
"status.edited": "Senest redigeret {date}",
"status.edited_x_times": "Redigeret {count, plural, one {{count} gang} other {{count} gange}}",
"status.embed": "Hent indlejringskode",
"status.favourite": "Favorit",
"status.favourite": "Favoritmarkér",
"status.favourites_count": "{count, plural, one {{counter} favorit} other {{counter} favoritter}}",
"status.filter": "Filtrér dette indlæg",
"status.history.created": "{name} oprettet {date}",
@@ -991,7 +991,7 @@
"units.short.million": "{count} mio.",
"units.short.thousand": "{count} tusind",
"upload_area.title": "Træk og slip for at uploade",
"upload_button.label": "Tilføj billed-, video- eller lydfil(er)",
"upload_button.label": "Tilføj billeder, en video- eller lydfil",
"upload_error.limit": "Grænse for filupload nået.",
"upload_error.poll": "Filupload ikke tilladt for afstemninger.",
"upload_error.quote": "Fil-upload ikke tilladt i citater.",

View File

@@ -242,7 +242,7 @@
"confirmations.follow_to_list.message": "Du musst {name} folgen, um das Profil zu einer Liste hinzufügen zu können.",
"confirmations.follow_to_list.title": "Profil folgen?",
"confirmations.logout.confirm": "Abmelden",
"confirmations.logout.message": "Möchtest du dich wirklich abmelden?",
"confirmations.logout.message": "Bist du sicher, dass du dich abmelden möchtest?",
"confirmations.logout.title": "Abmelden?",
"confirmations.missing_alt_text.confirm": "Bildbeschreibung hinzufügen",
"confirmations.missing_alt_text.message": "Dein Beitrag enthält Medien ohne Bildbeschreibung. Mit ALT-Texten erreichst Du auch Menschen, die blind oder sehbehindert sind.",
@@ -584,8 +584,8 @@
"navigation_bar.follows_and_followers": "Follower und Folge ich",
"navigation_bar.import_export": "Importieren und exportieren",
"navigation_bar.lists": "Listen",
"navigation_bar.live_feed_local": "Live-Feed (lokal)",
"navigation_bar.live_feed_public": "Live-Feed (öffentlich)",
"navigation_bar.live_feed_local": "Live-Feed (Dieser Server)",
"navigation_bar.live_feed_public": "Live-Feed (Alle Server)",
"navigation_bar.logout": "Abmelden",
"navigation_bar.moderation": "Moderation",
"navigation_bar.more": "Mehr",

View File

@@ -233,7 +233,7 @@
"confirmations.discard_draft.edit.cancel": "Palaa muokkaamaan",
"confirmations.discard_draft.edit.message": "Jatkaminen tuhoaa kaikki muutokset, joita olet tehnyt julkaisuun, jota olet parhaillaan muokkaamassa.",
"confirmations.discard_draft.edit.title": "Hylätäänkö luonnosjulkaisusi muutokset?",
"confirmations.discard_draft.post.cancel": "Palaa lunnokseen",
"confirmations.discard_draft.post.cancel": "Palaa luonnokseen",
"confirmations.discard_draft.post.message": "Jatkaminen tuhoaa julkaisun, jota olet parhaillaan laatimassa.",
"confirmations.discard_draft.post.title": "Hylätäänkö luonnosjulkaisusi?",
"confirmations.discard_edit_media.confirm": "Hylkää",

View File

@@ -528,7 +528,7 @@
"limited_account_hint.action": "Afficher le profil quand même",
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
"link_preview.author": "Par {name}",
"link_preview.more_from_author": "Plus via {name}",
"link_preview.more_from_author": "Voir plus de {name}",
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
"lists.add_member": "Ajouter",
"lists.add_to_list": "Ajouter à la liste",

View File

@@ -528,7 +528,7 @@
"limited_account_hint.action": "Afficher le profil quand même",
"limited_account_hint.title": "Ce profil a été masqué par la modération de {domain}.",
"link_preview.author": "Par {name}",
"link_preview.more_from_author": "Plus via {name}",
"link_preview.more_from_author": "Voir plus de {name}",
"link_preview.shares": "{count, plural, one {{counter} message} other {{counter} messages}}",
"lists.add_member": "Ajouter",
"lists.add_to_list": "Ajouter à la liste",

View File

@@ -189,7 +189,7 @@
"notification.update": "{name} nuntium correxit",
"notification_requests.accept": "Accipe",
"notification_requests.confirm_accept_multiple.message": "Tu es accepturus {count, plural, one {una notitia petitionem} other {# notitia petitiones}}. Certus esne procedere vis?",
"notification_requests.confirm_dismiss_multiple.message": "Tu {count, plural, one {unam petitionem notificationis} other {# petitiones notificationum}} abrogāre prōximum es. {count, plural, one {Illa} other {Eae}} facile accessū nōn erit. Certus es tē procedere velle?",
"notification_requests.confirm_dismiss_multiple.message": "Tu {count, plural, one {unam petitionem notificationis} other {# petitiones notificationum}} abrogāre prōximum es. {count, plural, one {it} other {Eae}} facile accessū nōn erit. Certus es tē procedere velle?",
"notifications.filter.all": "Omnia",
"notifications.filter.polls": "Eventus electionis",
"notifications.group": "{count} Notificātiōnēs",
@@ -246,19 +246,102 @@
"status.history.created": "{name} creatum {date}",
"status.history.edited": "{name} correxit {date}",
"status.open": "Expand this status",
"status.quotes.empty": "Nemo hanc commentationem adhuc citavit. Cum quis citaverit, hic apparebit.",
"status.quotes.local_other_disclaimer": "Citationes ab auctore reiec­tæ non monstrabuntur.",
"status.quotes.remote_other_disclaimer": "Tantum citae ex {domain} hic exhiberi praestantur. Citae ab auctore reiectae non exhibebuntur.",
"status.quotes_count": "{count, plural, one {{counter} citatio} other {{counter} citationes}}",
"status.read_more": "Plura lege",
"status.reblog": "Promovere",
"status.reblog_or_quote": "Promovere aut cita",
"status.reblog_private": "Iterum cum sectatoribus tuis communica",
"status.reblogged_by": "{name} adiuvavit",
"status.reblogs.empty": "Nemo hanc publicationem adhuc promovit. Cum quis eam promoveat, hic apparebunt.",
"status.reblogs_count": "{count, plural, one {{counter} incrementum} other {{counter} incrementa}}",
"status.redraft": "Dele et redig",
"status.remove_bookmark": "Tolle signum",
"status.remove_favourite": "Tolle ad delectis",
"status.remove_quote": "Tolle",
"status.replied_in_thread": "In filo responsum",
"status.replied_to": "{name} respondit",
"status.reply": "Respondere",
"status.replyAll": "Responde ad filum",
"status.report": "Referre @{name}",
"status.request_quote": "Pretium petere",
"status.revoke_quote": "Tolle nuntium meum ex nuntio @{name}",
"status.sensitive_warning": "Materia delicata",
"status.share": "Communica",
"status.show_less_all": "Omnibus minus monstra",
"status.show_more_all": "Omnibus plura monstra",
"status.show_original": "Monstra originalem",
"status.title.with_attachments": "{user} publicavit {attachmentCount, plural, one {unum annexum} other {{attachmentCount} annexa}}",
"status.translate": "Converte",
"status.translated_from_with": "Translatum ex {lang} per {provider}",
"status.uncached_media_warning": "Praevisum non praesto est",
"status.unmute_conversation": "Conversationem reserare",
"subscribed_languages.lead": "Tantum epistolae in linguis selectis in domo tua apparebunt et indices temporum post mutationem. Neminem eligatis qui epistolas in omnibus linguis recipiat.",
"subscribed_languages.save": "Servare mutationes",
"subscribed_languages.target": "Muta linguas subscriptas pro {target}",
"tabs_bar.home": "Domi",
"tabs_bar.menu": "Elenchus",
"tabs_bar.notifications": "Acta Vicimediorum",
"tabs_bar.publish": "Nova publicatio",
"tabs_bar.search": "Quaere",
"terms_of_service.effective_as_of": "Valet ex {date}",
"terms_of_service.title": "Termini servitii",
"terms_of_service.upcoming_changes_on": "Mutationes venturae die {date}",
"time_remaining.days": "{number, plural, one {# die} other {# dies}} restant",
"time_remaining.hours": "{number, plural, one {# hora} other {# horae}} restant",
"time_remaining.minutes": "{number, plural, one {# minutum} other {# minuta}} restant",
"time_remaining.moments": "Momenta reliqua",
"time_remaining.seconds": "{number, plural, one {# secundum} other {# secunda}} restant",
"trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {diē prīdiē} other {diēbus praeteritīs {days}}}",
"trends.counter_by_accounts": "{count, plural, one {{counter} persōna} other {{counter} persōnae}} in {days, plural, one {days} other {diēbus praeteritīs {days}}}",
"trends.trending_now": "Nunc in usu",
"ui.beforeunload": "Si Mastodon discesseris, tua epitome peribit.",
"units.short.billion": "{count} millia milionum",
"units.short.million": "{count} milionum",
"units.short.thousand": "{count} millia",
"upload_button.label": "Imaginēs, vīdeō aut fīle audītūs adde",
"upload_area.title": "Trahe et depone ad imponendum",
"upload_button.label": "Adde imagines, pelliculam, aut fasciculum sonorum.",
"upload_error.limit": "Limes onerationis superatus est.",
"upload_error.poll": "Nullis suffragiis licet fascicula imponere.",
"upload_error.quote": "Nullum oneramentum fasciculi cum citationibus permittitur.",
"upload_form.drag_and_drop.instructions": "Ad annexum mediorum tollendum, preme clavem \"Space\" aut \"Enter\". Dum traheis, utere clavibus sagittariis ad annexum mediorum in quamlibet partem movendum. Preme iterum \"Space\" aut \"Enter\" ad annexum mediorum in novo loco deponendum, aut preme \"Escape\" ad desinendum.",
"upload_form.drag_and_drop.on_drag_cancel": "Tractatio revocata est. Adiunctum medium {item} demissum est.",
"upload_form.drag_and_drop.on_drag_end": "Adhaesum medium {item} demissum est.",
"upload_form.drag_and_drop.on_drag_over": "Adhaesum medium {item} motum est.",
"upload_form.drag_and_drop.on_drag_start": "Adhaesum medium {item} sublatum est.",
"upload_form.edit": "Recolere",
"upload_progress.label": "Uploading…"
"upload_progress.label": "Oneratur...",
"upload_progress.processing": "Processus…",
"username.taken": "Illud nomen usoris occupatum est. Aliud tenta.",
"video.close": "Pelliculam claude",
"video.download": "Prehendere fasciculus",
"video.exit_fullscreen": "Exitus ex plenum monitorium",
"video.expand": "Expande pelliculam",
"video.fullscreen": "Plenum monitorium",
"video.hide": "celare pellicula",
"video.mute": "Mutus",
"video.pause": "intermittere",
"video.play": "gignere",
"video.skip_backward": "Redire",
"video.skip_forward": "Progredi",
"video.unmute": "Sordes tollere",
"video.volume_down": "Volumen deminui",
"video.volume_up": "Volumen augete",
"visibility_modal.button_title": "Visibilitatem statuere",
"visibility_modal.direct_quote_warning.text": "Si praesentia configuramenta servaveris, sententia inserta in nexum convertetur.",
"visibility_modal.direct_quote_warning.title": "Citatio in mentitionibus privatis inseriri non possunt.",
"visibility_modal.header": "Visibilitas et interactio",
"visibility_modal.helper.direct_quoting": "Mentiones privatae in Mastodon scriptae ab aliis citari non possunt.",
"visibility_modal.helper.privacy_editing": "Visibilitas mutari non potest postquam nuntius publicatus est.",
"visibility_modal.helper.privacy_private_self_quote": "Citationes propriae nuntiorum privatorum publicari non possunt.",
"visibility_modal.helper.private_quoting": "Nuntii in Mastodon a sequacibus tantum scriptī ab aliis citari non possunt.",
"visibility_modal.helper.unlisted_quoting": "Cum te citant, eorum scriptum etiam ex indicibus popularibus celabitur.",
"visibility_modal.instructions": "Régula quis cum hoc scripto agere possit. Potes etiam ordinationes omnibus scriptis futuris adhibere, navigando ad <link>Praeferentias > Regulas scriptionis praedefinitas</link>.",
"visibility_modal.privacy_label": "Visibilitas",
"visibility_modal.quote_followers": "Sectatores tantum",
"visibility_modal.quote_label": "Quis citare potest",
"visibility_modal.quote_nobody": "Sicut me",
"visibility_modal.quote_public": "quisquis",
"visibility_modal.save": "Servare"
}

View File

@@ -9,7 +9,6 @@ import { me, reduceMotion } from 'mastodon/initial_state';
import ready from 'mastodon/ready';
import { store } from 'mastodon/store';
import { initializeEmoji } from './features/emoji';
import { isProduction, isDevelopment } from './utils/environment';
function main() {
@@ -30,6 +29,7 @@ function main() {
});
}
const { initializeEmoji } = await import('./features/emoji/index');
initializeEmoji();
const root = createRoot(mountNode);

View File

@@ -341,8 +341,8 @@ export const composeReducer = (state = initialState, action) => {
const isDirect = state.get('privacy') === 'direct';
return state
.set('quoted_status_id', isDirect ? null : status.get('id'))
.set('spoiler', status.get('sensitive'))
.set('spoiler_text', status.get('spoiler_text'))
.update('spoiler', spoiler => (spoiler) || !!status.get('spoiler_text'))
.update('spoiler_text', (spoiler_text) => spoiler_text || status.get('spoiler_text'))
.update('privacy', (visibility) => {
if (['public', 'unlisted'].includes(visibility) && status.get('visibility') === 'private') {
return 'private';

View File

@@ -327,9 +327,9 @@ $content-width: 840px;
font-weight: 700;
color: $primary-text-color;
text-transform: none;
padding-bottom: 0;
padding-top: 0;
margin-bottom: 0;
border-bottom: 0;
border-top: 0;
.comment {
display: block;

View File

@@ -319,9 +319,9 @@ $content-width: 840px;
font-weight: 700;
color: var(--color-text-primary);
text-transform: none;
padding-bottom: 0;
padding-top: 0;
margin-bottom: 0;
border-bottom: 0;
border-top: 0;
.comment {
display: block;

View File

@@ -31,9 +31,10 @@ class IpBlock < ApplicationRecord
after_commit :reset_cache
def to_log_human_identifier
def to_cidr
"#{ip}/#{ip.prefix}"
end
alias to_log_human_identifier to_cidr
class << self
def blocked?(remote_ip)

View File

@@ -9,6 +9,6 @@ class REST::Admin::IpBlockSerializer < ActiveModel::Serializer
end
def ip
"#{object.ip}/#{object.ip.prefix}"
object.to_cidr
end
end

View File

@@ -3,7 +3,7 @@
= f.check_box :ip_block_ids, { multiple: true, include_hidden: false }, ip_block.id
.batch-table__row__content.pending-account
.pending-account__header
%samp= link_to "#{ip_block.ip}/#{ip_block.ip.prefix}", admin_accounts_path(ip: "#{ip_block.ip}/#{ip_block.ip.prefix}")
%samp= link_to ip_block.to_cidr, admin_accounts_path(ip: ip_block.to_cidr)
- if ip_block.comment.present?
·
= ip_block.comment

View File

@@ -477,7 +477,7 @@ da:
no_file: Ingen fil valgt
export_domain_blocks:
import:
description_html: En liste over domæneblokeringer er ved at blive importeret. Gennemgå listen meget nøje, især hvis man ikke selv har oprettet den.
description_html: Du er ved at importere en liste over domæneblokeringer. Gennemgå denne liste meget omhyggeligt, især hvis du ikke selv har udarbejdet den.
existing_relationships_warning: Eksisterende følge-relationer
private_comment_description_html: 'For at man lettere kan holde styr på, hvorfra importerede blokeringer kommer, oprettes disse med flg. private kommentar: <q>%{comment}</q>'
private_comment_template: Importeret fra %{source} d. %{date}
@@ -1186,7 +1186,7 @@ da:
new_trending_statuses:
title: Indlæg, der trender
new_trending_tags:
title: Hashtags, der trender
title: Populære hashtags
subject: Nye tendenser klar til gennemgang på %{instance}
aliases:
add_new: Opret alias

View File

@@ -1089,16 +1089,16 @@ de:
tag_servers_measure: Server
tag_uses_measure: insgesamt
description_html: Diese Hashtags werden derzeit in vielen Beiträgen verwendet, die dein Server sieht. Dies kann deinen Nutzer*innen helfen, herauszufinden, worüber die Leute im Moment am meisten schreiben. Hashtags werden erst dann öffentlich angezeigt, wenn du sie genehmigst.
listable: Kann vorgeschlagen werden
listable: Darf empfohlen werden
no_tag_selected: Keine Hashtags wurden geändert, da keine ausgewählt wurden
not_listable: Wird nicht vorgeschlagen
not_trendable: Wird in den Trends nicht angezeigt
not_usable: Kann nicht verwendet werden
not_listable: Darf nicht vorgeschlagen werden
not_trendable: In Trends nicht erlaubt
not_usable: In Beiträgen nicht erlaubt
peaked_on_and_decaying: In den Trends am %{date}, jetzt absteigend
title: Angesagte Hashtags
trendable: Darf in den Trends erscheinen
trendable: In Trends erlaubt
trending_rank: Platz %{rank}
usable: Darf verwendet werden
usable: In Beiträgen erlaubt
usage_comparison: Heute %{today}-mal und gestern %{yesterday}-mal verwendet
used_by_over_week:
one: In den vergangenen 7 Tagen von einem Profil verwendet
@@ -1122,7 +1122,7 @@ de:
title: Neue Regel für Profilnamen erstellen
no_username_block_selected: Keine Regeln für Profilnamen wurden geändert, weil keine ausgewählt wurde(n)
not_permitted: Nicht gestattet
title: Regeln für Profilnamen
title: Profilnamen
updated_msg: Regel für Profilnamen erfolgreich aktualisiert
warning_presets:
add_new: Neu hinzufügen
@@ -1285,7 +1285,7 @@ de:
new_confirmation_instructions_sent: In wenigen Minuten wirst du eine neue E-Mail mit dem Bestätigungslink erhalten!
title: Überprüfe dein E-Mail-Postfach
sign_in:
preamble_html: Melde dich mit deinen Zugangsdaten für <strong>%{domain}</strong> an. Falls dein Konto auf einem anderen Server erstellt wurde, ist eine Anmeldung hier nicht möglich.
preamble_html: Melde dich mit deinen Zugangsdaten für <strong>%{domain}</strong> an. Falls dein Konto auf einem anderen Mastodon-Server erstellt wurde, ist eine Anmeldung hier nicht möglich.
title: Bei %{domain} anmelden
sign_up:
manual_review: Registrierungen für den Server %{domain} werden manuell durch unsere Moderator*innen überprüft. Um uns dabei zu unterstützen, schreibe etwas über dich und sage uns, weshalb du ein Konto auf %{domain} anlegen möchtest.

View File

@@ -563,6 +563,7 @@ eu:
create: Gehitu Moderazio Oharra
created_msg: Instantziako moderazio oharra ongi sortu da!
description_html: Ikusi eta idatzi oharrak beste moderatzaileentzat eta zuretzat etorkizunerako
title: Moderazio oharrak
private_comment: Iruzkin pribatua
public_comment: Iruzkin publikoa
purge: Ezabatu betiko
@@ -771,6 +772,8 @@ eu:
description_html: Gehienek erabilera baldintzak irakurri eta onartu dituztela baieztatzen badute ere, orokorrean arazoren bat dagoen arte ez dituzte irakurtzen. <strong>Zerbitzariaren arauak begirada batean ikustea errazteko buletadun zerrenda batean bildu.</strong> Saiatu arauak labur eta sinple idazten, baina elementu askotan banatu gabe.
edit: Editatu araua
empty: Ez da zerbitzariko araurik definitu oraindik.
move_down: Behera mugitu
move_up: Mugitu gora
title: Zerbitzariaren arauak
translation: Itzulpena
translations: Itzulpenak
@@ -808,6 +811,14 @@ eu:
all: Guztiei
disabled: Inori ez
users: Saioa hasita duten erabiltzaile lokalei
feed_access:
modes:
public: Edonork
landing_page:
values:
about: Honi buruz
local_feed: Jario lokala
trends: Joerak
registrations:
moderation_recommandation: Mesedez, ziurtatu moderazio-talde egokia eta erreaktiboa duzula erregistroak guztiei ireki aurretik!
preamble: Kontrolatu nork sortu dezakeen kontua zerbitzarian.
@@ -862,6 +873,7 @@ eu:
original_status: Jatorrizko bidalketa
quotes: Aipuak
reblogs: Bultzadak
replied_to_html: "%{acct_link}(r)i erantzuten"
status_changed: Bidalketa aldatuta
status_title: "%{name} erabiltzailearen bidalketa"
trending: Joera

View File

@@ -1672,6 +1672,7 @@ hu:
disabled_account: A jelenlegi fiókod nem lesz teljesen használható ezután. Viszont elérhető lesz majd az adatexport funkció, valamint a reaktiválás is.
followers: Ez a művelet az összes követődet a jelenlegi fiókról az újra fogja költöztetni
only_redirect_html: Az is lehetséges, hogy <a href="%{path}">csak átirányítást raksz a profilodra</a>.
other_data: Semmilyen más adat (beleértve a bejegyzéseket és a követett fiókokat) nem lesz automatikusan áthelyezve
redirect: A jelenlegi fiókod profiljára átirányításról szóló figyelmeztetést rakunk, valamint már nem fogjuk mutatni a keresésekben
moderation:
title: Moderáció
@@ -1928,6 +1929,7 @@ hu:
errors:
in_reply_not_found: Már nem létezik az a bejegyzés, melyre válaszolni szeretnél.
quoted_status_not_found: Már nem létezik az a bejegyzés, amelyből idézni szeretnél.
quoted_user_not_mentioned: Nem idézhet meg nem említett felhasználót egy privát említési bejegyzésben.
over_character_limit: túllépted a maximális %{max} karakteres keretet
pin_errors:
direct: A csak a megemlített felhasználók számára látható bejegyzések nem tűzhetők ki

View File

@@ -928,6 +928,7 @@ ru:
no_status_selected: Ничего не изменилось, так как ни один пост не был выделен
open: Открыть запись
original_status: Оригинальный пост
quotes: Цитаты
reblogs: Продвинули
replied_to_html: Ответ пользователю %{acct_link}
status_changed: Пост изменен
@@ -935,6 +936,7 @@ ru:
title: Посты пользователя - @%{name}
trending: Популярное
view_publicly: Открыть по публичной ссылке
view_quoted_post: Просмотр цитируемого сообщения
visibility: Видимость
with_media: С файлами
strikes:

View File

@@ -374,7 +374,9 @@ cs:
jurisdiction: Právní příslušnost
min_age: Věková hranice
user:
date_of_birth_1i: Rok
date_of_birth_2i: Měsíc
date_of_birth_3i: Den
role: Role
time_zone: Časové pásmo
user_role:

View File

@@ -376,7 +376,9 @@ cy:
jurisdiction: Awdurdodaeth gyfreithiol
min_age: Isafswm oedran
user:
date_of_birth_1i: Blwyddyn
date_of_birth_2i: Mis
date_of_birth_3i: Diwrnod
role: Rôl
time_zone: Cylchfa amser
user_role:

View File

@@ -353,10 +353,10 @@ de:
indexable: Profilseite in Suchmaschinen einbeziehen
show_application: App anzeigen, über die ich einen Beitrag veröffentlicht habe
tag:
listable: Erlaube, dass dieser Hashtag in Suchen und Empfehlungen erscheint
listable: Dieser Hashtag darf in Suchen und Empfehlungen erscheinen
name: Hashtag
trendable: Erlaube, dass dieser Hashtag in den Trends erscheint
usable: Beiträge dürfen diesen Hashtag lokal verwenden
trendable: Dieser Hashtag darf in den Trends erscheinen
usable: Dieser Hashtag darf lokal in Beiträgen verwendet werden
terms_of_service:
changelog: Was hat sich geändert?
effective_date: Datum des Inkrafttretens

View File

@@ -88,6 +88,7 @@ hu:
activity_api_enabled: Helyi bejegyzések, aktív felhasználók és új regisztrációk száma heti bontásban
app_icon: WEBP, PNG, GIF vagy JPG. Mobileszközökön az alkalmazás alapértelmezett ikonját felülírja egy egyéni ikonnal.
backups_retention_period: A felhasználók archívumokat állíthatnak elő a bejegyzéseikből, hogy később letöltsék azokat. Ha pozitív értékre van állítva, akkor a megadott számú nap után automatikusan törölve lesznek a tárhelyedről.
bootstrap_timeline_accounts: Ezek a fiókok rögzítve lesznek az új felhasználók követési ajánlásai tetején. Add meg a fiókok vesszővel elválasztott listáját.
closed_registrations_message: Akkor jelenik meg, amikor a regisztráció le van zárva
content_cache_retention_period: Minden más kiszolgálóról származó bejegyzés (megtolásokkal és válaszokkal együtt) törölve lesz a megadott számú nap elteltével, függetlenül a helyi felhasználók ezekkel a bejegyzésekkel történő interakcióitól. Ebben azok a bejegyzések is benne vannak, melyeket a helyi felhasználó könyvjelzőzött vagy kedvencnek jelölt. A különböző kiszolgálók felhasználói közötti privát üzenetek is el fognak veszni visszaállíthatatlanul. Ennek a beállításnak a használata különleges felhasználási esetekre javasolt, mert számos felhasználói elvárás fog eltörni, ha általános céllal használják.
custom_css: A Mastodon webes verziójában használhatsz egyéni stílusokat.
@@ -371,7 +372,9 @@ hu:
jurisdiction: Joghatóság
min_age: Minimális életkor
user:
date_of_birth_1i: Év
date_of_birth_2i: Hónap
date_of_birth_3i: Nap
role: Szerep
time_zone: Időzóna
user_role:

View File

@@ -40,13 +40,15 @@ export function MastodonThemes(): Plugin {
// Get all files mentioned in the themes.yml file.
const themes = await loadThemesFromConfig(projectRoot);
const allThemes = {
...themes,
default_theme_tokens: 'styles_new/application.scss',
'mastodon-light_theme_tokens': 'styles_new/mastodon-light.scss',
contrast_theme_tokens: 'styles_new/contrast.scss',
};
for (const [themeName, themePath] of Object.entries(themes)) {
for (const [themeName, themePath] of Object.entries(allThemes)) {
entrypoints[`themes/${themeName}`] = path.resolve(jsRoot, themePath);
entrypoints[`themes/${themeName}_theme_tokens`] = path.resolve(
jsRoot,
themePath.replace('styles/', 'styles_new/'),
);
}
return {

View File

@@ -107,9 +107,9 @@ module Mastodon::CLI
IpBlock.severity_no_access.find_each do |ip_block|
case options[:format]
when 'nginx'
say "deny #{ip_block.ip}/#{ip_block.ip.prefix};"
say "deny #{ip_block.to_cidr};"
else
say "#{ip_block.ip}/#{ip_block.ip.prefix}"
say ip_block.to_cidr
end
end
end

View File

@@ -123,12 +123,12 @@ module Mastodon::CLI
progress.log("Moving #{previous_path} to #{upgraded_path}") if options[:verbose]
begin
move_previous_to_upgraded
move_previous_to_upgraded(previous_path, upgraded_path)
rescue => e
progress.log(pastel.red("Error processing #{previous_path}: #{e}"))
success = false
remove_directory
remove_directory(upgraded_path)
end
end

View File

@@ -251,12 +251,12 @@ RSpec.describe Mastodon::CLI::IpBlocks do
it 'exports blocked IPs with "no_access" severity in plain format' do
expect { subject }
.to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
.to output_results("#{first_ip_range_block.to_cidr}\n#{second_ip_range_block.to_cidr}")
end
it 'does not export blocked IPs with different severities' do
expect { subject }
.to_not output_results("#{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}")
.to_not output_results(third_ip_range_block.to_cidr)
end
end
@@ -265,19 +265,19 @@ RSpec.describe Mastodon::CLI::IpBlocks do
it 'exports blocked IPs with "no_access" severity in plain format' do
expect { subject }
.to output_results("deny #{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};\ndeny #{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix};")
.to output_results("deny #{first_ip_range_block.to_cidr};\ndeny #{second_ip_range_block.to_cidr};")
end
it 'does not export blocked IPs with different severities' do
expect { subject }
.to_not output_results("deny #{third_ip_range_block.ip}/#{first_ip_range_block.ip.prefix};")
.to_not output_results("deny #{third_ip_range_block.to_cidr};")
end
end
context 'when --format option is not provided' do
it 'exports blocked IPs in plain format by default' do
expect { subject }
.to output_results("#{first_ip_range_block.ip}/#{first_ip_range_block.ip.prefix}\n#{second_ip_range_block.ip}/#{second_ip_range_block.ip.prefix}")
.to output_results("#{first_ip_range_block.to_cidr}\n#{second_ip_range_block.to_cidr}")
end
end
end

View File

@@ -73,8 +73,6 @@ RSpec.describe Mastodon::RedisConfiguration do
end
shared_examples 'sentinel support' do |prefix = nil|
prefix = prefix ? "#{prefix}_" : ''
context 'when configuring sentinel support' do
around do |example|
ClimateControl.modify "#{prefix}REDIS_PASSWORD": 'testpass1', "#{prefix}REDIS_HOST": 'redis2.example.com', "#{prefix}REDIS_SENTINELS": '192.168.0.1:3000,192.168.0.2:4000', "#{prefix}REDIS_SENTINEL_MASTER": 'mainsentinel' do
@@ -199,7 +197,7 @@ RSpec.describe Mastodon::RedisConfiguration do
it_behaves_like 'secondary configuration', 'SIDEKIQ'
it_behaves_like 'setting a different driver'
it_behaves_like 'sentinel support', 'SIDEKIQ'
it_behaves_like 'sentinel support', 'SIDEKIQ_'
end
describe '#cache' do
@@ -225,6 +223,6 @@ RSpec.describe Mastodon::RedisConfiguration do
it_behaves_like 'secondary configuration', 'CACHE'
it_behaves_like 'setting a different driver'
it_behaves_like 'sentinel support', 'CACHE'
it_behaves_like 'sentinel support', 'CACHE_'
end
end

View File

@@ -26,6 +26,22 @@ RSpec.describe IpBlock do
end
end
describe '#to_cidr' do
subject { Fabricate.build(:ip_block, ip:).to_cidr }
context 'with an IP and a specified prefix' do
let(:ip) { '192.168.1.0/24' }
it { is_expected.to eq('192.168.1.0/24') }
end
context 'with an IP and a default prefix' do
let(:ip) { '192.168.1.0' }
it { is_expected.to eq('192.168.1.0/32') }
end
end
describe '.blocked?' do
context 'when the IP is blocked' do
it 'returns true' do

View File

@@ -97,7 +97,7 @@ RSpec.describe 'IP Blocks' do
expect(response.parsed_body)
.to include(
ip: eq("#{ip_block.ip}/#{ip_block.ip.prefix}"),
ip: eq(ip_block.to_cidr),
severity: eq(ip_block.severity.to_s)
)
end
@@ -216,7 +216,7 @@ RSpec.describe 'IP Blocks' do
expect(response.content_type)
.to start_with('application/json')
expect(response.parsed_body).to match(hash_including({
ip: "#{ip_block.ip}/#{ip_block.ip.prefix}",
ip: ip_block.to_cidr,
severity: 'sign_up_requires_approval',
comment: 'Decreasing severity',
}))

View File

@@ -50,6 +50,21 @@ RSpec.describe 'API V1 Statuses Translations' do
end
end
context 'with a public status marked with the same language as the current locale when translation backend cannot do same-language translation' do
let(:status) { Fabricate(:status, account: user.account, text: 'Esto está en español pero está marcado como inglés.', language: 'en') }
it 'returns http forbidden with error message' do
subject
expect(response)
.to have_http_status(403)
expect(response.media_type)
.to eq('application/json')
expect(response.parsed_body)
.to include(error: /not allowed/)
end
end
context 'with a private status' do
let(:status) { Fabricate(:status, visibility: :private, account: user.account, text: 'Hola', language: 'es') }

View File

@@ -97,39 +97,39 @@ __metadata:
languageName: node
linkType: hard
"@babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.28.0, @babel/core@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/core@npm:7.28.4"
"@babel/core@npm:^7.18.9, @babel/core@npm:^7.21.3, @babel/core@npm:^7.24.4, @babel/core@npm:^7.26.10, @babel/core@npm:^7.28.0, @babel/core@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/core@npm:7.28.5"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.3"
"@babel/generator": "npm:^7.28.5"
"@babel/helper-compilation-targets": "npm:^7.27.2"
"@babel/helper-module-transforms": "npm:^7.28.3"
"@babel/helpers": "npm:^7.28.4"
"@babel/parser": "npm:^7.28.4"
"@babel/parser": "npm:^7.28.5"
"@babel/template": "npm:^7.27.2"
"@babel/traverse": "npm:^7.28.4"
"@babel/types": "npm:^7.28.4"
"@babel/traverse": "npm:^7.28.5"
"@babel/types": "npm:^7.28.5"
"@jridgewell/remapping": "npm:^2.3.5"
convert-source-map: "npm:^2.0.0"
debug: "npm:^4.1.0"
gensync: "npm:^1.0.0-beta.2"
json5: "npm:^2.2.3"
semver: "npm:^6.3.1"
checksum: 10c0/ef5a6c3c6bf40d3589b5593f8118cfe2602ce737412629fb6e26d595be2fcbaae0807b43027a5c42ec4fba5b895ff65891f2503b5918c8a3ea3542ab44d4c278
checksum: 10c0/535f82238027621da6bdffbdbe896ebad3558b311d6f8abc680637a9859b96edbf929ab010757055381570b29cf66c4a295b5618318d27a4273c0e2033925e72
languageName: node
linkType: hard
"@babel/generator@npm:^7.28.3":
version: 7.28.3
resolution: "@babel/generator@npm:7.28.3"
"@babel/generator@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/generator@npm:7.28.5"
dependencies:
"@babel/parser": "npm:^7.28.3"
"@babel/types": "npm:^7.28.2"
"@babel/parser": "npm:^7.28.5"
"@babel/types": "npm:^7.28.5"
"@jridgewell/gen-mapping": "npm:^0.3.12"
"@jridgewell/trace-mapping": "npm:^0.3.28"
jsesc: "npm:^3.0.2"
checksum: 10c0/0ff58bcf04f8803dcc29479b547b43b9b0b828ec1ee0668e92d79f9e90f388c28589056637c5ff2fd7bcf8d153c990d29c448d449d852bf9d1bc64753ca462bc
checksum: 10c0/9f219fe1d5431b6919f1a5c60db8d5d34fe546c0d8f5a8511b32f847569234ffc8032beb9e7404649a143f54e15224ecb53a3d11b6bb85c3203e573d91fca752
languageName: node
linkType: hard
@@ -334,7 +334,7 @@ __metadata:
languageName: node
linkType: hard
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.3, @babel/parser@npm:^7.28.4":
"@babel/parser@npm:^7.1.0, @babel/parser@npm:^7.20.7, @babel/parser@npm:^7.24.4, @babel/parser@npm:^7.25.4, @babel/parser@npm:^7.27.2, @babel/parser@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/parser@npm:7.28.5"
dependencies:
@@ -1194,22 +1194,22 @@ __metadata:
languageName: node
linkType: hard
"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.4":
version: 7.28.4
resolution: "@babel/traverse@npm:7.28.4"
"@babel/traverse@npm:^7.18.9, @babel/traverse@npm:^7.26.10, @babel/traverse@npm:^7.27.1, @babel/traverse@npm:^7.28.0, @babel/traverse@npm:^7.28.3, @babel/traverse@npm:^7.28.5":
version: 7.28.5
resolution: "@babel/traverse@npm:7.28.5"
dependencies:
"@babel/code-frame": "npm:^7.27.1"
"@babel/generator": "npm:^7.28.3"
"@babel/generator": "npm:^7.28.5"
"@babel/helper-globals": "npm:^7.28.0"
"@babel/parser": "npm:^7.28.4"
"@babel/parser": "npm:^7.28.5"
"@babel/template": "npm:^7.27.2"
"@babel/types": "npm:^7.28.4"
"@babel/types": "npm:^7.28.5"
debug: "npm:^4.3.1"
checksum: 10c0/ee678fdd49c9f54a32e07e8455242390d43ce44887cea6567b233fe13907b89240c377e7633478a32c6cf1be0e17c2f7f3b0c59f0666e39c5074cc47b968489c
checksum: 10c0/f6c4a595993ae2b73f2d4cd9c062f2e232174d293edd4abe1d715bd6281da8d99e47c65857e8d0917d9384c65972f4acdebc6749a7c40a8fcc38b3c7fb3e706f
languageName: node
linkType: hard
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.2, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4":
"@babel/types@npm:^7.0.0, @babel/types@npm:^7.18.9, @babel/types@npm:^7.20.7, @babel/types@npm:^7.21.3, @babel/types@npm:^7.25.4, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.28.4, @babel/types@npm:^7.28.5, @babel/types@npm:^7.4.4":
version: 7.28.5
resolution: "@babel/types@npm:7.28.5"
dependencies:
@@ -3317,10 +3317,10 @@ __metadata:
languageName: node
linkType: hard
"@rolldown/pluginutils@npm:1.0.0-beta.43":
version: 1.0.0-beta.43
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.43"
checksum: 10c0/1c17a0b16c277a0fdbab080fd22ef91e37c1f0d710ecfdacb6a080068062eb14ff030d0e9d2ec2325a1d4246dba0c49625755c82c0090f6cbf98d16e80183e02
"@rolldown/pluginutils@npm:1.0.0-beta.47":
version: 1.0.0-beta.47
resolution: "@rolldown/pluginutils@npm:1.0.0-beta.47"
checksum: 10c0/eb0cfa7334d66f090c47eaac612174936b05f26e789352428cb6e03575b590f355de30d26b42576ea4e613d8887b587119d19b2e4b3a8909ceb232ca1cf746c8
languageName: node
linkType: hard
@@ -4815,18 +4815,18 @@ __metadata:
linkType: hard
"@vitejs/plugin-react@npm:^5.0.0":
version: 5.1.0
resolution: "@vitejs/plugin-react@npm:5.1.0"
version: 5.1.1
resolution: "@vitejs/plugin-react@npm:5.1.1"
dependencies:
"@babel/core": "npm:^7.28.4"
"@babel/core": "npm:^7.28.5"
"@babel/plugin-transform-react-jsx-self": "npm:^7.27.1"
"@babel/plugin-transform-react-jsx-source": "npm:^7.27.1"
"@rolldown/pluginutils": "npm:1.0.0-beta.43"
"@rolldown/pluginutils": "npm:1.0.0-beta.47"
"@types/babel__core": "npm:^7.20.5"
react-refresh: "npm:^0.18.0"
peerDependencies:
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
checksum: 10c0/e192a12e2b854df109eafb1d06c0bc848e8e2b162c686aa6b999b1048658983e72674b2068ccc37562fcce44d32ad92b65f3a4e1897a0cb7859c2ee69cc63eac
checksum: 10c0/e590efaea1eabfbb1beb6e8c9fac0742fd299808e3368e63b2825ce24740adb8a28fcb2668b14b7ca1bdb42890cfefe94d02dd358dcbbf8a27ddf377b9a82abf
languageName: node
linkType: hard