Merge commit '75f78244d5df52cc8242b6a7c6b8d1531963aa63' into glitch-soc/merge-upstream

This commit is contained in:
Claire
2025-09-11 12:13:47 +02:00
25 changed files with 525 additions and 56 deletions

View File

@@ -308,8 +308,8 @@ GEM
highline (3.1.2)
reline
hiredis (0.6.3)
hiredis-client (0.25.2)
redis-client (= 0.25.2)
hiredis-client (0.25.3)
redis-client (= 0.25.3)
hkdf (0.3.0)
htmlentities (4.3.4)
http (5.3.1)
@@ -725,7 +725,7 @@ GEM
reline
redcarpet (3.6.1)
redis (4.8.1)
redis-client (0.25.2)
redis-client (0.25.3)
connection_pool
regexp_parser (2.11.2)
reline (0.6.2)

View File

@@ -186,13 +186,16 @@ export const DropdownMenu = <Item = MenuItem,>({
(e: React.MouseEvent | React.KeyboardEvent) => {
const i = Number(e.currentTarget.getAttribute('data-index'));
const item = items?.[i];
const isItemDisabled = Boolean(
item && typeof item === 'object' && 'disabled' in item && item.disabled,
);
onClose();
if (!item) {
if (!item || isItemDisabled) {
return;
}
onClose();
if (typeof onItemClick === 'function') {
e.preventDefault();
onItemClick(item, i);
@@ -233,7 +236,7 @@ export const DropdownMenu = <Item = MenuItem,>({
onClick={handleItemClick}
onKeyUp={handleItemKeyUp}
data-index={i}
disabled={disabled}
aria-disabled={disabled}
>
<DropdownMenuItemContent item={option} />
</button>
@@ -320,7 +323,7 @@ export const DropdownMenu = <Item = MenuItem,>({
);
};
interface DropdownProps<Item = MenuItem> {
interface DropdownProps<Item extends object | null = MenuItem> {
children?: React.ReactElement;
icon?: string;
iconComponent?: IconProp;
@@ -348,7 +351,7 @@ interface DropdownProps<Item = MenuItem> {
const offset = [5, 5] as OffsetValue;
const popperConfig = { strategy: 'fixed' } as UsePopperOptions;
export const Dropdown = <Item = MenuItem,>({
export const Dropdown = <Item extends object | null = MenuItem>({
children,
icon,
iconComponent,

View File

@@ -180,7 +180,7 @@ const ReblogMenuItem: FC<ReblogMenuItemProps> = ({
<button
{...handlers}
ref={focusRefCallback}
disabled={disabled}
aria-disabled={disabled}
data-index={index}
>
<DropdownMenuItemContent item={item} />

View File

@@ -239,6 +239,10 @@
"confirmations.missing_alt_text.secondary": "Postio beth bynnag",
"confirmations.missing_alt_text.title": "Ychwanegu testun amgen?",
"confirmations.mute.confirm": "Tewi",
"confirmations.quiet_post_quote_info.dismiss": "Peidio fy atgoff eto",
"confirmations.quiet_post_quote_info.got_it": "Iawn",
"confirmations.quiet_post_quote_info.message": "Wrth ddyfynnu postiad cyhoeddus tawel, bydd eich postiad yn cael ei guddio rhag llinellau amser sy'n trendio.",
"confirmations.quiet_post_quote_info.title": "Yn dyfynnu postiadau cyhoeddus tawel",
"confirmations.redraft.confirm": "Dileu ac ailddrafftio",
"confirmations.redraft.message": "Ydych chi wir eisiau'r dileu'r postiad hwn a'i ail lunio? Bydd ffefrynnau a hybiau'n cael eu colli, a bydd atebion i'r postiad gwreiddiol yn mynd yn amddifad.",
"confirmations.redraft.title": "Dileu ac ail lunio'r postiad?",

View File

@@ -239,10 +239,10 @@
"confirmations.missing_alt_text.secondary": "Enviar de todos modos",
"confirmations.missing_alt_text.title": "¿Agregar texto alternativo?",
"confirmations.mute.confirm": "Silenciar",
"confirmations.quiet_post_quote_info.dismiss": "No me lo vuelvas a recordar",
"confirmations.quiet_post_quote_info.dismiss": "No recordar de nuevo",
"confirmations.quiet_post_quote_info.got_it": "Entendido",
"confirmations.quiet_post_quote_info.message": "Cuando cites una publicación pública silenciosa, tu publicación se ocultará de las cronologías de tendencias.",
"confirmations.quiet_post_quote_info.title": "Citar publicaciones públicas silenciosas",
"confirmations.quiet_post_quote_info.message": "Al citar un mensaje público pero silencioso, tu mensaje se ocultará de las líneas temporales de tendencias.",
"confirmations.quiet_post_quote_info.title": "Citado de mensajes públicos pero silenciosos",
"confirmations.redraft.confirm": "Eliminar mensaje original y editarlo",
"confirmations.redraft.message": "¿Estás seguro que querés eliminar este mensaje y volver a editarlo? Se perderán las veces marcadas como favorito y sus adhesiones, y las respuestas al mensaje original quedarán huérfanas.",
"confirmations.redraft.title": "¿Eliminar y volver a redactar mensaje?",

View File

@@ -239,6 +239,10 @@
"confirmations.missing_alt_text.secondary": "Posta allíkavæl",
"confirmations.missing_alt_text.title": "Legg alternativan tekst afturat?",
"confirmations.mute.confirm": "Doyv",
"confirmations.quiet_post_quote_info.dismiss": "Ikki minna meg á tað aftur",
"confirmations.quiet_post_quote_info.got_it": "Eg skilji",
"confirmations.quiet_post_quote_info.message": "Tá tú siterar ein stillan almennan post, verður posturin hjá tær fjaldur frá tíðarlinjum, ið vísa vælumtóktar postar.",
"confirmations.quiet_post_quote_info.title": "Sitera stillar almennar postar",
"confirmations.redraft.confirm": "Sletta og skriva umaftur",
"confirmations.redraft.message": "Vilt tú veruliga strika hendan postin og í staðin gera hann til eina nýggja kladdu? Yndisfrámerki og framhevjanir blíva burtur, og svar til upprunapostin missa tilknýtið.",
"confirmations.redraft.title": "Strika & ger nýtt uppskot um post?",
@@ -745,6 +749,7 @@
"privacy.quote.disabled": "{visibility}, siteringar óvirknar",
"privacy.quote.limited": "{visibility}, siteringar avmarkaðar",
"privacy.unlisted.additional": "Hetta er júst sum almenn, tó verður posturin ikki vístur í samtíðarrásum ella frámerkjum, rannsakan ella Mastodon leitingum, sjálvt um valið er galdandi fyri alla kontuna.",
"privacy.unlisted.long": "Fjalt frá Mastodon leitiúrslitum og vælumtóktum og almennum tíðarlinjum",
"privacy.unlisted.short": "Stillur almenningur",
"privacy_policy.last_updated": "Seinast dagført {date}",
"privacy_policy.title": "Privatlívspolitikkur",
@@ -912,6 +917,7 @@
"status.read_more": "Les meira",
"status.reblog": "Stimbra",
"status.reblog_or_quote": "Stimbra ella sitera",
"status.reblog_private": "Deil aftur við tínum fylgjarum",
"status.reblogged_by": "{name} stimbrað",
"status.reblogs": "{count, plural, one {stimbran} other {stimbranir}}",
"status.reblogs.empty": "Eingin hevur stimbrað hendan postin enn. Tá onkur stimbrar postin, verður hann sjónligur her.",
@@ -990,6 +996,7 @@
"visibility_modal.header": "Sýni og samvirkni",
"visibility_modal.helper.direct_quoting": "Privatar umrøður, sum eru skrivaðar á Mastodon, kunnu ikki siterast av øðrum.",
"visibility_modal.helper.privacy_editing": "Sýni kann ikki broytast eftir, at ein postur er útgivin.",
"visibility_modal.helper.privacy_private_self_quote": "Sjálv-sitering av privatum postum kann ikki gerast almenn.",
"visibility_modal.helper.private_quoting": "Postar, sum einans eru fyri fylgjarar á Mastodon, kunnu ikki siterast av øðrum.",
"visibility_modal.helper.unlisted_quoting": "Tá fólk sitera teg, so vera teirra postar eisini fjaldir frá tíðarlinjum við ráki.",
"visibility_modal.instructions": "Stýr hvør samvirka við hendan postin. Tú kanst eisini áseta stillingar til allar framtíðar postar við at fara til <link>Stillingar > Postingarstillingar</link>.",

View File

@@ -885,13 +885,13 @@
"status.mute_conversation": "Masquer la conversation",
"status.open": "Afficher la publication entière",
"status.pin": "Épingler sur profil",
"status.quote": "Citation",
"status.quote": "Citer",
"status.quote.cancel": "Annuler la citation",
"status.quote_error.filtered": "Caché en raison de l'un de vos filtres",
"status.quote_error.not_available": "Publication non disponible",
"status.quote_error.pending_approval": "Publication en attente",
"status.quote_error.pending_approval_popout.title": "Publication en attente ? Restez calme",
"status.quote_followers_only": "Seul·e·s mes abonné·e·s peuvent citer cette publication",
"status.quote_followers_only": "Seul·e·s les abonné·e·s peuvent citer cette publication",
"status.quote_manual_review": "L'auteur va vérifier manuellement",
"status.quote_policy_change": "Changer qui peut vous citer",
"status.quote_private": "Les publications privées ne peuvent pas être citées",
@@ -908,6 +908,7 @@
"status.reply": "Répondre",
"status.replyAll": "Répondre à cette discussion",
"status.report": "Signaler @{name}",
"status.request_quote": "Demander à citer",
"status.sensitive_warning": "Contenu sensible",
"status.share": "Partager",
"status.show_less_all": "Tout replier",

View File

@@ -885,13 +885,13 @@
"status.mute_conversation": "Masquer la conversation",
"status.open": "Afficher le message entier",
"status.pin": "Épingler sur le profil",
"status.quote": "Citation",
"status.quote": "Citer",
"status.quote.cancel": "Annuler la citation",
"status.quote_error.filtered": "Caché en raison de l'un de vos filtres",
"status.quote_error.not_available": "Publication non disponible",
"status.quote_error.pending_approval": "Publication en attente",
"status.quote_error.pending_approval_popout.title": "Publication en attente ? Restez calme",
"status.quote_followers_only": "Seul·e·s mes abonné·e·s peuvent citer cette publication",
"status.quote_followers_only": "Seul·e·s les abonné·e·s peuvent citer cette publication",
"status.quote_manual_review": "L'auteur va vérifier manuellement",
"status.quote_policy_change": "Changer qui peut vous citer",
"status.quote_private": "Les publications privées ne peuvent pas être citées",
@@ -908,6 +908,7 @@
"status.reply": "Répondre",
"status.replyAll": "Répondre au fil",
"status.report": "Signaler @{name}",
"status.request_quote": "Demander à citer",
"status.sensitive_warning": "Contenu sensible",
"status.share": "Partager",
"status.show_less_all": "Tout replier",

View File

@@ -239,6 +239,10 @@
"confirmations.missing_alt_text.secondary": "Post ar aon nós",
"confirmations.missing_alt_text.title": "Cuir téacs alt leis?",
"confirmations.mute.confirm": "Balbhaigh",
"confirmations.quiet_post_quote_info.dismiss": "Ná cuir i gcuimhne dom arís",
"confirmations.quiet_post_quote_info.got_it": "Tuigim é",
"confirmations.quiet_post_quote_info.message": "Agus post poiblí ciúin á lua, beidh do phost i bhfolach ó amlínte treochta.",
"confirmations.quiet_post_quote_info.title": "Ag lua poist phoiblí chiúine",
"confirmations.redraft.confirm": "Scrios ⁊ athdhréachtaigh",
"confirmations.redraft.message": "An bhfuil tú cinnte gur mhaith leat an postáil seo a scriosadh agus é a athdhréachtú? Caillfear ceanáin agus treisithe, agus dílleachtaí freagraí ar an mbunphostála.",
"confirmations.redraft.title": "Scrios & athdhréachtú postáil?",

View File

@@ -239,6 +239,10 @@
"confirmations.missing_alt_text.secondary": "Publicar igualmente",
"confirmations.missing_alt_text.title": "Engadir texto descritivo?",
"confirmations.mute.confirm": "Acalar",
"confirmations.quiet_post_quote_info.dismiss": "Non lembrarmo máis",
"confirmations.quiet_post_quote_info.got_it": "Entendido",
"confirmations.quiet_post_quote_info.message": "Ao citar unha publicación pública limitada a túa publicación non aparecerá nas cronoloxías de tendencias.",
"confirmations.quiet_post_quote_info.title": "Citar publicacións públicas limitadas",
"confirmations.redraft.confirm": "Eliminar e reescribir",
"confirmations.redraft.message": "Tes a certeza de querer eliminar esta publicación e reescribila? Perderás as promocións e favorecementos, e as respostas á publicación orixinal ficarán orfas.",
"confirmations.redraft.title": "Eliminar e reescribir a publicación?",
@@ -746,7 +750,7 @@
"privacy.quote.limited": "{visibility}, citas limitadas",
"privacy.unlisted.additional": "Do mesmo xeito que público, menos que a publicación non aparecerá nas cronoloxías en directo ou nos cancelos, en descubrir ou nas buscas de Mastodon, incluso se estivese establecido nas opcións xerais da conta.",
"privacy.unlisted.long": "Non aparece nos resultados de buscas en Mastodon, nas tendencias e cronoloxías públicas",
"privacy.unlisted.short": "Público limitado",
"privacy.unlisted.short": "Pública limitada",
"privacy_policy.last_updated": "Actualizado por última vez no {date}",
"privacy_policy.title": "Política de Privacidade",
"quote_error.poll": "Non se permite citar as enquisas.",

View File

@@ -241,6 +241,8 @@
"confirmations.mute.confirm": "Némítás",
"confirmations.quiet_post_quote_info.dismiss": "Ne emlékeztessen újra",
"confirmations.quiet_post_quote_info.got_it": "Rendben",
"confirmations.quiet_post_quote_info.message": "Ha csendes nyilvános bejegyzést idézel, akkor a bejegyzés el lesz rejtve a felkapottak idővonalairól.",
"confirmations.quiet_post_quote_info.title": "Csendes nyilvános bejegyzések idézése",
"confirmations.redraft.confirm": "Törlés és újraírás",
"confirmations.redraft.message": "Biztos, hogy ezt a bejegyzést szeretnéd törölni és újraírni? Minden megtolást és kedvencnek jelölést elvesztesz, az eredetire adott válaszok pedig elárvulnak.",
"confirmations.redraft.title": "Törlöd és újraírod a bejegyzést?",

View File

@@ -239,6 +239,8 @@
"confirmations.missing_alt_text.secondary": "Yine de gönder",
"confirmations.missing_alt_text.title": "Alternatif metin ekle?",
"confirmations.mute.confirm": "Sessize al",
"confirmations.quiet_post_quote_info.dismiss": "Bana bir daha hatırlatma",
"confirmations.quiet_post_quote_info.got_it": "Anladım",
"confirmations.redraft.confirm": "Sil Düzenle ve yeniden paylaş",
"confirmations.redraft.message": "Bu gönderiyi silip taslak haline getirmek istediğinize emin misiniz? Mevcut favoriler ve boostlar silinecek ve gönderiye verilen yanıtlar başıboş kalacak.",
"confirmations.redraft.title": "Gönderiyi sil veya taslağa dönüştür?",

View File

@@ -0,0 +1,69 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`html > htmlStringToComponents > copies attributes to props 1`] = `
[
<a
href="https://example.com"
rel="nofollow"
target="_blank"
>
link
</a>,
]
`;
exports[`html > htmlStringToComponents > handles nested elements 1`] = `
[
<p>
lorem
<strong>
ipsum
</strong>
</p>,
]
`;
exports[`html > htmlStringToComponents > ignores empty text nodes 1`] = `
[
<p>
<span>
lorem ipsum
</span>
</p>,
]
`;
exports[`html > htmlStringToComponents > respects allowedTags option 1`] = `
[
<p>
lorem
<em>
dolor
</em>
</p>,
]
`;
exports[`html > htmlStringToComponents > respects maxDepth option 1`] = `
[
<p>
<span />
</p>,
]
`;
exports[`html > htmlStringToComponents > returns converted nodes from string 1`] = `
[
<p>
lorem ipsum
</p>,
]
`;
exports[`html > htmlStringToComponents > uses default parsing if onElement returns undefined 1`] = `
[
<p>
lorem ipsum
</p>,
]
`;

View File

@@ -1,3 +1,5 @@
import React from 'react';
import * as html from '../html';
describe('html', () => {
@@ -9,4 +11,104 @@ describe('html', () => {
expect(output).toEqual('lorem\n\nipsum\n<br>');
});
});
describe('htmlStringToComponents', () => {
it('returns converted nodes from string', () => {
const input = '<p>lorem ipsum</p>';
const output = html.htmlStringToComponents(input);
expect(output).toMatchSnapshot();
});
it('handles nested elements', () => {
const input = '<p>lorem <strong>ipsum</strong></p>';
const output = html.htmlStringToComponents(input);
expect(output).toMatchSnapshot();
});
it('ignores empty text nodes', () => {
const input = '<p> <span>lorem ipsum</span> </p>';
const output = html.htmlStringToComponents(input);
expect(output).toMatchSnapshot();
});
it('copies attributes to props', () => {
const input =
'<a href="https://example.com" target="_blank" rel="nofollow">link</a>';
const output = html.htmlStringToComponents(input);
expect(output).toMatchSnapshot();
});
it('respects maxDepth option', () => {
const input = '<p><span>lorem <strong>ipsum</strong></span></p>';
const output = html.htmlStringToComponents(input, { maxDepth: 2 });
expect(output).toMatchSnapshot();
});
it('calls onText callback', () => {
const input = '<p>lorem ipsum</p>';
const onText = vi.fn((text: string) => text);
html.htmlStringToComponents(input, { onText });
expect(onText).toHaveBeenCalledExactlyOnceWith('lorem ipsum');
});
it('calls onElement callback', () => {
const input = '<p>lorem ipsum</p>';
const onElement = vi.fn(
(element: HTMLElement, children: React.ReactNode[]) =>
React.createElement(element.tagName.toLowerCase(), {}, ...children),
);
html.htmlStringToComponents(input, { onElement });
expect(onElement).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ tagName: 'P' }),
expect.arrayContaining(['lorem ipsum']),
);
});
it('uses default parsing if onElement returns undefined', () => {
const input = '<p>lorem ipsum</p>';
const onElement = vi.fn(() => undefined);
const output = html.htmlStringToComponents(input, { onElement });
expect(onElement).toHaveBeenCalledExactlyOnceWith(
expect.objectContaining({ tagName: 'P' }),
expect.arrayContaining(['lorem ipsum']),
);
expect(output).toMatchSnapshot();
});
it('calls onAttribute callback', () => {
const input =
'<a href="https://example.com" target="_blank" rel="nofollow">link</a>';
const onAttribute = vi.fn(
(name: string, value: string) =>
[name, value] satisfies [string, string],
);
html.htmlStringToComponents(input, { onAttribute });
expect(onAttribute).toHaveBeenCalledTimes(3);
expect(onAttribute).toHaveBeenCalledWith(
'href',
'https://example.com',
'a',
);
expect(onAttribute).toHaveBeenCalledWith('target', '_blank', 'a');
expect(onAttribute).toHaveBeenCalledWith('rel', 'nofollow', 'a');
});
it('respects allowedTags option', () => {
const input = '<p>lorem <strong>ipsum</strong> <em>dolor</em></p>';
const output = html.htmlStringToComponents(input, {
allowedTags: new Set(['p', 'em']),
});
expect(output).toMatchSnapshot();
});
it('ensure performance is acceptable with large input', () => {
const input = '<p>' + '<span>lorem</span>'.repeat(1_000) + '</p>';
const start = performance.now();
html.htmlStringToComponents(input);
const duration = performance.now() - start;
// Arbitrary threshold of 200ms for this test.
// Normally it's much less (<50ms), but the GH Action environment can be slow.
expect(duration).toBeLessThan(200);
});
});
});

View File

@@ -1,3 +1,5 @@
import React from 'react';
// NB: This function can still return unsafe HTML
export const unescapeHTML = (html: string) => {
const wrapper = document.createElement('div');
@@ -7,3 +9,177 @@ export const unescapeHTML = (html: string) => {
.replace(/<[^>]*>/g, '');
return wrapper.textContent;
};
interface QueueItem {
node: Node;
parent: React.ReactNode[];
depth: number;
}
interface Options {
maxDepth?: number;
onText?: (text: string) => React.ReactNode;
onElement?: (
element: HTMLElement,
children: React.ReactNode[],
) => React.ReactNode;
onAttribute?: (
name: string,
value: string,
tagName: string,
) => [string, unknown] | null;
allowedTags?: Set<string>;
}
const DEFAULT_ALLOWED_TAGS: ReadonlySet<string> = new Set([
'a',
'abbr',
'b',
'blockquote',
'br',
'cite',
'code',
'del',
'dfn',
'dl',
'dt',
'em',
'h1',
'h2',
'h3',
'h4',
'h5',
'h6',
'hr',
'i',
'li',
'ol',
'p',
'pre',
'small',
'span',
'strong',
'sub',
'sup',
'time',
'u',
'ul',
]);
export function htmlStringToComponents(
htmlString: string,
options: Options = {},
) {
const wrapper = document.createElement('template');
wrapper.innerHTML = htmlString;
const rootChildren: React.ReactNode[] = [];
const queue: QueueItem[] = [
{ node: wrapper.content, parent: rootChildren, depth: 0 },
];
const {
maxDepth = 10,
allowedTags = DEFAULT_ALLOWED_TAGS,
onAttribute,
onElement,
onText,
} = options;
while (queue.length > 0) {
const item = queue.shift();
if (!item) {
break;
}
const { node, parent, depth } = item;
// If maxDepth is exceeded, skip processing this node.
if (depth > maxDepth) {
continue;
}
switch (node.nodeType) {
// Just process children for fragments.
case Node.DOCUMENT_FRAGMENT_NODE: {
for (const child of node.childNodes) {
queue.push({ node: child, parent, depth: depth + 1 });
}
break;
}
// Text can be added directly if it has any non-whitespace content.
case Node.TEXT_NODE: {
const text = node.textContent;
if (text && text.trim() !== '') {
if (onText) {
parent.push(onText(text));
} else {
parent.push(text);
}
}
break;
}
// Process elements with attributes and then their children.
case Node.ELEMENT_NODE: {
if (!(node instanceof HTMLElement)) {
console.warn('Expected HTMLElement, got', node);
continue;
}
// If the tag is not allowed, skip it and its children.
if (!allowedTags.has(node.tagName.toLowerCase())) {
continue;
}
// Create the element and add it to the parent.
const children: React.ReactNode[] = [];
let element: React.ReactNode = undefined;
// If onElement is provided, use it to create the element.
if (onElement) {
const component = onElement(node, children);
// Check for undefined to allow returning null.
if (component !== undefined) {
element = component;
}
}
// If the element wasn't created, use the default conversion.
if (element === undefined) {
const props: Record<string, unknown> = {};
for (const attr of node.attributes) {
if (onAttribute) {
const result = onAttribute(
attr.name,
attr.value,
node.tagName.toLowerCase(),
);
if (result) {
const [name, value] = result;
props[name] = value;
}
} else {
props[attr.name] = attr.value;
}
}
element = React.createElement(
node.tagName.toLowerCase(),
props,
children,
);
}
// Push the element to the parent.
parent.push(element);
// Iterate over the node children with the newly created component.
for (const child of node.childNodes) {
queue.push({ node: child, parent: children, depth: depth + 1 });
}
break;
}
}
}
return rootChildren;
}

View File

@@ -2908,16 +2908,23 @@ a.account__display-name {
&:focus,
&:hover,
&:active {
&:not(:disabled) {
&:not(:disabled, [aria-disabled='true']) {
background: var(--dropdown-border-color);
outline: 0;
}
}
}
button:disabled {
button:disabled,
button[aria-disabled='true'] {
color: $dark-text-color;
cursor: default;
&:focus {
color: rgb(from $dark-text-color r g b / 70%);
background: var(--dropdown-border-color);
outline: 0;
}
}
}

View File

@@ -1,7 +1,7 @@
# frozen_string_literal: true
class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dimension::BaseDimension
include Redisable
include Admin::Metrics::Dimension::StoreHelper
def key
'software_versions'
@@ -45,13 +45,11 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
end
def redis_version
value = redis_info['redis_version']
{
key: 'redis',
human_key: 'Redis',
value: value,
human_value: value,
human_key: store_name,
value: store_version,
human_value: store_version,
}
end
@@ -117,8 +115,4 @@ class Admin::Metrics::Dimension::SoftwareVersionsDimension < Admin::Metrics::Dim
rescue Terrapin::CommandNotFoundError, Terrapin::ExitStatusError, Oj::ParseError
nil
end
def redis_info
@redis_info ||= redis.info
end
end

View File

@@ -1,8 +1,8 @@
# frozen_string_literal: true
class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension::BaseDimension
include Redisable
include ActionView::Helpers::NumberHelper
include Admin::Metrics::Dimension::StoreHelper
def key
'space_usage'
@@ -27,14 +27,12 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
end
def redis_size
value = redis_info['used_memory']
{
key: 'redis',
human_key: 'Redis',
value: value.to_s,
human_key: store_name,
value: store_size.to_s,
unit: 'bytes',
human_value: number_to_human_size(value),
human_value: number_to_human_size(store_size),
}
end
@@ -57,10 +55,6 @@ class Admin::Metrics::Dimension::SpaceUsageDimension < Admin::Metrics::Dimension
}
end
def redis_info
@redis_info ||= redis.info
end
def search_size
return unless Chewy.enabled?

View File

@@ -0,0 +1,26 @@
# frozen_string_literal: true
module Admin::Metrics::Dimension::StoreHelper
include Redisable
private
def store_name
return 'Valkey' if redis_info.key?('valkey_version')
return 'Dragonfly' if redis_info.key?('dragonfly_version')
'Redis'
end
def store_version
redis_info['valkey_version'] || redis_info['dragonfly_version'] || redis_info['redis_version']
end
def store_size
redis_info['used_memory']
end
def redis_info
@redis_info ||= redis.info
end
end

View File

@@ -8,7 +8,7 @@ class ActivityPub::QuoteAuthorizationSerializer < ActivityPub::Serializer
attributes :id, :type, :attributed_to, :interacting_object, :interaction_target
def id
ActivityPub::TagManager.instance.approval_uri_for(object)
ActivityPub::TagManager.instance.approval_uri_for(object, check_approval: !instance_options[:force_approval_id])
end
def type

View File

@@ -34,6 +34,6 @@ class RevokeQuoteService < BaseService
end
def signed_activity_json
@signed_activity_json ||= Oj.dump(serialize_payload(@quote, ActivityPub::DeleteQuoteAuthorizationSerializer, signer: @account, always_sign: true))
@signed_activity_json ||= Oj.dump(serialize_payload(@quote, ActivityPub::DeleteQuoteAuthorizationSerializer, signer: @account, always_sign: true, force_approval_id: true))
end
end

View File

@@ -1922,6 +1922,7 @@ fo:
private_long: Vís einans fyri fylgjarum
public: Alment
public_long: Øll í og uttanfyri Mastodon
unlisted: Stillur almenningur
unlisted_long: Fjalt frá leitiúrslitum, rákum og almennum tíðarlinjum í Mastodon
statuses_cleanup:
enabled: Strika gamlar postar sjálvvirkandi

View File

@@ -9,14 +9,44 @@ RSpec.describe Admin::Metrics::Dimension::SoftwareVersionsDimension do
let(:end_at) { Time.now.utc }
let(:limit) { 10 }
let(:params) { ActionController::Parameters.new }
let(:redis_human_key) { 'Redis' }
let(:redis_version) { '7.4.5' }
let(:redis_info) { { 'redis_version' => redis_version } }
describe '#data' do
it 'reports on the running software' do
expect(subject.data.map(&:symbolize_keys))
.to include(
include(key: 'mastodon', value: Mastodon::Version.to_s),
include(key: 'ruby', value: include(RUBY_VERSION))
)
shared_examples 'shared behavior' do
before do
allow(subject).to receive(:redis_info).and_return(redis_info) # rubocop:disable RSpec/SubjectStub
end
it 'reports on the running software' do
expect(subject.data.map(&:symbolize_keys))
.to include(
include(key: 'mastodon', value: Mastodon::Version.to_s),
include(key: 'ruby', value: include(RUBY_VERSION)),
include(key: 'redis', human_key: redis_human_key, value: redis_version)
)
end
end
context 'when using redis' do
it_behaves_like 'shared behavior'
end
context 'when using valkey' do
let(:redis_human_key) { 'Valkey' }
let(:redis_version) { '8.1.3' }
let(:redis_info) { { 'valkey_version' => redis_version } }
it_behaves_like 'shared behavior'
end
context 'when using dragonfly' do
let(:redis_human_key) { 'Dragonfly' }
let(:redis_version) { 'df-v1.32.0' }
let(:redis_info) { { 'dragonfly_version' => redis_version } }
it_behaves_like 'shared behavior'
end
end
end

View File

@@ -10,14 +10,41 @@ RSpec.describe Admin::Metrics::Dimension::SpaceUsageDimension do
let(:limit) { 10 }
let(:params) { ActionController::Parameters.new }
let(:redis_human_key) { 'Redis' }
let(:redis_info) { { 'redis_version' => '7.4.5', 'used_memory' => 1_024 } }
describe '#data' do
it 'reports on used storage space' do
expect(subject.data.map(&:symbolize_keys))
.to include(
include(key: 'media', value: /\d/),
include(key: 'postgresql', value: /\d/),
include(key: 'redis', value: /\d/)
)
shared_examples 'shared behavior' do
before do
allow(subject).to receive(:redis_info).and_return(redis_info) # rubocop:disable RSpec/SubjectStub
end
it 'reports on used storage space' do
expect(subject.data.map(&:symbolize_keys))
.to include(
include(key: 'media', value: /\d/),
include(key: 'postgresql', value: /\d/),
include(key: 'redis', human_key: redis_human_key, value: /\d/)
)
end
end
context 'when using redis' do
it_behaves_like 'shared behavior'
end
context 'when using valkey' do
let(:redis_human_key) { 'Valkey' }
let(:redis_info) { { 'valkey_version' => '8.1.3', 'used_memory' => 1_024 } }
it_behaves_like 'shared behavior'
end
context 'when using dragonfly' do
let(:redis_human_key) { 'Dragonfly' }
let(:redis_info) { { 'dragonfly_version' => 'df-v1.32.0', 'used_memory' => 1_024 } }
it_behaves_like 'shared behavior'
end
end
end

View File

@@ -20,7 +20,22 @@ RSpec.describe RevokeQuoteService do
it 'revokes the quote and sends a Delete activity' do
expect { described_class.new.call(quote) }
.to change { quote.reload.state }.from('accepted').to('revoked')
.and enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(/Delete/, alice.id, hank.inbox_url)
.and enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(
match_json_values(
type: 'Delete',
id: %r{https://.*},
object: include(
type: 'QuoteAuthorization',
id: %r{https://.*},
attributedTo: ActivityPub::TagManager.instance.uri_for(alice),
interactionTarget: ActivityPub::TagManager.instance.uri_for(status),
interactingObject: ActivityPub::TagManager.instance.uri_for(quote.status)
),
actor: ActivityPub::TagManager.instance.uri_for(alice)
),
alice.id,
hank.inbox_url
)
end
end
end