From 780c6378e01e05a72e71fd856f5c483b1f9e7f75 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:18:50 +0100 Subject: [PATCH 01/89] New Crowdin Translations (automated) (#37407) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/el.json | 2 +- app/javascript/mastodon/locales/sv.json | 20 ++++++++++++++++++++ config/locales/cs.yml | 1 + config/locales/cy.yml | 3 +++ config/locales/el.yml | 4 ++-- config/locales/fi.yml | 2 ++ config/locales/fo.yml | 1 + config/locales/gl.yml | 1 + config/locales/it.yml | 1 + config/locales/simple_form.cy.yml | 3 +++ config/locales/simple_form.sv.yml | 6 ++++++ config/locales/sv.yml | 7 +++++++ 12 files changed, 48 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 384a152268a..c2cf2a7231f 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -631,7 +631,7 @@ "navigation_panel.expand_followed_tags": "Επέκταση μενού ετικετών που ακολουθείτε", "navigation_panel.expand_lists": "Επέκταση μενού λίστας", "not_signed_in_indicator.not_signed_in": "Πρέπει να συνδεθείς για να αποκτήσεις πρόσβαση σε αυτόν τον πόρο.", - "notification.admin.report": "Ο/Η {name} ανέφερε τον {target}", + "notification.admin.report": "Ο/Η {name} ανέφερε τον/την {target}", "notification.admin.report_account": "Ο χρήστης {name} ανέφερε {count, plural, one {μία ανάρτηση} other {# αναρτήσεις}} από {target} για {category}", "notification.admin.report_account_other": "Ο χρήστης {name} ανέφερε {count, plural, one {μία ανάρτηση} other {# αναρτήσεις}} από {target}", "notification.admin.report_statuses": "Ο χρήστης {name} ανέφερε τον χρήστη {target} για {category}", diff --git a/app/javascript/mastodon/locales/sv.json b/app/javascript/mastodon/locales/sv.json index 461d7284084..67b8aea8c1b 100644 --- a/app/javascript/mastodon/locales/sv.json +++ b/app/javascript/mastodon/locales/sv.json @@ -122,6 +122,8 @@ "annual_report.shared_page.donate": "Donera", "annual_report.shared_page.footer": "Genererad med {heart} av Mastodon-teamet", "annual_report.shared_page.footer_server_info": "{username} använder {domain}, en av många forum som drivs av Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} jagade efter inlägg att boosta och förstärka andra skapare med perfekt sikte.", + "annual_report.summary.archetype.booster.desc_self": "Du jagade efter inlägg att boosta och förstärka andra skapare med perfekt sikte.", "annual_report.summary.archetype.booster.name": "Bågskytten", "annual_report.summary.archetype.die_drei_fragezeichen": "???", "annual_report.summary.archetype.lurker.desc_public": "Vi vet att {name} var där ute någonstans och njuter av Mastodon på sitt egna tysta sätt.", @@ -131,13 +133,31 @@ "annual_report.summary.archetype.oracle.desc_self": "Du skapade nya inlägg mer än svar och höll Mastodon fräscht och framtidsinriktat.", "annual_report.summary.archetype.oracle.name": "Oraklet", "annual_report.summary.archetype.pollster.desc_public": "{name} skapade fler undersökningar än andra inläggstyper och skapade nyfikenhet på Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Du skapade fler undersökningar än andra inläggstyper och skapade nyfikenhet på Mastodon.", + "annual_report.summary.archetype.pollster.name": "Undraren", + "annual_report.summary.archetype.replier.desc_public": "{name} svarade ofta på andras inlägg och pollinerade Mastodon med nya diskussioner.", + "annual_report.summary.archetype.replier.desc_self": "Du svarade ofta på andras inlägg och pollinerade Mastodon med nya diskussioner.", + "annual_report.summary.archetype.replier.name": "Fjärilen", + "annual_report.summary.archetype.reveal": "Avslöja min arketyp", + "annual_report.summary.archetype.reveal_description": "Tack för att du är en del av Mastodon! Dags att ta reda på vilken arketyp du förkroppsligade under {year}.", + "annual_report.summary.archetype.title_public": "{name}s arketyp", + "annual_report.summary.archetype.title_self": "Din arketyp", + "annual_report.summary.close": "Stäng", "annual_report.summary.copy_link": "Kopiera länk", + "annual_report.summary.followers.new_followers": "{count, plural, one {ny följare} other {nya följare}}", + "annual_report.summary.highlighted_post.boost_count": "Det här inlägget förstärktes {count, plural, one {en gång} other {# gånger}}.", + "annual_report.summary.highlighted_post.favourite_count": "Det här inlägget favoriserades {count, plural, one {en gång} other {# gånger}}.", + "annual_report.summary.highlighted_post.reply_count": "Det här inlägget fick {count, plural, one {ett svar} other {# svar}}.", + "annual_report.summary.highlighted_post.title": "Mest populära inlägg", "annual_report.summary.most_used_app.most_used_app": "mest använda app", "annual_report.summary.most_used_hashtag.most_used_hashtag": "mest använda hashtag", + "annual_report.summary.most_used_hashtag.used_count": "Du inkluderade denna hashtag i {count, plural, one {ett inlägg} other {# inlägg}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "{name} inkluderade denna hashtag i {count, plural, one {ett inlägg} other {# inlägg}}.", "annual_report.summary.new_posts.new_posts": "nya inlägg", "annual_report.summary.percentile.text": "Det placerar dig i toppbland {domain} användare.", "annual_report.summary.percentile.we_wont_tell_bernie": "Vi berättar inte för Bernie.", "annual_report.summary.share_elsewhere": "Dela någon annanstans", + "annual_report.summary.share_message": "Jag fick {archetype}-arketypen!", "annual_report.summary.share_on_mastodon": "Dela på Mastodon", "attachments_list.unprocessed": "(obehandlad)", "audio.hide": "Dölj audio", diff --git a/config/locales/cs.yml b/config/locales/cs.yml index 9c3975cb1c8..acbbf0cd440 100644 --- a/config/locales/cs.yml +++ b/config/locales/cs.yml @@ -2278,6 +2278,7 @@ cs: error: Při odstraňování bezpečnostního klíče došlo k chybě. Zkuste to prosím znovu. success: Váš bezpečnostní klíč byl úspěšně odstraněn. invalid_credential: Neplatný bezpečnostní klíč + nickname: Přezdívka nickname_hint: Zadejte přezdívku nového bezpečnostního klíče not_enabled: Zatím jste nepovolili WebAuthn not_supported: Tento prohlížeč nepodporuje bezpečnostní klíče diff --git a/config/locales/cy.yml b/config/locales/cy.yml index 83a9ab46c00..a74a51d31f0 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -2144,9 +2144,11 @@ cy: enabled: Dileu hen bostiadau'n awtomatig enabled_hint: Yn dileu eich postiadau yn awtomatig ar ôl iddyn nhw gyrraedd trothwy oed penodedig, oni bai eu bod yn cyfateb i un o'r eithriadau isod exceptions: Eithriadau + explanation: Caiff dileu awtomataidd ei berfformio â blaenoriaeth isel. Gall fod oedi rhwng cyrraedd y trothwy oed a chael ei ddileu. ignore_favs: Anwybyddu ffefrynnau ignore_reblogs: Anwybyddu hybiau interaction_exceptions: Eithriadau yn seiliedig ar ryngweithio + interaction_exceptions_explanation: Maen bosib cadw postiadau sy'n mynd dros dro dros y trothwy ffefryn neu hwb hyd yn oed os cawn nhw eu gostwng yn ddiweddarach. keep_direct: Cadw negeseuon uniongyrchol keep_direct_hint: Nid yw'n dileu unrhyw un o'ch negeseuon uniongyrchol keep_media: Cadw postiadau gydag atodiadau cyfryngau @@ -2363,6 +2365,7 @@ cy: error: Bu anhawster wrth ddileu eich allwedd ddiogelwch. Ceisiwch eto, os gwelwch yn dda. success: Cafodd eich allwedd ddiogelwch ei dileu'n llwyddiannus. invalid_credential: Allwedd ddiogelwch annilys + nickname: Llysenw nickname_hint: Rhowch lysenw eich allwedd ddiogelwch newydd not_enabled: Nid ydych wedi galluogi WebAuthn eto not_supported: Nid yw'r porwr hwn yn cynnal allweddi diogelwch diff --git a/config/locales/el.yml b/config/locales/el.yml index 10a88b36b58..55db7e29198 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -597,7 +597,7 @@ el: public_comment: Δημόσιο σχόλιο purge: Εκκαθάριση purge_description_html: Εάν πιστεύεις ότι αυτός ο τομέας είναι εκτός σύνδεσης μόνιμα, μπορείς να διαγράψεις όλες τις καταχωρήσεις λογαριασμών και τα σχετικά δεδομένα από αυτόν τον τομέα από τον αποθηκευτικό σου χώρο. Αυτό μπορεί να διαρκέσει λίγη ώρα. - title: Συναλλαγές + title: Ομοσπονδία total_blocked_by_us: Αποκλεισμένοι από εμάς total_followed_by_them: Ακολουθούνται από εκείνους total_followed_by_us: Ακολουθούνται από εμάς @@ -772,7 +772,7 @@ el: manage_blocks_description: Επιτρέπει στους χρήστες να αποκλείουν παρόχους email και διευθύνσεις IP manage_custom_emojis: Διαχείριση Προσαρμοσμένων Emojis manage_custom_emojis_description: Επιτρέπει στους χρήστες να διαχειρίζονται προσαρμοσμένα emojis στον διακομιστή - manage_federation: Διαχείριση Συναλλαγών + manage_federation: Διαχείριση Ομοσπονδίας manage_federation_description: Επιτρέπει στους χρήστες να αποκλείουν ή να επιτρέπουν τις συναλλαγές με άλλους τομείς και να ελέγχουν την παράδοση manage_invites: Διαχείριση Προσκλήσεων manage_invites_description: Επιτρέπει στους χρήστες να περιηγούνται και να απενεργοποιούν τους συνδέσμους πρόσκλησης diff --git a/config/locales/fi.yml b/config/locales/fi.yml index 514a7f77836..c2e5ef0750e 100644 --- a/config/locales/fi.yml +++ b/config/locales/fi.yml @@ -1973,9 +1973,11 @@ fi: enabled: Poista vanhat julkaisut automaattisesti enabled_hint: Poistaa julkaisusi automaattisesti, kun ne saavuttavat valitun ikäkynnyksen, ellei jokin alla olevista poikkeuksista tule kyseeseen exceptions: Poikkeukset + explanation: Automaattipoisto suoritetaan pienellä prioriteetilla. Ikäkynnyksen saavuttamisen ja poistetuksi tulemisen välillä saattaa olla viive. ignore_favs: Ohita suosikit ignore_reblogs: Ohita tehostukset interaction_exceptions: Vuorovaikutuksiin perustuvat poikkeukset + interaction_exceptions_explanation: Julkaisut, jotka ylittävät väliaikaisesti suosikki- tai tehostusrajan, voidaan säilyttää, vaikka rajat myöhemmin alittuisivat. keep_direct: Säilytä yksityisviestit keep_direct_hint: Ei poista yksityisviestejäsi keep_media: Säilytä julkaisut, joissa on medialiitteitä diff --git a/config/locales/fo.yml b/config/locales/fo.yml index c69fd8306ca..a531e0bc13c 100644 --- a/config/locales/fo.yml +++ b/config/locales/fo.yml @@ -2190,6 +2190,7 @@ fo: error: Ein trupulleiki var við at strika trygdarlykilin hjá tær. Vinarliga royn aftur. success: Trygdarlykilin hjá tær varð strikaður. invalid_credential: Ógyldugur trygdarlykil + nickname: Kallinavn nickname_hint: Skriva eyknevni á tínum nýggja trygdarlykli not_enabled: Tú hevur ikki gjørt WebAuthn virkið enn not_supported: Hesin kagin stuðlar ikki uppundir trygdarlyklar diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 3de76f110fb..6c9ed9b6c98 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -2190,6 +2190,7 @@ gl: error: Houbo un problema ó eliminar a túa chave de seguridade, inténtao outra vez. success: Eliminouse correctamente a chave de seguridade. invalid_credential: Chave de seguridade non válida + nickname: Sobrenome nickname_hint: Escribe un alcume para a túa nova chave de seguridade not_enabled: Aínda non tes activado WebAuthn not_supported: Este navegador non ten soporte para chaves de seguridade diff --git a/config/locales/it.yml b/config/locales/it.yml index 9e6e91e0a4b..49719320e3f 100644 --- a/config/locales/it.yml +++ b/config/locales/it.yml @@ -2190,6 +2190,7 @@ it: error: Si è verificato un problema durante la cancellazione della chiave di sicurezza. Dovresti riprovare. success: La chiave di sicurezza è stata cancellata. invalid_credential: Chiave di sicurezza non valida + nickname: Soprannome nickname_hint: Inserisci il soprannome della tua nuova chiave di sicurezza not_enabled: Non hai ancora abilitato WebAuthn not_supported: Questo browser non supporta le chiavi di sicurezza diff --git a/config/locales/simple_form.cy.yml b/config/locales/simple_form.cy.yml index b850cabb6e4..080603d66c0 100644 --- a/config/locales/simple_form.cy.yml +++ b/config/locales/simple_form.cy.yml @@ -243,6 +243,8 @@ cy: setting_always_send_emails: Anfonwch hysbysiadau e-bost bob amser setting_auto_play_gif: Chwarae GIFs wedi'u hanimeiddio yn awtomatig setting_boost_modal: Rheoli hybu gwelededd + setting_color_scheme: Modd + setting_contrast: Cyferbyniad setting_default_language: Iaith postio setting_default_privacy: Gwelededd postio setting_default_quote_policy: Pwy sy'n gallu dyfynnu @@ -317,6 +319,7 @@ cy: thumbnail: Bawdlun y gweinydd trendable_by_default: Caniatáu pynciau llosg heb adolygiad trends: Galluogi pynciau llosg + wrapstodon: Galluogi Wrapstodon interactions: must_be_follower: Blocio hysbysiadau o bobl nad ydynt yn eich dilyn must_be_following: Blocio hysbysiadau o bobl nad ydych yn eu dilyn diff --git a/config/locales/simple_form.sv.yml b/config/locales/simple_form.sv.yml index f3997a91c14..524ec6c8b1a 100644 --- a/config/locales/simple_form.sv.yml +++ b/config/locales/simple_form.sv.yml @@ -65,6 +65,7 @@ sv: setting_display_media_hide_all: Dölj alltid all media setting_display_media_show_all: Visa alltid media markerad som känslig setting_emoji_style: Hur emojier visas. "Automatiskt" kommer att försöka använda webbläsarens emojier, men faller tillbaka till Twemoji för äldre webbläsare. + setting_quick_boosting_html: När aktiverad, klicka på %{boost_icon} Boost-ikonen för att omedelbart boosta istället för att öppna boost/citera-rullgardinsmenyn. Flyttar citering till %{options_icon} (Alternativ)-menyn. setting_system_scrollbars_ui: Gäller endast för webbläsare som är baserade på Safari och Chrome setting_use_blurhash: Gradienter är baserade på färgerna av de dolda objekten men fördunklar alla detaljer setting_use_pending_items: Dölj tidslinjeuppdateringar bakom ett klick istället för att automatiskt bläddra i flödet @@ -78,6 +79,7 @@ sv: featured_tag: name: 'Här är några av de hashtaggar du använt nyligen:' filters: + action: Välj vilken åtgärd som ska utföras när ett inlägg matchar filtret actions: blur: Dölj media bakom en varning utan att dölja själva texten hide: Dölj det filtrerade innehållet helt, beter sig som om det inte fanns @@ -86,10 +88,12 @@ sv: activity_api_enabled: Antalet lokalt publicerade inlägg, aktiva användare och nya registrerade konton per vecka app_icon: WEBP, PNG, GIF eller JPG. Använd istället för appens egna ikon på mobila enheter. backups_retention_period: Användare har möjlighet att generera arkiv av sina inlägg för att ladda ned senare. När det sätts till ett positivt värde raderas dessa arkiv automatiskt från din lagring efter det angivna antalet dagar. + bootstrap_timeline_accounts: Dessa konton kommer att fästas på toppen av nya användares följ-rekommendationer. Ange en kommaseparerad lista över konton. closed_registrations_message: Visas när nyregistreringar är avstängda content_cache_retention_period: Alla inlägg från andra servrar (inklusive booster och svar) kommer att raderas efter det angivna antalet dagar, utan hänsyn till någon lokal användarinteraktion med dessa inlägg. Detta inkluderar inlägg där en lokal användare har markerat det som bokmärke eller favoriter. Privata omnämnanden mellan användare från olika instanser kommer också att gå förlorade och blir omöjliga att återställa. Användningen av denna inställning är avsedd för specialfall och bryter många användarförväntningar när de implementeras för allmänt bruk. custom_css: Du kan använda anpassade stilar på webbversionen av Mastodon. favicon: WEBP, PNG, GIF eller JPG. Används på mobila enheter istället för appens egen ikon. + landing_page: Väljer vilken sida nya besökare ser när de först anländer till din server. Om du väljer "Trender" måste trenderna aktiveras i Upptäckningsinställningarna. Om du väljer "Lokalt flöde" måste "Åtkomst till live-flöden med lokala inlägg" sättas till "Alla" i Upptäckningsinställningarna. mascot: Åsidosätter illustrationen i det avancerade webbgränssnittet. media_cache_retention_period: Mediafiler från inlägg som gjorts av fjärranvändare cachas på din server. När inställd på ett positivt värde kommer media att raderas efter det angivna antalet dagar. Om mediadatat begärs efter att det har raderats, kommer det att laddas ned igen om källinnehållet fortfarande är tillgängligt. På grund av begränsningar för hur ofta förhandsgranskningskort för länkar hämtas från tredjepartswebbplatser, rekommenderas det att ange detta värde till minst 14 dagar, annars kommer förhandsgranskningskorten inte att uppdateras på begäran före den tiden. min_age: Användare kommer att bli ombedda att bekräfta sitt födelsedatum under registreringen @@ -107,6 +111,7 @@ sv: thumbnail: En bild i cirka 2:1-proportioner som visas tillsammans med din serverinformation. trendable_by_default: Hoppa över manuell granskning av trendande innehåll. Enskilda objekt kan ändå raderas från trender retroaktivt. trends: Trender visar vilka inlägg, hashtaggar och nyheter det pratas om på din server. + wrapstodon: Erbjud lokala användare att generera en lekfull sammanfattning av deras Mastodon-användning under året. Denna funktion är tillgänglig mellan den 10 och 31 december varje år, och erbjuds till användare som gjort minst ett Offentligt eller Tyst Offentligt inlägg och använt minst en hashtag under året. form_challenge: current_password: Du går in i ett säkert område imports: @@ -233,6 +238,7 @@ sv: setting_aggregate_reblogs: Gruppera boostar i tidslinjer setting_always_send_emails: Skicka alltid e-postnotiser setting_auto_play_gif: Spela upp GIF:ar automatiskt + setting_boost_modal: Kontrollera boost-synlighet setting_color_scheme: Läge setting_contrast: Kontrast setting_default_language: Inläggsspråk diff --git a/config/locales/sv.yml b/config/locales/sv.yml index 5b50f5846d7..d390baf720e 100644 --- a/config/locales/sv.yml +++ b/config/locales/sv.yml @@ -7,6 +7,8 @@ sv: hosted_on: Mastodon-värd på %{domain} title: Om accounts: + errors: + cannot_be_added_to_collections: Detta konto kan inte läggas till i samlingar. followers: one: Följare other: Följare @@ -1714,12 +1716,16 @@ sv: body: 'Du nämndes av %{name} i:' subject: Du nämndes av %{name} title: Ny omnämning + moderation_warning: + subject: Du har mottagit en modereringsvarning poll: subject: En undersökning av %{name} har avslutats quote: body: 'Ditt inlägg citerades av %{name}:' subject: "%{name} citerade ditt inlägg" title: Nytt citat + quoted_update: + subject: "%{name} redigerade ett inlägg du har citerat" reblog: body: 'Ditt inlägg boostades av %{name}:' subject: "%{name} boostade ditt inlägg" @@ -2192,3 +2198,4 @@ sv: registered_on: Registrerad den %{date} wrapstodon: description: Se hur %{name} använde Mastodon i år! + title: Wrapstodon %{year} för %{name} From 25053db22ba1e7c6ea1fb08f4a18b95305305d42 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Fri, 9 Jan 2026 00:22:31 +1100 Subject: [PATCH 02/89] Fix modal closeModal ignoreFocus payload type (#37424) Co-authored-by: diondiondion --- .../mastodon/features/ui/containers/modal_container.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/ui/containers/modal_container.js b/app/javascript/mastodon/features/ui/containers/modal_container.js index fe873804319..2bf771e2a3d 100644 --- a/app/javascript/mastodon/features/ui/containers/modal_container.js +++ b/app/javascript/mastodon/features/ui/containers/modal_container.js @@ -23,7 +23,7 @@ const mapDispatchToProps = dispatch => ({ confirm: confirmationMessage.confirm, onConfirm: () => dispatch(closeModal({ modalType: undefined, - ignoreFocus: { ignoreFocus }, + ignoreFocus, })), }, }), @@ -31,7 +31,7 @@ const mapDispatchToProps = dispatch => ({ } else { dispatch(closeModal({ modalType: undefined, - ignoreFocus: { ignoreFocus }, + ignoreFocus, })); } }, From 628fc9b95b064f7feba7f54f3c4146210828f18d Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 9 Jan 2026 11:37:14 +0100 Subject: [PATCH 03/89] Emoji: Adds search (#37418) Co-authored-by: diondiondion --- .../mastodon/components/emoji/index.tsx | 5 +- .../mastodon/features/emoji/constants.ts | 2 + .../mastodon/features/emoji/database.test.ts | 105 +----- .../mastodon/features/emoji/database.ts | 305 +++++++++--------- .../mastodon/features/emoji/db-schema.ts | 198 ++++++++++++ .../mastodon/features/emoji/loader.ts | 26 +- .../mastodon/features/emoji/locale.ts | 7 + .../mastodon/features/emoji/mode.ts | 2 + .../mastodon/features/emoji/normalize.test.ts | 46 +-- .../mastodon/features/emoji/normalize.ts | 269 ++++++++------- .../mastodon/features/emoji/render.ts | 9 +- .../mastodon/features/emoji/types.ts | 16 +- .../mastodon/features/emoji/utils.ts | 5 + app/javascript/testing/factories.ts | 4 + 14 files changed, 571 insertions(+), 428 deletions(-) create mode 100644 app/javascript/mastodon/features/emoji/db-schema.ts diff --git a/app/javascript/mastodon/components/emoji/index.tsx b/app/javascript/mastodon/components/emoji/index.tsx index 0b1ba7fef38..9b917df6ea0 100644 --- a/app/javascript/mastodon/components/emoji/index.tsx +++ b/app/javascript/mastodon/components/emoji/index.tsx @@ -88,7 +88,10 @@ export const Emoji: FC = ({ ); } - const src = unicodeHexToUrl(state.code, appState.darkTheme); + const src = unicodeHexToUrl({ + unicodeHex: state.code, + ...appState, + }); return ( = {}): CompactEmoji { + return { + ...unicodeEmojiFactory(), + tags: ['test', 'emoji'], + ...data, + }; +} + describe('emoji database', () => { afterEach(() => { testClear(); @@ -32,7 +39,7 @@ describe('emoji database', () => { }); test('loads emoji into indexedDB', async () => { - await putEmojiData([unicodeEmojiFactory()], 'en'); + await putEmojiData([rawEmojiFactory()], 'en'); const { db } = await testGet(); await expect(db.get('en', 'test')).resolves.toEqual( unicodeEmojiFactory(), @@ -60,7 +67,7 @@ describe('emoji database', () => { }); await expect(db.get('custom', 'emoji1')).resolves.toBeUndefined(); await expect(db.get('custom', 'emoji2')).resolves.toEqual( - customEmojiFactory({ shortcode: 'emoji2' }), + customEmojiFactory({ shortcode: 'emoji2', tokens: ['emoji2'] }), ); }); }); @@ -79,12 +86,6 @@ describe('emoji database', () => { }); describe('loadEmojiByHexcode', () => { - test('throws if the locale is not loaded', async () => { - await expect(loadEmojiByHexcode('en', 'test')).rejects.toThrowError( - 'Locale en', - ); - }); - test('retrieves the emoji', async () => { await putEmojiData([unicodeEmojiFactory()], 'en'); await expect(loadEmojiByHexcode('test', 'en')).resolves.toEqual( @@ -98,90 +99,6 @@ describe('emoji database', () => { }); }); - describe('searchEmojisByHexcodes', () => { - const data = [ - unicodeEmojiFactory({ hexcode: 'not a number' }), - unicodeEmojiFactory({ hexcode: '1' }), - unicodeEmojiFactory({ hexcode: '2' }), - unicodeEmojiFactory({ hexcode: '3' }), - unicodeEmojiFactory({ hexcode: 'another not a number' }), - ]; - beforeEach(async () => { - await putEmojiData(data, 'en'); - }); - test('finds emoji in consecutive range', async () => { - const actual = await searchEmojisByHexcodes(['1', '2', '3'], 'en'); - expect(actual).toHaveLength(3); - }); - - test('finds emoji in split range', async () => { - const actual = await searchEmojisByHexcodes(['1', '3'], 'en'); - expect(actual).toHaveLength(2); - expect(actual).toContainEqual(data.at(1)); - expect(actual).toContainEqual(data.at(3)); - }); - - test('finds emoji with non-numeric range', async () => { - const actual = await searchEmojisByHexcodes( - ['3', 'not a number', '1'], - 'en', - ); - expect(actual).toHaveLength(3); - expect(actual).toContainEqual(data.at(0)); - expect(actual).toContainEqual(data.at(1)); - expect(actual).toContainEqual(data.at(3)); - }); - - test('not found emoji are not returned', async () => { - const actual = await searchEmojisByHexcodes(['not found'], 'en'); - expect(actual).toHaveLength(0); - }); - - test('only found emojis are returned', async () => { - const actual = await searchEmojisByHexcodes( - ['another not a number', 'not found'], - 'en', - ); - expect(actual).toHaveLength(1); - expect(actual).toContainEqual(data.at(4)); - }); - }); - - describe('searchEmojisByTag', () => { - const data = [ - unicodeEmojiFactory({ hexcode: 'test1', tags: ['test 1'] }), - unicodeEmojiFactory({ - hexcode: 'test2', - tags: ['test 2', 'something else'], - }), - unicodeEmojiFactory({ hexcode: 'test3', tags: ['completely different'] }), - ]; - beforeEach(async () => { - await putEmojiData(data, 'en'); - }); - test('finds emojis with tag', async () => { - const actual = await searchEmojisByTag('test 1', 'en'); - expect(actual).toHaveLength(1); - expect(actual).toContainEqual(data.at(0)); - }); - - test('finds emojis starting with tag', async () => { - const actual = await searchEmojisByTag('test', 'en'); - expect(actual).toHaveLength(2); - expect(actual).not.toContainEqual(data.at(2)); - }); - - test('does not find emojis ending with tag', async () => { - const actual = await searchEmojisByTag('else', 'en'); - expect(actual).toHaveLength(0); - }); - - test('finds nothing with invalid tag', async () => { - const actual = await searchEmojisByTag('not found', 'en'); - expect(actual).toHaveLength(0); - }); - }); - describe('loadLegacyShortcodesByShortcode', () => { const data = { hexcode: 'test_hexcode', diff --git a/app/javascript/mastodon/features/emoji/database.ts b/app/javascript/mastodon/features/emoji/database.ts index fe4010a861d..f64f3fb80d6 100644 --- a/app/javascript/mastodon/features/emoji/database.ts +++ b/app/javascript/mastodon/features/emoji/database.ts @@ -1,55 +1,25 @@ import { SUPPORTED_LOCALES } from 'emojibase'; -import type { Locale, ShortcodesDataset } from 'emojibase'; -import type { DBSchema, IDBPDatabase } from 'idb'; -import { openDB } from 'idb'; +import type { CompactEmoji, Locale, ShortcodesDataset } from 'emojibase'; + +import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import { EMOJI_DB_SHORTCODE_TEST } from './constants'; -import { toSupportedLocale, toSupportedLocaleOrCustom } from './locale'; -import type { CustomEmojiData, UnicodeEmojiData, EtagTypes } from './types'; +import { openEmojiDB } from './db-schema'; +import type { Database } from './db-schema'; +import { + localeToSegmenter, + toSupportedLocale, + toSupportedLocaleOrCustom, +} from './locale'; +import { + extractTokens, + skinHexcodeToEmoji, + transformCustomEmojiData, + transformEmojiData, +} from './normalize'; +import type { AnyEmojiData, EtagTypes } from './types'; import { emojiLogger } from './utils'; -interface EmojiDB extends LocaleTables, DBSchema { - custom: { - key: string; - value: CustomEmojiData; - indexes: { - category: string; - }; - }; - shortcodes: { - key: string; - value: { - hexcode: string; - shortcodes: string[]; - }; - indexes: { - hexcode: string; - shortcodes: string[]; - }; - }; - etags: { - key: EtagTypes; - value: string; - }; -} - -interface LocaleTable { - key: string; - value: UnicodeEmojiData; - indexes: { - group: number; - label: string; - order: number; - tags: string[]; - shortcodes: string[]; - }; -} -type LocaleTables = Record; - -type Database = IDBPDatabase; - -const SCHEMA_VERSION = 2; - const loadedLocales = new Set(); const log = emojiLogger('database'); @@ -60,75 +30,7 @@ const loadDB = (() => { // Actually load the DB. async function initDB() { - const db = await openDB('mastodon-emoji', SCHEMA_VERSION, { - upgrade(database, oldVersion, newVersion, trx) { - if (!database.objectStoreNames.contains('custom')) { - const customTable = database.createObjectStore('custom', { - keyPath: 'shortcode', - autoIncrement: false, - }); - customTable.createIndex('category', 'category'); - } - - if (!database.objectStoreNames.contains('etags')) { - database.createObjectStore('etags'); - } - - for (const locale of SUPPORTED_LOCALES) { - if (!database.objectStoreNames.contains(locale)) { - const localeTable = database.createObjectStore(locale, { - keyPath: 'hexcode', - autoIncrement: false, - }); - localeTable.createIndex('group', 'group'); - localeTable.createIndex('label', 'label'); - localeTable.createIndex('order', 'order'); - localeTable.createIndex('tags', 'tags', { multiEntry: true }); - localeTable.createIndex('shortcodes', 'shortcodes', { - multiEntry: true, - }); - } - // Added in version 2. - const localeTable = trx.objectStore(locale); - if (!localeTable.indexNames.contains('shortcodes')) { - localeTable.createIndex('shortcodes', 'shortcodes', { - multiEntry: true, - }); - } - } - - if (!database.objectStoreNames.contains('shortcodes')) { - const shortcodeTable = database.createObjectStore('shortcodes', { - keyPath: 'hexcode', - autoIncrement: false, - }); - shortcodeTable.createIndex('hexcode', 'hexcode'); - shortcodeTable.createIndex('shortcodes', 'shortcodes', { - multiEntry: true, - }); - } - - log( - 'Upgraded emoji database from version %d to %d', - oldVersion, - newVersion, - ); - }, - blocked(currentVersion, blockedVersion) { - log( - 'Emoji database upgrade from version %d to %d is blocked', - currentVersion, - blockedVersion, - ); - }, - blocking(currentVersion, blockedVersion) { - log( - 'Emoji database upgrade from version %d is blocking upgrade to %d', - currentVersion, - blockedVersion, - ); - }, - }); + const db = await openEmojiDB(); await syncLocales(db); log('Loaded database version %d', db.version); return db; @@ -149,11 +51,128 @@ const loadDB = (() => { return loadPromise; })(); -export async function putEmojiData(emojis: UnicodeEmojiData[], locale: Locale) { +export async function search({ + query, + locale: localeString, + limit = 0, +}: { + query: string; + locale: string; + limit?: number; +}) { + performance.mark('emoji-search-start'); + + // Get the locale, and extract tokens from the query. + const locale = await toLoadedLocale(localeString); + const segmenter = localeToSegmenter(locale); + const queryTokens = extractTokens(query, segmenter); + + if (queryTokens.length === 0) { + log('no tokens extracted from query "%s"', query); + return []; + } + const lastToken = queryTokens.at(-1); + if (!lastToken) { + throw new Error('Missing tokens from query'); + } + + log('searching for tokens %o in locale %s', queryTokens, locale); + + // Create an array of emoji results + const db = await loadDB(); + const resultArrays: Map[] = []; + for (let i = 0; i < queryTokens.length; i++) { + const token = queryTokens[i]; + if (!token) continue; + + // Only query the range for the last token to allow partial matches. + const range = + i === queryTokens.length - 1 + ? IDBKeyRange.bound(token, token + '\uffff') + : IDBKeyRange.only(token); + + const [unicodeResults, customResults] = await Promise.all([ + db.getAllFromIndex(locale, 'tokens', range), + db.getAllFromIndex('custom', 'tokens', range), + ]); + const resultMap = new Map([ + ...unicodeResults.map( + (emoji) => [emoji.hexcode, emoji] as [string, AnyEmojiData], + ), + ...customResults.map( + (emoji) => [emoji.shortcode, emoji] as [string, AnyEmojiData], + ), + ]); + log('found %d results for token "%s"', resultMap.size, token); + resultArrays.push(resultMap); + } + + // Utilize maps to find the intersection of all result sets. + const results = Array.from( + resultArrays + .reduce((prev, curr) => { + const intersection = new Map(); + for (const [code, emoji] of prev) { + if (curr.has(code)) { + intersection.set(code, emoji); + } + } + return intersection; + }) + .values(), + ); + + results.sort((a, b) => { + // Checks if a or b has the last token exactly, or only a prefix. + const aHasToken = a.tokens.includes(lastToken); + const bHasToken = b.tokens.includes(lastToken); + if (aHasToken && !bHasToken) { + return -1; + } else if (!aHasToken && bHasToken) { + return 1; + } + + // If one is a custom emoji, prioritize it over Unicode emojis. + if ('category' in a) { + return -1; + } else if ('category' in b) { + return 1; + } + + // If both are Unicode emojis, prioritize by order. + if ('order' in a && 'order' in b) { + return (a.order ?? 0) - (b.order ?? 0); // If these are both Unicode emojis, sort by order. + } + + // ¯\_(ツ)_/¯ + return 0; + }); + + const time = performance.measure('emoji-search-end', 'emoji-search-start'); + log( + 'search for "%s" in locale %s returned %d results and took %dms', + query, + locale, + results.length, + time.duration, + ); + if (limit > 0) { + return results.slice(0, limit); + } + return results; +} + +export async function putEmojiData(emojis: CompactEmoji[], locale: Locale) { loadedLocales.add(locale); const db = await loadDB(); const trx = db.transaction(locale, 'readwrite'); - await Promise.all(emojis.map((emoji) => trx.store.put(emoji))); + await trx.store.clear(); + const segmenter = localeToSegmenter(locale); + await Promise.all( + emojis + .sort((a, b) => (a.order ?? 0) - (b.order ?? 0)) + .map((emoji) => trx.store.put(transformEmojiData(emoji, segmenter))), + ); await trx.done; } @@ -161,7 +180,7 @@ export async function putCustomEmojiData({ emojis, clear = false, }: { - emojis: CustomEmojiData[]; + emojis: ApiCustomEmojiJSON[]; clear?: boolean; }) { const db = await loadDB(); @@ -173,7 +192,9 @@ export async function putCustomEmojiData({ log('Cleared existing custom emojis in database'); } - await Promise.all(emojis.map((emoji) => trx.store.put(emoji))); + await Promise.all( + emojis.map((emoji) => trx.store.put(transformCustomEmojiData(emoji))), + ); await trx.done; log('Imported %d custom emojis into database', emojis.length); @@ -210,32 +231,25 @@ export async function loadEmojiByHexcode( localeString: string, ) { const db = await loadDB(); - const locale = toLoadedLocale(localeString); - return db.get(locale, hexcode); -} + const locale = await toLoadedLocale(localeString); + const result = await db.get(locale, hexcode); + if (result) { + return result; + } -export async function searchEmojisByHexcodes( - hexcodes: string[], - localeString: string, -) { - const db = await loadDB(); - const locale = toLoadedLocale(localeString); - const sortedCodes = hexcodes.toSorted(); - const results = await db.getAll( + // If the emoji wasn't found, check if it's a skin tone variant. + const skinResult = await db.getFromIndex( locale, - IDBKeyRange.bound(sortedCodes.at(0), sortedCodes.at(-1)), + 'skinHexcodes', + IDBKeyRange.only(hexcode), ); - return results.filter((emoji) => hexcodes.includes(emoji.hexcode)); -} -export async function searchEmojisByTag(tag: string, localeString: string) { - const db = await loadDB(); - const locale = toLoadedLocale(localeString); - const range = IDBKeyRange.bound( - tag.toLowerCase(), - `${tag.toLowerCase()}\uffff`, - ); - return db.getAllFromIndex(locale, 'tags', range); + if (!skinResult) { + return skinResult; + } + + // Reconstruct the full unicode string from the skin tone hexcode. + return skinHexcodeToEmoji(hexcode, skinResult); } export async function loadCustomEmojiByShortcode(shortcode: string) { @@ -301,13 +315,16 @@ async function syncLocales(db: Database) { log('Loaded %d locales: %o', loadedLocales.size, loadedLocales); } -function toLoadedLocale(localeString: string) { +async function toLoadedLocale(localeString: string) { const locale = toSupportedLocale(localeString); if (localeString !== locale) { log(`Locale ${locale} is different from provided ${localeString}`); } if (!loadedLocales.has(locale)) { - throw new LocaleNotLoadedError(locale); + log('Locale %s not loaded, importing...', locale); + const { importEmojiData } = await import('./loader'); + await importEmojiData(locale); + return locale; } return locale; } diff --git a/app/javascript/mastodon/features/emoji/db-schema.ts b/app/javascript/mastodon/features/emoji/db-schema.ts new file mode 100644 index 00000000000..f5582cf0c33 --- /dev/null +++ b/app/javascript/mastodon/features/emoji/db-schema.ts @@ -0,0 +1,198 @@ +import { SUPPORTED_LOCALES } from 'emojibase'; +import type { Locale } from 'emojibase'; +import { openDB } from 'idb'; +import type { + DBSchema, + IDBPDatabase, + IDBPObjectStore, + IDBPTransaction, + IndexNames, + StoreNames, +} from 'idb'; + +import type { CustomEmojiData, EtagTypes, UnicodeEmojiData } from './types'; +import { emojiLogger } from './utils'; + +const log = emojiLogger('database'); + +interface EmojiDB extends LocaleTables, DBSchema { + custom: { + key: string; + value: CustomEmojiData; + indexes: { + tokens: string[]; + category: string; + }; + }; + shortcodes: { + key: string; + value: { + hexcode: string; + shortcodes: string[]; + }; + indexes: { + shortcodes: string[]; + }; + }; + etags: { + key: EtagTypes; + value: string; + }; +} + +interface LocaleTable { + key: string; + value: UnicodeEmojiData; + indexes: { + shortcodes: string[]; + groupOrder: [number, number]; + tokens: string[]; + skinHexcodes: string[]; + }; +} +type LocaleTables = Record; + +type Transaction = + IDBPTransaction[], Mode>; + +export type Database = IDBPDatabase; + +const SCHEMA_VERSION = 3; + +export async function openEmojiDB() { + const db = await openDB('mastodon-emoji', SCHEMA_VERSION, { + upgrade(database, oldVersion, newVersion, trx) { + if (!database.objectStoreNames.contains('custom')) { + database.createObjectStore('custom', { + keyPath: 'shortcode', + autoIncrement: false, + }); + } + maybeAddIndex({ trx, storeName: 'custom', indexName: 'category' }); + maybeAddIndex({ + trx, + storeName: 'custom', + indexName: 'tokens', + options: { multiEntry: true }, + }); + + if (!database.objectStoreNames.contains('etags')) { + database.createObjectStore('etags'); + } + + SUPPORTED_LOCALES.forEach((locale) => { + createLocaleTable(locale, database, trx); + }); + + const shortcodeTable = database.objectStoreNames.contains('shortcodes') + ? trx.objectStore('shortcodes') + : database.createObjectStore('shortcodes', { + keyPath: 'hexcode', + autoIncrement: false, + }); + maybeAddIndex({ + trx, + storeName: 'shortcodes', + indexName: 'shortcodes', + options: { multiEntry: true }, + }); + deleteOldIndexes(shortcodeTable, ['hexcode']); + + log( + 'Upgraded emoji database from version %d to %d', + oldVersion, + newVersion, + ); + }, + blocked(currentVersion, blockedVersion) { + log( + 'Emoji database upgrade from version %d to %d is blocked', + currentVersion, + blockedVersion, + ); + }, + blocking(currentVersion, blockedVersion) { + log( + 'Emoji database upgrade from version %d is blocking upgrade to %d', + currentVersion, + blockedVersion, + ); + }, + }); + + return db; +} + +function maybeAddIndex>({ + trx, + storeName, + indexName, + keys, + options, +}: { + trx: Transaction; + storeName: StoreName; + indexName: IndexNames; + keys?: string | string[]; + options?: IDBIndexParameters; +}) { + const store = trx.objectStore(storeName); + if (!store.indexNames.contains(indexName)) { + store.createIndex(indexName, keys ?? indexName, options); + } +} + +function createLocaleTable( + locale: Locale, + database: Database, + trx: Transaction, +) { + if (!database.objectStoreNames.contains(locale)) { + database.createObjectStore(locale, { + keyPath: 'hexcode', + autoIncrement: false, + }); + } + + maybeAddIndex({ + trx, + storeName: locale, + indexName: 'shortcodes', + options: { multiEntry: true }, + }); + maybeAddIndex({ + trx, + storeName: locale, + indexName: 'groupOrder', + keys: ['group', 'order'], + }); + maybeAddIndex({ + trx, + storeName: locale, + indexName: 'tokens', + keys: 'tokens', + options: { multiEntry: true }, + }); + maybeAddIndex({ + trx, + storeName: locale, + indexName: 'skinHexcodes', + keys: 'skinHexcodes', + options: { multiEntry: true }, + }); + + const oldIndexes = ['group', 'order', 'tag', 'label'] as const; + deleteOldIndexes(trx.objectStore(locale), oldIndexes); +} + +function deleteOldIndexes( + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- Type is too complex, so only any works here. + table: IDBPObjectStore, + indexes: readonly string[], +) { + for (const index of indexes) { + if (table.indexNames.contains(index)) { + table.deleteIndex(index); + } + } +} diff --git a/app/javascript/mastodon/features/emoji/loader.ts b/app/javascript/mastodon/features/emoji/loader.ts index c6b64fe29c0..7a94d604a98 100644 --- a/app/javascript/mastodon/features/emoji/loader.ts +++ b/app/javascript/mastodon/features/emoji/loader.ts @@ -1,10 +1,5 @@ -import { flattenEmojiData } from 'emojibase'; -import type { - CompactEmoji, - FlatCompactEmoji, - Locale, - ShortcodesDataset, -} from 'emojibase'; +import { joinShortcodes } from 'emojibase'; +import type { CompactEmoji, Locale, ShortcodesDataset } from 'emojibase'; import { putEmojiData, @@ -28,7 +23,7 @@ export async function importEmojiData(localeString: string, shortcodes = true) { shortcodes ? ' and shortcodes' : '', ); - const emojis = await fetchAndCheckEtag({ + let emojis = await fetchAndCheckEtag({ etagString: locale, path: localeToEmojiPath(locale), }); @@ -49,12 +44,10 @@ export async function importEmojiData(localeString: string, shortcodes = true) { } } - const flattenedEmojis: FlatCompactEmoji[] = flattenEmojiData( - emojis, - shortcodesData, - ); - await putEmojiData(flattenedEmojis, locale); - return flattenedEmojis; + emojis = joinShortcodes(emojis, shortcodesData); + + await putEmojiData(emojis, locale); + return emojis; } export async function importCustomEmojiData() { @@ -135,11 +128,11 @@ async function fetchAndCheckEtag({ checkEtag?: boolean; }): Promise { const etagName = toValidEtagName(etagString); + const oldEtag = checkEtag ? await loadLatestEtag(etagName) : null; // Use location.origin as this script may be loaded from a CDN domain. const url = new URL(path, location.origin); - const oldEtag = checkEtag ? await loadLatestEtag(etagName) : null; const response = await fetch(url, { headers: { 'Content-Type': 'application/json', @@ -148,6 +141,7 @@ async function fetchAndCheckEtag({ }); // If not modified, return null if (response.status === 304) { + log('etag not modified for %s', etagName); return null; } if (!response.ok) { @@ -163,6 +157,8 @@ async function fetchAndCheckEtag({ if (etag && checkEtag) { log(`storing new etag for ${etagName}: ${etag}`); await putLatestEtag(etag, etagName); + } else if (!etag) { + log(`no etag found in response for ${etagName}`); } return data; diff --git a/app/javascript/mastodon/features/emoji/locale.ts b/app/javascript/mastodon/features/emoji/locale.ts index f39b56d47c2..e8f2df340f1 100644 --- a/app/javascript/mastodon/features/emoji/locale.ts +++ b/app/javascript/mastodon/features/emoji/locale.ts @@ -32,6 +32,13 @@ export function toValidEtagName(input: string): EtagTypes { return toSupportedLocale(lower); } +export function localeToSegmenter(locale: Locale): Intl.Segmenter | null { + if (typeof Intl.Segmenter === 'function') { + return new Intl.Segmenter(locale, { granularity: 'word' }); + } + return null; +} + function isSupportedLocale(locale: string): locale is Locale { return SUPPORTED_LOCALES.includes(locale as Locale); } diff --git a/app/javascript/mastodon/features/emoji/mode.ts b/app/javascript/mastodon/features/emoji/mode.ts index 6950626375e..6763fc4b50b 100644 --- a/app/javascript/mastodon/features/emoji/mode.ts +++ b/app/javascript/mastodon/features/emoji/mode.ts @@ -2,6 +2,7 @@ // See: https://github.com/nolanlawson/emoji-picker-element/blob/master/src/picker/utils/testColorEmojiSupported.js import { createAppSelector, useAppSelector } from '@/mastodon/store'; +import { assetHost } from '@/mastodon/utils/config'; import { isDevelopment } from '@/mastodon/utils/environment'; import { isDarkMode } from '@/mastodon/utils/theme'; @@ -29,6 +30,7 @@ export function useEmojiAppState(): EmojiAppState { locales: [locale], mode, darkTheme: isDarkMode(), + assetHost, }; } diff --git a/app/javascript/mastodon/features/emoji/normalize.test.ts b/app/javascript/mastodon/features/emoji/normalize.test.ts index 8222ab81e58..8c6346709b8 100644 --- a/app/javascript/mastodon/features/emoji/normalize.test.ts +++ b/app/javascript/mastodon/features/emoji/normalize.test.ts @@ -4,11 +4,7 @@ import { basename, resolve } from 'path'; import { flattenEmojiData } from 'emojibase'; import unicodeRawEmojis from 'emojibase-data/en/data.json'; -import { - twemojiToUnicodeInfo, - unicodeToTwemojiHex, - emojiToUnicodeHex, -} from './normalize'; +import { unicodeToTwemojiHex } from './normalize'; const emojiSVGFiles = await readdir( // This assumes tests are run from project root @@ -26,23 +22,6 @@ const svgFileNamesWithoutBorder = svgFileNames.filter( const unicodeEmojis = flattenEmojiData(unicodeRawEmojis); -describe('emojiToUnicodeHex', () => { - test.concurrent.for([ - ['🎱', '1F3B1'], - ['🐜', '1F41C'], - ['⚫', '26AB'], - ['🖤', '1F5A4'], - ['💀', '1F480'], - ['❤️', '2764'], // Checks for trailing variation selector removal. - ['💂‍♂️', '1F482-200D-2642-FE0F'], - ] as const)( - 'emojiToUnicodeHex converts %s to %s', - ([emoji, hexcode], { expect }) => { - expect(emojiToUnicodeHex(emoji)).toBe(hexcode); - }, - ); -}); - describe('unicodeToTwemojiHex', () => { test.concurrent.for( unicodeEmojis @@ -54,26 +33,3 @@ describe('unicodeToTwemojiHex', () => { expect(svgFileNamesWithoutBorder).toContain(result); }); }); - -describe('twemojiToUnicodeInfo', () => { - const unicodeCodeSet = new Set(unicodeEmojis.map((emoji) => emoji.hexcode)); - - test.concurrent.for(svgFileNamesWithoutBorder)( - 'verifying SVG file %s maps to Unicode emoji', - (svgFileName, { expect }) => { - assert(!!svgFileName); - const result = twemojiToUnicodeInfo(svgFileName); - const hexcode = typeof result === 'string' ? result : result.unqualified; - if (!hexcode) { - // No hexcode means this is a special case like the Shibuya 109 emoji - expect(result).toHaveProperty('label'); - return; - } - assert(!!hexcode); - expect( - unicodeCodeSet.has(hexcode), - `${hexcode} (${svgFileName}) not found`, - ).toBeTruthy(); - }, - ); -}); diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index 24df808ae1f..bf06e058ef6 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -1,48 +1,124 @@ import { isList } from 'immutable'; -import { assetHost } from '@/mastodon/utils/config'; +import type { CompactEmoji, SkinTone } from 'emojibase'; +import { fromHexcodeToCodepoint } from 'emojibase'; + +import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import { VARIATION_SELECTOR_CODE, KEYCAP_CODE, - GENDER_FEMALE_CODE, - GENDER_MALE_CODE, - SKIN_TONE_CODES, EMOJIS_WITH_DARK_BORDER, EMOJIS_WITH_LIGHT_BORDER, EMOJIS_REQUIRING_INVERSION_IN_LIGHT_MODE, EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE, + EMOJI_MIN_TOKEN_LENGTH, } from './constants'; -import type { CustomEmojiMapArg, ExtraCustomEmojiMap } from './types'; +import type { + CustomEmojiData, + CustomEmojiMapArg, + ExtraCustomEmojiMap, + UnicodeEmojiData, +} from './types'; +import { emojiToUnicodeHex } from './utils'; -// Misc codes that have special handling -const SKIER_CODE = 0x26f7; -const CHRISTMAS_TREE_CODE = 0x1f384; -const MR_CLAUS_CODE = 0x1f385; -const EYE_CODE = 0x1f441; -const LEVITATING_PERSON_CODE = 0x1f574; -const SPEECH_BUBBLE_CODE = 0x1f5e8; -const MS_CLAUS_CODE = 0x1f936; +const SKIN_TONE_MAP: Record = { + 0x1f3fb: 1, // Light skin tone + 0x1f3fc: 2, // Medium-light skin tone + 0x1f3fd: 3, // Medium skin tone + 0x1f3fe: 4, // Medium-dark skin tone + 0x1f3ff: 5, // Dark skin tone +}; -export function emojiToUnicodeHex(emoji: string): string { - const codes: number[] = []; - for (const char of emoji) { - const code = char.codePointAt(0); - if (code !== undefined) { - codes.push(code); +export function transformEmojiData( + emoji: CompactEmoji, + segmenter: Intl.Segmenter | null, +): UnicodeEmojiData { + const { + shortcodes = [], + tags = [], + label, + emoticon, + hexcode, + unicode, + group, + order, + skins = [], + } = emoji; + const extract = (str: string) => extractTokens(str, segmenter); + + let normalizedEmoticons: string[] | undefined = undefined; + if (emoticon) { + normalizedEmoticons = Array.isArray(emoticon) ? emoticon : [emoticon]; + } + + const tokens = [ + ...new Set([ + ...shortcodes.map(extract).flat(), + ...tags.map(extract).flat(), + ...extract(label), + ...(normalizedEmoticons ?? []), + ]), + ].sort((a, b) => a.localeCompare(b)); + + const res: UnicodeEmojiData = { + tokens, + shortcodes, + label, + emoticons: normalizedEmoticons, + hexcode, + unicode, + group, + order, + }; + + for (const skin of skins) { + res.skinHexcodes ??= []; + res.skinHexcodes.push(skin.hexcode); + + res.skinTones ??= []; + for (const codePoint of skin.unicode) { + const tone = SKIN_TONE_MAP[codePoint.codePointAt(0) ?? 0]; + if (tone) { + res.skinTones.push(tone); + break; + } } } - // Handles how Emojibase removes the variation selector for single code emojis. - // See: https://emojibase.dev/docs/spec/#merged-variation-selectors - if (codes.at(1) === VARIATION_SELECTOR_CODE && codes.length === 2) { - codes.pop(); - } - return hexNumbersToString(codes); + return res; } +export function transformCustomEmojiData( + emoji: ApiCustomEmojiJSON, +): CustomEmojiData { + const tokens = emoji.shortcode + .split('_') + .filter((word) => word.length >= EMOJI_MIN_TOKEN_LENGTH) + .map((word) => word.toLowerCase()); + return { + ...emoji, + tokens, + }; +} + +export function skinHexcodeToEmoji( + skinHexcode: string, + emoji: UnicodeEmojiData, +): UnicodeEmojiData { + return { + ...emoji, + unicode: String.fromCodePoint(...fromHexcodeToCodepoint(skinHexcode)), + hexcode: skinHexcode, + }; +} + +// Misc codes that have special handling +const EYE_CODE = 0x1f441; +const SPEECH_BUBBLE_CODE = 0x1f5e8; + export function unicodeToTwemojiHex(unicodeHex: string): string { - const codes = hexStringToNumbers(unicodeHex); + const codes = fromHexcodeToCodepoint(unicodeHex); const normalizedCodes: number[] = []; for (let i = 0; i < codes.length; i++) { const code = codes[i]; @@ -64,19 +140,28 @@ export function unicodeToTwemojiHex(unicodeHex: string): string { normalizedCodes.push(code); } - return hexNumbersToString(normalizedCodes, 0).toLowerCase(); + return normalizedCodes + .map((code) => code.toString(16)) + .join('-') + .toLowerCase(); } -export const CODES_WITH_DARK_BORDER = - EMOJIS_WITH_DARK_BORDER.map(emojiToUnicodeHex); +const CODES_WITH_DARK_BORDER = EMOJIS_WITH_DARK_BORDER.map(emojiToUnicodeHex); -export const CODES_WITH_LIGHT_BORDER = - EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex); +const CODES_WITH_LIGHT_BORDER = EMOJIS_WITH_LIGHT_BORDER.map(emojiToUnicodeHex); -export function unicodeHexToUrl(unicodeHex: string, darkMode: boolean): string { +export function unicodeHexToUrl({ + unicodeHex, + darkTheme, + assetHost, +}: { + unicodeHex: string; + darkTheme: boolean; + assetHost: string; +}): string { const normalizedHex = unicodeToTwemojiHex(unicodeHex); let url = `${assetHost}/emoji/${normalizedHex}`; - if (darkMode && CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) { + if (darkTheme && CODES_WITH_LIGHT_BORDER.includes(normalizedHex)) { url += '_border'; } if (CODES_WITH_DARK_BORDER.includes(normalizedHex)) { @@ -86,78 +171,6 @@ export function unicodeHexToUrl(unicodeHex: string, darkMode: boolean): string { return url; } -interface TwemojiSpecificEmoji { - unqualified?: string; - gender?: number; - skin?: number; - label?: string; -} - -// Normalize man/woman to male/female -const GENDER_CODES_MAP: Record = { - [GENDER_FEMALE_CODE]: GENDER_FEMALE_CODE, - [GENDER_MALE_CODE]: GENDER_MALE_CODE, - // These are man/woman markers, but are used for gender sometimes. - [0x1f468]: GENDER_MALE_CODE, - [0x1f469]: GENDER_FEMALE_CODE, -}; - -const TWEMOJI_SPECIAL_CASES: Record = { - '1F441-200D-1F5E8': '1F441-FE0F-200D-1F5E8-FE0F', // Eye in speech bubble - // An emoji that was never ported to the Unicode standard. - // See: https://emojipedia.org/shibuya - E50A: { label: 'Shibuya 109' }, -}; - -export function twemojiToUnicodeInfo( - twemojiHex: string, -): TwemojiSpecificEmoji | string { - const specialCase = TWEMOJI_SPECIAL_CASES[twemojiHex.toUpperCase()]; - if (specialCase) { - return specialCase; - } - const codes = hexStringToNumbers(twemojiHex); - let gender: undefined | number; - let skin: undefined | number; - for (const code of codes) { - if (!gender && code in GENDER_CODES_MAP) { - gender = GENDER_CODES_MAP[code]; - } else if (!skin && code in SKIN_TONE_CODES) { - skin = code; - } - - // Exit if we have both skin and gender - if (skin && gender) { - break; - } - } - - let mappedCodes: unknown[] = codes; - - if (codes.at(-1) === CHRISTMAS_TREE_CODE && codes.length >= 3 && gender) { - // Twemoji uses the christmas tree with a ZWJ for Mr. and Mrs. Claus, - // but in Unicode that only works for Mx. Claus. - const START_CODE = - gender === GENDER_FEMALE_CODE ? MS_CLAUS_CODE : MR_CLAUS_CODE; - mappedCodes = [START_CODE, skin]; - } else if (codes.at(-1) === KEYCAP_CODE && codes.length === 2) { - // For key emoji, insert the variation selector - mappedCodes = [codes[0], VARIATION_SELECTOR_CODE, KEYCAP_CODE]; - } else if ( - (codes.at(0) === SKIER_CODE || codes.at(0) === LEVITATING_PERSON_CODE) && - codes.length > 1 - ) { - // Twemoji offers more gender and skin options for the skier and levitating person emoji. - return { - unqualified: hexNumbersToString([codes.at(0)]), - skin, - gender, - }; - } - - return hexNumbersToString(mappedCodes); -} - export function emojiToInversionClassName(emoji: string): string | null { if (EMOJIS_REQUIRING_INVERSION_IN_DARK_MODE.includes(emoji)) { return 'invert-on-dark'; @@ -189,19 +202,37 @@ export function cleanExtraEmojis(extraEmojis?: CustomEmojiMapArg) { return extraEmojis; } -function hexStringToNumbers(hexString: string): number[] { - return hexString - .split('-') - .map((code) => Number.parseInt(code, 16)) - .filter((code) => !Number.isNaN(code)); -} +/** + * Tokenizes an input string into words, using Intl.Segmenter if available. + * @param input Any input string. + * @param segmenter Segmenter, if available. + * @returns Array of tokens in lowercase. + */ +export function extractTokens( + input: string, + segmenter: Intl.Segmenter | null, +): string[] { + if (!input.trim()) { + return []; + } + const tokens: string[] = []; -function hexNumbersToString(codes: unknown[], padding = 4): string { - return codes - .filter( - (code): code is number => - typeof code === 'number' && code > 0 && !Number.isNaN(code), - ) - .map((code) => code.toString(16).padStart(padding, '0').toUpperCase()) - .join('-'); + // Prefer to use Intl.Segmenter if available for better locale support. + if (segmenter) { + for (const { isWordLike, segment } of segmenter.segment( + input.replaceAll('_', ' '), // Handle underscores from shortcodes. + )) { + if (isWordLike && segment.length >= EMOJI_MIN_TOKEN_LENGTH) { + tokens.push(segment.toLowerCase()); + } + } + } else { + // Fallback to simple splitting. + input.split(/[\s_-]+/).forEach((word) => { + if (/\w/.test(word) && word.length >= EMOJI_MIN_TOKEN_LENGTH) { + tokens.push(word.toLowerCase()); + } + }); + } + return tokens; } diff --git a/app/javascript/mastodon/features/emoji/render.ts b/app/javascript/mastodon/features/emoji/render.ts index 8fe311014a9..4b65f3abde5 100644 --- a/app/javascript/mastodon/features/emoji/render.ts +++ b/app/javascript/mastodon/features/emoji/render.ts @@ -4,7 +4,6 @@ import { EMOJI_TYPE_UNICODE, EMOJI_TYPE_CUSTOM, } from './constants'; -import { emojiToUnicodeHex } from './normalize'; import type { EmojiLoadedState, EmojiMode, @@ -16,6 +15,7 @@ import type { import { anyEmojiRegex, emojiLogger, + emojiToUnicodeHex, isCustomEmoji, isUnicodeEmoji, stringHasUnicodeFlags, @@ -140,12 +140,7 @@ export async function loadEmojiDataToState( } // If not found, assume it's not an emoji and return null. - log( - 'Could not find emoji %s of type %s for locale %s', - state.code, - state.type, - locale, - ); + log('Could not find emoji %s for locale %s', state.code, locale); return null; } catch (err: unknown) { // If the locale is not loaded, load it and retry once. diff --git a/app/javascript/mastodon/features/emoji/types.ts b/app/javascript/mastodon/features/emoji/types.ts index 03002dda645..8ab756972a8 100644 --- a/app/javascript/mastodon/features/emoji/types.ts +++ b/app/javascript/mastodon/features/emoji/types.ts @@ -1,6 +1,6 @@ import type { List as ImmutableList } from 'immutable'; -import type { FlatCompactEmoji, Locale } from 'emojibase'; +import type { CompactEmoji, Locale, SkinTone } from 'emojibase'; import type { ApiCustomEmojiJSON } from '@/mastodon/api_types/custom_emoji'; import type { CustomEmoji } from '@/mastodon/models/custom_emoji'; @@ -32,10 +32,20 @@ export interface EmojiAppState { currentLocale: Locale; mode: EmojiMode; darkTheme: boolean; + assetHost: string; } -export type CustomEmojiData = ApiCustomEmojiJSON; -export type UnicodeEmojiData = FlatCompactEmoji; +export type CustomEmojiData = ApiCustomEmojiJSON & { tokens: string[] }; +export interface UnicodeEmojiData extends Omit< + CompactEmoji, + 'emoticon' | 'skins' | 'tags' +> { + shortcodes: string[]; + tokens: string[]; + emoticons?: string[]; + skinHexcodes?: string[]; + skinTones?: (SkinTone | SkinTone[])[]; +} export type AnyEmojiData = CustomEmojiData | UnicodeEmojiData; type CustomEmojiRenderFields = Pick< diff --git a/app/javascript/mastodon/features/emoji/utils.ts b/app/javascript/mastodon/features/emoji/utils.ts index c567afc2ccc..cce8151fb35 100644 --- a/app/javascript/mastodon/features/emoji/utils.ts +++ b/app/javascript/mastodon/features/emoji/utils.ts @@ -1,4 +1,5 @@ import debug from 'debug'; +import { fromUnicodeToHexcode } from 'emojibase'; import { emojiRegexPolyfill } from '@/mastodon/polyfills'; @@ -44,6 +45,10 @@ export function anyEmojiRegex() { ); } +export function emojiToUnicodeHex(emoji: string): string { + return fromUnicodeToHexcode(emoji, false); +} + function supportsRegExpSets() { return 'unicodeSets' in RegExp.prototype; } diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index 26b020d8c26..6f2a45e58f2 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -119,6 +119,9 @@ export function unicodeEmojiFactory( label: 'Test', unicode: '🧪', shortcodes: ['test_emoji'], + tokens: ['emoji', 'test'], + group: 1, + order: 1, ...data, }; } @@ -131,6 +134,7 @@ export function customEmojiFactory( static_url: '/custom-emoji/logo.svg', url: '/custom-emoji/logo.svg', visible_in_picker: true, + tokens: ['custom'], ...data, }; } From 973fef4b697428c7e9f440e9d67fcdc027cff7be Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 9 Jan 2026 15:40:27 +0100 Subject: [PATCH 04/89] Further clean up types for the `card` object (#37023) --- app/javascript/mastodon/api_types/statuses.ts | 7 +++---- .../mastodon/features/status/components/card.tsx | 8 ++++++-- app/javascript/mastodon/models/status.ts | 4 +--- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/app/javascript/mastodon/api_types/statuses.ts b/app/javascript/mastodon/api_types/statuses.ts index 1451ea82a07..d61d8ceed06 100644 --- a/app/javascript/mastodon/api_types/statuses.ts +++ b/app/javascript/mastodon/api_types/statuses.ts @@ -41,11 +41,10 @@ export interface ApiPreviewCardJSON { url: string; title: string; description: string; - language: string; - type: string; + language: string | null; + type: 'video' | 'link'; author_name: string; author_url: string; - author_account?: ApiAccountJSON; provider_name: string; provider_url: string; html: string; @@ -55,7 +54,7 @@ export interface ApiPreviewCardJSON { image_description: string; embed_url: string; blurhash: string; - published_at: string; + published_at: string | null; authors: ApiPreviewCardAuthorJSON[]; } diff --git a/app/javascript/mastodon/features/status/components/card.tsx b/app/javascript/mastodon/features/status/components/card.tsx index d060d35c2cd..68eea5ea361 100644 --- a/app/javascript/mastodon/features/status/components/card.tsx +++ b/app/javascript/mastodon/features/status/components/card.tsx @@ -118,7 +118,7 @@ const Card: React.FC = ({ card, sensitive }) => { ? decodeIDNA(getHostname(card.get('url'))) : card.get('provider_name'); const interactive = card.get('type') === 'video'; - const language = card.get('language') || ''; + const language = card.get('language') ?? ''; const hasImage = (card.get('image')?.length ?? 0) > 0; const largeImage = (hasImage && card.get('width') > card.get('height')) || interactive; @@ -131,7 +131,11 @@ const Card: React.FC = ({ card, sensitive }) => { {card.get('published_at') && ( <> {' '} - · + ·{' '} + )} diff --git a/app/javascript/mastodon/models/status.ts b/app/javascript/mastodon/models/status.ts index 7f9144280cf..b043edb9ca6 100644 --- a/app/javascript/mastodon/models/status.ts +++ b/app/javascript/mastodon/models/status.ts @@ -7,8 +7,6 @@ export type { StatusVisibility } from 'mastodon/api_types/statuses'; // Temporary until we type it correctly export type Status = Immutable.Map; -type CardShape = Required; - -export type Card = RecordOf; +export type Card = RecordOf; export type MediaAttachment = Immutable.Map; From 1211bbc064942d5c4a32f25bfd3de4f022c9bbeb Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 17:20:33 +0100 Subject: [PATCH 05/89] Update dependency hiredis-client to v0.26.3 (#37404) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index d73316b4e98..bf76d1fbcd8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -305,8 +305,8 @@ GEM highline (3.1.2) reline hiredis (0.6.3) - hiredis-client (0.26.2) - redis-client (= 0.26.2) + hiredis-client (0.26.3) + redis-client (= 0.26.3) hkdf (0.3.0) htmlentities (4.3.4) http (5.3.1) @@ -706,7 +706,7 @@ GEM reline redcarpet (3.6.1) redis (4.8.1) - redis-client (0.26.2) + redis-client (0.26.3) connection_pool regexp_parser (2.11.3) reline (0.6.3) From f97a015cf9c7b16f54453e4c121d72587a84bb70 Mon Sep 17 00:00:00 2001 From: Shlee Date: Fri, 9 Jan 2026 23:20:50 +0700 Subject: [PATCH 06/89] SharedConnectionPool - NoMethodError: undefined method 'site' for Integer (#37374) --- app/lib/connection_pool/shared_connection_pool.rb | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/app/lib/connection_pool/shared_connection_pool.rb b/app/lib/connection_pool/shared_connection_pool.rb index 1cfcc5823b2..c7dd747edab 100644 --- a/app/lib/connection_pool/shared_connection_pool.rb +++ b/app/lib/connection_pool/shared_connection_pool.rb @@ -41,12 +41,17 @@ class ConnectionPool::SharedConnectionPool < ConnectionPool # ConnectionPool 2.4+ calls `checkin(force: true)` after fork. # When this happens, we should remove all connections from Thread.current - ::Thread.current.keys.each do |name| # rubocop:disable Style/HashEachMethods - next unless name.to_s.start_with?("#{@key}-") + connection_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key}-") && !key.to_s.start_with?("#{@key_count}-") } + count_keys = ::Thread.current.keys.select { |key| key.to_s.start_with?("#{@key_count}-") } - @available.push(::Thread.current[name]) - ::Thread.current[name] = nil + connection_keys.each do |key| + @available.push(::Thread.current[key]) + ::Thread.current[key] = nil end + count_keys.each do |key| + ::Thread.current[key] = nil + end + elsif ::Thread.current[key(preferred_tag)] if ::Thread.current[key_count(preferred_tag)] == 1 @available.push(::Thread.current[key(preferred_tag)]) From 48ce49260d39960bda70307c143153cc9befbb18 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Sat, 10 Jan 2026 03:20:59 +1100 Subject: [PATCH 07/89] Fix thread-unsafe ActivityPub activity dispatch (#37423) --- app/lib/activitypub/activity.rb | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index 64ee9acd052..d07d1c2f241 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -21,14 +21,13 @@ class ActivityPub::Activity class << self def factory(json, account, **) - @json = json - klass&.new(json, account, **) + klass_for(json)&.new(json, account, **) end private - def klass - case @json['type'] + def klass_for(json) + case json['type'] when 'Create' ActivityPub::Activity::Create when 'Announce' From b35b3a8bcc672f1acd76cc59077546ccad7be3f8 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Sat, 10 Jan 2026 03:21:05 +1100 Subject: [PATCH 08/89] Fix arg order for non_matching_uri_hosts? call in QuoteRequest (#37425) --- app/lib/activitypub/activity/quote_request.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/lib/activitypub/activity/quote_request.rb b/app/lib/activitypub/activity/quote_request.rb index 12f48ebb2b3..46c45cde276 100644 --- a/app/lib/activitypub/activity/quote_request.rb +++ b/app/lib/activitypub/activity/quote_request.rb @@ -47,7 +47,7 @@ class ActivityPub::Activity::QuoteRequest < ActivityPub::Activity # NOTE: Replacing the object's context by that of the parent activity is # not sound, but it's consistent with the rest of the codebase instrument = @json['instrument'].merge({ '@context' => @json['@context'] }) - return if non_matching_uri_hosts?(instrument['id'], @account.uri) + return if non_matching_uri_hosts?(@account.uri, instrument['id']) ActivityPub::FetchRemoteStatusService.new.call(instrument['id'], prefetched_body: instrument, on_behalf_of: quoted_status.account, request_id: @options[:request_id]) end From b55982cc1a532baad34de4edf32c78add7c366f8 Mon Sep 17 00:00:00 2001 From: Joshua Rogers Date: Sat, 10 Jan 2026 03:21:18 +1100 Subject: [PATCH 09/89] Fix Vary parsing in cache control enforcement (#37426) --- app/controllers/concerns/cache_concern.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/controllers/concerns/cache_concern.rb b/app/controllers/concerns/cache_concern.rb index b1b09f2aab0..3527cdaca03 100644 --- a/app/controllers/concerns/cache_concern.rb +++ b/app/controllers/concerns/cache_concern.rb @@ -19,7 +19,7 @@ module CacheConcern # from being used as cache keys, while allowing to `Vary` on them (to not serve # anonymous cached data to authenticated requests when authentication matters) def enforce_cache_control! - vary = response.headers['Vary']&.split&.map { |x| x.strip.downcase } + vary = response.headers['Vary'].to_s.split(',').map { |x| x.strip.downcase }.reject(&:empty?) return unless vary.present? && %w(cookie authorization signature).any? { |header| vary.include?(header) && request.headers[header].present? } response.cache_control.replace(private: true, no_store: true) From a4b8b9fe98c677f718e4b2c1ffe1755d58e7f8d7 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 9 Jan 2026 19:38:22 +0100 Subject: [PATCH 10/89] Fix bug with how Emojibase wasn't stripping characters (#37442) --- .../mastodon/features/emoji/utils.ts | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/features/emoji/utils.ts b/app/javascript/mastodon/features/emoji/utils.ts index cce8151fb35..d15aa961500 100644 --- a/app/javascript/mastodon/features/emoji/utils.ts +++ b/app/javascript/mastodon/features/emoji/utils.ts @@ -1,8 +1,9 @@ import debug from 'debug'; -import { fromUnicodeToHexcode } from 'emojibase'; import { emojiRegexPolyfill } from '@/mastodon/polyfills'; +import { VARIATION_SELECTOR_CODE } from './constants'; + export function emojiLogger(segment: string) { return debug(`emojis:${segment}`); } @@ -46,7 +47,24 @@ export function anyEmojiRegex() { } export function emojiToUnicodeHex(emoji: string): string { - return fromUnicodeToHexcode(emoji, false); + const codes: string[] = []; + for (const char of emoji) { + const code = char.codePointAt(0); + if (code !== undefined) { + codes.push(code.toString(16).toUpperCase().padStart(4, '0')); + } + } + + // Handles how Emojibase removes the variation selector for single code emojis. + // See: https://emojibase.dev/docs/spec/#merged-variation-selectors + if ( + codes.at(1) === VARIATION_SELECTOR_CODE.toString(16).toUpperCase() && + codes.length === 2 + ) { + codes.pop(); + } + + return codes.join('-'); } function supportsRegExpSets() { From a9cfddf28ef139fd8a431ec3dba93a03996e12b4 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Mon, 12 Jan 2026 09:39:25 +0100 Subject: [PATCH 11/89] AP/AS serialization of Collections (#37434) --- app/lib/activitypub/tag_manager.rb | 2 + app/models/collection.rb | 4 ++ .../featured_collection_serializer.rb | 54 +++++++++++++++++++ config/routes.rb | 1 + spec/fabricators/collection_fabricator.rb | 7 +++ spec/lib/activitypub/tag_manager_spec.rb | 17 ++++++ spec/models/collection_spec.rb | 6 +++ .../featured_collection_serializer_spec.rb | 48 +++++++++++++++++ 8 files changed, 139 insertions(+) create mode 100644 app/serializers/activitypub/featured_collection_serializer.rb create mode 100644 spec/serializers/activitypub/featured_collection_serializer_spec.rb diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 3174d1792e0..9f01b005780 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -62,6 +62,8 @@ class ActivityPub::TagManager emoji_url(target) when :flag target.uri + when :featured_collection + ap_account_featured_collection_url(target.account.id, target) end end diff --git a/app/models/collection.rb b/app/models/collection.rb index 2e352cbe876..b732a3d220b 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -60,6 +60,10 @@ class Collection < ApplicationRecord self.tag = Tag.find_or_create_by_names(new_name).first end + def object_type + :featured_collection + end + private def tag_is_usable diff --git a/app/serializers/activitypub/featured_collection_serializer.rb b/app/serializers/activitypub/featured_collection_serializer.rb new file mode 100644 index 00000000000..e70d155a1a0 --- /dev/null +++ b/app/serializers/activitypub/featured_collection_serializer.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer + class FeaturedItemSerializer < ActivityPub::Serializer + attributes :type, :featured_object, :featured_object_type + + def type + 'FeaturedItem' + end + + def featured_object + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def featured_object_type + object.account.actor_type || 'Person' + end + end + + attributes :id, :type, :total_items, :name, :summary, :attributed_to, + :sensitive, :discoverable, :published, :updated + + has_one :tag, key: :topic, serializer: ActivityPub::NoteSerializer::TagSerializer + + has_many :collection_items, key: :ordered_items, serializer: FeaturedItemSerializer + + def id + ActivityPub::TagManager.instance.uri_for(object) + end + + def type + 'FeaturedCollection' + end + + def summary + object.description + end + + def attributed_to + ActivityPub::TagManager.instance.uri_for(object.account) + end + + def total_items + object.collection_items.size + end + + def published + object.created_at.iso8601 + end + + def updated + object.updated_at.iso8601 + end +end diff --git a/config/routes.rb b/config/routes.rb index 3685e695f91..ff79c758fb3 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -96,6 +96,7 @@ Rails.application.routes.draw do get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" } concern :account_resources do + resources :featured_collections, only: [:show] resources :followers, only: [:index], controller: :follower_accounts resources :following, only: [:index], controller: :following_accounts diff --git a/spec/fabricators/collection_fabricator.rb b/spec/fabricators/collection_fabricator.rb index a6a8411ba00..7e0e14a765f 100644 --- a/spec/fabricators/collection_fabricator.rb +++ b/spec/fabricators/collection_fabricator.rb @@ -8,3 +8,10 @@ Fabricator(:collection) do sensitive false discoverable true end + +Fabricator(:remote_collection, from: :collection) do + account { Fabricate.build(:remote_account) } + local false + uri { sequence(:uri) { |i| "https://example.com/collections/#{i}" } } + original_number_of_items 0 +end diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 6cbb58055e5..bacbb3251cc 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -192,6 +192,23 @@ RSpec.describe ActivityPub::TagManager do expect(subject.uri_for(status.conversation)).to eq status.conversation.uri end end + + context 'with a local collection' do + let(:collection) { Fabricate(:collection) } + + it 'returns a string starting with web domain and with the expected path' do + expect(subject.uri_for(collection)) + .to eq("#{host_prefix}/ap/users/#{collection.account.id}/featured_collections/#{collection.id}") + end + end + + context 'with a remote collection' do + let(:collection) { Fabricate(:remote_collection) } + + it 'returns the expected URL' do + expect(subject.uri_for(collection)).to eq collection.uri + end + end end describe '#key_uri_for' do diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index 659e0178694..bcc31fd0877 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -126,4 +126,10 @@ RSpec.describe Collection do end end end + + describe '#object_type' do + it 'returns `:featured_collection`' do + expect(subject.object_type).to eq :featured_collection + end + end end diff --git a/spec/serializers/activitypub/featured_collection_serializer_spec.rb b/spec/serializers/activitypub/featured_collection_serializer_spec.rb new file mode 100644 index 00000000000..b01cce12d81 --- /dev/null +++ b/spec/serializers/activitypub/featured_collection_serializer_spec.rb @@ -0,0 +1,48 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::FeaturedCollectionSerializer do + subject { serialized_record_json(collection, described_class, adapter: ActivityPub::Adapter) } + + let(:collection) do + Fabricate(:collection, + name: 'Incredible people', + description: 'These are really amazing', + tag_name: '#people', + discoverable: false) + end + let!(:collection_items) { Fabricate.times(2, :collection_item, collection:) } + + it 'serializes to the expected structure' do + expect(subject).to include({ + 'type' => 'FeaturedCollection', + 'id' => ActivityPub::TagManager.instance.uri_for(collection), + 'name' => 'Incredible people', + 'summary' => 'These are really amazing', + 'attributedTo' => ActivityPub::TagManager.instance.uri_for(collection.account), + 'sensitive' => false, + 'discoverable' => false, + 'topic' => { + 'href' => match(%r{/tags/people$}), + 'type' => 'Hashtag', + 'name' => '#people', + }, + 'totalItems' => 2, + 'orderedItems' => [ + { + 'type' => 'FeaturedItem', + 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.first.account), + 'featuredObjectType' => 'Person', + }, + { + 'type' => 'FeaturedItem', + 'featuredObject' => ActivityPub::TagManager.instance.uri_for(collection_items.last.account), + 'featuredObjectType' => 'Person', + }, + ], + 'published' => match_api_datetime_format, + 'updated' => match_api_datetime_format, + }) + end +end From 0fb4e26cfe2fd7518c56694d46c1f6c8fc4ec3f5 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 12 Jan 2026 04:38:00 -0500 Subject: [PATCH 12/89] Add details to JS browser error spec failure helpers (#37441) --- spec/support/browser_errors.rb | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/spec/support/browser_errors.rb b/spec/support/browser_errors.rb index 6c101540a3a..860c8bd7316 100644 --- a/spec/support/browser_errors.rb +++ b/spec/support/browser_errors.rb @@ -4,6 +4,10 @@ module BrowserErrorsHelpers def ignore_js_error(error) @ignored_js_errors_for_spec << error end + + def error_message(error) + error.keys.map { |key| "#{key.to_s.titleize}: #{error[key]}" }.join("\n") + end end RSpec.configure do |config| @@ -15,7 +19,7 @@ RSpec.configure do |config| example.metadata[:js_console_messages] ||= [] Capybara.current_session.driver.with_playwright_page do |page| page.on('console', lambda { |msg| - example.metadata[:js_console_messages] << { type: msg.type, text: msg.text, location: msg.location } + example.metadata[:js_console_messages] << { type: msg.type, text: msg.text, location: msg.location, page: msg.page.url } }) end end @@ -34,7 +38,7 @@ RSpec.configure do |config| if errors.present? aggregate_failures 'browser errrors' do errors.each do |error| - expect(error[:type]).to_not eq('error'), error[:text] + expect(error[:type]).to_not eq('error'), error_message(error) next unless error[:type] == 'warning' warn 'WARN: browser warning' From 8f70cfc8b1cfea35658832922b55660a71045667 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:54:19 +0100 Subject: [PATCH 13/89] New Crowdin Translations (automated) (#37447) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/et.json | 1 + app/javascript/mastodon/locales/fr-CA.json | 22 +++---- app/javascript/mastodon/locales/fr.json | 22 +++---- app/javascript/mastodon/locales/ga.json | 10 ++-- app/javascript/mastodon/locales/it.json | 2 +- app/javascript/mastodon/locales/ms.json | 3 + app/javascript/mastodon/locales/pl.json | 36 ++++++++++++ config/locales/activerecord.ms.yml | 24 ++++++++ config/locales/devise.fr.yml | 2 +- config/locales/et.yml | 7 ++- config/locales/ga.yml | 3 + config/locales/nan.yml | 68 ++++++++++++++++++++++ config/locales/pl.yml | 24 ++++++++ config/locales/simple_form.pl.yml | 6 ++ config/locales/zh-CN.yml | 1 + config/locales/zh-TW.yml | 4 +- 16 files changed, 202 insertions(+), 33 deletions(-) diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 4a5ca61710c..854d70b0ce1 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -517,6 +517,7 @@ "keyboard_shortcuts.column": "Fookus veerule", "keyboard_shortcuts.compose": "Fookus teksti koostamise alale", "keyboard_shortcuts.description": "Kirjeldus", + "keyboard_shortcuts.direct": "Ava privaatsete mainimiste veerg", "keyboard_shortcuts.down": "Liigu loetelus alla", "keyboard_shortcuts.enter": "Ava postitus", "keyboard_shortcuts.favourite": "Lemmikpostitus", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 7de713fcff4..5cf1c8b4b9f 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -49,7 +49,7 @@ "account.followers": "abonné·e·s", "account.followers.empty": "Personne ne suit ce compte pour l'instant.", "account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}", - "account.followers_you_know_counter": "{count, plural, one {{counter} suivi}, other {{counter} suivis}}", + "account.followers_you_know_counter": "{counter} que vous suivez", "account.following": "Abonné·e", "account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}", "account.follows.empty": "Ce compte ne suit personne présentement.", @@ -164,10 +164,10 @@ "block_modal.remote_users_caveat": "Nous allons demander au serveur {domain} de respecter votre décision. Cependant, ce respect n'est pas garanti, car certains serveurs peuvent gérer différemment les blocages. Les messages publics peuvent rester visibles par les utilisateur·rice·s non connecté·e·s.", "block_modal.show_less": "Afficher moins", "block_modal.show_more": "Afficher plus", - "block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.", + "block_modal.they_cant_mention": "Elle ne pourra pas vous mentionner ou vous suivre.", "block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.", - "block_modal.they_will_know": "Il peut voir qu'il est bloqué.", - "block_modal.title": "Bloquer le compte ?", + "block_modal.they_will_know": "Elle pourra voir qu'elle est bloquée.", + "block_modal.title": "Bloquer cette personne ?", "block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour sauter ceci la prochaine fois", "boost_modal.reblog": "Booster le message ?", @@ -592,13 +592,13 @@ "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous avez déménagé sur {movedToAccount}.", "mute_modal.hide_from_notifications": "Cacher des notifications", "mute_modal.hide_options": "Masquer les options", - "mute_modal.indefinite": "Jusqu'à ce que je les réactive", + "mute_modal.indefinite": "Jusqu'à ce que je l'affiche à nouveau", "mute_modal.show_options": "Afficher les options", - "mute_modal.they_can_mention_and_follow": "Ils peuvent vous mentionner et vous suivre, mais vous ne les verrez pas.", - "mute_modal.they_wont_know": "Ils ne sauront pas qu'ils ont été rendus silencieux.", - "mute_modal.title": "Rendre cet utilisateur silencieux ?", - "mute_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", - "mute_modal.you_wont_see_posts": "Il peut toujours voir vos publications, mais vous ne verrez pas les siennes.", + "mute_modal.they_can_mention_and_follow": "Elle pourra vous mentionner et vous suivre, mais vous ne la verrez pas.", + "mute_modal.they_wont_know": "Elle ne saura pas qu'elle est masquée.", + "mute_modal.title": "Masquer cette personne ?", + "mute_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui la mentionnent.", + "mute_modal.you_wont_see_posts": "Elle pourra toujours voir vos publications, mais vous ne verrez pas les siennes.", "navigation_bar.about": "À propos", "navigation_bar.account_settings": "Mot de passe et sécurité", "navigation_bar.administration": "Administration", @@ -786,7 +786,7 @@ "privacy.private.short": "Abonnés", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.short": "Public", - "privacy.quote.anyone": "{visibility}, n'importe qui peut citer", + "privacy.quote.anyone": "{visibility}, citations autorisées", "privacy.quote.disabled": "{visibility}, citations désactivées", "privacy.quote.limited": "{visibility}, citations limitées", "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index d14d24e57be..d0ef2605bb8 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -49,7 +49,7 @@ "account.followers": "Abonné·e·s", "account.followers.empty": "Personne ne suit cet·te utilisateur·rice pour l’instant.", "account.followers_counter": "{count, plural, one {{counter} abonné·e} other {{counter} abonné·e·s}}", - "account.followers_you_know_counter": "{count, plural, one {{counter} suivi}, other {{counter} suivis}}", + "account.followers_you_know_counter": "{counter} que vous suivez", "account.following": "Abonnements", "account.following_counter": "{count, plural, one {{counter} abonnement} other {{counter} abonnements}}", "account.follows.empty": "Cet·te utilisateur·rice ne suit personne pour l’instant.", @@ -164,10 +164,10 @@ "block_modal.remote_users_caveat": "Nous allons demander au serveur {domain} de respecter votre décision. Cependant, ce respect n'est pas garanti, car certains serveurs peuvent gérer différemment les blocages. Les messages publics peuvent rester visibles par les utilisateur·rice·s non connecté·e·s.", "block_modal.show_less": "Afficher moins", "block_modal.show_more": "Afficher plus", - "block_modal.they_cant_mention": "Il ne peut pas vous mentionner ou vous suivre.", + "block_modal.they_cant_mention": "Elle ne pourra pas vous mentionner ou vous suivre.", "block_modal.they_cant_see_posts": "Il ne peut plus voir vos messages et vous ne verrez plus les siens.", - "block_modal.they_will_know": "Il peut voir qu'il est bloqué.", - "block_modal.title": "Bloquer le compte ?", + "block_modal.they_will_know": "Elle pourra voir qu'elle est bloquée.", + "block_modal.title": "Bloquer cette personne ?", "block_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", "boost_modal.combo": "Vous pouvez appuyer sur {combo} pour passer ceci la prochaine fois", "boost_modal.reblog": "Booster le message ?", @@ -592,13 +592,13 @@ "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous l'avez déplacé à {movedToAccount}.", "mute_modal.hide_from_notifications": "Cacher des notifications", "mute_modal.hide_options": "Masquer les options", - "mute_modal.indefinite": "Jusqu'à ce que je les réactive", + "mute_modal.indefinite": "Jusqu'à ce que je l'affiche à nouveau", "mute_modal.show_options": "Afficher les options", - "mute_modal.they_can_mention_and_follow": "Ils peuvent vous mentionner et vous suivre, mais vous ne les verrez pas.", - "mute_modal.they_wont_know": "Ils ne sauront pas qu'ils ont été rendus silencieux.", - "mute_modal.title": "Rendre cet utilisateur silencieux ?", - "mute_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui le mentionne.", - "mute_modal.you_wont_see_posts": "Il peut toujours voir vos publications, mais vous ne verrez pas les siennes.", + "mute_modal.they_can_mention_and_follow": "Elle pourra vous mentionner et vous suivre, mais vous ne la verrez pas.", + "mute_modal.they_wont_know": "Elle ne saura pas qu'elle est masquée.", + "mute_modal.title": "Masquer cette personne ?", + "mute_modal.you_wont_see_mentions": "Vous ne verrez pas les messages qui la mentionnent.", + "mute_modal.you_wont_see_posts": "Elle pourra toujours voir vos publications, mais vous ne verrez pas les siennes.", "navigation_bar.about": "À propos", "navigation_bar.account_settings": "Mot de passe et sécurité", "navigation_bar.administration": "Administration", @@ -786,7 +786,7 @@ "privacy.private.short": "Abonnés", "privacy.public.long": "Tout le monde sur et en dehors de Mastodon", "privacy.public.short": "Public", - "privacy.quote.anyone": "{visibility}, n'importe qui peut citer", + "privacy.quote.anyone": "{visibility}, citations autorisées", "privacy.quote.disabled": "{visibility}, citations désactivées", "privacy.quote.limited": "{visibility}, citations limitées", "privacy.unlisted.additional": "Se comporte exactement comme « public », sauf que le message n'apparaîtra pas dans les flux en direct, les hashtags, explorer ou la recherche Mastodon, même si vous les avez activé au niveau de votre compte.", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 4f65848449d..c3a57d6e09d 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -101,9 +101,9 @@ "admin.impact_report.instance_follows": "Leanúna a bheadh ​​​​a n-úsáideoirí chailleadh", "admin.impact_report.title": "Achoimre ar an tionchar", "alert.rate_limited.message": "Atriail aris tar éis {retry_time, time, medium}.", - "alert.rate_limited.title": "Rátatheoranta", + "alert.rate_limited.title": "Ráta teoranta", "alert.unexpected.message": "Tharla earráid gan choinne.", - "alert.unexpected.title": "Hiúps!", + "alert.unexpected.title": "Úps!", "alt_text_badge.title": "Téacs alt", "alt_text_modal.add_alt_text": "Cuir téacs alt leis", "alt_text_modal.add_text_from_image": "Cuir téacs ón íomhá leis", @@ -150,7 +150,7 @@ "annual_report.summary.highlighted_post.reply_count": "Fuair ​​an post seo {count, plural, one {freagra amháin} two {# freagraí} few {# freagraí} many {# freagraí} other {# freagraí}}.", "annual_report.summary.highlighted_post.title": "An post is mó tóir", "annual_report.summary.most_used_app.most_used_app": "aip is mó a úsáidtear", - "annual_report.summary.most_used_hashtag.most_used_hashtag": "hashtag is mó a úsáidtear", + "annual_report.summary.most_used_hashtag.most_used_hashtag": "haischlib is mó a úsáidtear", "annual_report.summary.most_used_hashtag.used_count": "Chuir tú an haischlib seo i {count, plural, one {post amháin} two {# poist} few {# poist} many {# poist} other {# poist}}.", "annual_report.summary.most_used_hashtag.used_count_public": "Chuir {name} an haischlib seo i {count, plural, one {post amháin} two {# poist} few {# poist} many {# poist} other {# poist}}.", "annual_report.summary.new_posts.new_posts": "postanna nua", @@ -167,14 +167,14 @@ "block_modal.they_cant_mention": "Ní féidir leo tú a lua ná a leanúint.", "block_modal.they_cant_see_posts": "Ní féidir leo do chuid postálacha a fheiceáil agus ní fheicfidh tú a gcuid postanna.", "block_modal.they_will_know": "Is féidir leo a fheiceáil go bhfuil bac orthu.", - "block_modal.title": "An bhfuil fonn ort an t-úsáideoir a bhlocáil?", + "block_modal.title": "Úsáideoir a bhlocáil?", "block_modal.you_wont_see_mentions": "Ní fheicfidh tú postálacha a luann iad.", "boost_modal.combo": "Is féidir leat {combo} a bhrú chun é seo a scipeáil an chéad uair eile", "boost_modal.reblog": "An post a threisiú?", "boost_modal.undo_reblog": "An deireadh a chur le postáil?", "bundle_column_error.copy_stacktrace": "Cóipeáil tuairisc earráide", "bundle_column_error.error.body": "Ní féidir an leathanach a iarradh a sholáthar. Seans gurb amhlaidh mar gheall ar fhabht sa chód, nó mar gheall ar mhíréireacht leis an mbrabhsálaí.", - "bundle_column_error.error.title": "Ó, níl sé sin go maith!", + "bundle_column_error.error.title": "Ó, ní hea!", "bundle_column_error.network.body": "Tharla earráid agus an leathanach á lódáil. Seans gur mar gheall ar fhadhb shealadach le do nasc idirlín nó i ndáil leis an bhfreastalaí seo atá sé.", "bundle_column_error.network.title": "Earráid líonra", "bundle_column_error.retry": "Bain triail as arís", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 7a0d542546f..8b6c70bd140 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -536,7 +536,7 @@ "keyboard_shortcuts.open_media": "Apre i multimedia", "keyboard_shortcuts.pinned": "Apre l'elenco dei post fissati", "keyboard_shortcuts.profile": "Apre il profilo dell'autore", - "keyboard_shortcuts.quote": "Cita il post", + "keyboard_shortcuts.quote": "Cita post", "keyboard_shortcuts.reply": "Risponde al post", "keyboard_shortcuts.requests": "Apre l'elenco delle richieste di seguirti", "keyboard_shortcuts.search": "Focalizza sulla barra di ricerca", diff --git a/app/javascript/mastodon/locales/ms.json b/app/javascript/mastodon/locales/ms.json index bbe3af8bc76..92b12958fe3 100644 --- a/app/javascript/mastodon/locales/ms.json +++ b/app/javascript/mastodon/locales/ms.json @@ -1,6 +1,7 @@ { "about.blocks": "Pelayan yang diselaraskan", "about.contact": "Hubungi:", + "about.default_locale": "Lalai", "about.disclaimer": "Mastodon ialah perisian sumber terbuka percuma, dan merupakan tanda dagangan Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Sebab tidak tersedia", "about.domain_blocks.preamble": "Secara amnya, Mastodon membenarkan anda melihat kandungan pengguna daripada mana-mana pelayan dalam alam bersekutu dan berinteraksi dengan mereka. Berikut ialah pengecualian yang khusus pada pelayan ini.", @@ -8,6 +9,7 @@ "about.domain_blocks.silenced.title": "Terhad", "about.domain_blocks.suspended.explanation": "Tiada data daripada pelayan ini yang akan diproses, disimpan atau ditukar, menjadikan sebarang interaksi atau perhubungan dengan pengguna daripada pelayan ini adalah mustahil.", "about.domain_blocks.suspended.title": "Digantung", + "about.language_label": "Bahasa", "about.not_available": "Maklumat ini belum tersedia pada pelayan ini.", "about.powered_by": "Media sosial terpencar yang dikuasakan oleh {mastodon}", "about.rules": "Peraturan pelayan", @@ -26,6 +28,7 @@ "account.disable_notifications": "Berhenti maklumkan saya apabila @{name} mengirim hantaran", "account.domain_blocking": "Blocking domain", "account.edit_profile": "Sunting profil", + "account.edit_profile_short": "Sunting", "account.enable_notifications": "Maklumi saya apabila @{name} mengirim hantaran", "account.endorse": "Tampilkan di profil", "account.familiar_followers_one": "melayuikutikut{name1}", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index dfeae82d4b6..f7aa78d1289 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -113,11 +113,37 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Opisz to dla osób niedowidzących…", "alt_text_modal.done": "Gotowe", "announcement.announcement": "Ogłoszenie", + "annual_report.announcement.action_build": "Zbuduj mój Wrapstodon", + "annual_report.announcement.action_dismiss": "Nie, dziękuję", + "annual_report.announcement.action_view": "Zobacz mój Wrapstodon", + "annual_report.announcement.description": "Odkryj więcej na temat swojego zaangażowania w Mastodon w ciągu ostatniego roku.", + "annual_report.announcement.title": "Wrapstodon roku {year} przybył", + "annual_report.nav_item.badge": "Nowe", + "annual_report.shared_page.donate": "Wesprzyj nas", + "annual_report.shared_page.footer": "Wygenerowane przez {heart} przez zespół Mastodon", + "annual_report.shared_page.footer_server_info": "{username} używa {domain}, jednej z wielu społeczności wspieranych przez Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "{name} został na polowaniu na posty do wzmocnienia, wzmacniając innych twórców doskonałym celem.", + "annual_report.summary.archetype.booster.desc_self": "Pozostałeś na polowaniu na posty do wzmocnienia i wzmocnienia innych twórców doskonałym celem.", + "annual_report.summary.archetype.booster.name": "Łucznik", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.name": "Stoik", + "annual_report.summary.archetype.oracle.name": "Wyrocznia", + "annual_report.summary.archetype.pollster.name": "Zastanawiacz", + "annual_report.summary.archetype.replier.name": "Motylek", + "annual_report.summary.archetype.reveal": "Pokaż mój archetyp", + "annual_report.summary.archetype.title_public": "Archetyp {name}", + "annual_report.summary.archetype.title_self": "Twój archetyp", + "annual_report.summary.close": "Zamknij", + "annual_report.summary.copy_link": "Skopiuj adres", + "annual_report.summary.followers.new_followers": "{count, plural, one {{counter} obserwujący} few {{counter} obserwujących} many {{counter} obserwujących} other {{counter} obserwujących}}", "annual_report.summary.most_used_app.most_used_app": "najczęściej używana aplikacja", "annual_report.summary.most_used_hashtag.most_used_hashtag": "najczęściej używany hashtag", "annual_report.summary.new_posts.new_posts": "nowe wpisy", "annual_report.summary.percentile.text": "To plasuje cię w czołówce użytkowników {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Nie powiemy Berniemu.", + "annual_report.summary.share_elsewhere": "Udostępnij gdziekolwiek", + "annual_report.summary.share_message": "Mam archetyp {archetype}!", + "annual_report.summary.share_on_mastodon": "Udostępnij na Mastodonie", "attachments_list.unprocessed": "(nieprzetworzone)", "audio.hide": "Ukryj dźwięk", "block_modal.remote_users_caveat": "Poprosimy serwer {domain} o uszanowanie twojej decyzji. Nie jest to jednak gwarantowane, bo niektóre serwery mogą obsługiwać blokady w inny sposób. Publiczne wpisy mogą być nadal widoczne dla niezalogowanych użytkowników.", @@ -159,6 +185,7 @@ "column.edit_list": "Edytuj listę", "column.favourites": "Ulubione", "column.firehose": "Aktualności", + "column.firehose_local": "Kanał na żywo dla tego serwera", "column.firehose_singular": "Na żywo", "column.follow_requests": "Prośby o obserwację", "column.home": "Strona główna", @@ -179,6 +206,7 @@ "community.column_settings.local_only": "Tylko lokalne", "community.column_settings.media_only": "Tylko multimedia", "community.column_settings.remote_only": "Tylko zdalne", + "compose.error.blank_post": "Post nie może być pusty.", "compose.language.change": "Zmień język", "compose.language.search": "Wyszukaj języki...", "compose.published.body": "Wpis został opublikowany.", @@ -231,6 +259,11 @@ "confirmations.missing_alt_text.secondary": "Opublikuj mimo wszystko", "confirmations.missing_alt_text.title": "Dodać tekst pomocniczy?", "confirmations.mute.confirm": "Wycisz", + "confirmations.private_quote_notify.cancel": "Wróć do edycji", + "confirmations.private_quote_notify.confirm": "Opublikuj wpis", + "confirmations.private_quote_notify.do_not_show_again": "Nie pokazuj tej wiadomości ponownie", + "confirmations.private_quote_notify.message": "Osoba, którą cytujesz, a inne wzmianki zostaną powiadomione i będą mogły zobaczyć Twój post, nawet jeśli nie obserwują Ciebie.", + "confirmations.private_quote_notify.title": "Udostępnić obserwującym i wspomnianym użytkownikom?", "confirmations.quiet_post_quote_info.dismiss": "Nie przypominaj mi ponownie", "confirmations.quiet_post_quote_info.got_it": "Rozumiem", "confirmations.quiet_post_quote_info.message": "Kiedy cytujesz niewidoczny wpis publiczny, twój wpis zostanie ukryty z popularnych osi czasu.", @@ -388,6 +421,9 @@ "follow_suggestions.who_to_follow": "Kogo warto obserwować", "followed_tags": "Obserwowane hasztagi", "footer.about": "O serwerze", + "footer.about_mastodon": "O Mastodonie", + "footer.about_server": "O {domain}", + "footer.about_this_server": "O nas", "footer.directory": "Katalog profili", "footer.get_app": "Pobierz aplikację", "footer.keyboard_shortcuts": "Skróty klawiszowe", diff --git a/config/locales/activerecord.ms.yml b/config/locales/activerecord.ms.yml index 5f282702f10..b31ccf9dfe7 100644 --- a/config/locales/activerecord.ms.yml +++ b/config/locales/activerecord.ms.yml @@ -15,9 +15,16 @@ ms: user/invite_request: text: Sebab errors: + attributes: + domain: + invalid: bukanlah nama domain yang sah + messages: + invalid_domain_on_line: "%{value} bukanlah nama domain yang sah" models: account: attributes: + fields: + fields_with_values_missing_labels: mengandungi nilai dengan label yang hilang username: invalid: hanya mengandungi aksara, nombor dan garis bawah sahaja reserved: dikhaskan @@ -25,6 +32,12 @@ ms: attributes: url: invalid: bukanlah URL yang sah + collection: + attributes: + collection_items: + too_many: terlalu banyak, tidak lebih daripada %{count} dibenarkan + tag: + unusable: mungkin tidak digunakan doorkeeper/application: attributes: website: @@ -33,12 +46,23 @@ ms: attributes: data: malformed: tersalah bentuk + list_account: + attributes: + account_id: + taken: sudah dalam senarai + must_be_following: hendaklah akaun yang diikuti status: attributes: reblog: taken: hantaran sudah wujud + terms_of_service: + attributes: + effective_date: + too_soon: terlalu awal, hendaklah selepas %{date} user: attributes: + date_of_birth: + below_limit: di bawah had umur email: blocked: menggunakan pembekal e-mel yang tidak dibenarkan unreachable: nampaknya tidak wujud diff --git a/config/locales/devise.fr.yml b/config/locales/devise.fr.yml index c2147fcf9fc..4df8d60dadc 100644 --- a/config/locales/devise.fr.yml +++ b/config/locales/devise.fr.yml @@ -24,7 +24,7 @@ fr: action_with_app: Confirmer et retourner à %{app} explanation: Vous avez créé un compte sur %{host} avec cette adresse courriel. Vous êtes à un clic de l’activer. Si ce n’était pas vous, veuillez ignorer ce courriel. explanation_when_pending: Vous avez demandé à vous inscrire à %{host} avec cette adresse de courriel. Une fois que vous aurez confirmé cette adresse, nous étudierons votre demande. Vous ne pourrez pas vous connecter d’ici-là. Si votre demande est refusée, vos données seront supprimées du serveur, aucune action supplémentaire de votre part n’est donc requise. Si vous n’êtes pas à l’origine de cette demande, veuillez ignorer ce message. - extra_html: Merci de consultez également les règles du serveur et nos conditions d’utilisation. + extra_html: Merci de consulter également les règles du serveur et nos conditions d’utilisation. subject: 'Mastodon : Merci de confirmer votre inscription sur %{instance}' title: Vérifiez l’adresse de courriel email_changed: diff --git a/config/locales/et.yml b/config/locales/et.yml index 91b34492989..508329e996f 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -38,7 +38,7 @@ et: add_email_domain_block: Blokeeri e-posti domeen approve: Võta vastu approved_msg: Kasutaja %{username} liitumisavaldus rahuldatud - are_you_sure: Oled kindel? + are_you_sure: Kas oled kindel? avatar: Profiilipilt by_domain: Domeen change_email: @@ -47,7 +47,7 @@ et: label: Muuda e-posti aadressi new_email: Uus е-posti aadress submit: Muuda e-posti aadressi - title: Muuda e-postiaadressi kasutajale %{username} + title: Muuda kasutaja %{username} e-posti aadressi change_role: changed_msg: Roll on muudetud! edit_roles: Halda kasutaja rolle @@ -1973,9 +1973,11 @@ et: enabled: Vanade postituste automaatne kustutamine enabled_hint: Kustutab automaatselt postitused, mis ületavad määratud ajalimiiti, välja arvatud allpool toodud erandite puhul exceptions: Erandid + explanation: Automaatne kustutamine toimib madala prioriteetusega protsessi abil. Vanuse ülempiiri ja tegeliku kustutamise vahel võib seetõttu tekkida viivitus. ignore_favs: Eira lemmikuid ignore_reblogs: Eira jagamisi interaction_exceptions: Interaktsioonidel põhinevad erandid + interaction_exceptions_explanation: Postitused, mis ajutiselt ületavad lemmikuks lisamise või hooandmise künnise, võivad jääda alles isegi siis, kui nimetatud tegevuste arv hiljem väheneb. keep_direct: Säilita otsesõnumid keep_direct_hint: Ei kustuta otsesõnumeid keep_media: Meedialisanditega postituste säilitamine @@ -2190,6 +2192,7 @@ et: error: Turvavõtme kustutamisel tekkis tõrge. Palun proovi uuesti. success: Turvavõtmed edukalt kustutatud. invalid_credential: Vigane turvavõti + nickname: Hüüdnimi nickname_hint: Uue turvavõtme hüüdnimi not_enabled: Veebiautentimine pole sisse lülitatud not_supported: See veebilehitseja ei toeta turvavõtmeid diff --git a/config/locales/ga.yml b/config/locales/ga.yml index 0349e1795ed..9024a66eb9c 100644 --- a/config/locales/ga.yml +++ b/config/locales/ga.yml @@ -2104,9 +2104,11 @@ ga: enabled: Scrios seanphostálacha go huathoibríoch enabled_hint: Scriostar do phostálacha go huathoibríoch nuair a shroicheann siad tairseach aoise sonraithe, ach amháin má thagann siad le ceann de na heisceachtaí thíos exceptions: Eisceachtaí + explanation: Déantar scriosadh uathoibrithe le tosaíocht íseal. D’fhéadfadh moill a bheith ann idir an aois a shroichtear agus an bhaint. ignore_favs: Tabhair neamhaird ar toghanna ignore_reblogs: Déan neamhaird de boosts interaction_exceptions: Eisceachtaí bunaithe ar idirghníomhaíochtaí + interaction_exceptions_explanation: Féadfar poist a sháraíonn an tairseach is fearr leat nó an tairseach borrtha a choinneáil fiú má laghdaítear iad níos déanaí. keep_direct: Coinnigh teachtaireachtaí díreacha keep_direct_hint: Ní scriosann sé aon cheann de do theachtaireachtaí díreacha keep_media: Coinnigh postálacha le ceangaltáin meán @@ -2322,6 +2324,7 @@ ga: error: Bhí fadhb ann agus d'eochair shlándála á scriosadh. Arís, le d'thoil. success: Scriosadh d'eochair shlándála go rathúil. invalid_credential: Eochair shlándála neamhbhailí + nickname: Leasainm nickname_hint: Cuir isteach leasainm d'eochair shlándála nua not_enabled: Níl WebAuthn cumasaithe agat fós not_supported: Ní thacaíonn an brabhsálaí seo le heochracha slándála diff --git a/config/locales/nan.yml b/config/locales/nan.yml index 5fdeb84200a..c19edeeed10 100644 --- a/config/locales/nan.yml +++ b/config/locales/nan.yml @@ -1681,6 +1681,74 @@ nan: subject: Lí有收著管理ê警告 poll: subject: "%{name} 舉辦ê投票suah ah" + quote: + body: Lí ê PO文hōo %{name} 引用: + subject: "%{name} 引用lí ê PO文" + title: 新ê引用 + quoted_update: + subject: "%{name} 編輯lí有引用ê PO文" + reblog: + body: Lí ê PO文hōo %{name} 轉送: + subject: "%{name} 轉送lí ê PO文" + title: 新ê轉送 + severed_relationships: + subject: 因為管理ê決定,lí失去聯絡ah + status: + subject: "%{name} 頭tú-á PO文" + update: + subject: "%{name} 有編輯PO文" + notifications: + administration_emails: 管理員ê電子phue通知 + email_events: 電子phue通知ê事件 + email_events_hint: 揀lí想beh收ê通知ê事件: + number: + human: + decimal_units: + format: "%n%u" + units: + billion: B + million: M + quadrillion: Q + thousand: K + trillion: T + otp_authentication: + code_hint: 請輸入lí ê驗證應用程式生成ê code,來確認 + description_html: Nā lí啟用用驗證應用程式ê雙因素驗證,登入需要lí有手機á,伊ê產生token hōo lí輸入。 + enable: 啟用 + instructions_html: "請用lí手機á ê TOTP應用程式(親像Google Authenticator)掃描tsit ê QR code。對tsit-má起,hit ê應用程式ê生token,佇lí登入ê時陣著輸入。" + manual_instructions: 若lí bē當掃描 QR code,需要手動輸入下kha ê光碼: + setup: 設定 + wrong_code: Lí輸入ê碼無效!服侍器時間kap設備時間kám lóng正確? + pagination: + newer: 較新ê + next: 後一頁 + older: 較舊ê + prev: 頂一頁 + truncate: "…" + polls: + errors: + already_voted: Lí有投票ah + duplicate_options: 包含重覆ê項目 + duration_too_long: 傷久ah + duration_too_short: 傷緊ah + expired: 投票結束ah + invalid_choice: 所揀ê選項無佇leh + over_character_limit: Bē當比 %{max} ê字元tsē + self_vote: Lí bē當佇lí開ê投票投 + too_few_options: 項目定著愛超過一ê + too_many_options: Bē當包含超過 %{max} ê項目 + vote: 投票 + posting_defaults: + explanation: Tsiah ê設定ē用做預設ê值,若lí開新ê PO文,毋過lí會當佇編輯器內底,個別編輯PO文。 + preferences: + other: 其他 + posting_defaults: PO文預設值 + public_timelines: 公共ê時間線 + privacy: + hint_html: "自訂lí beh án-nuá hōo lí ê個人資料kap PO文受lâng發現。Mastodon ê tsē-tsē功能nā是拍開,通幫tsān lí接觸kàu較闊ê觀眾。請開一寡時間重看tsiah ê設定,確認in合lí ê使用例。" + privacy: 隱私權 + privacy_hint_html: 控制lí想欲為別lâng ê利益,公開guā tsē內容。Lâng用瀏覽別lâng ê跟tuè koh看in用啥物應用程式PO文,來發現心適ê個人資料kap時行ê應用程式,但是lí凡勢beh保持khàm起來。 + reach: 接觸 scheduled_statuses: too_soon: Tio̍h用未來ê日期。 statuses: diff --git a/config/locales/pl.yml b/config/locales/pl.yml index ce998071be9..86f015878ac 100644 --- a/config/locales/pl.yml +++ b/config/locales/pl.yml @@ -866,10 +866,21 @@ pl: publish_statistics: Publikuj statystyki title: Odkrywanie trends: Trendy + wrapstodon: Wrapstodon domain_blocks: all: Każdemu disabled: Nikomu users: Zalogowanym lokalnym użytkownikom + feed_access: + modes: + authenticated: tylko zalogowani użytkownicy + disabled: Wymagaj określonej roli użytkownika + public: Wszyscy + landing_page: + values: + about: O nas + local_feed: Lokalny kanał + trends: Trendy registrations: moderation_recommandation: Upewnij się, że masz adekwatny i szybko reagujący zespół moderacyjny przed otwarciem rejestracji! preamble: Kontroluj, kto może utworzyć konto na Twoim serwerze. @@ -923,6 +934,7 @@ pl: no_status_selected: Żaden wpis nie został zmieniony, bo żaden nie został wybrany open: Otwarty post original_status: Oryginalny post + quotes: Cytaty reblogs: Podbicia replied_to_html: Odpowiedziano na %{acct_link} status_changed: Post zmieniony @@ -1209,6 +1221,7 @@ pl: hint_html: Jeżeli chcesz przenieść się z innego konta na to, możesz utworzyć alias, który jest wymagany zanim zaczniesz przenoszenie obserwacji z poprzedniego konta na to. To działanie nie wyrządzi szkód i jest odwracalne. Migracja konta jest inicjowana ze starego konta. remove: Odłącz alias appearance: + advanced_settings: Ustawienia zaawansowane animations_and_accessibility: Animacje i dostępność discovery: Odkrywanie localization: @@ -1326,6 +1339,13 @@ pl: hint_html: "Informacja: Nie będziemy prosić Cię o ponowne podanie hasła przez następną godzinę." invalid_password: Nieprawidłowe hasło prompt: Potwierdź hasło, aby kontynuować + color_scheme: + auto: Automatyczne + dark: Ciemny + light: Jasny + contrast: + auto: Automatyczny + high: Wysoki crypto: errors: invalid_key: nie jest prawidłowym kluczem Ed25519 lub Curve25519 @@ -1651,6 +1671,10 @@ pl: expires_at: Wygaśnie po uses: Użycia title: Zaproś użytkowników + link_preview: + author_html: Przez %{name} + potentially_sensitive_content: + action: Kliknij, aby pokazać lists: errors: limit: Przekroczono maksymalną liczbę utworzonych list diff --git a/config/locales/simple_form.pl.yml b/config/locales/simple_form.pl.yml index 192a30a53ae..3f186e1c0ba 100644 --- a/config/locales/simple_form.pl.yml +++ b/config/locales/simple_form.pl.yml @@ -237,6 +237,8 @@ pl: setting_always_send_emails: Zawsze wysyłaj powiadomienia e-mail setting_auto_play_gif: Automatycznie odtwarzaj animowane GIFy setting_boost_modal: Kontroluj widoczność podbić + setting_color_scheme: Tryb + setting_contrast: Kontrast setting_default_language: Język wpisów setting_default_privacy: Widoczność wpisów setting_default_quote_policy: Kto może cytować @@ -286,6 +288,7 @@ pl: content_cache_retention_period: Okres zachowywania zdalnych treści custom_css: Niestandardowy CSS favicon: Favicon + landing_page: Strona docelowa dla nowych odwiedzających local_live_feed_access: Uzyskaj dostęp do kanałów zawierających lokalne wpisy local_topic_feed_access: Uzyskaj dostęp do hashtagów i linków zawierających lokalne wpisy mascot: Własna ikona @@ -310,6 +313,7 @@ pl: thumbnail: Miniaturka serwera trendable_by_default: Zezwalaj na trendy bez wcześniejszego przeglądu trends: Włącz trendy + wrapstodon: Włącz Wrapstodon interactions: must_be_follower: Nie wyświetlaj powiadomień od osób, które Cię nie obserwują must_be_following: Nie wyświetlaj powiadomień od osób, których nie obserwujesz @@ -370,7 +374,9 @@ pl: jurisdiction: Jurysdykcja min_age: Wiek minimalny user: + date_of_birth_1i: Rok date_of_birth_2i: Miesiąc + date_of_birth_3i: Dzień role: Rola time_zone: Strefa czasowa user_role: diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 22afa3ab19f..4f1c03a46a9 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -2146,6 +2146,7 @@ zh-CN: error: 删除你的安全密钥时出错。请重试。 success: 你的安全密钥已成功删除。 invalid_credential: 无效的安全密钥 + nickname: 昵称 nickname_hint: 输入你的新安全密钥的昵称 not_enabled: 你尚未启用WebAuthn not_supported: 此浏览器不支持安全密钥 diff --git a/config/locales/zh-TW.yml b/config/locales/zh-TW.yml index c9609eb10e3..100c57ab1ca 100644 --- a/config/locales/zh-TW.yml +++ b/config/locales/zh-TW.yml @@ -1437,7 +1437,7 @@ zh-TW: statuses_hint_html: 此過濾器會套用至所選之各別嘟文,無論其是否符合下列關鍵字。審閱或自過濾條件移除嘟文。 title: 編輯過濾條件 errors: - deprecated_api_multiple_keywords: 這些參數無法自此應用程式中更改,因為它們適用於一或多個過濾器關鍵字。請使用較新的應用程式或是網頁介面。 + deprecated_api_multiple_keywords: 這些參數無法自此應用程式中更改,因為它們適用於一個以上之過濾器關鍵字。請使用較新的應用程式或是網頁介面。 invalid_context: 沒有提供內文或內文無效 index: contexts: "%{contexts} 中的過濾器" @@ -1737,7 +1737,7 @@ zh-TW: invalid_choice: 您所選的投票選項並不存在 over_character_limit: 不能多於 %{max} 個字元 self_vote: 您無法於您的嘟文投票 - too_few_options: 必須包含至少一個項目 + too_few_options: 必須包含一個項目以上 too_many_options: 不能包含多於 %{max} 個項目 vote: 投票 posting_defaults: From 012c62132f13cf07b03bf70d2b4308327fd667ed Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 10:55:12 +0100 Subject: [PATCH 14/89] Update dependency sidekiq-unique-jobs to v8.0.13 (#37408) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index bf76d1fbcd8..57f4f91b35b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -823,7 +823,7 @@ GEM sidekiq-scheduler (6.0.1) rufus-scheduler (~> 3.2) sidekiq (>= 7.3, < 9) - sidekiq-unique-jobs (8.0.11) + sidekiq-unique-jobs (8.0.13) concurrent-ruby (~> 1.0, >= 1.0.5) sidekiq (>= 7.0.0, < 9.0.0) thor (>= 1.0, < 3.0) From aa3d76999d355bb9708ce96808792efce58fbcbd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:09:04 +0100 Subject: [PATCH 15/89] Update dependency ioredis to v5.9.1 (#37388) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 233b83d0cbb..c6a4aa15282 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2819,10 +2819,10 @@ __metadata: languageName: node linkType: hard -"@ioredis/commands@npm:1.4.0": - version: 1.4.0 - resolution: "@ioredis/commands@npm:1.4.0" - checksum: 10c0/99afe21fba794f84a2b84cceabcc370a7622e7b8b97a6589456c07c9fa62a15d54c5546f6f7214fb9a2458b1fa87579d5c531aaf48e06cc9be156d5923892c8d +"@ioredis/commands@npm:1.5.0": + version: 1.5.0 + resolution: "@ioredis/commands@npm:1.5.0" + checksum: 10c0/2d192d967a21f0192e17310d27ead02b0bdd504e834c782714abe641190ebfb548ad307fd89fd2d80db97c462afdc69ab4a4383831ab64ce61fe92f130d8b466 languageName: node linkType: hard @@ -8747,10 +8747,10 @@ __metadata: linkType: hard "ioredis@npm:^5.3.2": - version: 5.8.2 - resolution: "ioredis@npm:5.8.2" + version: 5.9.1 + resolution: "ioredis@npm:5.9.1" dependencies: - "@ioredis/commands": "npm:1.4.0" + "@ioredis/commands": "npm:1.5.0" cluster-key-slot: "npm:^1.1.0" debug: "npm:^4.3.4" denque: "npm:^2.1.0" @@ -8759,7 +8759,7 @@ __metadata: redis-errors: "npm:^1.2.0" redis-parser: "npm:^3.0.0" standard-as-callback: "npm:^2.1.0" - checksum: 10c0/305e385f811d49908899e32c2de69616cd059f909afd9e0a53e54f596b1a5835ee3449bfc6a3c49afbc5a2fd27990059e316cc78f449c94024957bd34c826d88 + checksum: 10c0/0993372ac380332d487e329882f247254a77dbf2d6f746e67c2211b50dda8067c0bce16c120b77ffe1eb693766dc4f36fac8cf5679231ab0c27671e34ef13449 languageName: node linkType: hard From ed17b3a7ec52c0421ae2483a8e08431316f08b1c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:09:25 +0100 Subject: [PATCH 16/89] Update dependency use-debounce to v10.1.0 (#37432) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c6a4aa15282..3619430278d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14317,11 +14317,11 @@ __metadata: linkType: hard "use-debounce@npm:^10.0.0": - version: 10.0.6 - resolution: "use-debounce@npm:10.0.6" + version: 10.1.0 + resolution: "use-debounce@npm:10.1.0" peerDependencies: react: "*" - checksum: 10c0/f0745de48fc344e6f90ea24384f3c79bf0733d06649827241135847abd67ee8db32f523d490c3276bce9f5a6867194ab1c0187bf6b84cabe1dd679037f4a527e + checksum: 10c0/1d2c9ab71be283f7ea9f9c78f3574aeb6ff6fbcb18a9c5daf7f633521a8978f14d190016d39fd773227e40e9929e223677bb311343dadf33ee0763ef24bff510 languageName: node linkType: hard From 972ed730628d99c8a24fa6a32bcefbecf441ad71 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:03:56 +0100 Subject: [PATCH 17/89] Update DefinitelyTyped types (non-major) (#37456) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/yarn.lock b/yarn.lock index 3619430278d..67b27cfa1ff 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4452,9 +4452,9 @@ __metadata: linkType: hard "@types/lodash@npm:^4.14.195": - version: 4.17.21 - resolution: "@types/lodash@npm:4.17.21" - checksum: 10c0/73cb006e047d8871e9d63f3a165543bf16c44a5b6fe3f9f6299e37cb8d67a7b1d55ac730959a81f9def510fd07232ff7e30e05413e5d5a12793baad84ebe36c3 + version: 4.17.23 + resolution: "@types/lodash@npm:4.17.23" + checksum: 10c0/9d9cbfb684e064a2b78aab9e220d398c9c2a7d36bc51a07b184ff382fa043a99b3d00c16c7f109b4eb8614118f4869678dbae7d5c6700ed16fb9340e26cc0bf6 languageName: node linkType: hard @@ -4503,13 +4503,13 @@ __metadata: linkType: hard "@types/pg@npm:^8.6.6": - version: 8.15.6 - resolution: "@types/pg@npm:8.15.6" + version: 8.16.0 + resolution: "@types/pg@npm:8.16.0" dependencies: "@types/node": "npm:*" pg-protocol: "npm:*" pg-types: "npm:^2.2.0" - checksum: 10c0/7f93f83a4da0dc6133918f824d826fa34e78fb8cf86392d28a0e095c836c6910c014ced5d4b364d83e8485a65ce369adeb9663b14ba301241d4c0f80073007f3 + checksum: 10c0/421fe7c07d5c0226835d362414a63653f86251ee966150d807ed60174c13921d1b8a3e2f1c2bfba9659ec0282ca50974030c4c1efcd575003eb922ea12ca7d05 languageName: node linkType: hard From a63a0db568e7a4f5292a8726b68c6e4bbde00296 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:03:59 +0100 Subject: [PATCH 18/89] Update dependency postcss-preset-env to v10.6.1 (#37457) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/yarn.lock b/yarn.lock index 67b27cfa1ff..f1d036bab60 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1651,14 +1651,14 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-normalize-display-values@npm:^4.0.0": - version: 4.0.0 - resolution: "@csstools/postcss-normalize-display-values@npm:4.0.0" +"@csstools/postcss-normalize-display-values@npm:^4.0.1": + version: 4.0.1 + resolution: "@csstools/postcss-normalize-display-values@npm:4.0.1" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/d3a3a362b532163bd791f97348ef28b7a43baf01987c7702b06285e751cdc5ea3e3a2553f088260515b4d28263d5c475923d4d4780ecb4078ec66dff50c9e638 + checksum: 10c0/5d19364bad8554b047cebd94ad7e203723ed76abaf690e4b92c74e6fc7c3642cb8858ade3263da61aff26d97bb258af567b1036e97865b7aa3b17522241fd1e1 languageName: node linkType: hard @@ -11261,8 +11261,8 @@ __metadata: linkType: hard "postcss-preset-env@npm:^10.1.5": - version: 10.6.0 - resolution: "postcss-preset-env@npm:10.6.0" + version: 10.6.1 + resolution: "postcss-preset-env@npm:10.6.1" dependencies: "@csstools/postcss-alpha-function": "npm:^1.0.1" "@csstools/postcss-cascade-layers": "npm:^5.0.2" @@ -11289,7 +11289,7 @@ __metadata: "@csstools/postcss-media-minmax": "npm:^2.0.9" "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^3.0.5" "@csstools/postcss-nested-calc": "npm:^4.0.0" - "@csstools/postcss-normalize-display-values": "npm:^4.0.0" + "@csstools/postcss-normalize-display-values": "npm:^4.0.1" "@csstools/postcss-oklab-function": "npm:^4.0.12" "@csstools/postcss-position-area-property": "npm:^1.0.0" "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" @@ -11337,7 +11337,7 @@ __metadata: postcss-selector-not: "npm:^8.0.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/61162c9d675004db842d58829605c3c9ee81ed1a15684793a419b94c2c28e3be2ff9a7373f0996a1a255caf208d8f3d5dd907e61af1bbb0c7634e3215e87fc56 + checksum: 10c0/e8da96f208918ebc0dc9acc8ba8961a92569f1d130b29abe25adaf7dbd56ef29fc6f778b75964c80fe7f3469012c763ea9447e5c2f559a002a155bc0462cce35 languageName: node linkType: hard From 6cb533311a12ddb9ec84f8ac4ca7a8fa6a2fdd54 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:14:27 +0100 Subject: [PATCH 19/89] New Crowdin Translations (automated) (#37466) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/el.json | 56 ++++++++++++------------- app/javascript/mastodon/locales/fr.json | 2 +- config/locales/devise.el.yml | 4 +- config/locales/doorkeeper.el.yml | 2 +- config/locales/el.yml | 42 +++++++++---------- config/locales/simple_form.el.yml | 14 +++---- 6 files changed, 60 insertions(+), 60 deletions(-) diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index c2cf2a7231f..e31e6f3bde0 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -24,7 +24,7 @@ "account.blocking": "Αποκλείεται", "account.cancel_follow_request": "Απόσυρση αιτήματος παρακολούθησης", "account.copy": "Αντιγραφή συνδέσμου προφίλ", - "account.direct": "Ιδιωτική αναφορά @{name}", + "account.direct": "Ιδιωτική επισήμανση @{name}", "account.disable_notifications": "Σταμάτα να με ειδοποιείς όταν δημοσιεύει ο @{name}", "account.domain_blocking": "Αποκλείεται ο τομέας", "account.edit_profile": "Επεξεργασία προφίλ", @@ -62,20 +62,20 @@ "account.link_verified_on": "Η ιδιοκτησία αυτού του συνδέσμου ελέχθηκε στις {date}", "account.locked_info": "Η κατάσταση απορρήτου αυτού του λογαριασμού έχει ρυθμιστεί σε κλειδωμένη. Ο ιδιοκτήτης αξιολογεί χειροκίνητα ποιος μπορεί να τον ακολουθήσει.", "account.media": "Πολυμέσα", - "account.mention": "Ανάφερε @{name}", + "account.mention": "Επισήμανε @{name}", "account.moved_to": "Ο/Η {name} έχει υποδείξει ότι ο νέος λογαριασμός του/της είναι τώρα:", - "account.mute": "Σώπασε τον @{name}", + "account.mute": "Σίγαση @{name}", "account.mute_notifications_short": "Σίγαση ειδοποιήσεων", "account.mute_short": "Σίγαση", - "account.muted": "Αποσιωπημένος/η", + "account.muted": "Σε σίγαση", "account.muting": "Σίγαση", "account.mutual": "Ακολουθείτε ο ένας τον άλλο", "account.no_bio": "Δεν υπάρχει περιγραφή.", "account.open_original_page": "Άνοιγμα αυθεντικής σελίδας", - "account.posts": "Τουτ", - "account.posts_with_replies": "Τουτ και απαντήσεις", + "account.posts": "Αναρτήσεις", + "account.posts_with_replies": "Αναρτήσεις και απαντήσεις", "account.remove_from_followers": "Κατάργηση {name} από τους ακόλουθους", - "account.report": "Κατάγγειλε @{name}", + "account.report": "Αναφορά @{name}", "account.requested_follow": "Ο/Η {name} αιτήθηκε να σε ακολουθήσει", "account.requests_to_follow_you": "Αιτήματα για να σε ακολουθήσουν", "account.share": "Κοινοποίηση του προφίλ @{name}", @@ -87,7 +87,7 @@ "account.unblock_short": "Άρση αποκλεισμού", "account.unendorse": "Να μην παρέχεται στο προφίλ", "account.unfollow": "Άρση ακολούθησης", - "account.unmute": "Διακοπή σίγασης @{name}", + "account.unmute": "Άρση σίγασης @{name}", "account.unmute_notifications_short": "Σίγαση ειδοποιήσεων", "account.unmute_short": "Κατάργηση σίγασης", "account_note.placeholder": "Κάνε κλικ για να προσθέσεις σημείωση", @@ -168,7 +168,7 @@ "block_modal.they_cant_see_posts": "Δεν μπορεί να δει τις αναρτήσεις σου και δε θα δεις τις δικές του.", "block_modal.they_will_know": "Μπορούν να δει ότι έχει αποκλειστεί.", "block_modal.title": "Αποκλεισμός χρήστη;", - "block_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον αναφέρουν.", + "block_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον επισημαίνουν.", "boost_modal.combo": "Μπορείς να πατήσεις {combo} για να το προσπεράσεις την επόμενη φορά", "boost_modal.reblog": "Ενίσχυση ανάρτησης;", "boost_modal.undo_reblog": "Αναίρεση ενίσχυσης;", @@ -196,7 +196,7 @@ "column.bookmarks": "Σελιδοδείκτες", "column.community": "Τοπική ροή", "column.create_list": "Δημιουργία λίστας", - "column.direct": "Ιδιωτικές αναφορές", + "column.direct": "Ιδιωτικές επισημάνσεις", "column.directory": "Περιήγηση στα προφίλ", "column.domain_blocks": "Αποκλεισμένοι τομείς", "column.edit_list": "Επεξεργασία λίστας", @@ -275,7 +275,7 @@ "confirmations.missing_alt_text.message": "Η ανάρτησή σου περιέχει πολυμέσα χωρίς εναλλακτικό κείμενο. Η προσθήκη περιγραφών βοηθά να γίνει το περιεχόμενό σου προσβάσιμο σε περισσότερους ανθρώπους.", "confirmations.missing_alt_text.secondary": "Δημοσίευση όπως και να ΄χει", "confirmations.missing_alt_text.title": "Προσθήκη εναλλακτικού κειμένου;", - "confirmations.mute.confirm": "Αποσιώπηση", + "confirmations.mute.confirm": "Σίγαση", "confirmations.private_quote_notify.cancel": "Πίσω στην επεξεργασία", "confirmations.private_quote_notify.confirm": "Δημοσίευση ανάρτησης", "confirmations.private_quote_notify.do_not_show_again": "Να μην εμφανιστεί ξανά αυτό το μήνυμα", @@ -297,14 +297,14 @@ "confirmations.unblock.confirm": "Άρση αποκλεισμού", "confirmations.unblock.title": "Άρση αποκλεισμού {name};", "confirmations.unfollow.confirm": "Άρση ακολούθησης", - "confirmations.unfollow.title": "Κατάργηση ακολούθησης του/της {name};", + "confirmations.unfollow.title": "Άρση ακολούθησης του/της {name};", "confirmations.withdraw_request.confirm": "Απόσυρση αιτήματος", "confirmations.withdraw_request.title": "Απόσυρση αιτήματος για να ακολουθήσετε τον/την {name};", "content_warning.hide": "Απόκρυψη ανάρτησης", "content_warning.show": "Εμφάνιση ούτως ή άλλως", "content_warning.show_more": "Εμφάνιση περισσότερων", - "conversation.delete": "Διαγραφή συζήτησης", - "conversation.mark_as_read": "Σήμανση ως αναγνωσμένο", + "conversation.delete": "Διαγραφή συνομιλίας", + "conversation.mark_as_read": "Σήμανση ως αναγνωσμένη", "conversation.open": "Προβολή συνομιλίας", "conversation.with": "Με {names}", "copy_icon_button.copied": "Αντιγράφηκε στο πρόχειρο", @@ -403,7 +403,7 @@ "filter_modal.added.context_mismatch_title": "Ασυμφωνία περιεχομένου!", "filter_modal.added.expired_explanation": "Αυτή η κατηγορία φίλτρων έχει λήξει, πρέπει να αλλάξετε την ημερομηνία λήξης για να ισχύσει.", "filter_modal.added.expired_title": "Ληγμένο φίλτρο!", - "filter_modal.added.review_and_configure": "Για να επιθεωρήσετε και να εξειδικεύσετε περαιτέρω αυτή την κατηγορία φίλτρων, πηγαίνετε στο {settings_link}.", + "filter_modal.added.review_and_configure": "Για να ελέγξετε και να ρυθμίσετε περαιτέρω αυτή την κατηγορία φίλτρων, πηγαίνετε στο {settings_link}.", "filter_modal.added.review_and_configure_title": "Ρυθμίσεις φίλτρου", "filter_modal.added.settings_link": "σελίδα ρυθμίσεων", "filter_modal.added.short_explanation": "Αυτή η ανάρτηση έχει προστεθεί στην ακόλουθη κατηγορία φίλτρου: {title}.", @@ -423,7 +423,7 @@ "firehose.remote": "Άλλοι διακομιστές", "follow_request.authorize": "Εξουσιοδότησε", "follow_request.reject": "Απέρριψε", - "follow_requests.unlocked_explanation": "Παρόλο που ο λογαριασμός σου δεν είναι κλειδωμένος, το προσωπικό του {domain} θεώρησαν πως ίσως να θέλεις να ελέγξεις χειροκίνητα αυτά τα αιτήματα ακολούθησης.", + "follow_requests.unlocked_explanation": "Παρόλο που ο λογαριασμός σου δεν είναι κλειδωμένος, το προσωπικό του {domain} θεώρησε πως ίσως να θέλεις να ελέγχεις χειροκίνητα αυτά τα αιτήματα ακολούθησης.", "follow_suggestions.curated_suggestion": "Επιλογή προσωπικού", "follow_suggestions.dismiss": "Να μην εμφανιστεί ξανά", "follow_suggestions.featured_longer": "Προσεκτικά επιλεγμένα απ' την ομάδα του {domain}", @@ -498,7 +498,7 @@ "ignore_notifications_modal.new_accounts_title": "Αγνόηση ειδοποιήσεων από νέους λογαριασμούς;", "ignore_notifications_modal.not_followers_title": "Αγνόηση ειδοποιήσεων από άτομα που δε σας ακολουθούν;", "ignore_notifications_modal.not_following_title": "Αγνόηση ειδοποιήσεων από άτομα που δεν ακολουθείς;", - "ignore_notifications_modal.private_mentions_title": "Αγνόηση ειδοποιήσεων από μη ζητηθείσες ιδιωτικές αναφορές;", + "ignore_notifications_modal.private_mentions_title": "Αγνόηση ειδοποιήσεων από μη ζητηθείσες ιδιωτικές επισημάνσεις;", "info_button.label": "Βοήθεια", "info_button.what_is_alt_text": "Το εναλλακτικό κείμενο παρέχει περιγραφές εικόνας για άτομα με προβλήματα όρασης, διαδικτυακές συνδέσεις χαμηλής ταχύτητας ή για άτομα που αναζητούν επιπλέον περιεχόμενο.\\n\\nΜπορείς να βελτιώσεις την προσβασιμότητα και την κατανόηση για όλους, γράφοντας σαφές, συνοπτικό και αντικειμενικό εναλλακτικό κείμενο.\\n\\n
  • Κατέγραψε σημαντικά στοιχεία
  • \\n
  • Συνόψισε το κείμενο στις εικόνες
  • \\n
  • Χρησιμοποίησε δομή κανονικής πρότασης
  • \\n
  • Απέφυγε περιττές πληροφορίες
  • \\n
  • Εστίασε στις τάσεις και τα βασικά ευρήματα σε σύνθετα οπτικά στοιχεία (όπως διαγράμματα ή χάρτες)
", "interaction_modal.action": "Για να αλληλεπιδράσετε με την ανάρτηση του/της {name}, πρέπει να συνδεθείτε στον λογαριασμό σας σε οποιονδήποτε διακομιστή Mastodon χρησιμοποιείτε.", @@ -594,11 +594,11 @@ "mute_modal.hide_options": "Απόκρυψη επιλογών", "mute_modal.indefinite": "Μέχρι να κάνω άρση σίγασης", "mute_modal.show_options": "Εμφάνιση επιλογών", - "mute_modal.they_can_mention_and_follow": "Μπορεί να σε αναφέρει και να σε ακολουθήσει, αλλά δε θα τον βλέπεις.", + "mute_modal.they_can_mention_and_follow": "Μπορεί να σε επισημάνει και να σε ακολουθήσει, αλλά δε θα τον βλέπεις.", "mute_modal.they_wont_know": "Δε θα ξέρει ότι είναι σε σίγαση.", "mute_modal.title": "Σίγαση χρήστη;", - "mute_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον αναφέρουν.", - "mute_modal.you_wont_see_posts": "Μπορεί ακόμα να δει τις αναρτήσεις σου, αλλά δε θα βλέπεις τις δικές του.", + "mute_modal.you_wont_see_mentions": "Δε θα βλέπεις τις αναρτήσεις που τον επισημαίνουν.", + "mute_modal.you_wont_see_posts": "Μπορεί ακόμη να βλέπει τις αναρτήσεις σου, αλλά δε θα βλέπεις τις δικές του.", "navigation_bar.about": "Σχετικά με", "navigation_bar.account_settings": "Κωδικός πρόσβασης και ασφάλεια", "navigation_bar.administration": "Διαχείριση", @@ -745,8 +745,8 @@ "notifications.policy.filter_not_followers_title": "Άτομα που δε σε ακολουθούν", "notifications.policy.filter_not_following_hint": "Μέχρι να τους εγκρίνεις χειροκίνητα", "notifications.policy.filter_not_following_title": "Άτομα που δεν ακολουθείς", - "notifications.policy.filter_private_mentions_hint": "Φιλτραρισμένο εκτός αν είναι απάντηση σε δική σου αναφορά ή αν ακολουθείς τον αποστολέα", - "notifications.policy.filter_private_mentions_title": "Μη συναινετικές ιδιωτικές αναφορές", + "notifications.policy.filter_private_mentions_hint": "Φιλτράρισμα εκτός αν είναι απάντηση σε δική σου επισήμανση ή αν ακολουθείς τον αποστολέα", + "notifications.policy.filter_private_mentions_title": "Μη συναινετικές ιδιωτικές επισημάνσεις", "notifications.policy.title": "Διαχείριση ειδοποιήσεων από…", "notifications_permission_banner.enable": "Ενεργοποίηση ειδοποιήσεων επιφάνειας εργασίας", "notifications_permission_banner.how_to_control": "Για να λαμβάνεις ειδοποιήσεις όταν το Mastodon δεν είναι ανοιχτό, ενεργοποίησε τις ειδοποιήσεις επιφάνειας εργασίας. Μπορείς να ελέγξεις με ακρίβεια ποιοι τύποι αλληλεπιδράσεων δημιουργούν ειδοποιήσεις επιφάνειας εργασίας μέσω του κουμπιού {icon} μόλις ενεργοποιηθούν.", @@ -854,12 +854,12 @@ "report.statuses.subtitle": "Επίλεξε όλα όσα ισχύουν", "report.statuses.title": "Υπάρχουν αναρτήσεις που τεκμηριώνουν αυτή την αναφορά;", "report.submit": "Υποβολή", - "report.target": "Καταγγελία {target}", + "report.target": "Αναφορά {target}", "report.thanks.take_action": "Αυτές είναι οι επιλογές σας για να ελέγχετε τι βλέπετε στο Mastodon:", "report.thanks.take_action_actionable": "Ενώ το εξετάζουμε, μπορείς να δράσεις εναντίον του @{name}:", "report.thanks.title": "Δε θες να το βλέπεις;", "report.thanks.title_actionable": "Σε ευχαριστούμε για την αναφορά, θα το διερευνήσουμε.", - "report.unfollow": "Κατάργηση ακολούθησης του @{name}", + "report.unfollow": "Άρση ακολούθησης του @{name}", "report.unfollow_explanation": "Ακολουθείς αυτό τον λογαριασμό. Για να μη βλέπεις τις αναρτήσεις τους στη δική σου ροή, πάψε να τον ακολουθείς.", "report_notification.attached_statuses": "{count, plural, one {{count} ανάρτηση} other {{count} αναρτήσεις}} επισυνάπτονται", "report_notification.categories.legal": "Νομικά", @@ -926,7 +926,7 @@ "status.copy": "Αντιγραφή συνδέσμου ανάρτησης", "status.delete": "Διαγραφή", "status.delete.success": "Η ανάρτηση διαγράφηκε", - "status.detailed_status": "Προβολή λεπτομερούς συζήτησης", + "status.detailed_status": "Προβολή λεπτομερούς συνομιλίας", "status.direct": "Ιδιωτική επισήμανση @{name}", "status.direct_indicator": "Ιδιωτική επισήμανση", "status.edit": "Επεξεργασία", @@ -944,7 +944,7 @@ "status.media_hidden": "Κρυμμένο πολυμέσο", "status.mention": "Επισήμανε @{name}", "status.more": "Περισσότερα", - "status.mute": "Σίγαση σε @{name}", + "status.mute": "Σίγαση @{name}", "status.mute_conversation": "Σίγαση συνομιλίας", "status.open": "Επέκταση ανάρτησης", "status.pin": "Καρφίτσωσε στο προφίλ", @@ -997,7 +997,7 @@ "status.translate": "Μετάφραση", "status.translated_from_with": "Μεταφράστηκε από {lang} χρησιμοποιώντας {provider}", "status.uncached_media_warning": "Μη διαθέσιμη προεπισκόπηση", - "status.unmute_conversation": "Αναίρεση σίγασης συνομιλίας", + "status.unmute_conversation": "Άρση σίγασης συνομιλίας", "status.unpin": "Ξεκαρφίτσωσε από το προφίλ", "subscribed_languages.lead": "Μόνο αναρτήσεις σε επιλεγμένες γλώσσες θα εμφανίζονται στην αρχική σου και θα παραθέτονται ροές μετά την αλλαγή. Επέλεξε καμία για να λαμβάνεις αναρτήσεις σε όλες τις γλώσσες.", "subscribed_languages.save": "Αποθήκευση αλλαγών", @@ -1053,7 +1053,7 @@ "visibility_modal.direct_quote_warning.text": "Εάν αποθηκεύσετε τις τρέχουσες ρυθμίσεις, η ενσωματωμένη παράθεση θα μετατραπεί σε σύνδεσμο.", "visibility_modal.direct_quote_warning.title": "Οι παραθέσεις δεν μπορούν να ενσωματωθούν σε ιδιωτικές επισημάνσεις", "visibility_modal.header": "Ορατότητα και αλληλεπίδραση", - "visibility_modal.helper.direct_quoting": "Ιδιωτικές αναφορές που έχουν συνταχθεί στο Mastodon δεν μπορούν να γίνουν παράθεση από άλλους.", + "visibility_modal.helper.direct_quoting": "Ιδιωτικές επισημάνσεις που έχουν συνταχθεί στο Mastodon δεν μπορούν να γίνουν παράθεση από άλλους.", "visibility_modal.helper.privacy_editing": "Η ορατότητα δεν μπορεί να αλλάξει μετά τη δημοσίευση μιας ανάρτησης.", "visibility_modal.helper.privacy_private_self_quote": "Αυτο-παραθέσεις ιδιωτικών αναρτήσεων δεν μπορούν να γίνουν δημόσιες.", "visibility_modal.helper.private_quoting": "Αναρτήσεις για ακολούθους μόνο που έχουν συνταχθεί στο Mastodon, δεν μπορούν να γίνουν παράθεση από άλλους.", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index d0ef2605bb8..df03661c9df 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -4,7 +4,7 @@ "about.default_locale": "Défaut", "about.disclaimer": "Mastodon est un logiciel libre, open-source et une marque déposée de Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Raison non disponible", - "about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateurs et utilisatrices de n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur.", + "about.domain_blocks.preamble": "Mastodon vous permet de visualiser le contenu et d'interagir avec les utilisateurs et utilisatrices de généralement n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur.", "about.domain_blocks.silenced.explanation": "Vous ne verrez généralement pas les profils et le contenu de ce serveur, à moins que vous ne les recherchiez explicitement ou que vous ne choisissiez de les suivre.", "about.domain_blocks.silenced.title": "Limité", "about.domain_blocks.suspended.explanation": "Aucune donnée de ce serveur ne sera traitée, enregistrée ou échangée, rendant impossible toute interaction ou communication avec les comptes de ce serveur.", diff --git a/config/locales/devise.el.yml b/config/locales/devise.el.yml index 86134b9491f..3708d70ce6d 100644 --- a/config/locales/devise.el.yml +++ b/config/locales/devise.el.yml @@ -14,7 +14,7 @@ el: locked: Ο λογαριασμός σου κλειδώθηκε. not_found_in_database: Λάθος %{authentication_keys} ή συνθηματικό. omniauth_user_creation_failure: Σφάλμα δημιουργίας λογαριασμού για αυτήν την ταυτότητα. - pending: Εκκρεμεί η έγκριση του λογαριασμού σου. + pending: Εκκρεμεί ο έλεγχος του λογαριασμού σου. timeout: Η τρέχουσα σύνδεσή σου έληξε. Παρακαλούμε συνδέσου ξανά για να συνεχίσεις. unauthenticated: Πρέπει να συνδεθείς ή να εγγραφείς για να συνεχίσεις. unconfirmed: Πρέπει να επιβεβαιώσεις τη διεύθυνση email σου για να συνεχίσεις. @@ -23,7 +23,7 @@ el: action: Επιβεβαίωση διεύθυνσης email action_with_app: Επιβεβαίωση και επιστροφή στο %{app} explanation: Δημιούργησες έναν λογαριασμό στο %{host} με αυτή τη διεύθυνση email. Με ένα κλικ θα τον ενεργοποιήσεις. Αν δεν το έκανες εσύ, παρακαλούμε αγνόησε αυτό το email. - explanation_when_pending: Έχεις υποβάλλει αίτηση πρόσκλησης στο %{host} με αυτή την ηλεκτρονική διεύθυνση email. Μόλις επιβεβαιώσεις το email σου, θα ελέγξουμε την αίτηση σου. Μέχρι τότε δε θα μπορείς να συνδεθείς. Αν απορριφθεί η αίτησή σου, τα στοιχεία σου θα αφαιρεθούν, άρα δε θα χρειαστεί να κάνεις κάτι επιπλέον. Αν δεν υπέβαλες εσύ την αίτηση, αγνόησε αυτό το email. + explanation_when_pending: Έχεις υποβάλλει αίτηση πρόσκλησης στο %{host} με αυτή την διεύθυνση email. Μόλις επιβεβαιώσεις το email σου, θα ελέγξουμε την αίτηση σου. Μέχρι τότε δε θα μπορείς να συνδεθείς. Αν απορριφθεί η αίτησή σου, τα στοιχεία σου θα αφαιρεθούν, άρα δε θα χρειαστεί να κάνεις κάτι επιπλέον. Αν δεν υπέβαλες εσύ την αίτηση, αγνόησε αυτό το email. extra_html: Παρακαλούμε να διαβάσεις του κανόνες αυτού του κόμβου και τους όρους χρήσης της υπηρεσίας μας. subject: 'Mastodon: Οδηγίες επιβεβαίωσης για %{instance}' title: Επιβεβαίωσε διεύθυνση email diff --git a/config/locales/doorkeeper.el.yml b/config/locales/doorkeeper.el.yml index 114194f6df4..1632767bc79 100644 --- a/config/locales/doorkeeper.el.yml +++ b/config/locales/doorkeeper.el.yml @@ -192,7 +192,7 @@ el: write:follows: ακολούθηση ατόμων write:lists: δημιουργία λιστών write:media: να ανεβάζει πολυμέσα - write:mutes: σίγαση ατόμων και συζητήσεων + write:mutes: σίγαση ατόμων και συνομιλιών write:notifications: καθαρισμός των ειδοποιήσεων σου write:reports: αναφορά άλλων ατόμων write:statuses: δημοσίευση αναρτήσεων diff --git a/config/locales/el.yml b/config/locales/el.yml index 55db7e29198..1a32ecd57f1 100644 --- a/config/locales/el.yml +++ b/config/locales/el.yml @@ -108,7 +108,7 @@ el: no_limits_imposed: Χωρίς όρια no_role_assigned: Δεν έχει ανατεθεί ρόλος not_subscribed: Δεν έγινε εγγραφή - pending: Εκκρεμεί αξιολόγηση + pending: Εκκρεμεί έλεγχος perform_full_suspension: Αναστολή previous_strikes: Προηγούμενα παραπτώματα previous_strikes_description_html: @@ -327,7 +327,7 @@ el: create: Δημιουργία ανακοίνωσης title: Νέα ανακοίνωση preview: - disclaimer: Δεδομένου ότι οι χρήστες δεν μπορούν να εξαιρεθούν από αυτά, οι ειδοποιήσεις μέσω ηλεκτρονικού ταχυδρομείου θα πρέπει να περιορίζονται σε σημαντικές ανακοινώσεις, όπως η παραβίαση προσωπικών δεδομένων ή οι ειδοποιήσεις κλεισίματος διακομιστή. + disclaimer: Δεδομένου ότι οι χρήστες δεν μπορούν να εξαιρεθούν από αυτά, οι ειδοποιήσεις μέσω email θα πρέπει να περιορίζονται σε σημαντικές ανακοινώσεις, όπως η παραβίαση προσωπικών δεδομένων ή οι ειδοποιήσεις κλεισίματος διακομιστή. explanation_html: 'Το email θα αποσταλεί σε %{display_count} χρήστες. Το ακόλουθο κείμενο θα συμπεριληφθεί στο e-mail:' title: Προεπισκόπηση ειδοποίησης ανακοίνωσης publish: Δημοσίευση @@ -449,7 +449,7 @@ el: reject_media: Απόρριψη αρχείων πολυμέσων reject_media_hint: Αφαιρεί τα τοπικά αποθηκευμένα αρχεία πολυμέσων και αποτρέπει τη λήψη άλλων στο μέλλον. Δεν έχει σημασία για τις αναστολές reject_reports: Απόρριψη αναφορών - reject_reports_hint: Αγνόησε όσων αναφορών που προέρχονται από αυτό τον τομέα. Δεν σχετίζεται με τις παύσεις + reject_reports_hint: Αγνόηση όσων αναφορών προέρχονται από αυτό τον τομέα. Δεν σχετίζεται με τις παύσεις undo: Αναίρεση αποκλεισμού τομέα view: Εμφάνιση αποκλεισμού τομέα email_domain_blocks: @@ -727,7 +727,7 @@ el: close_report: 'Επισήμανση αναφοράς #%{id} ως επιλυμένη' close_reports_html: Επισήμανε όλες τις αναφορές ενάντια στον λογαριασμό @%{acct} ως επιλυμένες delete_data_html: Διάγραψε το προφίλ και το περιεχόμενο του @%{acct} σε 30 ημέρες από τώρα εκτός αν, εν τω μεταξύ, ανακληθεί η αναστολή - preview_preamble_html: 'Ο @%{acct} θα λάβει μια προειδοποίηση με τα ακόλουθο περιεχόμενο:' + preview_preamble_html: 'Ο/Η @%{acct} θα λάβει μια προειδοποίηση με τα ακόλουθα περιεχόμενα:' record_strike_html: Κατάγραψε ένα παράπτωμα εναντίον του @%{acct} για να σε βοηθήσει να αποφασίσεις σε μελλοντικές παραβιάσεις από αυτόν τον λογαριασμό send_email_html: Στείλε στον λογαριασμό @%{acct} ένα προειδοποιητικό email warning_placeholder: Προαιρετικές επιπλέον εξηγήσεις για αυτή την ενέργεια από την ομάδα συντονισμού. @@ -949,7 +949,7 @@ el: elasticsearch_health_yellow: message_html: Το σύμπλεγμα Elasticsearch δεν είναι υγιές (κίτρινη κατάσταση), ίσως θες να διαπιστώσεις την αιτία elasticsearch_index_mismatch: - message_html: Οι αντιστοιχές δείκτη του Elasticsearch δεν είναι ενημερωμένες. Παρακαλώ εκτέλεσε το tootctl search deploy --only=%{value} + message_html: Οι αντιστοιχήσεις του δείκτη Elasticsearch δεν είναι ενημερωμένες. Παρακαλώ εκτέλεσε το tootctl search deploy --only=%{value} elasticsearch_preset: action: Δες το εγχειρίδιο message_html: Το σύμπλεγμα Elasticsearch σου, έχει παραπάνω από ένα κόμβο, το Mastodon δεν είναι ρυθμισμένο για να τους χρησιμοποιεί. @@ -987,9 +987,9 @@ el: moderation: not_trendable: Δε δημιουργεί τάσεις not_usable: Μη χρησιμοποιήσιμη - pending_review: Εκκρεμεί αξιολόγηση - review_requested: Αιτήθηκε αξιολόγηση - reviewed: Αξιολογήθηκε + pending_review: Εκκρεμεί έλεγχος + review_requested: Αιτήθηκε έλεγχος + reviewed: Ελέγχθηκε title: Κατάσταση trendable: Πιθανό για τάσεις unreviewed: Μη ελεγμένη @@ -999,7 +999,7 @@ el: oldest: Παλαιότερη όλων open: Προβολή Δημόσια reset: Επαναφορά - review: Κατάσταση αξιολόγησης + review: Κατάσταση ελέγχου search: Αναζήτηση title: Ετικέτες updated_msg: Οι ρυθμίσεις των ετικετών ενημερώθηκαν επιτυχώς @@ -1012,7 +1012,7 @@ el: generate: Χρήση προτύπου generates: action: Δημιουργία - chance_to_review_html: "Οι παραγόμενοι όροι υπηρεσίας δε θα δημοσιεύονται αυτόματα. Θα έχεις την ευκαιρία να εξετάσεις το αποτέλεσμα. Παρακαλούμε συμπλήρωσε τις απαιτούμενες πληροφορίες για να συνεχίσεις." + chance_to_review_html: "Οι παραγόμενοι όροι υπηρεσίας δε θα δημοσιεύονται αυτόματα. Θα έχεις την ευκαιρία να ελέγξεις το αποτέλεσμα. Παρακαλούμε συμπλήρωσε τις απαιτούμενες πληροφορίες για να συνεχίσεις." explanation_html: Το πρότυπο όρων υπηρεσίας που παρέχονται είναι μόνο για ενημερωτικούς σκοπούς και δε θα πρέπει να ερμηνεύονται ως νομικές συμβουλές για οποιοδήποτε θέμα. Παρακαλούμε συμβουλέψου τον νομικό σου σύμβουλο σχετικά με την περίπτωσή σου και τις συγκεκριμένες νομικές ερωτήσεις που έχεις. title: Ρύθμιση Όρων Παροχής Υπηρεσιών going_live_on_html: Ενεργό, σε ισχύ από %{date} @@ -1060,7 +1060,7 @@ el: usage_comparison: Κοινοποιήθηκε %{today} φορές σήμερα, σε σύγκριση με %{yesterday} χθες not_allowed_to_trend: Δεν επιτρέπεται να γίνει δημοφιλές only_allowed: Μόνο επιτρεπόμενα - pending_review: Εκκρεμεί αξιολόγηση + pending_review: Εκκρεμεί έλεγχος preview_card_providers: allowed: Σύνδεσμοι από αυτόν τον εκδότη μπορούν να γίνουν δημοφιλείς description_html: Αυτοί είναι τομείς από τους οποίους οι σύνδεσμοι συχνά κοινοποιούνται στον διακομιστή σας. Οι σύνδεσμοι δεν θα γίνουν δημοφιλείς δημοσίως εκτός και αν ο τομέας του συνδέσμου εγκριθεί. Η έγκρισή σας (ή απόρριψη) περιλαμβάνει και τους υποτομείς. @@ -1155,7 +1155,7 @@ el: webhook: Webhook admin_mailer: auto_close_registrations: - body: Λόγω έλλειψης πρόσφατης δραστηριότητας συντονιστών, οι εγγραφές στο %{instance} έχουν αλλάξει αυτόματα στην απαίτηση χειροκίνητης αξιολόγησης, για να αποτρέψει το %{instance} από το να χρησιμοποιηθεί ως πλατφόρμα για πιθανούς κακούς ηθοποιούς. Μπορείς να το αλλάξεις ξανά για να ανοίξετε εγγραφές ανά πάσα στιγμή. + body: Λόγω έλλειψης πρόσφατης δραστηριότητας συντονιστών, οι εγγραφές στο %{instance} έχουν αλλάξει αυτόματα στην απαίτηση χειροκίνητου ελέγχου, για να αποτρέψει το %{instance} από το να χρησιμοποιηθεί ως πλατφόρμα για πιθανούς κακούς παράγοντες. Μπορείς να το αλλάξεις ξανά για να ανοίξετε εγγραφές ανά πάσα στιγμή. subject: Οι εγγραφές για το %{instance} έχουν αλλάξει αυτόματα σε απαίτηση έγκρισης new_appeal: actions: @@ -1174,7 +1174,7 @@ el: subject: Κρίσιμες ενημερώσεις Mastodon είναι διαθέσιμες για το %{instance}! new_pending_account: body: Τα στοιχεία του νέου λογαριασμού είναι παρακάτω. Μπορείς να εγκρίνεις ή να απορρίψεις αυτή την αίτηση. - subject: Νέος λογαριασμός προς έγκριση στο %{instance} (%{username}) + subject: Νέος λογαριασμός προς έλεγχο στο %{instance} (%{username}) new_report: body: Ο/Η %{reporter} ανέφερε τον/την %{target} body_remote: Κάποιος/α από τον τομέα %{domain} ανέφερε τον/την %{target} @@ -1183,14 +1183,14 @@ el: body: Έχουν κυκλοφορήσει νέες εκδόσεις Mastodon, ίσως θέλεις να ενημερώσεις! subject: Νέες εκδόσεις Mastodon είναι διαθέσιμες για το %{instance}! new_trends: - body: 'Τα ακόλουθα στοιχεία χρειάζονται αξιολόγηση για να μπορούν να προβληθούν δημόσια:' + body: 'Τα ακόλουθα στοιχεία χρειάζονται έλεγχο για να μπορούν να προβληθούν δημόσια:' new_trending_links: title: Σύνδεσμοι σε τάση new_trending_statuses: title: Αναρτήσεις σε τάση new_trending_tags: title: Ετικέτες σε τάση - subject: Νέες τάσεις προς αξιολόγηση στο %{instance} + subject: Νέες τάσεις προς έλεγχο στο %{instance} aliases: add_new: Δημιουργία ψευδώνυμου created_msg: Δημιουργήθηκε νέο ψευδώνυμο. Τώρα μπορείς να ξεκινήσεις τη μεταφορά από τον παλιό λογαριασμό. @@ -1291,14 +1291,14 @@ el: preamble_html: Συνδεθείτε με τα διαπιστευτήριά σας στον %{domain}. Αν ο λογαριασμός σας φιλοξενείται σε διαφορετικό διακομιστή, δε θα μπορείτε να συνδεθείτε εδώ. title: Συνδεθείτε στο %{domain} sign_up: - manual_review: Οι εγγραφές στο %{domain} περνούν από χειροκίνητη αξιολόγηση από τους συντονιστές μας. Για να μας βοηθήσεις να επεξεργαστούμε την εγγραφή σου, γράψε λίγα λόγια για τον εαυτό σου και γιατί θέλεις έναν λογαριασμό στο %{domain}. + manual_review: Οι εγγραφές στο %{domain} περνούν από χειροκίνητο έλεγχο από τους συντονιστές μας. Για να μας βοηθήσεις να επεξεργαστούμε την εγγραφή σου, γράψε λίγα λόγια για τον εαυτό σου και γιατί θέλεις έναν λογαριασμό στο %{domain}. preamble: Με έναν λογαριασμό σ' αυτόν τον διακομιστή Mastodon, θα μπορείς να ακολουθήσεις οποιοδήποτε άλλο άτομο στο δίκτυο, ανεξάρτητα από το πού φιλοξενείται ο λογαριασμός του. title: Ας ξεκινήσουμε τις ρυθμίσεις στο %{domain}. status: account_status: Κατάσταση λογαριασμού confirming: Αναμονή για ολοκλήρωση επιβεβαίωσης του email. functional: Ο λογαριασμός σας είναι πλήρως λειτουργικός. - pending: Η εφαρμογή σου εκκρεμεί έγκρισης. Ίσως θα διαρκέσει κάποιο χρόνο. Θα λάβεις email αν εγκριθεί. + pending: Η εφαρμογή σου εκκρεμεί έλεγχο από το προσωπικό μας. Ίσως θα διαρκέσει κάποιο χρόνο. Θα λάβεις email αν εγκριθεί. redirecting_to: Ο λογαριασμός σου είναι ανενεργός γιατί επί του παρόντος ανακατευθύνει στον %{acct}. self_destruct: Καθώς το %{domain} κλείνει, θα έχεις μόνο περιορισμένη πρόσβαση στον λογαριασμό σου. view_strikes: Προβολή προηγούμενων ποινών εναντίον του λογαριασμού σας @@ -1446,7 +1446,7 @@ el: home: Αρχική σελίδα και λίστες notifications: Ειδοποιήσεις public: Δημόσιες ροές - thread: Συζητήσεις + thread: Συνομιλίες edit: add_keyword: Προσθήκη λέξης-κλειδιού keywords: Λέξεις-κλειδιά @@ -1579,7 +1579,7 @@ el: type: Τύπος εισαγωγής type_groups: constructive: Ακολουθείς & Σελιδοδείκτες - destructive: Μπλοκ & σίγαση + destructive: Αποκλεισμοί & σιγάσεις types: blocking: Λίστα αποκλεισμού bookmarks: Σελιδοδείκτες @@ -1688,7 +1688,7 @@ el: title: Συντονισμός move_handler: carry_blocks_over_text: Ο χρήστης μετακόμισε από το %{acct}, που είχες αποκλείσει. - carry_mutes_over_text: Ο χρήστης μετακόμισε από το %{acct}, που είχες αποσιωπήσει. + carry_mutes_over_text: Ο χρήστης μετακόμισε από το %{acct}, που είχες σε σίγαση. copy_account_note_text: 'Ο χρήστης μετακόμισε από τον %{acct}, ορίστε οι προηγούμενες σημειώσεις σου για εκείνον:' navigation: toggle_menu: Εμφάνιση/Απόκρυψη μενού @@ -1948,7 +1948,7 @@ el: quoted_user_not_mentioned: Δεν είναι δυνατή η παράθεση ενός μη επισημασμένου χρήστη σε μια ανάρτηση Ιδιωτικής επισήμανσης. over_character_limit: υπέρβαση μέγιστου ορίου %{max} χαρακτήρων pin_errors: - direct: Αναρτήσεις που είναι ορατές μόνο στους αναφερόμενους χρήστες δεν μπορούν να καρφιτσωθούν + direct: Αναρτήσεις που είναι ορατές μόνο στους επισημασμένους χρήστες δεν μπορούν να καρφιτσωθούν limit: Έχεις ήδη καρφιτσώσει το μέγιστο αριθμό επιτρεπτών αναρτήσεων ownership: Δεν μπορείς να καρφιτσώσεις ανάρτηση κάποιου άλλου reblog: Οι ενισχύσεις δεν καρφιτσώνονται diff --git a/config/locales/simple_form.el.yml b/config/locales/simple_form.el.yml index 9c77f798daa..3c06421aee9 100644 --- a/config/locales/simple_form.el.yml +++ b/config/locales/simple_form.el.yml @@ -16,7 +16,7 @@ el: account_migration: acct: Όρισε το username@domain του λογαριασμού στον οποίο θέλεις να μετακινηθείς account_warning_preset: - text: Μπορείς να χρησιμοποιήσεις το ίδιο συντακτικό των αναρτήσεων όπως URL, ετικέτες και αναφορές + text: Μπορείς να χρησιμοποιήσεις το ίδιο συντακτικό των αναρτήσεων όπως URL, ετικέτες και επισημάνσεις title: Προαιρετικό. Δεν εμφανίζεται στον παραλήπτη admin_account_action: include_statuses: Ο χρήστης θα δει ποιες αναρτήσεις προκάλεσαν την προειδοποίηση ή την ενέργεια των συντονιστών @@ -90,7 +90,7 @@ el: backups_retention_period: Οι χρήστες έχουν τη δυνατότητα να δημιουργήσουν αρχεία των αναρτήσεων τους για να κατεβάσουν αργότερα. Όταν οριστεί μια θετική τιμή, αυτά τα αρχεία θα διαγράφονται αυτόματα από τον αποθηκευτικό σου χώρο μετά τον καθορισμένο αριθμό ημερών. bootstrap_timeline_accounts: Αυτοί οι λογαριασμοί θα καρφιτσωθούν στην κορυφή των προτεινόμενων ακολουθήσεων για νέους χρήστες. Παρέχετε μια λίστα λογαριασμών χωρισμένη με κόμμα. closed_registrations_message: Εμφανίζεται όταν κλείνουν οι εγγραφές - content_cache_retention_period: Όλες οι αναρτήσεις από άλλους διακομιστές (συμπεριλαμβανομένων των ενισχύσεων και απαντήσεων) θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών, χωρίς να λαμβάνεται υπόψη οποιαδήποτε αλληλεπίδραση τοπικού χρήστη με αυτές τις αναρτήσεις. Αυτό περιλαμβάνει αναρτήσεις όπου ένας τοπικός χρήστης την έχει χαρακτηρίσει ως σελιδοδείκτη ή αγαπημένη. Θα χαθούν επίσης ιδιωτικές αναφορές μεταξύ χρηστών από διαφορετικές οντότητες και θα είναι αδύνατο να αποκατασταθούν. Η χρήση αυτής της ρύθμισης προορίζεται για οντότητες ειδικού σκοπού και χαλάει πολλές προσδοκίες του χρήστη όταν εφαρμόζεται για χρήση γενική σκοπού. + content_cache_retention_period: Όλες οι αναρτήσεις από άλλους διακομιστές (συμπεριλαμβανομένων των ενισχύσεων και απαντήσεων) θα διαγραφούν μετά τον καθορισμένο αριθμό ημερών, χωρίς να λαμβάνεται υπόψη οποιαδήποτε αλληλεπίδραση τοπικού χρήστη με αυτές τις αναρτήσεις. Αυτό περιλαμβάνει αναρτήσεις όπου ένας τοπικός χρήστης την έχει χαρακτηρίσει ως σελιδοδείκτη ή αγαπημένη. Θα χαθούν επίσης ιδιωτικές επισημάνσεις μεταξύ χρηστών από διαφορετικές οντότητες και θα είναι αδύνατο να αποκατασταθούν. Η χρήση αυτής της ρύθμισης προορίζεται για οντότητες ειδικού σκοπού και χαλάει πολλές προσδοκίες του χρήστη όταν εφαρμόζεται για χρήση γενική σκοπού. custom_css: Μπορείς να εφαρμόσεις προσαρμοσμένα στυλ στην έκδοση ιστοσελίδας του Mastodon. favicon: WEBP, PNG, GIF ή JPG. Παρακάμπτει το προεπιλεγμένο favicon του Mastodon με ένα προσαρμοσμένο εικονίδιο. landing_page: Επιλέγει ποια σελίδα βλέπουν οι νέοι επισκέπτες όταν φτάνουν για πρώτη φορά στο διακομιστή σας. Αν επιλέξετε "Τάσεις", τότε οι τάσεις πρέπει να είναι ενεργοποιημένες στις Ρυθμίσεις Ανακάλυψης. Αν επιλέξετε "Τοπική ροή", τότε το "Πρόσβαση σε ζωντανές ροές με τοπικές αναρτήσεις" πρέπει να οριστεί σε "Όλοι" στις Ρυθμίσεις Ανακάλυψης. @@ -117,7 +117,7 @@ el: imports: data: Αρχείο CSV που έχει εξαχθεί από διαφορετικό διακομιστή Mastodon invite_request: - text: Αυτό θα μας βοηθήσει να επιθεωρήσουμε την αίτησή σου + text: Αυτό θα μας βοηθήσει να ελέγξουμε την αίτησή σου ip_block: comment: Προαιρετικό. Θυμηθείτε γιατί προσθέσατε αυτόν τον κανόνα. expires_in: Οι διευθύνσεις IP είναι ένας πεπερασμένος πόρος, μερικές φορές μοιράζονται και συχνά αλλάζουν χέρια. Για το λόγο αυτό, δεν συνιστώνται αόριστοι αποκλεισμοί διευθύνσεων IP. @@ -190,7 +190,7 @@ el: text: Προκαθορισμένο κείμενο title: Τίτλος admin_account_action: - include_statuses: Συμπερίληψη των καταγγελλομένων τουτ στο email + include_statuses: Συμπερίληψη των αναφερόμενων αναρτήσεων στο email send_email_notification: Ενημέρωση χρήστη μέσω email text: Προσαρμοσμένη προειδοποίηση type: Ενέργεια @@ -313,7 +313,7 @@ el: status_page_url: URL σελίδας κατάστασης theme: Προεπιλεγμένο θέμα thumbnail: Μικρογραφία διακομιστή - trendable_by_default: Επίτρεψε τις τάσεις χωρίς προηγούμενη αξιολόγηση + trendable_by_default: Επίτρεψε τις τάσεις χωρίς προηγούμενο έλεγχο trends: Ενεργοποίηση τάσεων wrapstodon: Ενεργοποίηση Wrapstodon interactions: @@ -339,7 +339,7 @@ el: follow: Κάποιος σε ακολούθησε follow_request: Κάποιος ζήτησε να σε ακολουθήσει mention: Κάποιος σε επισήμανε - pending_account: Νέος λογαριασμός χρειάζεται αναθεώρηση + pending_account: Νέος λογαριασμός χρειάζεται έλεγχο quote: Κάποιος σε παρέθεσε reblog: Κάποιος ενίσχυσε την ανάρτηση σου report: Υποβλήθηκε νέα αναφορά @@ -349,7 +349,7 @@ el: label: Μια νέα έκδοση του Mastodon είναι διαθέσιμη none: Να μην ειδοποιούμαι ποτέ για ενημερώσεις (δεν συνιστάται) patch: Ειδοποίηση για ενημερώσεις σφαλμάτων - trending_tag: Νέο περιεχόμενο προς τάση απαιτεί αξιολόγηση + trending_tag: Νέο περιεχόμενο προς τάση απαιτεί έλεγχο rule: hint: Επιπρόσθετες πληροφορίες text: Κανόνας From f8b8fa16f5629e3a0dc8a48d20662b367356d26e Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 10:18:34 +0100 Subject: [PATCH 20/89] Update dependency pino to v10.1.1 (#37443) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 27 +++++++++------------------ 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/yarn.lock b/yarn.lock index f1d036bab60..424b0d623fd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10813,15 +10813,6 @@ __metadata: languageName: node linkType: hard -"pino-abstract-transport@npm:^2.0.0": - version: 2.0.0 - resolution: "pino-abstract-transport@npm:2.0.0" - dependencies: - split2: "npm:^4.0.0" - checksum: 10c0/02c05b8f2ffce0d7c774c8e588f61e8b77de8ccb5f8125afd4a7325c9ea0e6af7fb78168999657712ae843e4462bb70ac550dfd6284f930ee57f17f486f25a9f - languageName: node - linkType: hard - "pino-abstract-transport@npm:^3.0.0": version: 3.0.0 resolution: "pino-abstract-transport@npm:3.0.0" @@ -10874,23 +10865,23 @@ __metadata: linkType: hard "pino@npm:^10.0.0": - version: 10.1.0 - resolution: "pino@npm:10.1.0" + version: 10.1.1 + resolution: "pino@npm:10.1.1" dependencies: "@pinojs/redact": "npm:^0.4.0" atomic-sleep: "npm:^1.0.0" on-exit-leak-free: "npm:^2.1.0" - pino-abstract-transport: "npm:^2.0.0" + pino-abstract-transport: "npm:^3.0.0" pino-std-serializers: "npm:^7.0.0" process-warning: "npm:^5.0.0" quick-format-unescaped: "npm:^4.0.3" real-require: "npm:^0.2.0" safe-stable-stringify: "npm:^2.3.1" sonic-boom: "npm:^4.0.1" - thread-stream: "npm:^3.0.0" + thread-stream: "npm:^4.0.0" bin: pino: bin.js - checksum: 10c0/49c1dd80d5f99f02bde1acf2f60cef7686948a937f751f6cb368c2868c7e82e54aeabac63a34587e16019965cbf0eb6e609edf92c439a98a0a4fcb0add277eaf + checksum: 10c0/c0a78ae4209b81879e3b2c72abd523c6045412066d214b7577f4073b23369751bbb7a1c6abb691bdf347ac81fab5e9cad9d8db0612fe2e5a9607ef895651a1bb languageName: node linkType: hard @@ -13673,12 +13664,12 @@ __metadata: languageName: node linkType: hard -"thread-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "thread-stream@npm:3.0.0" +"thread-stream@npm:^4.0.0": + version: 4.0.0 + resolution: "thread-stream@npm:4.0.0" dependencies: real-require: "npm:^0.2.0" - checksum: 10c0/1f4da5a8c93b170cdc7c1ad774af49bb2af43f73cfd9a7f8fb02b766255b483eb6d0b734502c880397baa95c0ce3490088b9a487cff32d4e481aab6fe76560f5 + checksum: 10c0/f0a47a673af574062df20140ec3e857d679365253fcaa98a76c167c9a053ee03291f4b25bd89b078c7f6a48f07f49d5a49e4f5598bb1c8a263ec15955a018fbd languageName: node linkType: hard From 9332f48abebd4ce26636df1dddb53937d5aadfa7 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 13 Jan 2026 11:18:26 +0100 Subject: [PATCH 21/89] Simplify status batch removal SQL query (#37469) --- app/services/batched_remove_status_service.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/services/batched_remove_status_service.rb b/app/services/batched_remove_status_service.rb index 5d6ea2550e4..826dbcc720e 100644 --- a/app/services/batched_remove_status_service.rb +++ b/app/services/batched_remove_status_service.rb @@ -31,7 +31,7 @@ class BatchedRemoveStatusService < BaseService # transaction lock the database, but we use the delete method instead # of destroy to avoid all callbacks. We rely on foreign keys to # cascade the delete faster without loading the associations. - statuses_and_reblogs.each_slice(50) { |slice| Status.where(id: slice.map(&:id)).delete_all } + statuses_and_reblogs.each_slice(50) { |slice| Status.unscoped.where(id: slice.pluck(:id)).delete_all } # Since we skipped all callbacks, we also need to manually # deindex the statuses From 9b5986b36e86d50d19b68b10788b3316012894c3 Mon Sep 17 00:00:00 2001 From: Shlee Date: Tue, 13 Jan 2026 17:40:08 +0700 Subject: [PATCH 22/89] Fix `quote_approval_policy` being reset to user defaults when omitted in status update (#37436) --- app/controllers/api/v1/statuses_controller.rb | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/app/controllers/api/v1/statuses_controller.rb b/app/controllers/api/v1/statuses_controller.rb index ec6b93e4085..f07dda1247f 100644 --- a/app/controllers/api/v1/statuses_controller.rb +++ b/app/controllers/api/v1/statuses_controller.rb @@ -106,9 +106,7 @@ class Api::V1::StatusesController < Api::BaseController @status = Status.where(account: current_account).find(params[:id]) authorize @status, :update? - UpdateStatusService.new.call( - @status, - current_account.id, + update_options = { text: status_params[:status], media_ids: status_params[:media_ids], media_attributes: status_params[:media_attributes], @@ -116,8 +114,11 @@ class Api::V1::StatusesController < Api::BaseController language: status_params[:language], spoiler_text: status_params[:spoiler_text], poll: status_params[:poll], - quote_approval_policy: quote_approval_policy - ) + } + + update_options[:quote_approval_policy] = quote_approval_policy if status_params[:quote_approval_policy].present? + + UpdateStatusService.new.call(@status, current_account.id, update_options) render json: @status, serializer: REST::StatusSerializer end From 5b54cd7f766dda19cbfdb02a1f94f45de1c18379 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 13 Jan 2026 11:40:26 +0100 Subject: [PATCH 23/89] Add ability to include inline javascript (#37459) --- app/helpers/theme_helper.rb | 16 ++++++++++ app/javascript/inline/theme-selection.js | 23 ++++++++++++++ app/lib/inline_script_manager.rb | 31 +++++++++++++++++++ app/views/layouts/application.html.haml | 1 + spec/requests/content_security_policy_spec.rb | 2 +- 5 files changed, 72 insertions(+), 1 deletion(-) create mode 100644 app/javascript/inline/theme-selection.js create mode 100644 app/lib/inline_script_manager.rb diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index 00b4a6d2b3f..1d642056809 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -1,6 +1,22 @@ # frozen_string_literal: true module ThemeHelper + def javascript_inline_tag(path) + entry = InlineScriptManager.instance.file(path) + + # Only add hash if we don't allow arbitrary includes already, otherwise it's going + # to break the React Tools browser extension or other inline scripts + unless Rails.env.development? && request.content_security_policy.dup.script_src.include?("'unsafe-inline'") + request.content_security_policy = request.content_security_policy.clone.tap do |policy| + values = policy.script_src + values << "'sha256-#{entry[:digest]}'" + policy.script_src(*values) + end + end + + content_tag(:script, entry[:contents], type: 'text/javascript') + end + def theme_style_tags(theme) if theme == 'system' ''.html_safe.tap do |tags| diff --git a/app/javascript/inline/theme-selection.js b/app/javascript/inline/theme-selection.js new file mode 100644 index 00000000000..b3a2b03163e --- /dev/null +++ b/app/javascript/inline/theme-selection.js @@ -0,0 +1,23 @@ +(function (element) { + const {userTheme} = element.dataset; + + const colorSchemeMediaWatcher = window.matchMedia('(prefers-color-scheme: dark)'); + const contrastMediaWatcher = window.matchMedia('(prefers-contrast: more)'); + + const updateColorScheme = () => { + const useDarkMode = userTheme === 'system' ? colorSchemeMediaWatcher.matches : userTheme !== 'mastodon-light'; + element.dataset.mode = useDarkMode ? 'dark' : 'light'; + }; + + const updateContrast = () => { + const useHighContrast = userTheme === 'contrast' || contrastMediaWatcher.matches; + + element.dataset.contrast = useHighContrast ? 'high' : 'default'; + } + + colorSchemeMediaWatcher.addEventListener('change', updateColorScheme); + contrastMediaWatcher.addEventListener('change', updateContrast); + + updateColorScheme(); + updateContrast(); +})(document.documentElement); diff --git a/app/lib/inline_script_manager.rb b/app/lib/inline_script_manager.rb new file mode 100644 index 00000000000..bca7c98f6b7 --- /dev/null +++ b/app/lib/inline_script_manager.rb @@ -0,0 +1,31 @@ +# frozen_string_literal: true + +require 'singleton' + +class InlineScriptManager + include Singleton + include ActionView::Helpers::TagHelper + include ActionView::Helpers::JavaScriptHelper + + def initialize + @cached_files = {} + end + + def file(name) + @cached_files[name] ||= load_file(name) + end + + private + + def load_file(name) + path = Pathname.new(name).cleanpath + raise ArgumentError, "Invalid inline javascript path: #{path}" if path.to_s.start_with?('..') + + path = Rails.root.join('app', 'javascript', 'inline', path) + + contents = javascript_cdata_section(path.read) + digest = Digest::SHA256.base64digest(contents) + + { contents:, digest: } + end +end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 47e602f0f3f..dccb6035cae 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -20,6 +20,7 @@ - if use_mask_icon? %link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/ %link{ rel: 'manifest', href: manifest_path(format: :json) }/ + = javascript_inline_tag 'theme-selection.js' = theme_color_tags current_theme %meta{ name: 'mobile-web-app-capable', content: 'yes' }/ diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb index 0a58a03ffad..0aa4494ef0b 100644 --- a/spec/requests/content_security_policy_spec.rb +++ b/spec/requests/content_security_policy_spec.rb @@ -32,7 +32,7 @@ RSpec.describe 'Content-Security-Policy' do img-src 'self' data: blob: #{local_domain} manifest-src 'self' #{local_domain} media-src 'self' data: #{local_domain} - script-src 'self' #{local_domain} 'wasm-unsafe-eval' + script-src 'self' #{local_domain} 'wasm-unsafe-eval' 'sha256-Q/2Cjx8v06hAdOF8/DeBUpsmBcSj7sLN3I/WpTF8T8c=' style-src 'self' #{local_domain} 'nonce-ZbA+JmE7+bK8F5qvADZHuQ==' worker-src 'self' blob: #{local_domain} CSP From be60c4585ebdac0cc236fb597246922af3fb6ce8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=F0=9F=95=B4=EF=B8=8F?= Date: Tue, 13 Jan 2026 05:47:04 -0500 Subject: [PATCH 24/89] Fix keyboard navigation in media modal after clicking image (#37464) --- app/javascript/mastodon/features/ui/components/media_modal.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/mastodon/features/ui/components/media_modal.tsx b/app/javascript/mastodon/features/ui/components/media_modal.tsx index ac762aa18d8..25101376820 100644 --- a/app/javascript/mastodon/features/ui/components/media_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/media_modal.tsx @@ -85,7 +85,7 @@ export const MediaModal: FC = forwardRef< setIndex(newIndex); setZoomedIn(false); if (animate) { - void api.start({ x: `-${newIndex * 100}%` }); + void api.start({ x: `calc(-${newIndex * 100}% + 0px)` }); } }, [api, media.size], From 41639655ffaa9914632fe937c9583b901b36f5cc Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 13 Jan 2026 12:06:54 +0100 Subject: [PATCH 25/89] Fix `isDarkMode` utility (#37470) --- app/javascript/mastodon/utils/theme.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/app/javascript/mastodon/utils/theme.ts b/app/javascript/mastodon/utils/theme.ts index 767d97cf5c9..921787a6c43 100644 --- a/app/javascript/mastodon/utils/theme.ts +++ b/app/javascript/mastodon/utils/theme.ts @@ -5,9 +5,7 @@ export function getUserTheme() { export function isDarkMode() { const { userTheme } = document.documentElement.dataset; - return ( - (userTheme === 'system' && - window.matchMedia('(prefers-color-scheme: dark)').matches) || - userTheme !== 'mastodon-light' - ); + return userTheme === 'system' + ? window.matchMedia('(prefers-color-scheme: dark)').matches + : userTheme !== 'mastodon-light'; } From d5264b37224632682c15b5e565fcc5fee3ae4834 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 14:32:11 +0100 Subject: [PATCH 26/89] Update dependency aws-sdk-s3 to v1.211.0 (#37396) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 57f4f91b35b..2167d017395 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -96,8 +96,8 @@ GEM ast (2.4.3) attr_required (1.0.2) aws-eventstream (1.4.0) - aws-partitions (1.1200.0) - aws-sdk-core (3.240.0) + aws-partitions (1.1201.0) + aws-sdk-core (3.241.3) aws-eventstream (~> 1, >= 1.3.0) aws-partitions (~> 1, >= 1.992.0) aws-sigv4 (~> 1.9) @@ -105,11 +105,11 @@ GEM bigdecimal jmespath (~> 1, >= 1.6.1) logger - aws-sdk-kms (1.118.0) - aws-sdk-core (~> 3, >= 3.239.1) + aws-sdk-kms (1.120.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sigv4 (~> 1.5) - aws-sdk-s3 (1.209.0) - aws-sdk-core (~> 3, >= 3.234.0) + aws-sdk-s3 (1.211.0) + aws-sdk-core (~> 3, >= 3.241.3) aws-sdk-kms (~> 1) aws-sigv4 (~> 1.5) aws-sigv4 (1.12.1) From 232b9e9cc60506822bd6121b5dcd9707b1ea8c24 Mon Sep 17 00:00:00 2001 From: Shlee Date: Tue, 13 Jan 2026 20:49:36 +0700 Subject: [PATCH 27/89] Fix delivery worker counting unsalvageable HTTP errors as successes (#37235) --- app/workers/activitypub/delivery_worker.rb | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/app/workers/activitypub/delivery_worker.rb b/app/workers/activitypub/delivery_worker.rb index ade7175c9d5..8cd39f700ca 100644 --- a/app/workers/activitypub/delivery_worker.rb +++ b/app/workers/activitypub/delivery_worker.rb @@ -38,7 +38,7 @@ class ActivityPub::DeliveryWorker if @inbox_url.present? if @performed failure_tracker.track_success! - else + elsif !@unsalvageable failure_tracker.track_failure! end end @@ -62,9 +62,13 @@ class ActivityPub::DeliveryWorker stoplight_wrapper.run do request_pool.with(@host) do |http_client| build_request(http_client).perform do |response| - raise Mastodon::UnexpectedResponseError, response unless response_successful?(response) || response_error_unsalvageable?(response) || unsalvageable_authorization_failure?(response) - - @performed = true + if response_successful?(response) + @performed = true + elsif response_error_unsalvageable?(response) || unsalvageable_authorization_failure?(response) + @unsalvageable = true + else + raise Mastodon::UnexpectedResponseError, response + end end end end From 122b1592ed543838f2047ef82e570ca58a087b32 Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 13 Jan 2026 15:17:43 +0100 Subject: [PATCH 28/89] Add feature flag detection for profile redesign (#37472) --- app/javascript/mastodon/utils/environment.ts | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/app/javascript/mastodon/utils/environment.ts b/app/javascript/mastodon/utils/environment.ts index 95075454f23..84767322b04 100644 --- a/app/javascript/mastodon/utils/environment.ts +++ b/app/javascript/mastodon/utils/environment.ts @@ -12,8 +12,21 @@ export function isProduction() { else return import.meta.env.PROD; } -export type Features = 'fasp' | 'http_message_signatures'; +export type ServerFeatures = 'fasp'; -export function isFeatureEnabled(feature: Features) { +export function isServerFeatureEnabled(feature: ServerFeatures) { return initialState?.features.includes(feature) ?? false; } + +type ClientFeatures = 'profile_redesign'; + +export function isClientFeatureEnabled(feature: ClientFeatures) { + try { + const features = + window.localStorage.getItem('experiments')?.split(',') ?? []; + return features.includes(feature); + } catch (err) { + console.warn('Could not access localStorage to get client features', err); + return false; + } +} From c6be114cef98b219869cd3181d4eda9243dce5c6 Mon Sep 17 00:00:00 2001 From: Shlee Date: Tue, 13 Jan 2026 22:47:48 +0700 Subject: [PATCH 29/89] Non-ActivityPub Link header alternate blocks HTML ActivityPub discovery in FetchResourceService (#37439) Co-authored-by: Claire --- app/services/fetch_resource_service.rb | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/app/services/fetch_resource_service.rb b/app/services/fetch_resource_service.rb index 666eccb9ce5..514c838d648 100644 --- a/app/services/fetch_resource_service.rb +++ b/app/services/fetch_resource_service.rb @@ -58,13 +58,7 @@ class FetchResourceService < BaseService [@url, { prefetched_body: body }] elsif !terminal - link_header = response['Link'] && parse_link_header(response) - - if link_header&.find_link(%w(rel alternate)) - process_link_headers(link_header) - elsif response.mime_type == 'text/html' - process_html(response) - end + process_link_headers(response) || process_html(response) end end @@ -73,13 +67,18 @@ class FetchResourceService < BaseService end def process_html(response) + return unless response.mime_type == 'text/html' + page = Nokogiri::HTML5(response.body_with_limit) json_link = page.xpath('//link[nokogiri:link_rel_include(@rel, "alternate")]', NokogiriHandler).find { |link| ACTIVITY_STREAM_LINK_TYPES.include?(link['type']) } process(json_link['href'], terminal: true) unless json_link.nil? end - def process_link_headers(link_header) + def process_link_headers(response) + link_header = response['Link'] && parse_link_header(response) + return if link_header.nil? + json_link = link_header.find_link(%w(rel alternate), %w(type application/activity+json)) || link_header.find_link(%w(rel alternate), ['type', 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"']) process(json_link.href, terminal: true) unless json_link.nil? From 92ad380e118ebf12db344786f6862b1eb64365d0 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 13 Jan 2026 11:21:25 -0500 Subject: [PATCH 30/89] Update rubocop to version 1.82.1 (#37475) --- Gemfile.lock | 6 ++--- app/workers/move_worker.rb | 36 ++++++++++++++------------ lib/paperclip/attachment_extensions.rb | 3 +-- 3 files changed, 24 insertions(+), 21 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index 2167d017395..e7519ea90bc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -752,7 +752,7 @@ GEM rspec-mocks (~> 3.0) sidekiq (>= 5, < 9) rspec-support (3.13.6) - rubocop (1.81.7) + rubocop (1.82.1) json (~> 2.3) language_server-protocol (~> 3.17.0.2) lint_roller (~> 1.1.0) @@ -760,7 +760,7 @@ GEM parser (>= 3.3.0.2) rainbow (>= 2.2.2, < 4.0) regexp_parser (>= 2.9.3, < 3.0) - rubocop-ast (>= 1.47.1, < 2.0) + rubocop-ast (>= 1.48.0, < 2.0) ruby-progressbar (~> 1.7) unicode-display_width (>= 2.4.0, < 4.0) rubocop-ast (1.49.0) @@ -782,7 +782,7 @@ GEM rack (>= 1.1) rubocop (>= 1.75.0, < 2.0) rubocop-ast (>= 1.44.0, < 2.0) - rubocop-rspec (3.8.0) + rubocop-rspec (3.9.0) lint_roller (~> 1.1) rubocop (~> 1.81) rubocop-rspec_rails (2.32.0) diff --git a/app/workers/move_worker.rb b/app/workers/move_worker.rb index 1a5745a86ae..43238b72b20 100644 --- a/app/workers/move_worker.rb +++ b/app/workers/move_worker.rb @@ -45,30 +45,34 @@ class MoveWorker end # Then handle accounts that follow both the old and new account - @source_account.passive_relationships - .where(account: Account.local) - .where(account: @target_account.followers.local) - .in_batches do |follows| - ListAccount.where(follow: follows).includes(:list).find_each do |list_account| - list_account.list.accounts << @target_account - rescue ActiveRecord::RecordInvalid - nil - end + source_local_followers + .where(account: @target_account.followers.local) + .in_batches do |follows| + ListAccount.where(follow: follows).includes(:list).find_each do |list_account| + list_account.list.accounts << @target_account + rescue ActiveRecord::RecordInvalid + nil + end end # Finally, handle the common case of accounts not following the new account - @source_account.passive_relationships - .where(account: Account.local) - .where.not(account: @target_account.followers.local) - .where.not(account_id: @target_account.id) - .in_batches do |follows| - ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id) - num_moved += follows.update_all(target_account_id: @target_account.id) + source_local_followers + .where.not(account: @target_account.followers.local) + .where.not(account_id: @target_account.id) + .in_batches do |follows| + ListAccount.where(follow: follows).in_batches.update_all(account_id: @target_account.id) + num_moved += follows.update_all(target_account_id: @target_account.id) end num_moved end + def source_local_followers + @source_account + .passive_relationships + .where(account: Account.local) + end + def queue_follow_unfollows! bypass_locked = @target_account.local? diff --git a/lib/paperclip/attachment_extensions.rb b/lib/paperclip/attachment_extensions.rb index 011e165ed74..7141adc9edc 100644 --- a/lib/paperclip/attachment_extensions.rb +++ b/lib/paperclip/attachment_extensions.rb @@ -16,8 +16,7 @@ module Paperclip # if we're processing the original, close + unlink the source tempfile intermediate_files << original if name == :original - @queued_for_write[name] = style.processors - .inject(original) do |file, processor| + @queued_for_write[name] = style.processors.inject(original) do |file, processor| file = Paperclip.processor(processor).make(file, style.processor_options, self) intermediate_files << file unless file == original file From 19bc3e76ea1d7fa2d9af711613a77fc0f5f0d1b5 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Tue, 13 Jan 2026 11:21:55 -0500 Subject: [PATCH 31/89] Add spec for quote policy update change (#37474) --- spec/requests/api/v1/statuses_spec.rb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/spec/requests/api/v1/statuses_spec.rb b/spec/requests/api/v1/statuses_spec.rb index 5db9889e2d5..3fbf26c54a7 100644 --- a/spec/requests/api/v1/statuses_spec.rb +++ b/spec/requests/api/v1/statuses_spec.rb @@ -508,6 +508,15 @@ RSpec.describe '/api/v1/statuses' do .to start_with('application/json') end end + + context 'when status has non-default quote policy and param is omitted' do + let(:status) { Fabricate(:status, account: user.account, quote_approval_policy: 'nobody') } + + it 'preserves existing quote approval policy' do + expect { subject } + .to_not(change { status.reload.quote_approval_policy }) + end + end end end From ccf3ed0ddf8811d212dd3f94f177eb54471ed609 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:02:07 +0100 Subject: [PATCH 32/89] Update Node.js to 24.13 (#37473) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- .nvmrc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.nvmrc b/.nvmrc index b0195acf781..12fd1fc2777 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -24.12 +24.13 From 2f91d9755d77fbe5902593ac49da1a3665e7b3a6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:02:11 +0100 Subject: [PATCH 33/89] New Crowdin Translations (automated) (#37482) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/gd.json | 51 ++++++++++++++++++++++++- config/locales/activerecord.gd.yml | 6 +++ config/locales/gd.yml | 22 +++++++++++ config/locales/simple_form.gd.yml | 7 ++++ 4 files changed, 85 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index 2f4cb44de80..e48c60ff2ce 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -113,11 +113,52 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Mìnich seo dhan fheadhainn air a bheil cion-lèirsinne…", "alt_text_modal.done": "Deiseil", "announcement.announcement": "Brath-fios", + "annual_report.announcement.action_build": "Cruthaich Wrapstodon dhomh", + "annual_report.announcement.action_dismiss": "Na cruthaich", + "annual_report.announcement.action_view": "Seall an Wrapstodon agam", + "annual_report.announcement.description": "Fidir mar a chaidh leat air Mastodon am bliadhna.", + "annual_report.announcement.title": "Tha Wrapstodon {year} air a thighinn", + "annual_report.nav_item.badge": "Ùr", + "annual_report.shared_page.donate": "Thoir tabhartas", + "annual_report.shared_page.footer": "Chaidh a ghintinn le {heart} le sgioba Mhastodon", + "annual_report.shared_page.footer_server_info": "Tha {username} a’ cleachdadh {domain}, sin fear de dh’iomadh coimhearsnachd le cumhachd Mastodon.", + "annual_report.summary.archetype.booster.desc_public": "Bhiodh {name} an toir air postaichean rim brosnachadh, a’ toirt taic do chruthadairean gu sgiobalta.", + "annual_report.summary.archetype.booster.desc_self": "Bhiodh tu an toir air postaichean rim brosnachadh, a’ toirt taic do chruthadairean gu sgiobalta.", + "annual_report.summary.archetype.booster.name": "Am Boghadair", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Tha fios againn gun robh {name} ann am badeigin, a’ gabhail tlachd à Mastodon air an dòigh fhèin.", + "annual_report.summary.archetype.lurker.desc_self": "Tha fios againn gun robh thu ann am badeigin, a’ gabhail tlachd à Mastodon air do dhòigh fhèin.", + "annual_report.summary.archetype.lurker.name": "An Socaireach", + "annual_report.summary.archetype.oracle.desc_public": "Chruthaich {name} barrachd phostaichean ùra na freagairtean, a’ cumail Mastodon ùr ’s beòthail.", + "annual_report.summary.archetype.oracle.desc_self": "Chruthaich thu barrachd phostaichean ùra na freagairtean, a’ cumail Mastodon ùr ’s beòthail.", + "annual_report.summary.archetype.oracle.name": "Coinneach Odhar", + "annual_report.summary.archetype.pollster.desc_public": "Chruthaich {name} barrachd chunntasan-beachd na seòrsaichean eile de phost, a’ cur ris an iongantas air Mastodon.", + "annual_report.summary.archetype.pollster.desc_self": "Chruthaich thu barrachd chunntasan-beachd na seòrsaichean eile de phost, a’ cur ris an iongantas air Mastodon.", + "annual_report.summary.archetype.pollster.name": "An Culaidh-iongantais", + "annual_report.summary.archetype.replier.desc_public": "Fhreagradh {name} gu tric air postaichean càich, a’ cur deasbadan ùra gu dol air Mastodon.", + "annual_report.summary.archetype.replier.desc_self": "Fhreagradh tu gu tric air postaichean càich, a’ cur deasbadan ùra gu dol air Mastodon.", + "annual_report.summary.archetype.replier.name": "An Dealan-dè", + "annual_report.summary.archetype.reveal": "Nochd am prìomh-choltas a th’ orm", + "annual_report.summary.archetype.reveal_description": "Mòran taing airson conaltradh air Mastodon! Thàinig an t-àm a gheibheadh tu a-mach dè am prìomh-choltas a bh’ ort {year}.", + "annual_report.summary.archetype.title_public": "Am prìomh-choltas air {name}", + "annual_report.summary.archetype.title_self": "Am prìomh-choltas a th’ ort", + "annual_report.summary.close": "Dùin", + "annual_report.summary.copy_link": "Dèan lethbhreac dhen cheangal", + "annual_report.summary.followers.new_followers": "{count, plural, one {neach-leantainn ùr} two {neach-leantainn ùra} few {luchd-leantainn ùra} other {luchd-leantainn ùra}}", + "annual_report.summary.highlighted_post.boost_count": "Chaidh am post seo a bhrosnachadh {count, plural, one {# turas} two {# thuras} few {# tursan} other {# turas}}.", + "annual_report.summary.highlighted_post.favourite_count": "Chaidh am post seo a chur ris na h-annsachdan {count, plural, one {# turas} two {# thuras} few {# tursan} other {# turas}}.", + "annual_report.summary.highlighted_post.reply_count": "Fhuair am post seo {count, plural, one {# fhreagairt} two {# fhreagairt} few {# freagairtean} other {# freagairt}}.", + "annual_report.summary.highlighted_post.title": "Am post air an robh fèill as motha", "annual_report.summary.most_used_app.most_used_app": "an aplacaid a chaidh a cleachdadh as trice", "annual_report.summary.most_used_hashtag.most_used_hashtag": "an taga hais a chaidh a cleachdadh as trice", + "annual_report.summary.most_used_hashtag.used_count": "Ghabh thu a-staigh an taga hais seo ann an {count, plural, one {# phost} two {# phost} few {# postaichean} other {# post}}.", + "annual_report.summary.most_used_hashtag.used_count_public": "Ghabh {name} a-staigh an taga hais seo ann an {count, plural, one {# phost} two {# phost} few {# postaichean} other {# post}}.", "annual_report.summary.new_posts.new_posts": "postaichean ùra", "annual_report.summary.percentile.text": "Tha thu am measgdhen luchd-cleachdaidh as cliùitiche air {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Ainmeil ’nad latha ’s ’nad linn.", + "annual_report.summary.share_elsewhere": "Co-roinn am badeigin eile", + "annual_report.summary.share_message": "’S e {archetype} am prìomh-choltas a th’ orm!", + "annual_report.summary.share_on_mastodon": "Co-roinn air Mastodon", "attachments_list.unprocessed": "(gun phròiseasadh)", "audio.hide": "Falaich an fhuaim", "block_modal.remote_users_caveat": "Iarraidh sinn air an fhrithealaiche {domain} gun gèill iad ri do cho-dhùnadh. Gidheadh, chan eil barantas gun gèill iad on a làimhsicheas cuid a fhrithealaichean bacaidhean air dòigh eadar-dhealaichte. Dh’fhaoidte gum faic daoine gun chlàradh a-steach na postaichean poblach agad fhathast.", @@ -162,7 +203,7 @@ "column.favourites": "Annsachdan", "column.firehose": "An saoghal beò", "column.firehose_local": "Loidhne-ama bheò an fhrithealaiche seo", - "column.firehose_singular": "Loidhne-ama bheò beò", + "column.firehose_singular": "Loidhne-ama bheò", "column.follow_requests": "Iarrtasan leantainn", "column.home": "Dachaigh", "column.list_members": "Stiùir buill na liosta", @@ -343,6 +384,7 @@ "empty_column.notification_requests": "Glan! Chan eil dad an-seo. Nuair a gheibh thu brathan ùra, nochdaidh iad an-seo a-rèir nan roghainnean agad.", "empty_column.notifications": "Cha d’ fhuair thu brath sam bith fhathast. Nuair a nì càch conaltradh leat, chì thu an-seo e.", "empty_column.public": "Chan eil dad an-seo! Sgrìobh rudeigin gu poblach no lean càch o fhrithealaichean eile a làimh airson seo a lìonadh", + "error.no_hashtag_feed_access": "Cruthaich cunntas no clàraich a-steach airson an taga hais seo a shealltainn is leantainn.", "error.unexpected_crash.explanation": "Air sàilleibh buga sa chòd againn no duilgheadas co-chòrdalachd leis a’ bhrabhsair, chan urrainn dhuinn an duilleag seo a shealltainn mar bu chòir.", "error.unexpected_crash.explanation_addons": "Cha b’ urrainn dhuinn an duilleag seo a shealltainn mar bu chòir. Tha sinn an dùil gu do dh’adhbharaich tuilleadan a’ bhrabhsair no inneal eadar-theangachaidh fèin-obrachail a’ mhearachd.", "error.unexpected_crash.next_steps": "Feuch an ath-nuadhaich thu an duilleag seo. Mura cuidich sin, dh’fhaoidte gur urrainn dhut Mastodon a chleachdadh fhathast le brabhsair eile no le aplacaid thùsail.", @@ -399,6 +441,8 @@ "follow_suggestions.who_to_follow": "Molaidhean leantainn", "followed_tags": "Tagaichean hais ’gan leantainn", "footer.about": "Mu dhèidhinn", + "footer.about_mastodon": "Mu Mhastodon", + "footer.about_server": "Mu {domain}", "footer.about_this_server": "Mu dhèidhinn", "footer.directory": "Eòlaire nam pròifil", "footer.get_app": "Faigh an aplacaid", @@ -473,6 +517,7 @@ "keyboard_shortcuts.column": "Cuir am fòcas air colbh", "keyboard_shortcuts.compose": "Cuir am fòcas air raon teacsa an sgrìobhaidh", "keyboard_shortcuts.description": "Tuairisgeul", + "keyboard_shortcuts.direct": "Fosgail colbh nan iomraidhean prìobhaideach", "keyboard_shortcuts.down": "Gluais sìos air an liosta", "keyboard_shortcuts.enter": "Fosgail post", "keyboard_shortcuts.favourite": "Cuir am post ris na h-annsachdan", @@ -500,6 +545,7 @@ "keyboard_shortcuts.toggle_hidden": "Seall/Falaich an teacsa fo rabhadh susbainte", "keyboard_shortcuts.toggle_sensitivity": "Seall/Falaich na meadhanan", "keyboard_shortcuts.toot": "Tòisich air post ùr", + "keyboard_shortcuts.top": "Gluais gu bàrr na liosta", "keyboard_shortcuts.translate": "airson post eadar-theangachadh", "keyboard_shortcuts.unfocus": "Thoir am fòcas far raon teacsa an sgrìobhaidh/an luirg", "keyboard_shortcuts.up": "Gluais suas air an liosta", @@ -888,6 +934,7 @@ "status.edited_x_times": "Chaidh a dheasachadh {count, plural, one {{count} turas} two {{count} thuras} few {{count} tursan} other {{count} turas}}", "status.embed": "Faigh còd leabachaidh", "status.favourite": "Cuir ris na h-annsachdan", + "status.favourites_count": "{count, plural, one {{counter} annsachd} two {{counter} annsachd} few {{counter} annsachdan} other {{counter} annsachd}}", "status.filter": "Criathraich am post seo", "status.history.created": "Chruthaich {name} {date} e", "status.history.edited": "Dheasaich {name} {date} e", @@ -922,12 +969,14 @@ "status.quotes.empty": "Chan deach am post seo a luaidh le duine sam bith fhathast. Nuair a luaidheas cuideigin e, nochdaidh iad an-seo.", "status.quotes.local_other_disclaimer": "Cha tèid luaidhean a dhiùilt an ùghdar a shealltainn.", "status.quotes.remote_other_disclaimer": "Cha dèid ach luaidhean o {domain} a shealltainn an-seo le cinnt. Cha dèid luaidhean a dhiùilt an ùghdar a shealltainn.", + "status.quotes_count": "{count, plural, one {{counter} luaidh} two {{counter} luaidh} few {{counter} luaidhean} other {{counter} luaidh}}", "status.read_more": "Leugh an còrr", "status.reblog": "Brosnaich", "status.reblog_or_quote": "Brosnaich no luaidh", "status.reblog_private": "Co-roinn leis an luchd-leantainn agad a-rithist", "status.reblogged_by": "’Ga bhrosnachadh le {name}", "status.reblogs.empty": "Chan deach am post seo a bhrosnachadh le duine sam bith fhathast. Nuair a bhrosnaicheas cuideigin e, nochdaidh iad an-seo.", + "status.reblogs_count": "{count, plural, one {{counter} bhrosnachadh} two {{counter} bhrosnachadh} few {{counter} brosnachaidhean} other {{counter} brosnachadh}}", "status.redraft": "Sguab às ⁊ dèan dreachd ùr", "status.remove_bookmark": "Thoir an comharra-lìn air falbh", "status.remove_favourite": "Thoir air falbh o na h-annsachdan", diff --git a/config/locales/activerecord.gd.yml b/config/locales/activerecord.gd.yml index 6b068d04dc6..572fcc83df5 100644 --- a/config/locales/activerecord.gd.yml +++ b/config/locales/activerecord.gd.yml @@ -32,6 +32,12 @@ gd: attributes: url: invalid: "– chan eil seo ’na URL dligheach" + collection: + attributes: + collection_items: + too_many: "– tha cus dhiubh ann, chan eil còrr is %{count} ceadaichte" + tag: + unusable: "– chan fhaodar seo a chleachdadh" doorkeeper/application: attributes: website: diff --git a/config/locales/gd.yml b/config/locales/gd.yml index b637a9eb71f..8f64ee1984b 100644 --- a/config/locales/gd.yml +++ b/config/locales/gd.yml @@ -7,6 +7,8 @@ gd: hosted_on: Mastodon ’ga òstadh air %{domain} title: Mu dhèidhinn accounts: + errors: + cannot_be_added_to_collections: Cha ghabh an cunntas seo a chur ri cruinneachadh. followers: few: Luchd-leantainn one: Neach-leantainn @@ -874,6 +876,7 @@ gd: publish_statistics: Foillsich an stadastaireachd title: Rùrachadh trends: Treandaichean + wrapstodon: Wrapstodon domain_blocks: all: Dhan a h-uile duine disabled: Na seall idir @@ -1354,6 +1357,13 @@ gd: hint_html: "Gliocas: Chan iarr sinn am facal-faire agad ort a-rithist fad uair a thìde." invalid_password: Facal-faire mì-dhligheach prompt: Dearbh am facal-faire airson leantainn air adhart + color_scheme: + auto: Fèin-obrachail + dark: Dorcha + light: Soilleir + contrast: + auto: Fèin-obrachail + high: Àrd crypto: errors: invalid_key: "– chan e iuchair Ed25519 no Curve25519 dhligheach a th’ ann" @@ -1784,16 +1794,22 @@ gd: body: 'Thug %{name} iomradh ort an-seo:' subject: Thug %{name} iomradh ort title: Iomradh ùr + moderation_warning: + subject: Fhuair thu rabhadh on mhaorsainneachd poll: subject: Thàinig cunntas-bheachd le %{name} gu crìoch quote: body: 'Chaidh post a luaidh le %{name}:' subject: Luaidh %{name} am post agad title: Luaidh ùr + quoted_update: + subject: Dheasaich %{name} post a luaidh thu reblog: body: 'Chaidh am post agad a bhrosnachadh le %{name}:' subject: Bhrosnaich %{name} am post agad title: Brosnachadh ùr + severed_relationships: + subject: Chaill thu dàimhean ri linn co-dhùnadh na maorsainneachd status: subject: Tha %{name} air post a sgrìobhadh update: @@ -2043,9 +2059,11 @@ gd: enabled: Sguab às seann-phostaichean gu fèin-obrachail enabled_hint: Sguabaidh seo às na seann-phostaichean agad gu fèin-obrachail nuair a ruigeas iad stairsneach aoise sònraichte ach ma fhreagras iad ri gin dhe na h-eisgeachdan gu h-ìosal exceptions: Eisgeachdan + explanation: Tha prìomhachas ìosal air an sguabadh às fhèin-obrachail. Dh’fhaoidte gum bi dàil eadar ruigsinn stairsneach na h-aoise agus an toirt air falbh. ignore_favs: Leig seachad na h-annsachdan ignore_reblogs: Leig seachad na brosnachaidhean interaction_exceptions: Eisgeachdan stèidhichte air eadar-ghnìomhan + interaction_exceptions_explanation: Faodaidh postaichean mairsinn ma thèid iad thar stairsneach nan annsachdan no brosnachaidhean rè seal fiù ma bhios an cunntas as ìsle an uairsin. keep_direct: Cùm na teachdaireachdan dìreach keep_direct_hint: Cha dèid gin dhe na teachdaireachdan dìreach agad a sguabadh às keep_media: Cùm postaichean le ceanglachan meadhain @@ -2260,8 +2278,12 @@ gd: error: Bha duilgheadas ann le bhith a’ sguabadh às an iuchair tèarainteachd agad. Feuch ris a-rithist. success: Chaidh an iuchair tèarainteachd agad a sguabadh às. invalid_credential: Iuchair tèarainteachd mì-dhligheach + nickname: Far-ainm nickname_hint: Cuir a-steach far-ainm na h-iuchrach tèarainteachd ùir agad not_enabled: Cha do chuir thu WebAuthn an comas fhathast not_supported: Cha chuir am brabhsair seo taic ri iuchraichean tèarainteachd otp_required: Mus cleachd thu iuchraichean tèarainteachd, feumaidh tu an dearbhadh dà-cheumnach a chur an comas. registered_on: Air a chlàradh %{date} + wrapstodon: + description: Seall mar a chleachd %{name} Mastodon am bliadhna! + title: Wrapstodon %{year} dha %{name} diff --git a/config/locales/simple_form.gd.yml b/config/locales/simple_form.gd.yml index e11b300b33a..2673988df10 100644 --- a/config/locales/simple_form.gd.yml +++ b/config/locales/simple_form.gd.yml @@ -88,6 +88,7 @@ gd: activity_api_enabled: Cunntasan nam postaichean a chaidh fhoillseachadh gu h-ionadail, nan cleachdaichean gnìomhach ’s nan clàraidhean ùra an am bucaidean seachdaineil app_icon: WEBP, PNG, GIF no JPG. Tar-àithnidh seo ìomhaigheag bhunaiteach na h-aplacaid air uidheaman mobile le ìomhaigheag ghnàthaichte. backups_retention_period: "’S urrainn do chleachdaichean tasg-lannan dhe na postaichean aca a gintinn airson an luchdadh a-nuas an uairsin. Nuair a bhios luach dearbh air, thèid na tasg-lannan a sguabadh às on stòras agad gu fèin-obrachail às dèidh an àireamh de làithean a shònraich thu." + bootstrap_timeline_accounts: Thèid na cunntasan seo a phrìneachadh air bàrr nam molaidhean leantainn dhan luchd-cleachdaidh ùr. Solar liosta de chunntasan sgaraichte le cromagan. closed_registrations_message: Thèid seo a shealltainn nuair a bhios an clàradh dùinte content_cache_retention_period: Thèid a h-uile post o fhrithealaiche sam bith eile (a’ gabhail a-staigh brosnachaidhean is freagairtean) a sguabadh às às dèidh na h-àireimh de làithean a shònraich thu ’s gun diù a chon air eadar-ghabhail ionadail air na postaichean ud. Gabhaidh seo a-steach na postaichean a chuir cleachdaiche ionadail ris na h-annsachdan aca no comharran-lìn riutha. Thèid iomraidhean prìobhaideach eadar cleachdaichean o ionstansan diofraichte air chall cuideachd agus cha ghabh an aiseag idir. Tha an roghainn seo do dh’ionstansan sònraichte a-mhàin agus briseadh e dùilean an luchd-cleachdaidh nuair a rachadh a chleachdadh gu coitcheann. custom_css: "’S urrainn dhut stoidhlean gnàthaichte a chur an sàs air an tionndadh-lìn de Mhastodon." @@ -110,6 +111,7 @@ gd: thumbnail: Dealbh mu 2:1 a thèid a shealltainn ri taobh fiosrachadh an fhrithealaiche agad. trendable_by_default: Geàrr leum thar lèirmheas a làimh na susbainte a’ treandadh. Gabhaidh nithean fa leth a thoirt far nan treandaichean fhathast an uairsin. trends: Seallaidh na treandaichean na postaichean, tagaichean hais is naidheachdan a tha fèill mhòr orra air an fhrithealaiche agad. + wrapstodon: Tairg gintinn geàrr-chunntais àbhaich air mar a chleachd iad Mastodon rè a’ bhliadhna dhan luchd-cleachdaidh ionadail. Bidh an gleus seo ri fhaighinn eadar an 10mh is 31mh dhen Dùbhlachd gach bliadhna ’s thèid a thairgsinn dhan luchd-cleachdaidh a rinn co-dhiù aon post poblach no sàmhach ’s a chleachd co-dhiù aon taga hais rè a’ bhliadhna. form_challenge: current_password: Tha thu a’ tighinn a-steach gu raon tèarainte imports: @@ -239,6 +241,8 @@ gd: setting_always_send_emails: Cuir brathan puist-d an-còmhnaidh setting_auto_play_gif: Cluich GIFs beòthaichte gu fèin-obrachail setting_boost_modal: Smachd air faicsinneachd nam brosnachaidhean + setting_color_scheme: Modh + setting_contrast: Iomsgaradh setting_default_language: Cànan postaidh setting_default_privacy: Faicsinneachd nam post setting_default_quote_policy: Cò dh’fhaodas luaidh @@ -313,6 +317,7 @@ gd: thumbnail: Dealbhag an fhrithealaiche trendable_by_default: Ceadaich treandaichean gun lèirmheas ro làimh trends: Cuir na treandaichean an comas + wrapstodon: Cuir Wrapstodon an comas interactions: must_be_follower: Bac na brathan nach eil o luchd-leantainn must_be_following: Bac na brathan o dhaoine nach lean thu @@ -373,7 +378,9 @@ gd: jurisdiction: Uachdranas laghail min_age: An aois as lugha user: + date_of_birth_1i: Bliadhna date_of_birth_2i: Mìos + date_of_birth_3i: Latha role: Dreuchd time_zone: Roinn-tìde user_role: From c21884920409cbc691735bc55807507649479275 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 14 Jan 2026 11:08:29 +0100 Subject: [PATCH 34/89] Add collection endpoint (#37468) --- .../activitypub/collections_controller.rb | 2 + app/controllers/collections_controller.rb | 46 ++++++ app/lib/activitypub/tag_manager.rb | 4 +- app/policies/collection_policy.rb | 20 ++- config/routes.rb | 4 +- spec/lib/activitypub/tag_manager_spec.rb | 2 +- spec/policies/collection_policy_spec.rb | 14 +- spec/requests/activitypub/collections_spec.rb | 13 +- spec/requests/collections_spec.rb | 138 ++++++++++++++++++ 9 files changed, 223 insertions(+), 20 deletions(-) create mode 100644 app/controllers/collections_controller.rb create mode 100644 spec/requests/collections_spec.rb diff --git a/app/controllers/activitypub/collections_controller.rb b/app/controllers/activitypub/collections_controller.rb index c80db3500de..a03f424e0f1 100644 --- a/app/controllers/activitypub/collections_controller.rb +++ b/app/controllers/activitypub/collections_controller.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true class ActivityPub::CollectionsController < ActivityPub::BaseController + SUPPORTED_COLLECTIONS = %w(featured tags).freeze + vary_by -> { 'Signature' if authorized_fetch_mode? } before_action :require_account_signature!, if: :authorized_fetch_mode? diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb new file mode 100644 index 00000000000..3e2ba714702 --- /dev/null +++ b/app/controllers/collections_controller.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +class CollectionsController < ApplicationController + include WebAppControllerConcern + include SignatureAuthentication + include Authorization + include AccountOwnedConcern + + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + + before_action :check_feature_enabled + before_action :require_account_signature!, only: :show, if: -> { request.format == :json && authorized_fetch_mode? } + before_action :set_collection + + skip_around_action :set_locale, if: -> { request.format == :json } + skip_before_action :require_functional!, only: :show, unless: :limited_federation_mode? + + def show + respond_to do |format| + # TODO: format.html + + format.json do + expires_in expiration_duration, public: true if public_fetch_mode? + render_with_cache json: @collection, content_type: 'application/activity+json', serializer: ActivityPub::FeaturedCollectionSerializer, adapter: ActivityPub::Adapter + end + end + end + + private + + def set_collection + @collection = @account.collections.find(params[:id]) + authorize @collection, :show? + rescue ActiveRecord::RecordNotFound, Mastodon::NotPermittedError + not_found + end + + def expiration_duration + recently_updated = @collection.updated_at > 15.minutes.ago + recently_updated ? 30.seconds : 5.minutes + end + + def check_feature_enabled + raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? + end +end diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index 9f01b005780..e6714c51abb 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -63,7 +63,7 @@ class ActivityPub::TagManager when :flag target.uri when :featured_collection - ap_account_featured_collection_url(target.account.id, target) + ap_account_collection_url(target.account.id, target) end end @@ -135,7 +135,7 @@ class ActivityPub::TagManager def collection_uri_for(target, ...) raise ArgumentError, 'target must be a local account' unless target.local? - target.numeric_ap_id? ? ap_account_collection_url(target.id, ...) : account_collection_url(target, ...) + target.numeric_ap_id? ? ap_account_actor_collection_url(target.id, ...) : account_actor_collection_url(target, ...) end def inbox_uri_for(target) diff --git a/app/policies/collection_policy.rb b/app/policies/collection_policy.rb index 12adfbcad1a..70a869d16ad 100644 --- a/app/policies/collection_policy.rb +++ b/app/policies/collection_policy.rb @@ -6,7 +6,7 @@ class CollectionPolicy < ApplicationPolicy end def show? - true + current_account.nil? || (!owner_blocking? && !owner_blocking_domain?) end def create? @@ -24,6 +24,22 @@ class CollectionPolicy < ApplicationPolicy private def owner? - current_account == record.account + current_account == owner + end + + def owner_blocking_domain? + return false if current_account.nil? || current_account.domain.nil? + + owner.domain_blocking?(current_account.domain) + end + + def owner_blocking? + return false if current_account.nil? + + current_account.blocked_by?(owner) + end + + def owner + record.account end end diff --git a/config/routes.rb b/config/routes.rb index ff79c758fb3..b3338a725eb 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -96,14 +96,14 @@ Rails.application.routes.draw do get '/authorize_follow', to: redirect { |_, request| "/authorize_interaction?#{request.params.to_query}" } concern :account_resources do - resources :featured_collections, only: [:show] + resources :collections, only: [:show], constraints: { id: /\d+/ } resources :followers, only: [:index], controller: :follower_accounts resources :following, only: [:index], controller: :following_accounts scope module: :activitypub do resource :outbox, only: [:show] resource :inbox, only: [:create] - resources :collections, only: [:show] + resources :collections, only: [:show], as: :actor_collections, constraints: { id: Regexp.union(ActivityPub::CollectionsController::SUPPORTED_COLLECTIONS) } resource :followers_synchronization, only: [:show] resources :quote_authorizations, only: [:show] end diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index bacbb3251cc..55e54ede5e1 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -198,7 +198,7 @@ RSpec.describe ActivityPub::TagManager do it 'returns a string starting with web domain and with the expected path' do expect(subject.uri_for(collection)) - .to eq("#{host_prefix}/ap/users/#{collection.account.id}/featured_collections/#{collection.id}") + .to eq("#{host_prefix}/ap/users/#{collection.account.id}/collections/#{collection.id}") end end diff --git a/spec/policies/collection_policy_spec.rb b/spec/policies/collection_policy_spec.rb index 156e1a76572..ecef6e899d9 100644 --- a/spec/policies/collection_policy_spec.rb +++ b/spec/policies/collection_policy_spec.rb @@ -16,11 +16,23 @@ RSpec.describe CollectionPolicy do end permissions :show? do - it 'permits everyone to show' do + it 'permits when no user is given' do expect(policy).to permit(nil, collection) + end + + it 'permits unblocked users' do expect(policy).to permit(owner, collection) expect(policy).to permit(other_user, collection) end + + it 'denies blocked users' do + domain_blocked_user = Fabricate(:remote_account) + owner.block_domain!(domain_blocked_user.domain) + owner.block!(other_user) + + expect(policy).to_not permit(domain_blocked_user, collection) + expect(policy).to_not permit(other_user, collection) + end end permissions :create? do diff --git a/spec/requests/activitypub/collections_spec.rb b/spec/requests/activitypub/collections_spec.rb index d2761f98ea3..39bd2252e78 100644 --- a/spec/requests/activitypub/collections_spec.rb +++ b/spec/requests/activitypub/collections_spec.rb @@ -14,7 +14,7 @@ RSpec.describe 'ActivityPub Collections' do end describe 'GET #show' do - subject { get account_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account } + subject { get account_actor_collection_path(id: id, account_username: account.username), headers: nil, sign_with: remote_account } context 'when id is "featured"' do let(:id) { 'featured' } @@ -131,16 +131,5 @@ RSpec.describe 'ActivityPub Collections' do end end end - - context 'when id is not "featured"' do - let(:id) { 'hoge' } - - it 'returns http not found' do - subject - - expect(response) - .to have_http_status(404) - end - end end end diff --git a/spec/requests/collections_spec.rb b/spec/requests/collections_spec.rb new file mode 100644 index 00000000000..fece4b62b82 --- /dev/null +++ b/spec/requests/collections_spec.rb @@ -0,0 +1,138 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Collections' do + describe 'GET /@:account_username/collections/:id', feature: :collections do + subject { get account_collection_path(account, collection, format: :json) } + + let(:collection) { Fabricate(:collection) } + let(:account) { collection.account } + + context 'when signed out' do + context 'when account is permanently suspended' do + before do + account.suspend! + account.deletion_request.destroy + end + + it 'returns http gone' do + subject + + expect(response) + .to have_http_status(410) + end + end + + context 'when account is temporarily suspended' do + before { account.suspend! } + + it 'returns http forbidden' do + subject + + expect(response) + .to have_http_status(403) + end + end + + context 'when account is accessible' do + context 'with JSON' do + subject { get ap_account_collection_path(account.id, collection, format: :json) } + + it 'renders ActivityPub FeaturedCollection object successfully', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie') + + expect(response.headers).to include( + 'Content-Type' => include('application/activity+json') + ) + expect(response.parsed_body) + .to include({ + 'type' => 'FeaturedCollection', + 'name' => collection.name, + }) + end + end + end + end + + context 'when signed in' do + let(:user) { Fabricate(:user) } + + before do + post user_session_path, params: { user: { email: user.email, password: user.password } } + end + + context 'when account blocks user' do + before { account.block!(user.account) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + end + + context 'with "HTTP Signature" access signed by a remote account' do + subject do + get account_collection_path(account, collection, format: :json), + headers: nil, + sign_with: remote_account + end + + let(:remote_account) { Fabricate(:account, domain: 'host.example') } + + context 'when account blocks the remote account' do + before { account.block!(remote_account) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + + context 'when account domain blocks the domain of the remote account' do + before { account.block_domain!(remote_account.domain) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + + context 'with JSON' do + subject do + get ap_account_collection_path(account.id, collection, format: :json), + headers: nil, + sign_with: remote_account + end + + it 'renders ActivityPub FeaturedCollection object successfully', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie') + + expect(response.headers).to include( + 'Content-Type' => include('application/activity+json') + ) + expect(response.parsed_body) + .to include({ + 'type' => 'FeaturedCollection', + 'name' => collection.name, + }) + end + end + end + end +end From 8390f0dbbf5e809ad67a0d41cb2e84e4bb2c4754 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:10:52 +0100 Subject: [PATCH 35/89] Update dependency rqrcode to v3.2.0 (#37431) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index e7519ea90bc..d2feffceafc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -720,10 +720,10 @@ GEM rotp (6.3.0) rouge (4.7.0) rpam2 (4.0.2) - rqrcode (3.1.1) + rqrcode (3.2.0) chunky_png (~> 1.0) rqrcode_core (~> 2.0) - rqrcode_core (2.0.1) + rqrcode_core (2.1.0) rspec (3.13.2) rspec-core (~> 3.13.0) rspec-expectations (~> 3.13.0) From d612119b3ec564e0d6c3547d0b2cf07ab08560ea Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:17:40 +0100 Subject: [PATCH 36/89] Update dependency thor to v1.5.0 (#37406) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Gemfile.lock b/Gemfile.lock index d2feffceafc..a54c979764f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -860,7 +860,7 @@ GEM terrapin (1.1.1) climate_control test-prof (1.5.0) - thor (1.4.0) + thor (1.5.0) tilt (2.6.1) timeout (0.6.0) tpm-key_attestation (0.14.1) From 6fdef1191a9e88591dec786e767f800b28df8d02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 11:35:40 +0100 Subject: [PATCH 37/89] Update dependency globals to v17 (#37360) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- streaming/package.json | 2 +- yarn.lock | 12 ++++++------ 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package.json b/package.json index 8a1ecc77374..73eb15b74ee 100644 --- a/package.json +++ b/package.json @@ -175,7 +175,7 @@ "eslint-plugin-react-hooks": "^7.0.1", "eslint-plugin-storybook": "^10.0.2", "fake-indexeddb": "^6.0.1", - "globals": "^16.0.0", + "globals": "^17.0.0", "husky": "^9.0.11", "lint-staged": "^16.2.6", "msw": "^2.12.1", diff --git a/streaming/package.json b/streaming/package.json index 0f6651b741a..7684ed7cc85 100644 --- a/streaming/package.json +++ b/streaming/package.json @@ -36,7 +36,7 @@ "@types/express": "^5.0.5", "@types/pg": "^8.6.6", "@types/ws": "^8.5.9", - "globals": "^16.0.0", + "globals": "^17.0.0", "pino-pretty": "^13.0.0", "typescript": "~5.9.0", "typescript-eslint": "^8.28.0" diff --git a/yarn.lock b/yarn.lock index 424b0d623fd..9ae3614dee7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3039,7 +3039,7 @@ __metadata: fake-indexeddb: "npm:^6.0.1" fast-glob: "npm:^3.3.3" fuzzysort: "npm:^3.0.0" - globals: "npm:^16.0.0" + globals: "npm:^17.0.0" history: "npm:^4.10.1" hoist-non-react-statics: "npm:^3.3.2" http-link-header: "npm:^1.1.1" @@ -3131,7 +3131,7 @@ __metadata: cors: "npm:^2.8.5" dotenv: "npm:^16.0.3" express: "npm:^5.1.0" - globals: "npm:^16.0.0" + globals: "npm:^17.0.0" ioredis: "npm:^5.3.2" jsdom: "npm:^27.0.0" pg: "npm:^8.5.0" @@ -8275,10 +8275,10 @@ __metadata: languageName: node linkType: hard -"globals@npm:^16.0.0": - version: 16.5.0 - resolution: "globals@npm:16.5.0" - checksum: 10c0/615241dae7851c8012f5aa0223005b1ed6607713d6813de0741768bd4ddc39353117648f1a7086b4b0fa45eae733f1c0a0fe369aa4e543bb63f8de8990178ea9 +"globals@npm:^17.0.0": + version: 17.0.0 + resolution: "globals@npm:17.0.0" + checksum: 10c0/e3c169fdcb0fc6755707b697afb367bea483eb29992cfc0de1637382eb893146e17f8f96db6d7453e3696b478a7863ae2000e6c71cd2f4061410285106d3847a languageName: node linkType: hard From 5d03d5d15c1bf91845241dbe77c8b71d4c1eb2c1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:37:05 +0000 Subject: [PATCH 38/89] Update dependency libvips to v8.18.0 (#37282) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 865d14402cd..b9dcbe59fd2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -183,7 +183,7 @@ FROM build AS libvips # libvips version to compile, change with [--build-arg VIPS_VERSION="8.15.2"] # renovate: datasource=github-releases depName=libvips packageName=libvips/libvips -ARG VIPS_VERSION=8.17.3 +ARG VIPS_VERSION=8.18.0 # libvips download URL, change with [--build-arg VIPS_URL="https://github.com/libvips/libvips/releases/download"] ARG VIPS_URL=https://github.com/libvips/libvips/releases/download From 4bdc9d5e1e4af9299867c4c3d700371349181a94 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 14 Jan 2026 10:37:43 +0000 Subject: [PATCH 39/89] Update dependency vite to v7.3.1 (#37248) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 271 +----------------------------------------------------- 1 file changed, 5 insertions(+), 266 deletions(-) diff --git a/yarn.lock b/yarn.lock index 9ae3614dee7..4a8e1fa07f6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2095,13 +2095,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/aix-ppc64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/aix-ppc64@npm:0.25.5" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/aix-ppc64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/aix-ppc64@npm:0.27.2" @@ -2109,13 +2102,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/android-arm64@npm:0.25.5" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/android-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/android-arm64@npm:0.27.2" @@ -2123,13 +2109,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-arm@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/android-arm@npm:0.25.5" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - "@esbuild/android-arm@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/android-arm@npm:0.27.2" @@ -2137,13 +2116,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/android-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/android-x64@npm:0.25.5" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - "@esbuild/android-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/android-x64@npm:0.27.2" @@ -2151,13 +2123,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/darwin-arm64@npm:0.25.5" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/darwin-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/darwin-arm64@npm:0.27.2" @@ -2165,13 +2130,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/darwin-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/darwin-x64@npm:0.25.5" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - "@esbuild/darwin-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/darwin-x64@npm:0.27.2" @@ -2179,13 +2137,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/freebsd-arm64@npm:0.25.5" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/freebsd-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/freebsd-arm64@npm:0.27.2" @@ -2193,13 +2144,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/freebsd-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/freebsd-x64@npm:0.25.5" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/freebsd-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/freebsd-x64@npm:0.27.2" @@ -2207,13 +2151,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-arm64@npm:0.25.5" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/linux-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-arm64@npm:0.27.2" @@ -2221,13 +2158,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-arm@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-arm@npm:0.25.5" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - "@esbuild/linux-arm@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-arm@npm:0.27.2" @@ -2235,13 +2165,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ia32@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-ia32@npm:0.25.5" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/linux-ia32@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-ia32@npm:0.27.2" @@ -2249,13 +2172,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-loong64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-loong64@npm:0.25.5" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - "@esbuild/linux-loong64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-loong64@npm:0.27.2" @@ -2263,13 +2179,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-mips64el@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-mips64el@npm:0.25.5" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - "@esbuild/linux-mips64el@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-mips64el@npm:0.27.2" @@ -2277,13 +2186,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-ppc64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-ppc64@npm:0.25.5" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - "@esbuild/linux-ppc64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-ppc64@npm:0.27.2" @@ -2291,13 +2193,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-riscv64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-riscv64@npm:0.25.5" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - "@esbuild/linux-riscv64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-riscv64@npm:0.27.2" @@ -2305,13 +2200,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-s390x@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-s390x@npm:0.25.5" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - "@esbuild/linux-s390x@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-s390x@npm:0.27.2" @@ -2319,13 +2207,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/linux-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/linux-x64@npm:0.25.5" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - "@esbuild/linux-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/linux-x64@npm:0.27.2" @@ -2333,13 +2214,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/netbsd-arm64@npm:0.25.5" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/netbsd-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/netbsd-arm64@npm:0.27.2" @@ -2347,13 +2221,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/netbsd-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/netbsd-x64@npm:0.25.5" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/netbsd-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/netbsd-x64@npm:0.27.2" @@ -2361,13 +2228,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/openbsd-arm64@npm:0.25.5" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/openbsd-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/openbsd-arm64@npm:0.27.2" @@ -2375,13 +2235,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/openbsd-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/openbsd-x64@npm:0.25.5" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - "@esbuild/openbsd-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/openbsd-x64@npm:0.27.2" @@ -2396,13 +2249,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/sunos-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/sunos-x64@npm:0.25.5" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - "@esbuild/sunos-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/sunos-x64@npm:0.27.2" @@ -2410,13 +2256,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-arm64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/win32-arm64@npm:0.25.5" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - "@esbuild/win32-arm64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/win32-arm64@npm:0.27.2" @@ -2424,13 +2263,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-ia32@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/win32-ia32@npm:0.25.5" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - "@esbuild/win32-ia32@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/win32-ia32@npm:0.27.2" @@ -2438,13 +2270,6 @@ __metadata: languageName: node linkType: hard -"@esbuild/win32-x64@npm:0.25.5": - version: 0.25.5 - resolution: "@esbuild/win32-x64@npm:0.25.5" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - "@esbuild/win32-x64@npm:0.27.2": version: 0.27.2 resolution: "@esbuild/win32-x64@npm:0.27.2" @@ -7118,7 +6943,7 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0": +"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0, esbuild@npm:^0.27.0": version: 0.27.2 resolution: "esbuild@npm:0.27.2" dependencies: @@ -7207,92 +7032,6 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.25.0": - version: 0.25.5 - resolution: "esbuild@npm:0.25.5" - dependencies: - "@esbuild/aix-ppc64": "npm:0.25.5" - "@esbuild/android-arm": "npm:0.25.5" - "@esbuild/android-arm64": "npm:0.25.5" - "@esbuild/android-x64": "npm:0.25.5" - "@esbuild/darwin-arm64": "npm:0.25.5" - "@esbuild/darwin-x64": "npm:0.25.5" - "@esbuild/freebsd-arm64": "npm:0.25.5" - "@esbuild/freebsd-x64": "npm:0.25.5" - "@esbuild/linux-arm": "npm:0.25.5" - "@esbuild/linux-arm64": "npm:0.25.5" - "@esbuild/linux-ia32": "npm:0.25.5" - "@esbuild/linux-loong64": "npm:0.25.5" - "@esbuild/linux-mips64el": "npm:0.25.5" - "@esbuild/linux-ppc64": "npm:0.25.5" - "@esbuild/linux-riscv64": "npm:0.25.5" - "@esbuild/linux-s390x": "npm:0.25.5" - "@esbuild/linux-x64": "npm:0.25.5" - "@esbuild/netbsd-arm64": "npm:0.25.5" - "@esbuild/netbsd-x64": "npm:0.25.5" - "@esbuild/openbsd-arm64": "npm:0.25.5" - "@esbuild/openbsd-x64": "npm:0.25.5" - "@esbuild/sunos-x64": "npm:0.25.5" - "@esbuild/win32-arm64": "npm:0.25.5" - "@esbuild/win32-ia32": "npm:0.25.5" - "@esbuild/win32-x64": "npm:0.25.5" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-arm64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10c0/aba8cbc11927fa77562722ed5e95541ce2853f67ad7bdc40382b558abc2e0ec57d92ffb820f082ba2047b4ef9f3bc3da068cdebe30dfd3850cfa3827a78d604e - languageName: node - linkType: hard - "escalade@npm:^3.1.1, escalade@npm:^3.2.0": version: 3.2.0 resolution: "escalade@npm:3.2.0" @@ -14449,10 +14188,10 @@ __metadata: linkType: hard "vite@npm:^6.0.0 || ^7.0.0, vite@npm:^7.1.1": - version: 7.2.7 - resolution: "vite@npm:7.2.7" + version: 7.3.1 + resolution: "vite@npm:7.3.1" dependencies: - esbuild: "npm:^0.25.0" + esbuild: "npm:^0.27.0" fdir: "npm:^6.5.0" fsevents: "npm:~2.3.3" picomatch: "npm:^4.0.3" @@ -14499,7 +14238,7 @@ __metadata: optional: true bin: vite: bin/vite.js - checksum: 10c0/0c502d9eb898d9c05061dbd8fd199f280b524bbb4c12ab5f88c7b12779947386684a269e4dd0aa424aa35bcd857f1aa44aadb9ea764702a5043af433052455b5 + checksum: 10c0/5c7548f5f43a23533e53324304db4ad85f1896b1bfd3ee32ae9b866bac2933782c77b350eb2b52a02c625c8ad1ddd4c000df077419410650c982cd97fde8d014 languageName: node linkType: hard From 10de65e41c3b01bd938f97fdb60fc96c26673966 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 14 Jan 2026 11:51:23 +0100 Subject: [PATCH 40/89] Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486) --- app/lib/feed_manager.rb | 1 + 1 file changed, 1 insertion(+) diff --git a/app/lib/feed_manager.rb b/app/lib/feed_manager.rb index 9c5c306e966..ab5ee106c7e 100644 --- a/app/lib/feed_manager.rb +++ b/app/lib/feed_manager.rb @@ -450,6 +450,7 @@ class FeedManager return :filter if status.reply? && (status.in_reply_to_id.nil? || status.in_reply_to_account_id.nil?) return :skip_home if timeline_type != :list && crutches[:exclusive_list_users][status.account_id].present? return :filter if crutches[:languages][status.account_id].present? && status.language.present? && !crutches[:languages][status.account_id].include?(status.language) + return :filter if status.reblog? && status.reblog.blank? check_for_blocks = crutches[:active_mentions][status.id] || [] check_for_blocks.push(status.account_id) From 8eff59b106e1a760b92d083ba887069f2ba2866d Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:13:53 +0100 Subject: [PATCH 41/89] Update dependency httplog to '~> 1.8.0' (#37484) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile | 2 +- Gemfile.lock | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/Gemfile b/Gemfile index 3cc9580fea2..c223ca0a52c 100644 --- a/Gemfile +++ b/Gemfile @@ -55,7 +55,7 @@ gem 'hiredis-client' gem 'htmlentities', '~> 4.3' gem 'http', '~> 5.3.0' gem 'http_accept_language', '~> 2.1' -gem 'httplog', '~> 1.7.0', require: false +gem 'httplog', '~> 1.8.0', require: false gem 'i18n' gem 'idn-ruby', require: 'idn' gem 'inline_svg' diff --git a/Gemfile.lock b/Gemfile.lock index a54c979764f..ad61b51752f 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -320,7 +320,8 @@ GEM http_accept_language (2.1.1) httpclient (2.9.0) mutex_m - httplog (1.7.3) + httplog (1.8.0) + benchmark rack (>= 2.0) rainbow (>= 2.0.0) i18n (1.14.8) @@ -985,7 +986,7 @@ DEPENDENCIES htmlentities (~> 4.3) http (~> 5.3.0) http_accept_language (~> 2.1) - httplog (~> 1.7.0) + httplog (~> 1.8.0) i18n i18n-tasks (~> 1.0) idn-ruby From dd52e61c24f99a85b1200c2b793af9f10b0fc866 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:13:56 +0100 Subject: [PATCH 42/89] Update opentelemetry-ruby (non-major) (#37480) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile | 12 ++++++------ Gemfile.lock | 27 ++++++++++++++------------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/Gemfile b/Gemfile index c223ca0a52c..f5da754b1a7 100644 --- a/Gemfile +++ b/Gemfile @@ -109,12 +109,12 @@ group :opentelemetry do gem 'opentelemetry-instrumentation-active_job', '~> 0.10.0', require: false gem 'opentelemetry-instrumentation-active_model_serializers', '~> 0.24.0', require: false gem 'opentelemetry-instrumentation-concurrent_ruby', '~> 0.24.0', require: false - gem 'opentelemetry-instrumentation-excon', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-faraday', '~> 0.30.0', require: false - gem 'opentelemetry-instrumentation-http', '~> 0.27.0', require: false - gem 'opentelemetry-instrumentation-http_client', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-net_http', '~> 0.26.0', require: false - gem 'opentelemetry-instrumentation-pg', '~> 0.34.0', require: false + gem 'opentelemetry-instrumentation-excon', '~> 0.27.0', require: false + gem 'opentelemetry-instrumentation-faraday', '~> 0.31.0', require: false + gem 'opentelemetry-instrumentation-http', '~> 0.28.0', require: false + gem 'opentelemetry-instrumentation-http_client', '~> 0.27.0', require: false + gem 'opentelemetry-instrumentation-net_http', '~> 0.27.0', require: false + gem 'opentelemetry-instrumentation-pg', '~> 0.35.0', require: false gem 'opentelemetry-instrumentation-rack', '~> 0.29.0', require: false gem 'opentelemetry-instrumentation-rails', '~> 0.39.0', require: false gem 'opentelemetry-instrumentation-redis', '~> 0.28.0', require: false diff --git a/Gemfile.lock b/Gemfile.lock index ad61b51752f..f920acecbf8 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -521,7 +521,8 @@ GEM opentelemetry-semantic_conventions opentelemetry-helpers-sql (0.3.0) opentelemetry-api (~> 1.7) - opentelemetry-helpers-sql-processor (0.3.1) + opentelemetry-helpers-sql-processor (0.4.0) + opentelemetry-api (~> 1.0) opentelemetry-common (~> 0.21) opentelemetry-instrumentation-action_mailer (0.6.1) opentelemetry-instrumentation-active_support (~> 0.10) @@ -545,17 +546,17 @@ GEM opentelemetry-registry (~> 0.1) opentelemetry-instrumentation-concurrent_ruby (0.24.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-excon (0.26.1) + opentelemetry-instrumentation-excon (0.27.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-faraday (0.30.1) + opentelemetry-instrumentation-faraday (0.31.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-http (0.27.1) + opentelemetry-instrumentation-http (0.28.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-http_client (0.26.1) + opentelemetry-instrumentation-http_client (0.27.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-net_http (0.26.1) + opentelemetry-instrumentation-net_http (0.27.0) opentelemetry-instrumentation-base (~> 0.25) - opentelemetry-instrumentation-pg (0.34.1) + opentelemetry-instrumentation-pg (0.35.0) opentelemetry-helpers-sql opentelemetry-helpers-sql-processor opentelemetry-instrumentation-base (~> 0.25) @@ -1022,12 +1023,12 @@ DEPENDENCIES opentelemetry-instrumentation-active_job (~> 0.10.0) opentelemetry-instrumentation-active_model_serializers (~> 0.24.0) opentelemetry-instrumentation-concurrent_ruby (~> 0.24.0) - opentelemetry-instrumentation-excon (~> 0.26.0) - opentelemetry-instrumentation-faraday (~> 0.30.0) - opentelemetry-instrumentation-http (~> 0.27.0) - opentelemetry-instrumentation-http_client (~> 0.26.0) - opentelemetry-instrumentation-net_http (~> 0.26.0) - opentelemetry-instrumentation-pg (~> 0.34.0) + opentelemetry-instrumentation-excon (~> 0.27.0) + opentelemetry-instrumentation-faraday (~> 0.31.0) + opentelemetry-instrumentation-http (~> 0.28.0) + opentelemetry-instrumentation-http_client (~> 0.27.0) + opentelemetry-instrumentation-net_http (~> 0.27.0) + opentelemetry-instrumentation-pg (~> 0.35.0) opentelemetry-instrumentation-rack (~> 0.29.0) opentelemetry-instrumentation-rails (~> 0.39.0) opentelemetry-instrumentation-redis (~> 0.28.0) From 1482a7bf201103b10fe9c38b0ee5e82a69eba27c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:14:00 +0100 Subject: [PATCH 43/89] Update dependency postcss-preset-env to v11 (#37485) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 1463 ++++++++++++++++++++++++++------------------------ 2 files changed, 765 insertions(+), 700 deletions(-) diff --git a/package.json b/package.json index 73eb15b74ee..701719d7cff 100644 --- a/package.json +++ b/package.json @@ -85,7 +85,7 @@ "lodash": "^4.17.21", "marky": "^1.2.5", "path-complete-extname": "^1.0.0", - "postcss-preset-env": "^10.1.5", + "postcss-preset-env": "^11.0.0", "prop-types": "^15.8.1", "punycode": "^2.3.0", "react": "^18.2.0", diff --git a/yarn.lock b/yarn.lock index 4a8e1fa07f6..c93adccaa13 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1248,13 +1248,13 @@ __metadata: languageName: node linkType: hard -"@csstools/cascade-layer-name-parser@npm:^2.0.5": - version: 2.0.5 - resolution: "@csstools/cascade-layer-name-parser@npm:2.0.5" +"@csstools/cascade-layer-name-parser@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/cascade-layer-name-parser@npm:3.0.0" peerDependencies: - "@csstools/css-parser-algorithms": ^3.0.5 - "@csstools/css-tokenizer": ^3.0.4 - checksum: 10c0/b6c73d5c8132f922edc88b9df5272c93c9753945f1e1077b80d03b314076ffe03c2cc9bf6cbc85501ee7c7f27e477263df96997c9125fd2fd0cfe82fe2d7c141 + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/657b325261dfa567e26b56a1a6a00fcfc877b9e95ce8ae3595ec95c89323fc2123dad7a4ca79bede8dec475ccc41d9d528470b0be01c4f22104f6962f2bfb40c languageName: node linkType: hard @@ -1265,6 +1265,13 @@ __metadata: languageName: node linkType: hard +"@csstools/color-helpers@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/color-helpers@npm:6.0.0" + checksum: 10c0/784447fa6ba2f5fec30f8676c48f9f66bff30a7a16582e3c3b3e9aa2574df1ac4e5f8e7455dfa6d991fd84ceceb22ff4016e2ea3a8116e76772b3972dbab7bec + languageName: node + linkType: hard + "@csstools/css-calc@npm:^2.1.4": version: 2.1.4 resolution: "@csstools/css-calc@npm:2.1.4" @@ -1275,6 +1282,16 @@ __metadata: languageName: node linkType: hard +"@csstools/css-calc@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/css-calc@npm:3.0.0" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/2f062db206dcdcb561a802d791aaf8b410f0e4d91ded89eb4075f75f1eafe8f5392c431a8d602d6ae660fca6299e02fcc555efdb235f3403ffc56ac6f14a1c2b + languageName: node + linkType: hard + "@csstools/css-color-parser@npm:^3.1.0": version: 3.1.0 resolution: "@csstools/css-color-parser@npm:3.1.0" @@ -1288,6 +1305,19 @@ __metadata: languageName: node linkType: hard +"@csstools/css-color-parser@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-color-parser@npm:4.0.0" + dependencies: + "@csstools/color-helpers": "npm:^6.0.0" + "@csstools/css-calc": "npm:^3.0.0" + peerDependencies: + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/bbfa3855bd53d5f31e2e9d40d8bc7b1143c7efd3dd6fa43c0ef2222a66bdc94f7ffedc8932a4eb4f10c5cbc38d1ed110fda99240037e0a2dae61c6b25527b0a2 + languageName: node + linkType: hard + "@csstools/css-parser-algorithms@npm:^3.0.5": version: 3.0.5 resolution: "@csstools/css-parser-algorithms@npm:3.0.5" @@ -1297,6 +1327,15 @@ __metadata: languageName: node linkType: hard +"@csstools/css-parser-algorithms@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-parser-algorithms@npm:4.0.0" + peerDependencies: + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/94558c2428d6ef0ddef542e86e0a8376aa1263a12a59770abb13ba50d7b83086822c75433f32aa2e7fef00555e1cc88292f9ca5bce79aed232bb3fed73b1528d + languageName: node + linkType: hard + "@csstools/css-syntax-patches-for-csstree@npm:1.0.14": version: 1.0.14 resolution: "@csstools/css-syntax-patches-for-csstree@npm:1.0.14" @@ -1320,6 +1359,13 @@ __metadata: languageName: node linkType: hard +"@csstools/css-tokenizer@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/css-tokenizer@npm:4.0.0" + checksum: 10c0/669cf3d0f9c8e1ffdf8c9955ad8beba0c8cfe03197fe29a4fcbd9ee6f7a18856cfa42c62670021a75183d9ab37f5d14a866e6a9df753a6c07f59e36797a9ea9f + languageName: node + linkType: hard + "@csstools/media-query-list-parser@npm:^4.0.3": version: 4.0.3 resolution: "@csstools/media-query-list-parser@npm:4.0.3" @@ -1330,513 +1376,523 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-alpha-function@npm:^1.0.1": - version: 1.0.1 - resolution: "@csstools/postcss-alpha-function@npm:1.0.1" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" +"@csstools/media-query-list-parser@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/media-query-list-parser@npm:5.0.0" peerDependencies: - postcss: ^8.4 - checksum: 10c0/35ca209e572534ade21ac5c18aad702aa492eb39e2d0e475f441371063418fe9650554e6a59b1318d3a615da83ef54d9a588faa27063ecc0a568ef7290a6b488 + "@csstools/css-parser-algorithms": ^4.0.0 + "@csstools/css-tokenizer": ^4.0.0 + checksum: 10c0/dbc22654769eca02c182f3a57be02cd5b8d0b958adc8397e66770b64b0e8fcd32faa93a3f6a99e1457bde11862485de3cd83a31dac7b03925d32f9891b31ccfd languageName: node linkType: hard -"@csstools/postcss-cascade-layers@npm:^5.0.2": - version: 5.0.2 - resolution: "@csstools/postcss-cascade-layers@npm:5.0.2" - dependencies: - "@csstools/selector-specificity": "npm:^5.0.0" - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/dd8e29cfd3a93932fa35e3a59aa62fd2e720772d450f40f38f65ce1e736e2fe839635eb6f033abcc8ee8bc2856161a297f4458b352b26d2216856feb03176612 - languageName: node - linkType: hard - -"@csstools/postcss-color-function-display-p3-linear@npm:^1.0.1": - version: 1.0.1 - resolution: "@csstools/postcss-color-function-display-p3-linear@npm:1.0.1" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/d02d45410c9257f5620c766f861f8fa3762b74ef01fdba8060b33a4c98f929e2219cd476b25bd4181ac186158a4d99a0da555c0b6ba45a7ac4a3a5885baad1f5 - languageName: node - linkType: hard - -"@csstools/postcss-color-function@npm:^4.0.12": - version: 4.0.12 - resolution: "@csstools/postcss-color-function@npm:4.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/a355b04d90f89c8e37a4a23543151558060acc68fb2e7d1c3549bebeeae2b147eec26af1fbc6ee690f0ba4830263f2d181f5331d16d3483b5542be46996fa755 - languageName: node - linkType: hard - -"@csstools/postcss-color-mix-function@npm:^3.0.12": - version: 3.0.12 - resolution: "@csstools/postcss-color-mix-function@npm:3.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/3e98a5118852083d1f87a3f842f78088192b1f9f08fdf1f3b3ef1e8969e18fdadc1e3bcac3d113a07c8917a7e8fa65fdec55a31df9a1b726c8d7ae89db86e8e5 - languageName: node - linkType: hard - -"@csstools/postcss-color-mix-variadic-function-arguments@npm:^1.0.2": - version: 1.0.2 - resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:1.0.2" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/34073f0f0d33e4958f90763e692955a8e8c678b74284234497c4aa0d2143756e1b3616e0c09832caad498870e227ca0a681316afe3a71224fc40ade0ead1bdd9 - languageName: node - linkType: hard - -"@csstools/postcss-content-alt-text@npm:^2.0.8": - version: 2.0.8 - resolution: "@csstools/postcss-content-alt-text@npm:2.0.8" - dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/4c330cc2a1e434688a62613ecceb1434cd725ce024c1ad8d4a4c76b9839d1f3ea8566a8c6494921e2b46ec7feef6af8ed6548c216dcb8f0feab4b1d52c96228e - languageName: node - linkType: hard - -"@csstools/postcss-contrast-color-function@npm:^2.0.12": - version: 2.0.12 - resolution: "@csstools/postcss-contrast-color-function@npm:2.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/b783ce948cdf1513ee238e9115b42881a8d3e5d13c16038601b1c470d661cfaeeece4eea29904fb9fcae878bad86f766810fa798a703ab9ad4b0cf276b173f8f - languageName: node - linkType: hard - -"@csstools/postcss-exponential-functions@npm:^2.0.9": - version: 2.0.9 - resolution: "@csstools/postcss-exponential-functions@npm:2.0.9" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/78ea627a87fb23e12616c4e54150363b0e8793064634983dbe0368a0aca1ff73206c2d1f29845773daaf42787e7d1f180ce1b57c43e2b0d10da450101f9f34b6 - languageName: node - linkType: hard - -"@csstools/postcss-font-format-keywords@npm:^4.0.0": - version: 4.0.0 - resolution: "@csstools/postcss-font-format-keywords@npm:4.0.0" - dependencies: - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/eb794fb95fefcac75e606d185255e601636af177866a317b0c6b6c375055e7240be53918229fd8d4bba00df01bedd2256bdac2b0ad4a4c2ec64f9d27cd6ff639 - languageName: node - linkType: hard - -"@csstools/postcss-gamut-mapping@npm:^2.0.11": - version: 2.0.11 - resolution: "@csstools/postcss-gamut-mapping@npm:2.0.11" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/490b8ccf10e30879a4415afbdd3646e1cdac3671586b7916855cf47a536f3be75eed014396056bde6528e0cb76d904e79bad78afc0b499e837264cf22519d145 - languageName: node - linkType: hard - -"@csstools/postcss-gradients-interpolation-method@npm:^5.0.12": - version: 5.0.12 - resolution: "@csstools/postcss-gradients-interpolation-method@npm:5.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/70b3d6c7050ce882ed2281e71eb4493531ae8d55d21899920eeeb6c205d90aaf430419a66235484ccce3a1a1891367dfc0ef772f3866ae3a9d8ec5ddd0cfe894 - languageName: node - linkType: hard - -"@csstools/postcss-hwb-function@npm:^4.0.12": - version: 4.0.12 - resolution: "@csstools/postcss-hwb-function@npm:4.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/d0dac34da9d7ac654060b6b27690a419718e990b21ff3e63266ea59934a865bc6aeae8eb8e1ca3e227a8b2a208657e3ab70ccdf0437f1f09d21ab848bbffcaa2 - languageName: node - linkType: hard - -"@csstools/postcss-ic-unit@npm:^4.0.4": - version: 4.0.4 - resolution: "@csstools/postcss-ic-unit@npm:4.0.4" - dependencies: - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/20168e70ecb4abf7a69e407d653b6c7c9c82f2c7b1da0920e1d035f62b5ef8552cc7f1b62e0dca318df13c348e79fba862e1a4bb0e9432119a82b10aeb511752 - languageName: node - linkType: hard - -"@csstools/postcss-initial@npm:^2.0.1": - version: 2.0.1 - resolution: "@csstools/postcss-initial@npm:2.0.1" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/dbff7084ef4f1c4647efe2b147001daf172003c15b5e22689f0540d03c8d362f2a332cd9cf136e6c8dcda7564ee30492a4267ea188f72cb9c1000fb9bcfbfef8 - languageName: node - linkType: hard - -"@csstools/postcss-is-pseudo-class@npm:^5.0.3": - version: 5.0.3 - resolution: "@csstools/postcss-is-pseudo-class@npm:5.0.3" - dependencies: - "@csstools/selector-specificity": "npm:^5.0.0" - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/7980f1cabf32850bac72552e4e9de47412359e36e259a92b9b9af25dae4cce42bbcc5fdca8f384a589565bf383ecb23dec3af9f084d8df18b82552318b2841b6 - languageName: node - linkType: hard - -"@csstools/postcss-light-dark-function@npm:^2.0.11": - version: 2.0.11 - resolution: "@csstools/postcss-light-dark-function@npm:2.0.11" - dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/0175be41bb0044a48bc98d5c55cce41ed6b9ada88253c5f20d0ca17287cba4b429742b458ac5744675b9a286109e13ac51d64e226ab16040d7b051ba64c0c77b - languageName: node - linkType: hard - -"@csstools/postcss-logical-float-and-clear@npm:^3.0.0": - version: 3.0.0 - resolution: "@csstools/postcss-logical-float-and-clear@npm:3.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/71a20e8c37877bf68ae615d7bb93fc11b4f8da8be8b1dc1a6e0fc69e27f189712ed71436b8ed51fa69fdb98b8e6718df2b5f42f246c4d39badaf0e43020fcfd4 - languageName: node - linkType: hard - -"@csstools/postcss-logical-overflow@npm:^2.0.0": +"@csstools/postcss-alpha-function@npm:^2.0.0": version: 2.0.0 - resolution: "@csstools/postcss-logical-overflow@npm:2.0.0" + resolution: "@csstools/postcss-alpha-function@npm:2.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/0e103343d3ff8b34eef01b02355c5e010d272fd12d149a242026bb13ab1577b7f3a11fd4514be9342d96f73d61dac1f093a9bd36ece591753ed09a84eb7fca0a + checksum: 10c0/b267a4d48c132a2e0a9160a17811bf6bda09e4ff4b393d6d7643cdd8c9335a2db37d956abe8960545139cf503ed5c1c57e433456a19d7361a0f206b96291b7f7 languageName: node linkType: hard -"@csstools/postcss-logical-overscroll-behavior@npm:^2.0.0": +"@csstools/postcss-cascade-layers@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/postcss-cascade-layers@npm:6.0.0" + dependencies: + "@csstools/selector-specificity": "npm:^6.0.0" + postcss-selector-parser: "npm:^7.1.1" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/f9cf3fa52d0f2615a69ad52067cc2229573151525792272eb82a28f0feca64f9ca54e8459c0ae5c254807507e4630aa9866690a4b22e57250c353ee28db86a50 + languageName: node + linkType: hard + +"@csstools/postcss-color-function-display-p3-linear@npm:^2.0.0": version: 2.0.0 - resolution: "@csstools/postcss-logical-overscroll-behavior@npm:2.0.0" + resolution: "@csstools/postcss-color-function-display-p3-linear@npm:2.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/1649601bb26f04d760fb5ebc42cdf414fa2a380b8ec22fe1c117f664c286665a786bd7bbda01b7e7567eaf3cc018a4f36a5c9805f6751cc497da223e0ffe9524 + checksum: 10c0/c2d007dfa7500b6b54bca3a43f92f272cbdd8acaa9d4924eafa320c448f1cee77e7c00a229d5a6ffcc5211664b1dcfd7d54bb6622f2e7956e21ac51ea883165c languageName: node linkType: hard -"@csstools/postcss-logical-resize@npm:^3.0.0": +"@csstools/postcss-color-function@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-color-function@npm:5.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/b01b0e86df5cca222b9859601a4da18ee7a3bade52d6c7fada739cc6c6a4741173f0dccce05f4b307776bc8c0e156923930ce869faae9231e59aa2abab522581 + languageName: node + linkType: hard + +"@csstools/postcss-color-mix-function@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/postcss-color-mix-function@npm:4.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/60fa7fbc5e97dc295fd01a90e866d3923bfe47a6e2e3317f01ec8ab0aca687901ef569cdda867780b3aa37a02efa44531bfa0d0d602b90803333baa4cfbf6063 + languageName: node + linkType: hard + +"@csstools/postcss-color-mix-variadic-function-arguments@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-color-mix-variadic-function-arguments@npm:2.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/1206543ec6472f9dd7b67d3311128b66e3be2c6f1bdf9da1b6e4b4f9d69e9388e2896128bb3d1834d825ae4de9455524d7749501a60be740c52c0f7b67287263 + languageName: node + linkType: hard + +"@csstools/postcss-content-alt-text@npm:^3.0.0": version: 3.0.0 - resolution: "@csstools/postcss-logical-resize@npm:3.0.0" + resolution: "@csstools/postcss-content-alt-text@npm:3.0.0" dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/88f62ade9fa8af8b3292d9437e4df2002d79c139fc63e3030a72276a56cc5a13904fb18d20f07b09de45904da1a37eee4b448cdad07487fc28f96a0e3209bb9d + languageName: node + linkType: hard + +"@csstools/postcss-contrast-color-function@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-contrast-color-function@npm:3.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/7dec6e3bac9d45ae22b5dfcaec9ce8894a1ad80d1e62eb58634eebaa4a02a4f43f5b795a65224442b90b2285c0a0e1d7e80d9528fc1480348ab9a03bf9be2fe0 + languageName: node + linkType: hard + +"@csstools/postcss-exponential-functions@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-exponential-functions@npm:3.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/911101ec98ac89f56cb7ea52ab2d836646b48e13163d46e1a1f70b680dcc903d314ddf80eeeecfa7b561a99fad2f4b6ce2b0702fbcaa6d379b2c047bde20d398 + languageName: node + linkType: hard + +"@csstools/postcss-font-format-keywords@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-font-format-keywords@npm:5.0.0" + dependencies: + "@csstools/utilities": "npm:^3.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/4f12efcaf5468ff359bb3f32f0f66034b9acc9b3ac21fcd2f30a1c8998fc653ebac0091f35c8b7e8dbfe6ccf595aee67f9b06a67adf45a8844e49a82d98b4386 + checksum: 10c0/ae776eb164a4501549924a0b153c47cac31b4d288867c71775ff34941653bfe38d3d5a27888ab76fbef0289163e1687daae74e0b317547b7bd281e63b75afa98 languageName: node linkType: hard -"@csstools/postcss-logical-viewport-units@npm:^3.0.4": - version: 3.0.4 - resolution: "@csstools/postcss-logical-viewport-units@npm:3.0.4" +"@csstools/postcss-gamut-mapping@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-gamut-mapping@npm:3.0.0" dependencies: - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/utilities": "npm:^2.0.0" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/f0b5ba38acde3bf0ca880c6e0a883950c99fa9919b0e6290c894d5716569663590f26aa1170fd9483ce14544e46afac006ab3b02781410d5e7c8dd1467c674ce + checksum: 10c0/da138bee13c3af9e25962a425c70e242581722881fa52faa090dbb62d22adeced112d3589304651c759bae5fc0060eefb7eb62114798e96d3925bb01febc5a99 languageName: node linkType: hard -"@csstools/postcss-media-minmax@npm:^2.0.9": - version: 2.0.9 - resolution: "@csstools/postcss-media-minmax@npm:2.0.9" +"@csstools/postcss-gradients-interpolation-method@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/postcss-gradients-interpolation-method@npm:6.0.0" dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/media-query-list-parser": "npm:^4.0.3" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/d82622ee9de6eacba1abbf31718cd58759d158ed8a575f36f08e982d07a7d83e51fb184178b96c6f7b76cb333bb33cac04d06a750b6b9c5c43ae1c56232880f9 + checksum: 10c0/33da282f65480d1a4e8a2ce336bf5e0948d47e9fdab0847d3c9e839ae640cb4af96defdcf4cb3d59e43019dec83a5e48fcd0fb96be927c43c7369e8be316fc79 languageName: node linkType: hard -"@csstools/postcss-media-queries-aspect-ratio-number-values@npm:^3.0.5": - version: 3.0.5 - resolution: "@csstools/postcss-media-queries-aspect-ratio-number-values@npm:3.0.5" +"@csstools/postcss-hwb-function@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-hwb-function@npm:5.0.0" dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/media-query-list-parser": "npm:^4.0.3" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/a47abdaa7f4b26596bd9d6bb77aed872a232fc12bd144d2c062d9da626e8dfd8336e2fff67617dba61a1666c2b8027145b390d70d5cd4d4f608604e077cfb04e + checksum: 10c0/7f3e6d46531334621cc2c525df5c76cf916a53d0d9c82008b97b4b6b42f1b07b5729b4c3a8e5ba7b77c51ca44bbd216f83549e978f1aaf75e060335f754aacb2 languageName: node linkType: hard -"@csstools/postcss-nested-calc@npm:^4.0.0": +"@csstools/postcss-ic-unit@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-ic-unit@npm:5.0.0" + dependencies: + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/04148390bcdd0722af7bab6e05dbc573d8d1b5a216ca1f1021ea0cec955a8c3489e4788b3649f3a7be4a9267e0b3a6012f5e9d80d6f3ac3f1f52e585e5ce0c6e + languageName: node + linkType: hard + +"@csstools/postcss-initial@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-initial@npm:3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/4c59994c1ff6443f69ba54d2177adf3441756f067876500f281bd3021da1d9a18ae36cf7264a7ef7ca720508cac936f1120dc872f57fb3f6f28ac523c111a890 + languageName: node + linkType: hard + +"@csstools/postcss-is-pseudo-class@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/postcss-is-pseudo-class@npm:6.0.0" + dependencies: + "@csstools/selector-specificity": "npm:^6.0.0" + postcss-selector-parser: "npm:^7.1.1" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/55dfb215843258167426eecf7cb8dd2361c2502722469ffc4f3aa7bad6987a08c5ff819ef6bca427c59ee9ad7b3942b89b17bde1d0288ece03f8760c56df7542 + languageName: node + linkType: hard + +"@csstools/postcss-light-dark-function@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-light-dark-function@npm:3.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/769324f8acda2ce759cad959f43d49ccbc578ccb326362260019a7f7a126408c925d49f437c85d28fb2696cfe1405740a271c250184d9f34ea5603a0b8220936 + languageName: node + linkType: hard + +"@csstools/postcss-logical-float-and-clear@npm:^4.0.0": version: 4.0.0 - resolution: "@csstools/postcss-nested-calc@npm:4.0.0" - dependencies: - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" + resolution: "@csstools/postcss-logical-float-and-clear@npm:4.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/fb61512fa4909bdf0ee32a23e771145086c445f2208a737b52093c8adfab7362c56d3aeaf2a6e33ffcec067e99a07219775465d2fbb1a3ac30cdcfb278b218b7 + checksum: 10c0/bbb2e69878965943fc9686c4827a2f363f7a42c051e6df288e68d68741b424fb3cbdc2aa3e2d2169bcf1a4cfd6a24f0aa06c3061ea02ac03e0ad0bfae8fdc999 languageName: node linkType: hard -"@csstools/postcss-normalize-display-values@npm:^4.0.1": - version: 4.0.1 - resolution: "@csstools/postcss-normalize-display-values@npm:4.0.1" - dependencies: - postcss-value-parser: "npm:^4.2.0" +"@csstools/postcss-logical-overflow@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-logical-overflow@npm:3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/5d19364bad8554b047cebd94ad7e203723ed76abaf690e4b92c74e6fc7c3642cb8858ade3263da61aff26d97bb258af567b1036e97865b7aa3b17522241fd1e1 + checksum: 10c0/cca3397de39eb858a216d0566598ff24a8c94d20c12630568437d8145d78c0d5f36533c72dab72f50702dabbcada87fd34b1129aa4d9c24b427aaa211df124c1 languageName: node linkType: hard -"@csstools/postcss-oklab-function@npm:^4.0.12": - version: 4.0.12 - resolution: "@csstools/postcss-oklab-function@npm:4.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" +"@csstools/postcss-logical-overscroll-behavior@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-logical-overscroll-behavior@npm:3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/40d4f51b568c8299c054f8971d0e85fa7da609ba23ce6c84dc17e16bc3838640ed6da75c3886dc9a96a11005773c6e23cba13a5510c781b2d633d07ad7bda6b7 + checksum: 10c0/0138d89c739dae70b3496d68e08109d97c82d0038fe9bcc5b40f6efb136428def62e81c316f1902d2e4e5611c90c66f70e2008e99fdd0c7358dcfdffb4e326cc languageName: node linkType: hard -"@csstools/postcss-position-area-property@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/postcss-position-area-property@npm:1.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/38f770454d46bfed01d43a3f5e7ac07d3111399b374a7198ae6503cdb6288e410c7b4199f5a7af8f16aeb688216445ade97be417c084313d6c56f55e50d34559 - languageName: node - linkType: hard - -"@csstools/postcss-progressive-custom-properties@npm:^4.2.1": - version: 4.2.1 - resolution: "@csstools/postcss-progressive-custom-properties@npm:4.2.1" - dependencies: - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/56e9a147799719fd5c550c035437693dd50cdfef46d66a4f2ce8f196e1006a096aa47d412710a89c3dc9808068a0a101c7f607a507ed68e925580c6f921e84d5 - languageName: node - linkType: hard - -"@csstools/postcss-property-rule-prelude-list@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/postcss-property-rule-prelude-list@npm:1.0.0" - dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/ae8bbca3a77ca59c21c11899a904f9d9417a19a3359d01dee042e0489b7ddfe7cea13ae275b7e7936d9b0b99c0a13f7f685f962cd63ca3d3d2b6e5eacc293a0d - languageName: node - linkType: hard - -"@csstools/postcss-random-function@npm:^2.0.1": - version: 2.0.1 - resolution: "@csstools/postcss-random-function@npm:2.0.1" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/475bacf685b8bb82942d388e9e3b95f4156800f370299f19f5acc490475dc2813100de81a5a6bf48b696b4d83247622005b616af3166a668556b4b1aceded70d - languageName: node - linkType: hard - -"@csstools/postcss-relative-color-syntax@npm:^3.0.12": - version: 3.0.12 - resolution: "@csstools/postcss-relative-color-syntax@npm:3.0.12" - dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/11af386c8193e22c148ac034eee94c56da3060bdbde3196d2d641b088e12de35bef187bcd7d421f9e4d49c4f1cfc28b24e136e62107e02ed7007a3a28f635d06 - languageName: node - linkType: hard - -"@csstools/postcss-scope-pseudo-class@npm:^4.0.1": - version: 4.0.1 - resolution: "@csstools/postcss-scope-pseudo-class@npm:4.0.1" - dependencies: - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/6a0ca50fae655f4498200d1ce298ca794c85fbe2e3fd5d6419843254f055df5007a973e09b5f1e78e376c02b54278e411516c8d824300c68b265d3e5b311d7ee - languageName: node - linkType: hard - -"@csstools/postcss-sign-functions@npm:^1.1.4": - version: 1.1.4 - resolution: "@csstools/postcss-sign-functions@npm:1.1.4" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/ff58108b2527832a84c571a1f40224b5c8d2afa8db2fe3b1e3599ff6f3469d9f4c528a70eb3c25c5d7801e30474fabfec04e7c23bfdad8572ad492053cd4f899 - languageName: node - linkType: hard - -"@csstools/postcss-stepped-value-functions@npm:^4.0.9": - version: 4.0.9 - resolution: "@csstools/postcss-stepped-value-functions@npm:4.0.9" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/f143ca06338c30abb2aa37adc3d7e43a78f3b4493093160cb5babe3ec8cf6b86d83876746ee8e162db87b5e9af6e0066958d89fe8b4a503a29568e5c57c1bf8a - languageName: node - linkType: hard - -"@csstools/postcss-syntax-descriptor-syntax-production@npm:^1.0.1": - version: 1.0.1 - resolution: "@csstools/postcss-syntax-descriptor-syntax-production@npm:1.0.1" - dependencies: - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/b9b3d84a50b86b1af1b8b7e56a64d5eebc1c89c323a5263306c5c69ddb05a4d468d7072a7786b0ea6601629035df0089565e9d98d55d0f4eb7201cf7ed1bb3e9 - languageName: node - linkType: hard - -"@csstools/postcss-system-ui-font-family@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/postcss-system-ui-font-family@npm:1.0.0" - dependencies: - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/6a81761ae3cae643659b1416a7a892cf1505474896193b8abc26cff319cb6b1a20b64c5330d64019fba458e058da3abc9407d0ebf0c102289c0b79ef99b4c6d6 - languageName: node - linkType: hard - -"@csstools/postcss-text-decoration-shorthand@npm:^4.0.3": - version: 4.0.3 - resolution: "@csstools/postcss-text-decoration-shorthand@npm:4.0.3" - dependencies: - "@csstools/color-helpers": "npm:^5.1.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/f6af7d5dcf599edcf76c5e396ef2d372bbe1c1f3fbaaccd91e91049e64b6ff68b44f459277aef0a8110baca3eaa21275012adc52ccb8c0fc526a4c35577f8fce - languageName: node - linkType: hard - -"@csstools/postcss-trigonometric-functions@npm:^4.0.9": - version: 4.0.9 - resolution: "@csstools/postcss-trigonometric-functions@npm:4.0.9" - dependencies: - "@csstools/css-calc": "npm:^2.1.4" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/6ba3d381c977c224f01d47a36f78c9b99d3b89d060a357a9f8840537fdf497d9587a28165dc74e96abdf02f8db0a277d3558646355085a74c8915ee73c6780d1 - languageName: node - linkType: hard - -"@csstools/postcss-unset-value@npm:^4.0.0": +"@csstools/postcss-logical-resize@npm:^4.0.0": version: 4.0.0 - resolution: "@csstools/postcss-unset-value@npm:4.0.0" + resolution: "@csstools/postcss-logical-resize@npm:4.0.0" + dependencies: + postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/8424ac700ded5bf59d49310335896f10c069e2c3fc6a676b5d13ca5a6fb78689b948f50494df875da284c4c76651deb005eafba70d87e693274628c5a685abfa + checksum: 10c0/b9a7eb1ecb1637dac76c957e05f3579e390514d3e93245b5e0a3764542e7875c89811c3526548d466a2fdf76f1cd1a8fadb62c536b3d2521d4310a2f143e405e languageName: node linkType: hard -"@csstools/selector-resolve-nested@npm:^3.1.0": - version: 3.1.0 - resolution: "@csstools/selector-resolve-nested@npm:3.1.0" +"@csstools/postcss-logical-viewport-units@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/postcss-logical-viewport-units@npm:4.0.0" + dependencies: + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: - postcss-selector-parser: ^7.0.0 - checksum: 10c0/c2b1a930ad03c1427ab90b28c4940424fb39e8175130148f16209be3a3937f7a146d5483ca1da1dfc100aa7ae86df713f0ee82d4bbaa9b986e7f47f35cb67cca + postcss: ^8.4 + checksum: 10c0/1213ee92b5d9aad68c65478d4243dbd1bd75b88090edec18e1ebaf0aa38459a53609b9aa0e8c48070d546a1c873b1de94a5449fa173a3440334a3ba366c14549 + languageName: node + linkType: hard + +"@csstools/postcss-media-minmax@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-media-minmax@npm:3.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/media-query-list-parser": "npm:^5.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/0b03ab1ad162d53eef69470246025d01390cc84d2cb0983ffaf09c47bb3d27951e4794638430e778c737cf57c891f834adf3be08fcd5a84c4f6698a03ee9be4a + languageName: node + linkType: hard + +"@csstools/postcss-media-queries-aspect-ratio-number-values@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/postcss-media-queries-aspect-ratio-number-values@npm:4.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/media-query-list-parser": "npm:^5.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/80397e21d16f443445d03b79171c5c5444ef0e921a6eb1e0419eec24150bd9112469e2e9825aaf0612fa98d05fb0a50cf09f09c2b86d01a238229f205c15d2b2 + languageName: node + linkType: hard + +"@csstools/postcss-nested-calc@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-nested-calc@npm:5.0.0" + dependencies: + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/57764637a4855e941ab9360e1b4603e1fa0a29043148b67b80629d12ae4c93b4d2f156069826c4b8100ad3a90552744424b9a1e05710fe9b6958e1a817e6e07e + languageName: node + linkType: hard + +"@csstools/postcss-normalize-display-values@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-normalize-display-values@npm:5.0.0" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/d43a2658604bdd037ab1516bffadea4db3224390838f5096fccdfbae12a2db08e56ca576724a0279968e6af39419e5fe2963632754dd0956fda2d01b848cf97e + languageName: node + linkType: hard + +"@csstools/postcss-oklab-function@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-oklab-function@npm:5.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/af3413b1667be101f39b15b9528a57ff764e5abcf80578590b95c27c42a493ed9dcd2927834371d211b025c5d5df103cded6f2def98c9c79301f76ca379d7dbf + languageName: node + linkType: hard + +"@csstools/postcss-position-area-property@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-position-area-property@npm:2.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/54b9a97a9ef636dfc8b62cdabb7d194438c6f510effdff4339073626446c078033d475dfec8c82fdbcf25745fb05caafffc9512e3be7ff53ced55f1d38d2da6a + languageName: node + linkType: hard + +"@csstools/postcss-progressive-custom-properties@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-progressive-custom-properties@npm:5.0.0" + dependencies: + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/8476a58b777e7015f40fea53829d31429c8bab114b884b87c1706b5df095102232fb46b275a76c719f73399d3dbdfec2c172fb68c2fb362dbe68569446366a1a + languageName: node + linkType: hard + +"@csstools/postcss-property-rule-prelude-list@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-property-rule-prelude-list@npm:2.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/6ce0c962391b27966fba1623bca2dacc61fb7b1c9916f08d17256a800b0f8476d34835d50cb2ed77bdc4e920e4be5dddc1569c7e5140895e7b0076694d769182 + languageName: node + linkType: hard + +"@csstools/postcss-random-function@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/postcss-random-function@npm:3.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/bf9b8836ce3eafd670bfb9fa16d3befde8827ad220edc80eb4301fa716c1a4ff444254ab7612a6f6f952d16d3ac6d6e594448dcdd860f6ca8b575ffe59946864 + languageName: node + linkType: hard + +"@csstools/postcss-relative-color-syntax@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/postcss-relative-color-syntax@npm:4.0.0" + dependencies: + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/52f21acc66a0aa591d99173b9813f0b9f375de462bc657e909916ed176590b8d530a2eff83e71da488e2c8f476f0397d4a79bb7b7ef13664176d9e744a49510a + languageName: node + linkType: hard + +"@csstools/postcss-scope-pseudo-class@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-scope-pseudo-class@npm:5.0.0" + dependencies: + postcss-selector-parser: "npm:^7.1.1" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/2b67bd6af9a1175ca47c6593ee73564d70cb39c1adc9ffc5099ba513b49055994786c60687b44d88beccd09f1a6196e5e9157fd439632ae34381a60adaef5246 + languageName: node + linkType: hard + +"@csstools/postcss-sign-functions@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-sign-functions@npm:2.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/e5239e012e78cfcb244e8599d93700bbbcd15fd78e4a71f8bd5296022bd1d1ead750689bdacc35a47d2061729dd727e2fb1a185bee0620a62b64e4ee3434861e + languageName: node + linkType: hard + +"@csstools/postcss-stepped-value-functions@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-stepped-value-functions@npm:5.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/a8354fa0f4b74def1c258bf267c00d029f65618ccebb9c14837522f11eeb6b673717b56720588d5fec7bd07036ed84826dea01c44e9a085ae7af78a2b4ea7ec0 + languageName: node + linkType: hard + +"@csstools/postcss-syntax-descriptor-syntax-production@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-syntax-descriptor-syntax-production@npm:2.0.0" + dependencies: + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/0c9ef2f5cd21079db1df497adf99a3d66c912f69c471dd89283a1b8ed3a9c4e7670bfa439b615ac27acd86ce2e1adf4a5eb7b551113ecf5e95ee77269446bbbe + languageName: node + linkType: hard + +"@csstools/postcss-system-ui-font-family@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/postcss-system-ui-font-family@npm:2.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/e4f59c093565ed3753f3561a754fe3c0c8152a3b25f184cb370fa5cb1e9b54ad62fe53f7886ed840d445201120197ae6d42c6266ef4d5cfd063226f95c433c06 + languageName: node + linkType: hard + +"@csstools/postcss-text-decoration-shorthand@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-text-decoration-shorthand@npm:5.0.0" + dependencies: + "@csstools/color-helpers": "npm:^6.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/0d1dc47da13adc1f0863017a3448f3a79774f00f1952fc2bfc09404b00b03d125b54dedce036e0e5c73a6f2a64cdabe702e9bc03dc6b88d3b71a6424cd0cec80 + languageName: node + linkType: hard + +"@csstools/postcss-trigonometric-functions@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-trigonometric-functions@npm:5.0.0" + dependencies: + "@csstools/css-calc": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/8b2e149f4fb8ac1bc7a642c99265df7f54729fbace29cde24f65078c0d97e9050e75576ef55b869ab78fbb94a2408adae2bc6f3ced759fca96519c6ceb92cc20 + languageName: node + linkType: hard + +"@csstools/postcss-unset-value@npm:^5.0.0": + version: 5.0.0 + resolution: "@csstools/postcss-unset-value@npm:5.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/21e26cccf7824e4366378a20bc043034105db654632b941f77a0cbbf4fe0de291d1ae94845280c08c8d851ae14ce6d07bc09a33c5fbbcb7ce1f21b10212df1f1 + languageName: node + linkType: hard + +"@csstools/selector-resolve-nested@npm:^4.0.0": + version: 4.0.0 + resolution: "@csstools/selector-resolve-nested@npm:4.0.0" + peerDependencies: + postcss-selector-parser: ^7.1.1 + checksum: 10c0/f6bccfe24a47c3e55710d421740550b868bbe7f0820f32d14d1eb737eefe7ec26261fb726cc5cfdf8236b3173d0a657ebdc9602e0c53824603f719b14a76bcaf languageName: node linkType: hard @@ -1849,6 +1905,15 @@ __metadata: languageName: node linkType: hard +"@csstools/selector-specificity@npm:^6.0.0": + version: 6.0.0 + resolution: "@csstools/selector-specificity@npm:6.0.0" + peerDependencies: + postcss-selector-parser: ^7.1.1 + checksum: 10c0/7a93973f9054f2e1f03c8543cde68e0b0c65e5e72da6e4e959974d28fe809e11bd2afa1ff2ca11a1690a4c9a2f2bbe00d00e2b07fb2108bf89c5e48fe441c432 + languageName: node + linkType: hard + "@csstools/stylelint-formatter-github@npm:^1.0.0": version: 1.0.0 resolution: "@csstools/stylelint-formatter-github@npm:1.0.0" @@ -1858,12 +1923,12 @@ __metadata: languageName: node linkType: hard -"@csstools/utilities@npm:^2.0.0": - version: 2.0.0 - resolution: "@csstools/utilities@npm:2.0.0" +"@csstools/utilities@npm:^3.0.0": + version: 3.0.0 + resolution: "@csstools/utilities@npm:3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/be5c31437b726928f64cd4bb3e47f5b90bfd2e2a69a8eaabd8e89cc6c0977e4f0f7ee48de50c8ed8b07e04e3956a02293247e0da3236d521fb2e836f88f65822 + checksum: 10c0/65b6f6aa4ea899777bea710aab5f17028aa5e1aa41f7fe892c2e916b188da2b929c76be643d4a5d8d9ca5a7853df94f05c0247dd6b63540b360055d36db3104f languageName: node linkType: hard @@ -2881,7 +2946,7 @@ __metadata: msw-storybook-addon: "npm:^2.0.6" path-complete-extname: "npm:^1.0.0" playwright: "npm:^1.57.0" - postcss-preset-env: "npm:^10.1.5" + postcss-preset-env: "npm:^11.0.0" prettier: "npm:^3.3.3" prop-types: "npm:^15.8.1" punycode: "npm:^2.3.0" @@ -6219,14 +6284,14 @@ __metadata: languageName: node linkType: hard -"css-blank-pseudo@npm:^7.0.1": - version: 7.0.1 - resolution: "css-blank-pseudo@npm:7.0.1" +"css-blank-pseudo@npm:^8.0.1": + version: 8.0.1 + resolution: "css-blank-pseudo@npm:8.0.1" dependencies: - postcss-selector-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/46c3d3a611972fdb0c264db7c0b34fe437bc4300961d11945145cf04962f52a545a6ef55bc8ff4afd82b605bd692b4970f2b54582616dea00441105e725d4618 + checksum: 10c0/851d38cb3e11a63db4a0f44751cde99d6825bb11adc78418e8e83f32f0a3dcc3245e6e4b5053e796635560a1224af8ae63dfe330f87175fcc7bf34a1af3ecddc languageName: node linkType: hard @@ -6237,25 +6302,25 @@ __metadata: languageName: node linkType: hard -"css-has-pseudo@npm:^7.0.3": - version: 7.0.3 - resolution: "css-has-pseudo@npm:7.0.3" +"css-has-pseudo@npm:^8.0.0": + version: 8.0.0 + resolution: "css-has-pseudo@npm:8.0.0" dependencies: - "@csstools/selector-specificity": "npm:^5.0.0" - postcss-selector-parser: "npm:^7.0.0" + "@csstools/selector-specificity": "npm:^6.0.0" + postcss-selector-parser: "npm:^7.1.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/c89f68e17bed229e9a3e98da5032e1360c83d45d974bc3fb8d6b5358399bca80cce7929e4a621a516a75536edb78678dc486eb41841eeed28cca79e3be4bdc27 + checksum: 10c0/fb623c09d3cb1d5ec029c61b5a24484ee712fbd20bffae37a20a2843917706cadb1c4d695585382e4839dde136922ffee6924ec83f633dde2e2afc6d77a26be8 languageName: node linkType: hard -"css-prefers-color-scheme@npm:^10.0.0": - version: 10.0.0 - resolution: "css-prefers-color-scheme@npm:10.0.0" +"css-prefers-color-scheme@npm:^11.0.0": + version: 11.0.0 + resolution: "css-prefers-color-scheme@npm:11.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/a66c727bb2455328b18862f720819fc98ff5c1486b69f758bdb5c66f46cc6d484f9fc0bfa4f00f2693c5da6707ad136ca789496982f713ade693f08af624930e + checksum: 10c0/dea9f17cc33b5bedb660ed3925a53c9034bd5f4f94222d3b6df1dffd011061c124800dfc3bbeb1136fd69e84be577c63ac3da9220e175ea10b49275d7d0ada40 languageName: node linkType: hard @@ -10673,14 +10738,14 @@ __metadata: languageName: node linkType: hard -"postcss-attribute-case-insensitive@npm:^7.0.1": - version: 7.0.1 - resolution: "postcss-attribute-case-insensitive@npm:7.0.1" +"postcss-attribute-case-insensitive@npm:^8.0.0": + version: 8.0.0 + resolution: "postcss-attribute-case-insensitive@npm:8.0.0" dependencies: - postcss-selector-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/48945abe2024e2d2e4c37d30b8c1aaf37af720f24f6a996f7ea7e7ed33621f5c22cf247ed22028c0c922de040c58c0802729bc39b903cb1693f4b63c0b49da34 + checksum: 10c0/d6442d1580a3ec5083ff41435e8423dfe9c5b3b65b610e082769d27f11b98e0699d5361f7e9a9e499805c0a78d83a8aec3dd9f9d9280636cca82ec1aea79e928 languageName: node linkType: hard @@ -10695,131 +10760,131 @@ __metadata: languageName: node linkType: hard -"postcss-color-functional-notation@npm:^7.0.12": - version: 7.0.12 - resolution: "postcss-color-functional-notation@npm:7.0.12" +"postcss-color-functional-notation@npm:^8.0.0": + version: 8.0.0 + resolution: "postcss-color-functional-notation@npm:8.0.0" dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/dc80ba1a956ae9b396596bda72d9bdb92de96874378a38ba4e2177ffa35339dc76d894920bb013b6f10c9b75cfb41778e09956a438c2e9ea41b684f766c55f4a + checksum: 10c0/4b23633475b57f5d0076340ab4f434506891b5a25254d077215c08d08bbdab369484488bbc249595618730afea3546329e5633cc9e119f31c756b1c224dd0300 languageName: node linkType: hard -"postcss-color-hex-alpha@npm:^10.0.0": +"postcss-color-hex-alpha@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-color-hex-alpha@npm:11.0.0" + dependencies: + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/375509c404b0ac496f66bf0910d12e5aabca058c0d9de9d10719e9a0729013d5f4fafd06d8c8073b2d68613b3346e8ff79ef223a5ce4ddf0d4c4f645fd7a5436 + languageName: node + linkType: hard + +"postcss-color-rebeccapurple@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-color-rebeccapurple@npm:11.0.0" + dependencies: + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/9f22fff4ce2f0d7ce07be2ba2d07697e24f7db83a6252f02f36aa3a556cf69e1346a3cbab3908264e267306cc5a79b5d95c21e6fa4257a68c13c7411224543b9 + languageName: node + linkType: hard + +"postcss-custom-media@npm:^12.0.0": + version: 12.0.0 + resolution: "postcss-custom-media@npm:12.0.0" + dependencies: + "@csstools/cascade-layer-name-parser": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/media-query-list-parser": "npm:^5.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/bf1f3939d83a24eb7ef86f9fc2cb799026d4dc4620d3769eef9234d23fd528c73e907c2b88cb239290d169e5ddf8f0624cf8b22084874953c224e6000d55bf50 + languageName: node + linkType: hard + +"postcss-custom-properties@npm:^15.0.0": + version: 15.0.0 + resolution: "postcss-custom-properties@npm:15.0.0" + dependencies: + "@csstools/cascade-layer-name-parser": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/d766f1632075f8db2cd5075967cc4397c46b59699dd7a70762d9bd3e1ff91d3ada167b4f79e7e254518cdaae8d71645cd92faeb89559175ed50337c0acf5b0b4 + languageName: node + linkType: hard + +"postcss-custom-selectors@npm:^9.0.0": + version: 9.0.0 + resolution: "postcss-custom-selectors@npm:9.0.0" + dependencies: + "@csstools/cascade-layer-name-parser": "npm:^3.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + postcss-selector-parser: "npm:^7.1.1" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/a99d87639e8bca0a3eebd910356cecd67a4153789a77e15604db68caee415e64bc90d3e58a8322ed3d37addb80574a1180e58d603ac6541364b373b322e7de9f + languageName: node + linkType: hard + +"postcss-dir-pseudo-class@npm:^10.0.0": version: 10.0.0 - resolution: "postcss-color-hex-alpha@npm:10.0.0" + resolution: "postcss-dir-pseudo-class@npm:10.0.0" dependencies: - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/8a6dcb27403d04b55d6de88bf3074622bcea537fc4436bbcb346e92289c4d17059444e2e6c3554c325e7a777bb4cdc711e764a83123b4000aec211052e957d5b + checksum: 10c0/c7cae0b4e96bf0e51845c20b6fae7eec7b428b569b4fbae639b4f1f3b16a0ced4f5610ffb0209484ba4dfc8858abc64a9577cc8d2e3f1dbf3154f4ed0eb07669 languageName: node linkType: hard -"postcss-color-rebeccapurple@npm:^10.0.0": +"postcss-double-position-gradients@npm:^7.0.0": + version: 7.0.0 + resolution: "postcss-double-position-gradients@npm:7.0.0" + dependencies: + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" + postcss-value-parser: "npm:^4.2.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/0a9a1c567c79ddbe7bbeafe9770f2393d4d0eb344a79a2823dc5944690d74c311841ff6fd533b823d282029eff07418eeb671fabc85db6414ace48cce37223e8 + languageName: node + linkType: hard + +"postcss-focus-visible@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-focus-visible@npm:11.0.0" + dependencies: + postcss-selector-parser: "npm:^7.1.1" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/001cdfb200fe83b25a0fdbb7315b70fcf91c71d0134e16d48334fc7eab3d41c7cd3b8fe7bb8201a0ef9f242afb10f71aed238715a1d6923f68a02e6dc4219916 + languageName: node + linkType: hard + +"postcss-focus-within@npm:^10.0.0": version: 10.0.0 - resolution: "postcss-color-rebeccapurple@npm:10.0.0" + resolution: "postcss-focus-within@npm:10.0.0" dependencies: - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/308e33f76f2b48c1c2121d4502fc053e869f3415898de7d30314353df680e79b37497e7b628e3447edc1049091da3672f7d891e45604f238598e846e06b893ed - languageName: node - linkType: hard - -"postcss-custom-media@npm:^11.0.6": - version: 11.0.6 - resolution: "postcss-custom-media@npm:11.0.6" - dependencies: - "@csstools/cascade-layer-name-parser": "npm:^2.0.5" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/media-query-list-parser": "npm:^4.0.3" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/62dcb2858fd490d90aab32062621d58892a7b2a54948ee63af81a2cd61807a11815d28d4ef6bc800c5e142ac73098f7e56822c7cc63192eb20d5b16071543a73 - languageName: node - linkType: hard - -"postcss-custom-properties@npm:^14.0.6": - version: 14.0.6 - resolution: "postcss-custom-properties@npm:14.0.6" - dependencies: - "@csstools/cascade-layer-name-parser": "npm:^2.0.5" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/0eeef77bc713551f5cb8fa5982d24da4e854075f3af020f1c94366c47a23a4cc225ebfecc978bdb17f00ee0bdee9d2c784e0d01adc64a447321e408abbe2c83b - languageName: node - linkType: hard - -"postcss-custom-selectors@npm:^8.0.5": - version: 8.0.5 - resolution: "postcss-custom-selectors@npm:8.0.5" - dependencies: - "@csstools/cascade-layer-name-parser": "npm:^2.0.5" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/bd8f2f85bbec4bd56ff408cb699d9fe649e2af0db82d5752eee05481ae522f06f5a47950ca22fcb4c8601071c03346df67cf20b0b0bcade32ce58d07ebaf9b32 - languageName: node - linkType: hard - -"postcss-dir-pseudo-class@npm:^9.0.1": - version: 9.0.1 - resolution: "postcss-dir-pseudo-class@npm:9.0.1" - dependencies: - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/da9d3387648c5c3161a653d354c8f3e70a299108df3977e8aa65cf10793e4dd58a2711b3426cd63716245b13584ca8d95adcd6e10e3c9adbc61d08743e2d8690 - languageName: node - linkType: hard - -"postcss-double-position-gradients@npm:^6.0.4": - version: 6.0.4 - resolution: "postcss-double-position-gradients@npm:6.0.4" - dependencies: - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" - postcss-value-parser: "npm:^4.2.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/6dbbe7a3855e84a9319df434e210225f6dfa7262e5959611355f1769c2c9d30d37a19737712f20eac6354876fff4ba556d8d0b12a90c78d8ab97c9a8da534a7c - languageName: node - linkType: hard - -"postcss-focus-visible@npm:^10.0.1": - version: 10.0.1 - resolution: "postcss-focus-visible@npm:10.0.1" - dependencies: - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/c5ecc8536a708a49a99d0abd68a88a160664e6c832c808db8edd9f0221e7017a258daa87e49daf2cb098cb037005d46cf492403c8c9c92ad8835d30adaccf665 - languageName: node - linkType: hard - -"postcss-focus-within@npm:^9.0.1": - version: 9.0.1 - resolution: "postcss-focus-within@npm:9.0.1" - dependencies: - postcss-selector-parser: "npm:^7.0.0" - peerDependencies: - postcss: ^8.4 - checksum: 10c0/d6ab49d2a7f33485a9e137dc77ec92c5619a3ec92e1e672734fc604853ff1f3c0c189085c12461614be4fcb03ea0347d91791a45986a18d50b5228d161eda57a + checksum: 10c0/297ba52c07ba9284e7bf862e23ce90312bc2068d005a3c95833d832462691fac276850dd5508228fc808ec5f2963cb61a8b510645e08b78a74a9aaff9a79b02b languageName: node linkType: hard @@ -10832,39 +10897,39 @@ __metadata: languageName: node linkType: hard -"postcss-gap-properties@npm:^6.0.0": - version: 6.0.0 - resolution: "postcss-gap-properties@npm:6.0.0" +"postcss-gap-properties@npm:^7.0.0": + version: 7.0.0 + resolution: "postcss-gap-properties@npm:7.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/4e07e0d3927d0e65d67eaf047ac39e08d39cb1bf74e16e10c7df7f0d01b184a77ea59f63fd5691b5ed6df159970b972db28cb784d883e26e981137696460897d + checksum: 10c0/c93a0d15d40a7ae474a1bf05bd8f77410c6a353c7ea3bbad3aa61c38f6471d9bf23bac738aa1b3ca58805a33a730a4e6d15b7d03e0d2f1a29ef70969c14b483d languageName: node linkType: hard -"postcss-image-set-function@npm:^7.0.0": - version: 7.0.0 - resolution: "postcss-image-set-function@npm:7.0.0" +"postcss-image-set-function@npm:^8.0.0": + version: 8.0.0 + resolution: "postcss-image-set-function@npm:8.0.0" dependencies: - "@csstools/utilities": "npm:^2.0.0" + "@csstools/utilities": "npm:^3.0.0" postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/913fd9492f00122aa0c2550fb0d72130428cbe1e6465bc65e8fe71e9deb10ac0c01d7caceb68b560da759139e8cbc6c90ed22dfe6cf34949af49bb86bcbf4d3a + checksum: 10c0/b2c527d8731c97f09e79ab477069138eba994a656473f57e1afd2480b18cd54cdb6600888e44d208b11efae01c123bc81629ecb77d8ed19397bc2a4b8da6da52 languageName: node linkType: hard -"postcss-lab-function@npm:^7.0.12": - version: 7.0.12 - resolution: "postcss-lab-function@npm:7.0.12" +"postcss-lab-function@npm:^8.0.0": + version: 8.0.0 + resolution: "postcss-lab-function@npm:8.0.0" dependencies: - "@csstools/css-color-parser": "npm:^3.1.0" - "@csstools/css-parser-algorithms": "npm:^3.0.5" - "@csstools/css-tokenizer": "npm:^3.0.4" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/utilities": "npm:^2.0.0" + "@csstools/css-color-parser": "npm:^4.0.0" + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/de39b59da3b97c18d055d81fba68993e93253184ed76f103c888273584f868c551d047814dd54445980a1bdc5987e8f8af141383d84ecc641e5a6ee7bd901095 + checksum: 10c0/8805a853016c5919afd95f7cbee9ba624f398d25c81a2a7987edc0d0031fee08732b02f64cb51f537f364b0de07fc837bb4bd375e0ea96a489860c59a94c0590 languageName: node linkType: hard @@ -10886,14 +10951,14 @@ __metadata: languageName: node linkType: hard -"postcss-logical@npm:^8.1.0": - version: 8.1.0 - resolution: "postcss-logical@npm:8.1.0" +"postcss-logical@npm:^9.0.0": + version: 9.0.0 + resolution: "postcss-logical@npm:9.0.0" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/0e2e9e901d8a550db7f682d46b1f7e4f363c1ada061dc8e4548e2b563c5e39f3684a2d7c3f11fe061188782bca37874e34967fc6179fa6d98a49ff66a0076d27 + checksum: 10c0/058394f2234464b043249317b2c0a2271fa5194ada0f39afdb0f5b7e2ad172b1f24da7ae2641dfbd44d1ee98f5438844bc85d6005b881f691630381673707c32 languageName: node linkType: hard @@ -10937,16 +11002,16 @@ __metadata: languageName: node linkType: hard -"postcss-nesting@npm:^13.0.2": - version: 13.0.2 - resolution: "postcss-nesting@npm:13.0.2" +"postcss-nesting@npm:^14.0.0": + version: 14.0.0 + resolution: "postcss-nesting@npm:14.0.0" dependencies: - "@csstools/selector-resolve-nested": "npm:^3.1.0" - "@csstools/selector-specificity": "npm:^5.0.0" - postcss-selector-parser: "npm:^7.0.0" + "@csstools/selector-resolve-nested": "npm:^4.0.0" + "@csstools/selector-specificity": "npm:^6.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/bfa0578b3b686c6374f5a7b2f6ef955cb7e13400de95a919975a982ae43c1e25db37385618f210715ff15393dc7ff8c26c7b156f06b8fb3118a426099cf7f1f2 + checksum: 10c0/ffa1799ed49759be7f09940614a8cfd06f15bd13e01a7eaabf788939c23a6b617ef281ae383f5b3b3078ddd6d78fd90624af164ea5a421698a1b0972cf14253c languageName: node linkType: hard @@ -10959,14 +11024,14 @@ __metadata: languageName: node linkType: hard -"postcss-overflow-shorthand@npm:^6.0.0": - version: 6.0.0 - resolution: "postcss-overflow-shorthand@npm:6.0.0" +"postcss-overflow-shorthand@npm:^7.0.0": + version: 7.0.0 + resolution: "postcss-overflow-shorthand@npm:7.0.0" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/6598321b2ed0b68461135395bba9c7f76a4672617770df1e8487f459bc975f4ded6c3d37b6f72a44f4f77f7b6789e0c6f927e66dbbf1bcde1537167dbea39968 + checksum: 10c0/706430a2b1c28f361aad1d90e618c04c7ad7861971f2447fbd0e34c1d095bec0e5f4ed2857c98e05f5a7bcc694b7b763d08d5f515d5d45dd942f4b046593d638 languageName: node linkType: hard @@ -10979,106 +11044,106 @@ __metadata: languageName: node linkType: hard -"postcss-place@npm:^10.0.0": - version: 10.0.0 - resolution: "postcss-place@npm:10.0.0" +"postcss-place@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-place@npm:11.0.0" dependencies: postcss-value-parser: "npm:^4.2.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/ebb13deaac7648ba6042622375a31f78fbcc5209b7d196e478debbdf94525963fe621c932f4737a5b6b3d487af3b5ed6d059ed6193fdcbff6d3d5b150886ccc1 + checksum: 10c0/0b6b27426b198507583388a0e4f27552a136e5b9ccd1fc356dbd9a04c91e2bf6dcd680c3ad540ffc5aa1a63779467b9c7dc43d2164f83a0658505ca2408d79a9 languageName: node linkType: hard -"postcss-preset-env@npm:^10.1.5": - version: 10.6.1 - resolution: "postcss-preset-env@npm:10.6.1" +"postcss-preset-env@npm:^11.0.0": + version: 11.0.1 + resolution: "postcss-preset-env@npm:11.0.1" dependencies: - "@csstools/postcss-alpha-function": "npm:^1.0.1" - "@csstools/postcss-cascade-layers": "npm:^5.0.2" - "@csstools/postcss-color-function": "npm:^4.0.12" - "@csstools/postcss-color-function-display-p3-linear": "npm:^1.0.1" - "@csstools/postcss-color-mix-function": "npm:^3.0.12" - "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^1.0.2" - "@csstools/postcss-content-alt-text": "npm:^2.0.8" - "@csstools/postcss-contrast-color-function": "npm:^2.0.12" - "@csstools/postcss-exponential-functions": "npm:^2.0.9" - "@csstools/postcss-font-format-keywords": "npm:^4.0.0" - "@csstools/postcss-gamut-mapping": "npm:^2.0.11" - "@csstools/postcss-gradients-interpolation-method": "npm:^5.0.12" - "@csstools/postcss-hwb-function": "npm:^4.0.12" - "@csstools/postcss-ic-unit": "npm:^4.0.4" - "@csstools/postcss-initial": "npm:^2.0.1" - "@csstools/postcss-is-pseudo-class": "npm:^5.0.3" - "@csstools/postcss-light-dark-function": "npm:^2.0.11" - "@csstools/postcss-logical-float-and-clear": "npm:^3.0.0" - "@csstools/postcss-logical-overflow": "npm:^2.0.0" - "@csstools/postcss-logical-overscroll-behavior": "npm:^2.0.0" - "@csstools/postcss-logical-resize": "npm:^3.0.0" - "@csstools/postcss-logical-viewport-units": "npm:^3.0.4" - "@csstools/postcss-media-minmax": "npm:^2.0.9" - "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^3.0.5" - "@csstools/postcss-nested-calc": "npm:^4.0.0" - "@csstools/postcss-normalize-display-values": "npm:^4.0.1" - "@csstools/postcss-oklab-function": "npm:^4.0.12" - "@csstools/postcss-position-area-property": "npm:^1.0.0" - "@csstools/postcss-progressive-custom-properties": "npm:^4.2.1" - "@csstools/postcss-property-rule-prelude-list": "npm:^1.0.0" - "@csstools/postcss-random-function": "npm:^2.0.1" - "@csstools/postcss-relative-color-syntax": "npm:^3.0.12" - "@csstools/postcss-scope-pseudo-class": "npm:^4.0.1" - "@csstools/postcss-sign-functions": "npm:^1.1.4" - "@csstools/postcss-stepped-value-functions": "npm:^4.0.9" - "@csstools/postcss-syntax-descriptor-syntax-production": "npm:^1.0.1" - "@csstools/postcss-system-ui-font-family": "npm:^1.0.0" - "@csstools/postcss-text-decoration-shorthand": "npm:^4.0.3" - "@csstools/postcss-trigonometric-functions": "npm:^4.0.9" - "@csstools/postcss-unset-value": "npm:^4.0.0" + "@csstools/postcss-alpha-function": "npm:^2.0.0" + "@csstools/postcss-cascade-layers": "npm:^6.0.0" + "@csstools/postcss-color-function": "npm:^5.0.0" + "@csstools/postcss-color-function-display-p3-linear": "npm:^2.0.0" + "@csstools/postcss-color-mix-function": "npm:^4.0.0" + "@csstools/postcss-color-mix-variadic-function-arguments": "npm:^2.0.0" + "@csstools/postcss-content-alt-text": "npm:^3.0.0" + "@csstools/postcss-contrast-color-function": "npm:^3.0.0" + "@csstools/postcss-exponential-functions": "npm:^3.0.0" + "@csstools/postcss-font-format-keywords": "npm:^5.0.0" + "@csstools/postcss-gamut-mapping": "npm:^3.0.0" + "@csstools/postcss-gradients-interpolation-method": "npm:^6.0.0" + "@csstools/postcss-hwb-function": "npm:^5.0.0" + "@csstools/postcss-ic-unit": "npm:^5.0.0" + "@csstools/postcss-initial": "npm:^3.0.0" + "@csstools/postcss-is-pseudo-class": "npm:^6.0.0" + "@csstools/postcss-light-dark-function": "npm:^3.0.0" + "@csstools/postcss-logical-float-and-clear": "npm:^4.0.0" + "@csstools/postcss-logical-overflow": "npm:^3.0.0" + "@csstools/postcss-logical-overscroll-behavior": "npm:^3.0.0" + "@csstools/postcss-logical-resize": "npm:^4.0.0" + "@csstools/postcss-logical-viewport-units": "npm:^4.0.0" + "@csstools/postcss-media-minmax": "npm:^3.0.0" + "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^4.0.0" + "@csstools/postcss-nested-calc": "npm:^5.0.0" + "@csstools/postcss-normalize-display-values": "npm:^5.0.0" + "@csstools/postcss-oklab-function": "npm:^5.0.0" + "@csstools/postcss-position-area-property": "npm:^2.0.0" + "@csstools/postcss-progressive-custom-properties": "npm:^5.0.0" + "@csstools/postcss-property-rule-prelude-list": "npm:^2.0.0" + "@csstools/postcss-random-function": "npm:^3.0.0" + "@csstools/postcss-relative-color-syntax": "npm:^4.0.0" + "@csstools/postcss-scope-pseudo-class": "npm:^5.0.0" + "@csstools/postcss-sign-functions": "npm:^2.0.0" + "@csstools/postcss-stepped-value-functions": "npm:^5.0.0" + "@csstools/postcss-syntax-descriptor-syntax-production": "npm:^2.0.0" + "@csstools/postcss-system-ui-font-family": "npm:^2.0.0" + "@csstools/postcss-text-decoration-shorthand": "npm:^5.0.0" + "@csstools/postcss-trigonometric-functions": "npm:^5.0.0" + "@csstools/postcss-unset-value": "npm:^5.0.0" autoprefixer: "npm:^10.4.23" browserslist: "npm:^4.28.1" - css-blank-pseudo: "npm:^7.0.1" - css-has-pseudo: "npm:^7.0.3" - css-prefers-color-scheme: "npm:^10.0.0" + css-blank-pseudo: "npm:^8.0.1" + css-has-pseudo: "npm:^8.0.0" + css-prefers-color-scheme: "npm:^11.0.0" cssdb: "npm:^8.6.0" - postcss-attribute-case-insensitive: "npm:^7.0.1" + postcss-attribute-case-insensitive: "npm:^8.0.0" postcss-clamp: "npm:^4.1.0" - postcss-color-functional-notation: "npm:^7.0.12" - postcss-color-hex-alpha: "npm:^10.0.0" - postcss-color-rebeccapurple: "npm:^10.0.0" - postcss-custom-media: "npm:^11.0.6" - postcss-custom-properties: "npm:^14.0.6" - postcss-custom-selectors: "npm:^8.0.5" - postcss-dir-pseudo-class: "npm:^9.0.1" - postcss-double-position-gradients: "npm:^6.0.4" - postcss-focus-visible: "npm:^10.0.1" - postcss-focus-within: "npm:^9.0.1" + postcss-color-functional-notation: "npm:^8.0.0" + postcss-color-hex-alpha: "npm:^11.0.0" + postcss-color-rebeccapurple: "npm:^11.0.0" + postcss-custom-media: "npm:^12.0.0" + postcss-custom-properties: "npm:^15.0.0" + postcss-custom-selectors: "npm:^9.0.0" + postcss-dir-pseudo-class: "npm:^10.0.0" + postcss-double-position-gradients: "npm:^7.0.0" + postcss-focus-visible: "npm:^11.0.0" + postcss-focus-within: "npm:^10.0.0" postcss-font-variant: "npm:^5.0.0" - postcss-gap-properties: "npm:^6.0.0" - postcss-image-set-function: "npm:^7.0.0" - postcss-lab-function: "npm:^7.0.12" - postcss-logical: "npm:^8.1.0" - postcss-nesting: "npm:^13.0.2" + postcss-gap-properties: "npm:^7.0.0" + postcss-image-set-function: "npm:^8.0.0" + postcss-lab-function: "npm:^8.0.0" + postcss-logical: "npm:^9.0.0" + postcss-nesting: "npm:^14.0.0" postcss-opacity-percentage: "npm:^3.0.0" - postcss-overflow-shorthand: "npm:^6.0.0" + postcss-overflow-shorthand: "npm:^7.0.0" postcss-page-break: "npm:^3.0.4" - postcss-place: "npm:^10.0.0" - postcss-pseudo-class-any-link: "npm:^10.0.1" + postcss-place: "npm:^11.0.0" + postcss-pseudo-class-any-link: "npm:^11.0.0" postcss-replace-overflow-wrap: "npm:^4.0.0" - postcss-selector-not: "npm:^8.0.1" + postcss-selector-not: "npm:^9.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/e8da96f208918ebc0dc9acc8ba8961a92569f1d130b29abe25adaf7dbd56ef29fc6f778b75964c80fe7f3469012c763ea9447e5c2f559a002a155bc0462cce35 + checksum: 10c0/f4f3a01b1e20fd784c9e4233ed84718293adbf040db78f6af6f703fce0c320c921bd4131657df01099e37bd7082a815b9387ca92c4406dccd4808ceb0c7ab9ca languageName: node linkType: hard -"postcss-pseudo-class-any-link@npm:^10.0.1": - version: 10.0.1 - resolution: "postcss-pseudo-class-any-link@npm:10.0.1" +"postcss-pseudo-class-any-link@npm:^11.0.0": + version: 11.0.0 + resolution: "postcss-pseudo-class-any-link@npm:11.0.0" dependencies: - postcss-selector-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/95e883996e87baf14fc09d25f9a763a2e9d599eb3b9c6b736e83a8c3d0b55841bcb886bccdf51b5b7fefc128cbd0187ad8841f59878f85bd1613642e592d7673 + checksum: 10c0/bd7e1daa582bc19fe0e30c3ab8b13f8cf75ae2f116124960e9d28092255ede77455cd982bef8010d7ea64fdffc66a14624e63fe438e6d957415f5a4bc16b59a4 languageName: node linkType: hard @@ -11116,24 +11181,24 @@ __metadata: languageName: node linkType: hard -"postcss-selector-not@npm:^8.0.1": - version: 8.0.1 - resolution: "postcss-selector-not@npm:8.0.1" +"postcss-selector-not@npm:^9.0.0": + version: 9.0.0 + resolution: "postcss-selector-not@npm:9.0.0" dependencies: - postcss-selector-parser: "npm:^7.0.0" + postcss-selector-parser: "npm:^7.1.1" peerDependencies: postcss: ^8.4 - checksum: 10c0/491ea3dcc421cd90135be786078521605e2062fb93624ea8813cfd5ba0d35143f931e2e608d5f20effd5ea7d3f4786d2afea2afa42d117779a0288e135f132b6 + checksum: 10c0/090ce6d65667b8b6b9c1c26b7bd7740a31f3e7f36c29574e622d5fac5ab2ce291ac11622914745f22dda531d75e36fcdd395339fbe8fac0fc04fa7d750bb5248 languageName: node linkType: hard -"postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.0": - version: 7.1.0 - resolution: "postcss-selector-parser@npm:7.1.0" +"postcss-selector-parser@npm:^7.0.0, postcss-selector-parser@npm:^7.1.0, postcss-selector-parser@npm:^7.1.1": + version: 7.1.1 + resolution: "postcss-selector-parser@npm:7.1.1" dependencies: cssesc: "npm:^3.0.0" util-deprecate: "npm:^1.0.2" - checksum: 10c0/0fef257cfd1c0fe93c18a3f8a6e739b4438b527054fd77e9a62730a89b2d0ded1b59314a7e4aaa55bc256204f40830fecd2eb50f20f8cb7ab3a10b52aa06c8aa + checksum: 10c0/02d3b1589ddcddceed4b583b098b95a7266dacd5135f041e5d913ebb48e874fd333a36e564cc9a2ec426a464cb18db11cb192ac76247aced5eba8c951bf59507 languageName: node linkType: hard From 47557f5b9ecfe6126eb938ce4e911344f4b8c752 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 15 Jan 2026 11:41:11 +0100 Subject: [PATCH 44/89] New Crowdin Translations (automated) (#37492) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/gd.json | 4 ++-- config/locales/gd.yml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/javascript/mastodon/locales/gd.json b/app/javascript/mastodon/locales/gd.json index e48c60ff2ce..50db4dfb4bf 100644 --- a/app/javascript/mastodon/locales/gd.json +++ b/app/javascript/mastodon/locales/gd.json @@ -283,7 +283,7 @@ "confirmations.private_quote_notify.title": "A bheil thu airson a cho-roinneadh leis an luchd-leantainn ’s na cleachdaichean le iomradh orra?", "confirmations.quiet_post_quote_info.dismiss": "Na cuiribh seo ’nam chuimhne a-rithist", "confirmations.quiet_post_quote_info.got_it": "Tha mi agaibh", - "confirmations.quiet_post_quote_info.message": "Nuair a luaidheas tu post a tha poblach ach sàmhach, thèid am post agad fhalach o loidhnichean-ama nan treandaichean.", + "confirmations.quiet_post_quote_info.message": "Nuair a luaidheas tu post sàmhach, thèid am post agad fhalach o loidhnichean-ama nan treandaichean.", "confirmations.quiet_post_quote_info.title": "Luaidh air postaichean sàmhach", "confirmations.redraft.confirm": "Sguab às ⁊ dèan dreachd ùr", "confirmations.redraft.message": "A bheil thu cinnteach gu bheil thu airson am post seo a sguabadh às agus dreachd ùr a thòiseachadh? Caillidh tu gach annsachd is brosnachadh air agus thèid freagairtean dhan phost thùsail ’nan dìlleachdanan.", @@ -790,7 +790,7 @@ "privacy.quote.disabled": "{visibility}, luaidh à comas", "privacy.quote.limited": "{visibility}, luaidh cuingichte", "privacy.unlisted.additional": "Tha seo coltach ris an fhaicsinneachd phoblach ach cha nochd am post air loidhnichean-ama an t-saoghail phoblaich, nan tagaichean hais no an rùrachaidh no ann an toraidhean luirg Mhastodon fiù ’s ma thug thu ro-aonta airson sin seachad.", - "privacy.unlisted.long": "Poblach ach falaichte o na toraidhean-luirg, na treandaichean ’s na loichnichean-ama poblach", + "privacy.unlisted.long": "Falaichte o na toraidhean-luirg, na treandaichean ’s na loidhnichean-ama poblach", "privacy.unlisted.short": "Sàmhach", "privacy_policy.last_updated": "An t-ùrachadh mu dheireadh {date}", "privacy_policy.title": "Poileasaidh prìobhaideachd", diff --git a/config/locales/gd.yml b/config/locales/gd.yml index 8f64ee1984b..543cd0cb460 100644 --- a/config/locales/gd.yml +++ b/config/locales/gd.yml @@ -2054,7 +2054,7 @@ gd: public: Poblach public_long: Neach sam bith taobh a-staigh no a-muigh Mhastodon unlisted: Sàmhach - unlisted_long: Falaichte o na toraidhean-luirg, na treandaichean ’s na loichnichean-ama poblach + unlisted_long: Falaichte o na toraidhean-luirg, na treandaichean ’s na loidhnichean-ama poblach statuses_cleanup: enabled: Sguab às seann-phostaichean gu fèin-obrachail enabled_hint: Sguabaidh seo às na seann-phostaichean agad gu fèin-obrachail nuair a ruigeas iad stairsneach aoise sònraichte ach ma fhreagras iad ri gin dhe na h-eisgeachdan gu h-ìosal From 2a6c084aa126c7171fb8a1042b9d1b35a1c3236d Mon Sep 17 00:00:00 2001 From: Claire Date: Thu, 15 Jan 2026 14:17:41 +0100 Subject: [PATCH 45/89] Update SECURITY.md (#37506) --- SECURITY.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/SECURITY.md b/SECURITY.md index 12052652e6c..e5790a66fa2 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -18,5 +18,4 @@ A "vulnerability in Mastodon" is a vulnerability in the code distributed through | 4.5.x | Yes | | 4.4.x | Yes | | 4.3.x | Until 2026-05-06 | -| 4.2.x | Until 2026-01-08 | -| < 4.2 | No | +| < 4.3 | No | From f2fb232e37d382308c4afa69fb189585306ec4c4 Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 15 Jan 2026 14:45:30 +0100 Subject: [PATCH 46/89] Account header split up (#37490) --- .../components/account_header.tsx | 809 +----------------- .../account_timeline/components/badges.tsx | 43 + .../account_timeline/components/buttons.tsx | 134 +++ .../account_timeline/components/info.tsx | 68 ++ .../account_timeline/components/links.tsx | 58 ++ .../account_timeline/components/menu.tsx | 373 ++++++++ .../account_timeline/components/tabs.tsx | 27 + app/javascript/mastodon/hooks/useAccount.ts | 26 + 8 files changed, 776 insertions(+), 762 deletions(-) create mode 100644 app/javascript/mastodon/features/account_timeline/components/badges.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/buttons.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/info.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/links.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/menu.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/tabs.tsx create mode 100644 app/javascript/mastodon/hooks/useAccount.ts diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 040ca16c720..4244d7f7026 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -1,177 +1,35 @@ -import { useCallback, useMemo } from 'react'; +import { useCallback } from 'react'; -import { defineMessages, useIntl, FormattedMessage } from 'react-intl'; +import { useIntl, FormattedMessage } from 'react-intl'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; -import { NavLink } from 'react-router-dom'; import { AccountBio } from '@/mastodon/components/account_bio'; import { AccountFields } from '@/mastodon/components/account_fields'; import { DisplayName } from '@/mastodon/components/display_name'; import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; -import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; -import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; -import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react'; -import ShareIcon from '@/material-icons/400-24px/share.svg?react'; -import { - followAccount, - unblockAccount, - unmuteAccount, - pinAccount, - unpinAccount, - removeAccountFromFollowers, -} from 'mastodon/actions/accounts'; -import { initBlockModal } from 'mastodon/actions/blocks'; -import { mentionCompose, directCompose } from 'mastodon/actions/compose'; -import { - initDomainBlockModal, - unblockDomain, -} from 'mastodon/actions/domain_blocks'; import { openModal } from 'mastodon/actions/modal'; -import { initMuteModal } from 'mastodon/actions/mutes'; -import { initReport } from 'mastodon/actions/reports'; import { Avatar } from 'mastodon/components/avatar'; -import { Badge, AutomatedBadge, GroupBadge } from 'mastodon/components/badge'; -import { CopyIconButton } from 'mastodon/components/copy_icon_button'; -import { - FollowersCounter, - FollowingCounter, - StatusesCounter, -} from 'mastodon/components/counters'; -import { Dropdown } from 'mastodon/components/dropdown_menu'; -import { FollowButton } from 'mastodon/components/follow_button'; import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; import { Icon } from 'mastodon/components/icon'; -import { IconButton } from 'mastodon/components/icon_button'; -import { ShortNumber } from 'mastodon/components/short_number'; import { AccountNote } from 'mastodon/features/account/components/account_note'; import { DomainPill } from 'mastodon/features/account/components/domain_pill'; import FollowRequestNoteContainer from 'mastodon/features/account/containers/follow_request_note_container'; -import { useIdentity } from 'mastodon/identity_context'; import { autoPlayGif, me, domain as localDomain } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; -import type { MenuItem } from 'mastodon/models/dropdown_menu'; -import { - PERMISSION_MANAGE_USERS, - PERMISSION_MANAGE_FEDERATION, -} from 'mastodon/permissions'; import { getAccountHidden } from 'mastodon/selectors/accounts'; import { useAppSelector, useAppDispatch } from 'mastodon/store'; +import { AccountBadges } from './badges'; +import { AccountButtons } from './buttons'; import { FamiliarFollowers } from './familiar_followers'; +import { AccountInfo } from './info'; +import { AccountLinks } from './links'; import { MemorialNote } from './memorial_note'; import { MovedNote } from './moved_note'; - -const messages = defineMessages({ - unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, - edit_profile: { id: 'account.edit_profile', defaultMessage: 'Edit profile' }, - linkVerifiedOn: { - id: 'account.link_verified_on', - defaultMessage: 'Ownership of this link was checked on {date}', - }, - account_locked: { - id: 'account.locked_info', - defaultMessage: - 'This account privacy status is set to locked. The owner manually reviews who can follow them.', - }, - mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, - direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' }, - unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, - block: { id: 'account.block', defaultMessage: 'Block @{name}' }, - mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, - report: { id: 'account.report', defaultMessage: 'Report @{name}' }, - share: { id: 'account.share', defaultMessage: "Share @{name}'s profile" }, - copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' }, - media: { id: 'account.media', defaultMessage: 'Media' }, - blockDomain: { - id: 'account.block_domain', - defaultMessage: 'Block domain {domain}', - }, - unblockDomain: { - id: 'account.unblock_domain', - defaultMessage: 'Unblock domain {domain}', - }, - hideReblogs: { - id: 'account.hide_reblogs', - defaultMessage: 'Hide boosts from @{name}', - }, - showReblogs: { - id: 'account.show_reblogs', - defaultMessage: 'Show boosts from @{name}', - }, - enableNotifications: { - id: 'account.enable_notifications', - defaultMessage: 'Notify me when @{name} posts', - }, - disableNotifications: { - id: 'account.disable_notifications', - defaultMessage: 'Stop notifying me when @{name} posts', - }, - preferences: { - id: 'navigation_bar.preferences', - defaultMessage: 'Preferences', - }, - follow_requests: { - id: 'navigation_bar.follow_requests', - defaultMessage: 'Follow requests', - }, - favourites: { id: 'navigation_bar.favourites', defaultMessage: 'Favorites' }, - lists: { id: 'navigation_bar.lists', defaultMessage: 'Lists' }, - followed_tags: { - id: 'navigation_bar.followed_tags', - defaultMessage: 'Followed hashtags', - }, - blocks: { id: 'navigation_bar.blocks', defaultMessage: 'Blocked users' }, - domain_blocks: { - id: 'navigation_bar.domain_blocks', - defaultMessage: 'Blocked domains', - }, - mutes: { id: 'navigation_bar.mutes', defaultMessage: 'Muted users' }, - endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, - unendorse: { - id: 'account.unendorse', - defaultMessage: "Don't feature on profile", - }, - add_or_remove_from_list: { - id: 'account.add_or_remove_from_list', - defaultMessage: 'Add or Remove from lists', - }, - admin_account: { - id: 'status.admin_account', - defaultMessage: 'Open moderation interface for @{name}', - }, - admin_domain: { - id: 'status.admin_domain', - defaultMessage: 'Open moderation interface for {domain}', - }, - languages: { - id: 'account.languages', - defaultMessage: 'Change subscribed languages', - }, - openOriginalPage: { - id: 'account.open_original_page', - defaultMessage: 'Open original page', - }, - removeFromFollowers: { - id: 'account.remove_from_followers', - defaultMessage: 'Remove {name} from followers', - }, - confirmRemoveFromFollowersTitle: { - id: 'confirmations.remove_from_followers.title', - defaultMessage: 'Remove follower?', - }, - confirmRemoveFromFollowersMessage: { - id: 'confirmations.remove_from_followers.message', - defaultMessage: - '{name} will stop following you. Are you sure you want to proceed?', - }, - confirmRemoveFromFollowersButton: { - id: 'confirmations.remove_from_followers.confirm', - defaultMessage: 'Remove follower', - }, -}); +import { AccountTabs } from './tabs'; const titleFromAccount = (account: Account) => { const displayName = account.display_name; @@ -191,149 +49,12 @@ export const AccountHeader: React.FC<{ }> = ({ accountId, hideTabs }) => { const dispatch = useAppDispatch(); const intl = useIntl(); - const { signedIn, permissions } = useIdentity(); const account = useAppSelector((state) => state.accounts.get(accountId)); const relationship = useAppSelector((state) => state.relationships.get(accountId), ); const hidden = useAppSelector((state) => getAccountHidden(state, accountId)); - const handleBlock = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.blocking) { - dispatch(unblockAccount(account.id)); - } else { - dispatch(initBlockModal(account)); - } - }, [dispatch, account, relationship]); - - const handleMention = useCallback(() => { - if (!account) { - return; - } - - dispatch(mentionCompose(account)); - }, [dispatch, account]); - - const handleDirect = useCallback(() => { - if (!account) { - return; - } - - dispatch(directCompose(account)); - }, [dispatch, account]); - - const handleReport = useCallback(() => { - if (!account) { - return; - } - - dispatch(initReport(account)); - }, [dispatch, account]); - - const handleReblogToggle = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.showing_reblogs) { - dispatch(followAccount(account.id, { reblogs: false })); - } else { - dispatch(followAccount(account.id, { reblogs: true })); - } - }, [dispatch, account, relationship]); - - const handleNotifyToggle = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.notifying) { - dispatch(followAccount(account.id, { notify: false })); - } else { - dispatch(followAccount(account.id, { notify: true })); - } - }, [dispatch, account, relationship]); - - const handleMute = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.muting) { - dispatch(unmuteAccount(account.id)); - } else { - dispatch(initMuteModal(account)); - } - }, [dispatch, account, relationship]); - - const handleBlockDomain = useCallback(() => { - if (!account) { - return; - } - - dispatch(initDomainBlockModal(account)); - }, [dispatch, account]); - - const handleUnblockDomain = useCallback(() => { - if (!account) { - return; - } - - const domain = account.acct.split('@')[1]; - - if (!domain) { - return; - } - - dispatch(unblockDomain(domain)); - }, [dispatch, account]); - - const handleEndorseToggle = useCallback(() => { - if (!account) { - return; - } - - if (relationship?.endorsed) { - dispatch(unpinAccount(account.id)); - } else { - dispatch(pinAccount(account.id)); - } - }, [dispatch, account, relationship]); - - const handleAddToList = useCallback(() => { - if (!account) { - return; - } - - dispatch( - openModal({ - modalType: 'LIST_ADDER', - modalProps: { - accountId: account.id, - }, - }), - ); - }, [dispatch, account]); - - const handleChangeLanguages = useCallback(() => { - if (!account) { - return; - } - - dispatch( - openModal({ - modalType: 'SUBSCRIBED_LANGUAGES', - modalProps: { - accountId: account.id, - }, - }), - ); - }, [dispatch, account]); - const handleOpenAvatar = useCallback( (e: React.MouseEvent) => { if (e.button !== 0 || e.ctrlKey || e.metaKey) { @@ -359,410 +80,14 @@ export const AccountHeader: React.FC<{ [dispatch, account], ); - const handleShare = useCallback(() => { - if (!account) { - return; - } - - void navigator.share({ - url: account.url, - }); - }, [account]); - - const suspended = account?.suspended; - const isRemote = account?.acct !== account?.username; - const remoteDomain = isRemote ? account?.acct.split('@')[1] : null; - - const menuItems = useMemo(() => { - const arr: MenuItem[] = []; - - if (!account) { - return arr; - } - - if (signedIn && !account.suspended) { - arr.push({ - text: intl.formatMessage(messages.mention, { - name: account.username, - }), - action: handleMention, - }); - arr.push({ - text: intl.formatMessage(messages.direct, { - name: account.username, - }), - action: handleDirect, - }); - arr.push(null); - } - - if (isRemote) { - arr.push({ - text: intl.formatMessage(messages.openOriginalPage), - href: account.url, - }); - arr.push(null); - } - - if (signedIn) { - if (relationship?.following) { - if (!relationship.muting) { - if (relationship.showing_reblogs) { - arr.push({ - text: intl.formatMessage(messages.hideReblogs, { - name: account.username, - }), - action: handleReblogToggle, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.showReblogs, { - name: account.username, - }), - action: handleReblogToggle, - }); - } - - arr.push({ - text: intl.formatMessage(messages.languages), - action: handleChangeLanguages, - }); - arr.push(null); - } - - arr.push({ - text: intl.formatMessage( - relationship.endorsed ? messages.unendorse : messages.endorse, - ), - action: handleEndorseToggle, - }); - arr.push({ - text: intl.formatMessage(messages.add_or_remove_from_list), - action: handleAddToList, - }); - arr.push(null); - } - - if (relationship?.followed_by) { - const handleRemoveFromFollowers = () => { - dispatch( - openModal({ - modalType: 'CONFIRM', - modalProps: { - title: intl.formatMessage( - messages.confirmRemoveFromFollowersTitle, - ), - message: intl.formatMessage( - messages.confirmRemoveFromFollowersMessage, - { name: {account.acct} }, - ), - confirm: intl.formatMessage( - messages.confirmRemoveFromFollowersButton, - ), - onConfirm: () => { - void dispatch(removeAccountFromFollowers({ accountId })); - }, - }, - }), - ); - }; - - arr.push({ - text: intl.formatMessage(messages.removeFromFollowers, { - name: account.username, - }), - action: handleRemoveFromFollowers, - dangerous: true, - }); - } - - if (relationship?.muting) { - arr.push({ - text: intl.formatMessage(messages.unmute, { - name: account.username, - }), - action: handleMute, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.mute, { - name: account.username, - }), - action: handleMute, - dangerous: true, - }); - } - - if (relationship?.blocking) { - arr.push({ - text: intl.formatMessage(messages.unblock, { - name: account.username, - }), - action: handleBlock, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.block, { - name: account.username, - }), - action: handleBlock, - dangerous: true, - }); - } - - if (!account.suspended) { - arr.push({ - text: intl.formatMessage(messages.report, { - name: account.username, - }), - action: handleReport, - dangerous: true, - }); - } - } - - if (signedIn && isRemote) { - arr.push(null); - - if (relationship?.domain_blocking) { - arr.push({ - text: intl.formatMessage(messages.unblockDomain, { - domain: remoteDomain, - }), - action: handleUnblockDomain, - }); - } else { - arr.push({ - text: intl.formatMessage(messages.blockDomain, { - domain: remoteDomain, - }), - action: handleBlockDomain, - dangerous: true, - }); - } - } - - if ( - (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || - (isRemote && - (permissions & PERMISSION_MANAGE_FEDERATION) === - PERMISSION_MANAGE_FEDERATION) - ) { - arr.push(null); - if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { - arr.push({ - text: intl.formatMessage(messages.admin_account, { - name: account.username, - }), - href: `/admin/accounts/${account.id}`, - }); - } - if ( - isRemote && - (permissions & PERMISSION_MANAGE_FEDERATION) === - PERMISSION_MANAGE_FEDERATION - ) { - arr.push({ - text: intl.formatMessage(messages.admin_domain, { - domain: remoteDomain, - }), - href: `/admin/instances/${remoteDomain}`, - }); - } - } - - return arr; - }, [ - dispatch, - accountId, - account, - relationship, - permissions, - isRemote, - remoteDomain, - intl, - signedIn, - handleAddToList, - handleBlock, - handleBlockDomain, - handleChangeLanguages, - handleDirect, - handleEndorseToggle, - handleMention, - handleMute, - handleReblogToggle, - handleReport, - handleUnblockDomain, - ]); - - const menu = accountId !== me && ( - - ); - if (!account) { return null; } - let actionBtn: React.ReactNode, - bellBtn: React.ReactNode, - lockedIcon: React.ReactNode, - shareBtn: React.ReactNode; - - const info: React.ReactNode[] = []; - - if (me !== account.id && relationship) { - if ( - relationship.followed_by && - (relationship.following || relationship.requested) - ) { - info.push( - - - , - ); - } else if (relationship.followed_by) { - info.push( - - - , - ); - } else if (relationship.requested_by) { - info.push( - - - , - ); - } - - if (relationship.blocking) { - info.push( - - - , - ); - } - - if (relationship.muting) { - info.push( - - - , - ); - } - - if (relationship.domain_blocking) { - info.push( - - - , - ); - } - } - - if (relationship?.requested || relationship?.following) { - bellBtn = ( - - ); - } - - if ('share' in navigator) { - shareBtn = ( - - ); - } else { - shareBtn = ( - - ); - } - - const isMovedAndUnfollowedAccount = account.moved && !relationship?.following; - - if (!isMovedAndUnfollowedAccount) { - actionBtn = ( - - ); - } - - if (account.locked) { - lockedIcon = ( - - ); - } - - const fields = account.fields; + const suspendedOrHidden = hidden || account.suspended; const isLocal = !account.acct.includes('@'); const username = account.acct.split('@')[0]; const domain = isLocal ? localDomain : account.acct.split('@')[1]; - const isIndexable = !account.noindex; - - const badges = []; - - if (account.bot) { - badges.push(); - } else if (account.group) { - badges.push(); - } - - account.roles.forEach((role) => { - badges.push( - {role.get('name')}} - domain={domain} - roleId={role.get('id')} - />, - ); - }); return (
@@ -776,15 +101,16 @@ export const AccountHeader: React.FC<{ inactive: !!account.moved, })} > - {!(suspended || hidden || account.moved) && - relationship?.requested_by && ( - - )} + {!suspendedOrHidden && !account.moved && relationship?.requested_by && ( + + )}
-
{info}
+ {me !== account.id && relationship && ( + + )} - {!(suspended || hidden) && ( + {!suspendedOrHidden && (
@@ -829,29 +153,37 @@ export const AccountHeader: React.FC<{ domain={domain ?? ''} isSelf={me === account.id} /> - {lockedIcon} + {account.locked && ( + + )}
- {badges.length > 0 && ( -
{badges}
- )} + - {account.id !== me && signedIn && !(suspended || hidden) && ( + {me && account.id !== me && !suspendedOrHidden && ( )} -
- {!hidden && actionBtn} - {!hidden && bellBtn} - {menu} -
+ - {!(suspended || hidden) && ( + {!suspendedOrHidden && (
- {account.id !== me && signedIn && ( + {me && account.id !== me && ( )} @@ -878,73 +210,26 @@ export const AccountHeader: React.FC<{ - +
-
- - - - - - - - - - - -
+
)} - {!(hideTabs || hidden) && ( -
- - - - - - - - - - - - -
- )} + {!hideTabs && !hidden && } {titleFromAccount(account)} diff --git a/app/javascript/mastodon/features/account_timeline/components/badges.tsx b/app/javascript/mastodon/features/account_timeline/components/badges.tsx new file mode 100644 index 00000000000..cc6c456e9c1 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/badges.tsx @@ -0,0 +1,43 @@ +import type { FC } from 'react'; + +import { AutomatedBadge, Badge, GroupBadge } from '@/mastodon/components/badge'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { useAppSelector } from '@/mastodon/store'; + +export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { + const account = useAccount(accountId); + const localDomain = useAppSelector( + (state) => state.meta.get('domain') as string, + ); + const badges = []; + + if (!account) { + return null; + } + + if (account.bot) { + badges.push(); + } else if (account.group) { + badges.push(); + } + + const domain = account.acct.includes('@') + ? account.acct.split('@')[1] + : localDomain; + account.roles.forEach((role) => { + badges.push( + {role.get('name')}} + domain={domain} + roleId={role.get('id')} + />, + ); + }); + + if (!badges.length) { + return null; + } + + return
{badges}
; +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/buttons.tsx b/app/javascript/mastodon/features/account_timeline/components/buttons.tsx new file mode 100644 index 00000000000..c998d1472ce --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/buttons.tsx @@ -0,0 +1,134 @@ +import { useCallback } from 'react'; +import type { FC } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import classNames from 'classnames'; + +import { followAccount } from '@/mastodon/actions/accounts'; +import { CopyIconButton } from '@/mastodon/components/copy_icon_button'; +import { FollowButton } from '@/mastodon/components/follow_button'; +import { IconButton } from '@/mastodon/components/icon_button'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { getAccountHidden } from '@/mastodon/selectors/accounts'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import NotificationsIcon from '@/material-icons/400-24px/notifications.svg?react'; +import NotificationsActiveIcon from '@/material-icons/400-24px/notifications_active-fill.svg?react'; +import ShareIcon from '@/material-icons/400-24px/share.svg?react'; + +import { AccountMenu } from './menu'; + +const messages = defineMessages({ + enableNotifications: { + id: 'account.enable_notifications', + defaultMessage: 'Notify me when @{name} posts', + }, + disableNotifications: { + id: 'account.disable_notifications', + defaultMessage: 'Stop notifying me when @{name} posts', + }, + share: { id: 'account.share', defaultMessage: "Share @{name}'s profile" }, + copy: { id: 'account.copy', defaultMessage: 'Copy link to profile' }, +}); + +interface AccountButtonsProps { + accountId: string; + className?: string; + noShare?: boolean; +} + +export const AccountButtons: FC = ({ + accountId, + className, + noShare, +}) => { + const hidden = useAppSelector((state) => getAccountHidden(state, accountId)); + const me = useAppSelector((state) => state.meta.get('me') as string); + + return ( +
+ {!hidden && ( + + )} + {accountId !== me && } +
+ ); +}; + +const AccountButtonsOther: FC< + Pick +> = ({ accountId, noShare }) => { + const intl = useIntl(); + const account = useAccount(accountId); + const relationship = useAppSelector((state) => + state.relationships.get(accountId), + ); + + const dispatch = useAppDispatch(); + const handleNotifyToggle = useCallback(() => { + if (account) { + dispatch(followAccount(account.id, { notify: !relationship?.notifying })); + } + }, [dispatch, account, relationship]); + const accountUrl = account?.url; + const handleShare = useCallback(() => { + if (accountUrl) { + void navigator.share({ + url: accountUrl, + }); + } + }, [accountUrl]); + + if (!account) { + return null; + } + + const isMovedAndUnfollowedAccount = account.moved && !relationship?.following; + const isFollowing = relationship?.requested || relationship?.following; + + return ( + <> + {!isMovedAndUnfollowedAccount && ( + + )} + {isFollowing && ( + + )} + {!noShare && + ('share' in navigator ? ( + + ) : ( + + ))} + + ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/info.tsx b/app/javascript/mastodon/features/account_timeline/components/info.tsx new file mode 100644 index 00000000000..bb99999c41e --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/info.tsx @@ -0,0 +1,68 @@ +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import type { Relationship } from '@/mastodon/models/relationship'; + +export const AccountInfo: FC<{ relationship?: Relationship }> = ({ + relationship, +}) => { + if (!relationship) { + return null; + } + return ( +
+ {(relationship.followed_by || relationship.requested_by) && ( + + + + )} + {relationship.blocking && ( + + + + )} + {relationship.muting && ( + + + + )} + {relationship.domain_blocking && ( + + + + )} +
+ ); +}; + +const AccountInfoFollower: FC<{ relationship: Relationship }> = ({ + relationship, +}) => { + if ( + relationship.followed_by && + (relationship.following || relationship.requested) + ) { + return ( + + ); + } else if (relationship.followed_by) { + return ( + + ); + } else if (relationship.requested_by) { + return ( + + ); + } + return null; +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/links.tsx b/app/javascript/mastodon/features/account_timeline/components/links.tsx new file mode 100644 index 00000000000..2e056e4e577 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/links.tsx @@ -0,0 +1,58 @@ +import type { FC } from 'react'; + +import { useIntl } from 'react-intl'; + +import { NavLink } from 'react-router-dom'; + +import { + FollowersCounter, + FollowingCounter, + StatusesCounter, +} from '@/mastodon/components/counters'; +import { ShortNumber } from '@/mastodon/components/short_number'; +import { useAccount } from '@/mastodon/hooks/useAccount'; + +export const AccountLinks: FC<{ accountId: string }> = ({ accountId }) => { + const intl = useIntl(); + const account = useAccount(accountId); + + if (!account) { + return null; + } + + return ( +
+ + + + + + + + + + + +
+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/menu.tsx b/app/javascript/mastodon/features/account_timeline/components/menu.tsx new file mode 100644 index 00000000000..ce98c61f76a --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/menu.tsx @@ -0,0 +1,373 @@ +import { useMemo } from 'react'; +import type { FC } from 'react'; + +import { defineMessages, useIntl } from 'react-intl'; + +import { + blockAccount, + followAccount, + pinAccount, + unblockAccount, + unmuteAccount, + unpinAccount, +} from '@/mastodon/actions/accounts'; +import { removeAccountFromFollowers } from '@/mastodon/actions/accounts_typed'; +import { directCompose, mentionCompose } from '@/mastodon/actions/compose'; +import { + initDomainBlockModal, + unblockDomain, +} from '@/mastodon/actions/domain_blocks'; +import { openModal } from '@/mastodon/actions/modal'; +import { initMuteModal } from '@/mastodon/actions/mutes'; +import { initReport } from '@/mastodon/actions/reports'; +import { Dropdown } from '@/mastodon/components/dropdown_menu'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { useIdentity } from '@/mastodon/identity_context'; +import type { MenuItem } from '@/mastodon/models/dropdown_menu'; +import { + PERMISSION_MANAGE_FEDERATION, + PERMISSION_MANAGE_USERS, +} from '@/mastodon/permissions'; +import { useAppDispatch, useAppSelector } from '@/mastodon/store'; +import MoreHorizIcon from '@/material-icons/400-24px/more_horiz.svg?react'; + +const messages = defineMessages({ + unblock: { id: 'account.unblock', defaultMessage: 'Unblock @{name}' }, + mention: { id: 'account.mention', defaultMessage: 'Mention @{name}' }, + direct: { id: 'account.direct', defaultMessage: 'Privately mention @{name}' }, + unmute: { id: 'account.unmute', defaultMessage: 'Unmute @{name}' }, + block: { id: 'account.block', defaultMessage: 'Block @{name}' }, + mute: { id: 'account.mute', defaultMessage: 'Mute @{name}' }, + report: { id: 'account.report', defaultMessage: 'Report @{name}' }, + blockDomain: { + id: 'account.block_domain', + defaultMessage: 'Block domain {domain}', + }, + unblockDomain: { + id: 'account.unblock_domain', + defaultMessage: 'Unblock domain {domain}', + }, + hideReblogs: { + id: 'account.hide_reblogs', + defaultMessage: 'Hide boosts from @{name}', + }, + showReblogs: { + id: 'account.show_reblogs', + defaultMessage: 'Show boosts from @{name}', + }, + endorse: { id: 'account.endorse', defaultMessage: 'Feature on profile' }, + unendorse: { + id: 'account.unendorse', + defaultMessage: "Don't feature on profile", + }, + add_or_remove_from_list: { + id: 'account.add_or_remove_from_list', + defaultMessage: 'Add or Remove from lists', + }, + admin_account: { + id: 'status.admin_account', + defaultMessage: 'Open moderation interface for @{name}', + }, + admin_domain: { + id: 'status.admin_domain', + defaultMessage: 'Open moderation interface for {domain}', + }, + languages: { + id: 'account.languages', + defaultMessage: 'Change subscribed languages', + }, + openOriginalPage: { + id: 'account.open_original_page', + defaultMessage: 'Open original page', + }, + removeFromFollowers: { + id: 'account.remove_from_followers', + defaultMessage: 'Remove {name} from followers', + }, + confirmRemoveFromFollowersTitle: { + id: 'confirmations.remove_from_followers.title', + defaultMessage: 'Remove follower?', + }, + confirmRemoveFromFollowersMessage: { + id: 'confirmations.remove_from_followers.message', + defaultMessage: + '{name} will stop following you. Are you sure you want to proceed?', + }, + confirmRemoveFromFollowersButton: { + id: 'confirmations.remove_from_followers.confirm', + defaultMessage: 'Remove follower', + }, +}); + +export const AccountMenu: FC<{ accountId: string }> = ({ accountId }) => { + const intl = useIntl(); + const { signedIn, permissions } = useIdentity(); + + const account = useAccount(accountId); + const relationship = useAppSelector((state) => + state.relationships.get(accountId), + ); + + const dispatch = useAppDispatch(); + const menuItems = useMemo(() => { + const arr: MenuItem[] = []; + + if (!account) { + return arr; + } + + const isRemote = account.acct !== account.username; + + if (signedIn && !account.suspended) { + arr.push({ + text: intl.formatMessage(messages.mention, { + name: account.username, + }), + action: () => { + dispatch(mentionCompose(account)); + }, + }); + arr.push({ + text: intl.formatMessage(messages.direct, { + name: account.username, + }), + action: () => { + dispatch(directCompose(account)); + }, + }); + arr.push(null); + } + + if (isRemote) { + arr.push({ + text: intl.formatMessage(messages.openOriginalPage), + href: account.url, + }); + arr.push(null); + } + + if (!signedIn) { + return arr; + } + + if (relationship?.following) { + if (!relationship.muting) { + if (relationship.showing_reblogs) { + arr.push({ + text: intl.formatMessage(messages.hideReblogs, { + name: account.username, + }), + action: () => { + dispatch(followAccount(account.id, { reblogs: false })); + }, + }); + } else { + arr.push({ + text: intl.formatMessage(messages.showReblogs, { + name: account.username, + }), + action: () => { + dispatch(followAccount(account.id, { reblogs: true })); + }, + }); + } + + arr.push({ + text: intl.formatMessage(messages.languages), + action: () => { + dispatch( + openModal({ + modalType: 'SUBSCRIBED_LANGUAGES', + modalProps: { + accountId: account.id, + }, + }), + ); + }, + }); + arr.push(null); + } + + arr.push({ + text: intl.formatMessage( + relationship.endorsed ? messages.unendorse : messages.endorse, + ), + action: () => { + if (relationship.endorsed) { + dispatch(unpinAccount(account.id)); + } else { + dispatch(pinAccount(account.id)); + } + }, + }); + arr.push({ + text: intl.formatMessage(messages.add_or_remove_from_list), + action: () => { + dispatch( + openModal({ + modalType: 'LIST_ADDER', + modalProps: { + accountId: account.id, + }, + }), + ); + }, + }); + arr.push(null); + } + + if (relationship?.followed_by) { + const handleRemoveFromFollowers = () => { + dispatch( + openModal({ + modalType: 'CONFIRM', + modalProps: { + title: intl.formatMessage( + messages.confirmRemoveFromFollowersTitle, + ), + message: intl.formatMessage( + messages.confirmRemoveFromFollowersMessage, + { name: {account.acct} }, + ), + confirm: intl.formatMessage( + messages.confirmRemoveFromFollowersButton, + ), + onConfirm: () => { + void dispatch( + removeAccountFromFollowers({ accountId: account.id }), + ); + }, + }, + }), + ); + }; + + arr.push({ + text: intl.formatMessage(messages.removeFromFollowers, { + name: account.username, + }), + action: handleRemoveFromFollowers, + dangerous: true, + }); + } + + if (relationship?.muting) { + arr.push({ + text: intl.formatMessage(messages.unmute, { + name: account.username, + }), + action: () => { + dispatch(unmuteAccount(account.id)); + }, + }); + } else { + arr.push({ + text: intl.formatMessage(messages.mute, { + name: account.username, + }), + action: () => { + dispatch(initMuteModal(account)); + }, + dangerous: true, + }); + } + + if (relationship?.blocking) { + arr.push({ + text: intl.formatMessage(messages.unblock, { + name: account.username, + }), + action: () => { + dispatch(unblockAccount(account.id)); + }, + }); + } else { + arr.push({ + text: intl.formatMessage(messages.block, { + name: account.username, + }), + action: () => { + dispatch(blockAccount(account.id)); + }, + dangerous: true, + }); + } + + if (!account.suspended) { + arr.push({ + text: intl.formatMessage(messages.report, { + name: account.username, + }), + action: () => { + dispatch(initReport(account)); + }, + dangerous: true, + }); + } + + const remoteDomain = isRemote ? account.acct.split('@')[1] : null; + if (remoteDomain) { + arr.push(null); + + if (relationship?.domain_blocking) { + arr.push({ + text: intl.formatMessage(messages.unblockDomain, { + domain: remoteDomain, + }), + action: () => { + dispatch(unblockDomain(remoteDomain)); + }, + }); + } else { + arr.push({ + text: intl.formatMessage(messages.blockDomain, { + domain: remoteDomain, + }), + action: () => { + dispatch(initDomainBlockModal(account)); + }, + dangerous: true, + }); + } + } + + if ( + (permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS || + (isRemote && + (permissions & PERMISSION_MANAGE_FEDERATION) === + PERMISSION_MANAGE_FEDERATION) + ) { + arr.push(null); + if ((permissions & PERMISSION_MANAGE_USERS) === PERMISSION_MANAGE_USERS) { + arr.push({ + text: intl.formatMessage(messages.admin_account, { + name: account.username, + }), + href: `/admin/accounts/${account.id}`, + }); + } + if ( + isRemote && + (permissions & PERMISSION_MANAGE_FEDERATION) === + PERMISSION_MANAGE_FEDERATION + ) { + arr.push({ + text: intl.formatMessage(messages.admin_domain, { + domain: remoteDomain, + }), + href: `/admin/instances/${remoteDomain}`, + }); + } + } + + return arr; + }, [account, signedIn, permissions, intl, relationship, dispatch]); + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/tabs.tsx b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx new file mode 100644 index 00000000000..c08de1390ec --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/tabs.tsx @@ -0,0 +1,27 @@ +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { NavLink } from 'react-router-dom'; + +export const AccountTabs: FC<{ acct: string }> = ({ acct }) => { + return ( +
+ + + + + + + + + + + + +
+ ); +}; diff --git a/app/javascript/mastodon/hooks/useAccount.ts b/app/javascript/mastodon/hooks/useAccount.ts new file mode 100644 index 00000000000..277fa16931d --- /dev/null +++ b/app/javascript/mastodon/hooks/useAccount.ts @@ -0,0 +1,26 @@ +import { useEffect } from 'react'; + +import { fetchAccount } from '../actions/accounts'; +import { createAppSelector, useAppDispatch, useAppSelector } from '../store'; + +export const accountSelector = createAppSelector( + [ + (state) => state.accounts, + (_, accountId: string | null | undefined) => accountId, + ], + (accounts, accountId) => (accountId ? accounts.get(accountId) : undefined), +); + +export function useAccount(accountId: string | null | undefined) { + const account = useAppSelector((state) => accountSelector(state, accountId)); + + const dispatch = useAppDispatch(); + const accountInStore = !!account; + useEffect(() => { + if (accountId && !accountInStore) { + dispatch(fetchAccount(accountId)); + } + }, [accountId, accountInStore, dispatch]); + + return account; +} From c09fbeb32fddad4ba131a438fa730cdf7cb5ed41 Mon Sep 17 00:00:00 2001 From: Echo Date: Thu, 15 Jan 2026 14:53:42 +0100 Subject: [PATCH 47/89] MiniCard and MiniCardList components (#37479) --- .../mastodon/components/mini_card/index.tsx | 33 +++ .../mastodon/components/mini_card/list.tsx | 208 ++++++++++++++++++ .../mini_card/mini_card.stories.tsx | 62 ++++++ .../components/mini_card/styles.module.css | 55 +++++ app/javascript/mastodon/locales/en.json | 1 + 5 files changed, 359 insertions(+) create mode 100644 app/javascript/mastodon/components/mini_card/index.tsx create mode 100644 app/javascript/mastodon/components/mini_card/list.tsx create mode 100644 app/javascript/mastodon/components/mini_card/mini_card.stories.tsx create mode 100644 app/javascript/mastodon/components/mini_card/styles.module.css diff --git a/app/javascript/mastodon/components/mini_card/index.tsx b/app/javascript/mastodon/components/mini_card/index.tsx new file mode 100644 index 00000000000..a619bb214a0 --- /dev/null +++ b/app/javascript/mastodon/components/mini_card/index.tsx @@ -0,0 +1,33 @@ +import type { FC, ReactNode } from 'react'; + +import classNames from 'classnames'; + +import classes from './styles.module.css'; + +export interface MiniCardProps { + label: ReactNode; + value: ReactNode; + className?: string; + hidden?: boolean; +} + +export const MiniCard: FC = ({ + label, + value, + className, + hidden, +}) => { + if (!label) { + return null; + } + + return ( +
+
{label}
+
{value}
+
+ ); +}; diff --git a/app/javascript/mastodon/components/mini_card/list.tsx b/app/javascript/mastodon/components/mini_card/list.tsx new file mode 100644 index 00000000000..b5b8fbc2c80 --- /dev/null +++ b/app/javascript/mastodon/components/mini_card/list.tsx @@ -0,0 +1,208 @@ +import { useCallback, useEffect, useRef, useState } from 'react'; +import type { FC, Key, MouseEventHandler } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import { MiniCard } from '.'; +import type { MiniCardProps } from '.'; +import classes from './styles.module.css'; + +interface MiniCardListProps { + cards?: (Pick & { key?: Key })[]; + onOverflowClick?: MouseEventHandler; +} + +export const MiniCardList: FC = ({ + cards = [], + onOverflowClick, +}) => { + const { + wrapperRef, + listRef, + hiddenCount, + hasOverflow, + hiddenIndex, + maxWidth, + } = useOverflow(); + + return ( +
+
+ {cards.map((card, index) => ( +
+ +
+ ); +}; + +function useOverflow() { + const [hiddenIndex, setHiddenIndex] = useState(-1); + const [hiddenCount, setHiddenCount] = useState(0); + const [maxWidth, setMaxWidth] = useState('none'); + + // This is the item container element. + const listRef = useRef(null); + + // The main recalculation function. + const handleRecalculate = useCallback(() => { + const listEle = listRef.current; + if (!listEle) return; + + const reset = () => { + setHiddenIndex(-1); + setHiddenCount(0); + setMaxWidth('none'); + }; + + // Calculate the width via the parent element, minus the more button, minus the padding. + const maxWidth = + (listEle.parentElement?.offsetWidth ?? 0) - + (listEle.nextElementSibling?.scrollWidth ?? 0) - + 4; + if (maxWidth <= 0) { + reset(); + return; + } + + // Iterate through children until we exceed max width. + let visible = 0; + let index = 0; + let totalWidth = 0; + for (const child of listEle.children) { + if (child instanceof HTMLElement) { + const rightOffset = child.offsetLeft + child.offsetWidth; + if (rightOffset <= maxWidth) { + visible += 1; + totalWidth = rightOffset; + } else { + break; + } + } + index++; + } + + // All are visible, so remove max-width restriction. + if (visible === listEle.children.length) { + reset(); + return; + } + + // Set the width to avoid wrapping, and set hidden count. + setHiddenIndex(index); + setHiddenCount(listEle.children.length - visible); + setMaxWidth(totalWidth); + }, []); + + // Set up observers to watch for size and content changes. + const resizeObserverRef = useRef(null); + const mutationObserverRef = useRef(null); + + // Helper to get or create the resize observer. + const resizeObserver = useCallback(() => { + const observer = (resizeObserverRef.current ??= new ResizeObserver( + handleRecalculate, + )); + return observer; + }, [handleRecalculate]); + + // Iterate through children and observe them for size changes. + const handleChildrenChange = useCallback(() => { + const listEle = listRef.current; + const observer = resizeObserver(); + + if (listEle) { + for (const child of listEle.children) { + if (child instanceof HTMLElement) { + observer.observe(child); + } + } + } + handleRecalculate(); + }, [handleRecalculate, resizeObserver]); + + // Helper to get or create the mutation observer. + const mutationObserver = useCallback(() => { + const observer = (mutationObserverRef.current ??= new MutationObserver( + handleChildrenChange, + )); + return observer; + }, [handleChildrenChange]); + + // Set up observers. + const handleObserve = useCallback(() => { + if (wrapperRef.current) { + resizeObserver().observe(wrapperRef.current); + } + if (listRef.current) { + mutationObserver().observe(listRef.current, { childList: true }); + handleChildrenChange(); + } + }, [handleChildrenChange, mutationObserver, resizeObserver]); + + // Watch the wrapper for size changes, and recalculate when it resizes. + const wrapperRef = useRef(null); + const wrapperRefCallback = useCallback( + (node: HTMLElement | null) => { + if (node) { + wrapperRef.current = node; + handleObserve(); + } + }, + [handleObserve], + ); + + // If there are changes to the children, recalculate which are visible. + const listRefCallback = useCallback( + (node: HTMLElement | null) => { + if (node) { + listRef.current = node; + handleObserve(); + } + }, + [handleObserve], + ); + + useEffect(() => { + handleObserve(); + + return () => { + if (resizeObserverRef.current) { + resizeObserverRef.current.disconnect(); + resizeObserverRef.current = null; + } + if (mutationObserverRef.current) { + mutationObserverRef.current.disconnect(); + mutationObserverRef.current = null; + } + }; + }, [handleObserve]); + + return { + hiddenCount, + hasOverflow: hiddenCount > 0, + wrapperRef: wrapperRefCallback, + hiddenIndex, + maxWidth, + listRef: listRefCallback, + recalculate: handleRecalculate, + }; +} diff --git a/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx new file mode 100644 index 00000000000..ada76011b27 --- /dev/null +++ b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx @@ -0,0 +1,62 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; +import { action } from 'storybook/actions'; + +import { MiniCardList } from './list'; + +const meta = { + title: 'Components/MiniCard', + component: MiniCardList, + args: { + cards: [ + { label: 'Pronouns', value: 'they/them' }, + { + label: 'Website', + value: bowie-the-db.meow, + }, + { + label: 'Free playlists', + value: soundcloud.com, + }, + { label: 'Location', value: 'Purris, France' }, + ], + onOverflowClick: action('Overflow clicked'), + }, + render(args) { + return ( +
+ +
+ ); + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; + +export const LongValue: Story = { + args: { + cards: [ + { + label: 'Username', + value: 'bowie-the-dj', + }, + { + label: 'Bio', + value: + 'Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.', + }, + ], + }, +}; diff --git a/app/javascript/mastodon/components/mini_card/styles.module.css b/app/javascript/mastodon/components/mini_card/styles.module.css new file mode 100644 index 00000000000..d912f1e5cf5 --- /dev/null +++ b/app/javascript/mastodon/components/mini_card/styles.module.css @@ -0,0 +1,55 @@ +.wrapper { + display: flex; + flex-wrap: nowrap; + justify-content: flex-start; + gap: 4px; +} + +.list { + min-width: 0; + display: flex; + gap: 4px; + overflow: hidden; + position: relative; +} + +.card, +.more { + border: 1px solid var(--color-border-primary); + padding: 8px; + border-radius: 8px; + flex-shrink: 0; +} + +.card { + max-width: 20vw; + overflow: hidden; +} + +.more { + color: var(--color-text-secondary); + font-weight: 600; + appearance: none; + background: none; +} + +.hidden { + display: none; +} + +.label { + color: var(--color-text-secondary); + margin-bottom: 2px; +} + +.value { + color: var(--color-text-primary); + font-weight: 600; +} + +.label, +.value { + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index f1ab23570af..77dd9538871 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading…", "media_gallery.hide": "Hide", + "minicard.more_items": "+ {count} more", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "mute_modal.hide_from_notifications": "Hide from notifications", "mute_modal.hide_options": "Hide options", From bc2f8a358f96a9540e6f39bb1c58273deb4545de Mon Sep 17 00:00:00 2001 From: diondiondion Date: Thu, 15 Jan 2026 17:04:27 +0100 Subject: [PATCH 48/89] Enable theming via new HTML element attributes (#37477) --- .storybook/preview-body.html | 2 +- app/helpers/application_helper.rb | 19 ++++++++ app/javascript/inline/theme-selection.js | 9 ++-- .../annual_report/announcement/index.tsx | 4 +- .../mastodon/features/annual_report/index.tsx | 2 +- .../mastodon/features/annual_report/modal.tsx | 7 +-- .../features/emoji/__tests__/emoji-test.js | 2 +- .../mastodon/features/emoji/emoji.js | 4 +- app/javascript/mastodon/utils/theme.ts | 12 ++--- .../styles/mastodon/theme/index.scss | 46 ++++++------------- app/views/layouts/application.html.haml | 2 +- app/views/wrapstodon/show.html.haml | 2 +- spec/requests/content_security_policy_spec.rb | 2 +- 13 files changed, 53 insertions(+), 60 deletions(-) diff --git a/.storybook/preview-body.html b/.storybook/preview-body.html index 7a92b6f95ff..7c078c0b3b7 100644 --- a/.storybook/preview-body.html +++ b/.storybook/preview-body.html @@ -1,2 +1,2 @@ - + \ No newline at end of file diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 1076d9ced84..b23968e3731 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -89,6 +89,12 @@ module ApplicationHelper Rails.env.production? ? site_title : "#{site_title} (Dev)" end + def page_color_scheme + return content_for(:force_color_scheme) if content_for(:force_color_scheme) + + color_scheme + end + def label_for_scope(scope) safe_join [ tag.samp(scope, class: { 'scope-danger' => SessionActivation::DEFAULT_SCOPES.include?(scope.to_s) }), @@ -153,6 +159,19 @@ module ApplicationHelper tag.meta(content: content, property: property) end + def html_attributes + base = { + lang: I18n.locale, + class: html_classes, + 'data-contrast': contrast.parameterize, + 'data-color-scheme': page_color_scheme.parameterize, + } + + base[:'data-system-theme'] = 'true' if page_color_scheme == 'auto' + + base + end + def html_classes output = [] output << content_for(:html_classes) diff --git a/app/javascript/inline/theme-selection.js b/app/javascript/inline/theme-selection.js index b3a2b03163e..680fbb23ec2 100644 --- a/app/javascript/inline/theme-selection.js +++ b/app/javascript/inline/theme-selection.js @@ -1,16 +1,17 @@ (function (element) { - const {userTheme} = element.dataset; + const {colorScheme, contrast} = element.dataset; const colorSchemeMediaWatcher = window.matchMedia('(prefers-color-scheme: dark)'); const contrastMediaWatcher = window.matchMedia('(prefers-contrast: more)'); const updateColorScheme = () => { - const useDarkMode = userTheme === 'system' ? colorSchemeMediaWatcher.matches : userTheme !== 'mastodon-light'; - element.dataset.mode = useDarkMode ? 'dark' : 'light'; + const useDarkMode = colorScheme === 'auto' ? colorSchemeMediaWatcher.matches : colorScheme === 'dark'; + + element.dataset.colorScheme = useDarkMode ? 'dark' : 'light'; }; const updateContrast = () => { - const useHighContrast = userTheme === 'contrast' || contrastMediaWatcher.matches; + const useHighContrast = contrast === 'high' || contrastMediaWatcher.matches; element.dataset.contrast = useHighContrast ? 'high' : 'default'; } diff --git a/app/javascript/mastodon/features/annual_report/announcement/index.tsx b/app/javascript/mastodon/features/annual_report/announcement/index.tsx index 283e95f5940..d96b1092715 100644 --- a/app/javascript/mastodon/features/annual_report/announcement/index.tsx +++ b/app/javascript/mastodon/features/annual_report/announcement/index.tsx @@ -1,7 +1,5 @@ import { FormattedMessage } from 'react-intl'; -import classNames from 'classnames'; - import type { ApiAnnualReportState } from '@/mastodon/api/annual_report'; import { Button } from '@/mastodon/components/button'; @@ -19,7 +17,7 @@ export const AnnualReportAnnouncement: React.FC< AnnualReportAnnouncementProps > = ({ year, state, onRequestBuild, onOpen, onDismiss }) => { return ( -
+
= ({ const topHashtag = report.data.top_hashtags[0]; return ( -
+

Wrapstodon {report.year}

{account &&

@{account.acct}

} diff --git a/app/javascript/mastodon/features/annual_report/modal.tsx b/app/javascript/mastodon/features/annual_report/modal.tsx index 01d7c4bbdbf..d591954bbf3 100644 --- a/app/javascript/mastodon/features/annual_report/modal.tsx +++ b/app/javascript/mastodon/features/annual_report/modal.tsx @@ -60,11 +60,8 @@ const AnnualReportModal: React.FC<{ // default modal backdrop, preventing clicks to pass through. // eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions
{!showAnnouncement ? ( diff --git a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js index 35804de82ae..5d0683dace8 100644 --- a/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js +++ b/app/javascript/mastodon/features/emoji/__tests__/emoji-test.js @@ -86,7 +86,7 @@ describe('emoji', () => { it('does an emoji containing ZWJ properly', () => { expect(emojify('💂‍♀️💂‍♂️')) - .toEqual('💂\u200D♀️💂\u200D♂️'); + .toEqual('💂‍♀️💂‍♂️'); }); it('keeps ordering as expected (issue fixed by PR 20677)', () => { diff --git a/app/javascript/mastodon/features/emoji/emoji.js b/app/javascript/mastodon/features/emoji/emoji.js index f8fa0ae1923..859665a5312 100644 --- a/app/javascript/mastodon/features/emoji/emoji.js +++ b/app/javascript/mastodon/features/emoji/emoji.js @@ -1,6 +1,6 @@ import Trie from 'substring-trie'; -import { getUserTheme, isDarkMode } from '@/mastodon/utils/theme'; +import { getIsSystemTheme, isDarkMode } from '@/mastodon/utils/theme'; import { assetHost } from 'mastodon/utils/config'; import { autoPlayGif } from '../../initial_state'; @@ -98,7 +98,7 @@ const emojifyTextNode = (node, customEmojis) => { const { filename, shortCode } = unicodeMapping[unicode_emoji]; const title = shortCode ? `:${shortCode}:` : ''; - const isSystemTheme = getUserTheme() === 'system'; + const isSystemTheme = getIsSystemTheme(); const theme = (isSystemTheme || !isDarkMode()) ? 'light' : 'dark'; diff --git a/app/javascript/mastodon/utils/theme.ts b/app/javascript/mastodon/utils/theme.ts index 921787a6c43..494ee3cb53b 100644 --- a/app/javascript/mastodon/utils/theme.ts +++ b/app/javascript/mastodon/utils/theme.ts @@ -1,11 +1,9 @@ -export function getUserTheme() { - const { userTheme } = document.documentElement.dataset; - return userTheme; +export function getIsSystemTheme() { + const { systemTheme } = document.documentElement.dataset; + return systemTheme === 'true'; } export function isDarkMode() { - const { userTheme } = document.documentElement.dataset; - return userTheme === 'system' - ? window.matchMedia('(prefers-color-scheme: dark)').matches - : userTheme !== 'mastodon-light'; + const { colorScheme } = document.documentElement.dataset; + return colorScheme === 'dark'; } diff --git a/app/javascript/styles/mastodon/theme/index.scss b/app/javascript/styles/mastodon/theme/index.scss index a907299887d..a84b8b80da2 100644 --- a/app/javascript/styles/mastodon/theme/index.scss +++ b/app/javascript/styles/mastodon/theme/index.scss @@ -5,49 +5,29 @@ html { @include base.palette; - - &:where([data-user-theme='system']) { - color-scheme: dark light; - - @media (prefers-color-scheme: dark) { - @include dark.tokens; - @include utils.invert-on-dark; - - @media (prefers-contrast: more) { - @include dark.contrast-overrides; - } - } - - @media (prefers-color-scheme: light) { - @include light.tokens; - @include utils.invert-on-light; - - @media (prefers-contrast: more) { - @include light.contrast-overrides; - } - } - } } -.theme-dark, -html:where( - :not([data-user-theme='mastodon-light'], [data-user-theme='system']) -) { +[data-color-scheme='dark'], +html:not([data-color-scheme]) { color-scheme: dark; @include dark.tokens; @include utils.invert-on-dark; + + &[data-contrast='high'], + [data-contrast='high'] & { + @include dark.contrast-overrides; + } } -html[data-user-theme='contrast'], -html[data-user-theme='contrast'] .theme-dark { - @include dark.contrast-overrides; -} - -.theme-light, -html:where([data-user-theme='mastodon-light']) { +[data-color-scheme='light'] { color-scheme: light; @include light.tokens; @include utils.invert-on-light; + + &[data-contrast='high'], + [data-contrast='high'] & { + @include light.contrast-overrides; + } } diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index dccb6035cae..0cbd51c85b0 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ lang: I18n.locale, class: html_classes, 'data-user-theme': current_theme.parameterize, 'data-contrast': contrast.parameterize, 'data-mode': color_scheme.parameterize } +%html{ html_attributes } %head %meta{ charset: 'utf-8' }/ %meta{ name: 'viewport', content: 'width=device-width, initial-scale=1, viewport-fit=cover' }/ diff --git a/app/views/wrapstodon/show.html.haml b/app/views/wrapstodon/show.html.haml index ed1e64d4665..8a20001ad55 100644 --- a/app/views/wrapstodon/show.html.haml +++ b/app/views/wrapstodon/show.html.haml @@ -13,7 +13,7 @@ = vite_typescript_tag 'wrapstodon.tsx', crossorigin: 'anonymous' -- content_for :html_classes, 'theme-dark' +- content_for :force_color_scheme, 'dark' #wrapstodon = render_wrapstodon_share_data @generated_annual_report diff --git a/spec/requests/content_security_policy_spec.rb b/spec/requests/content_security_policy_spec.rb index 0aa4494ef0b..c84c3802f24 100644 --- a/spec/requests/content_security_policy_spec.rb +++ b/spec/requests/content_security_policy_spec.rb @@ -32,7 +32,7 @@ RSpec.describe 'Content-Security-Policy' do img-src 'self' data: blob: #{local_domain} manifest-src 'self' #{local_domain} media-src 'self' data: #{local_domain} - script-src 'self' #{local_domain} 'wasm-unsafe-eval' 'sha256-Q/2Cjx8v06hAdOF8/DeBUpsmBcSj7sLN3I/WpTF8T8c=' + script-src 'self' #{local_domain} 'wasm-unsafe-eval' 'sha256-Z5KW83D+6/pygIQS3h9XDpF52xW3l3BHc7JL9tj3uMs=' style-src 'self' #{local_domain} 'nonce-ZbA+JmE7+bK8F5qvADZHuQ==' worker-src 'self' blob: #{local_domain} CSP From 93f5ed0fcecee13a59c2ca9e59c40b60574c0d4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Heath=20Dutton=F0=9F=95=B4=EF=B8=8F?= Date: Thu, 15 Jan 2026 12:02:17 -0500 Subject: [PATCH 49/89] Fix directory showing load more button when no more profiles exist (#37465) --- app/javascript/mastodon/actions/directory.ts | 13 +++++++++---- .../mastodon/features/directory/index.tsx | 5 ++++- app/javascript/mastodon/reducers/user_lists.js | 4 ++-- 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/app/javascript/mastodon/actions/directory.ts b/app/javascript/mastodon/actions/directory.ts index 34ac309c66c..a50a377ffcf 100644 --- a/app/javascript/mastodon/actions/directory.ts +++ b/app/javascript/mastodon/actions/directory.ts @@ -6,15 +6,17 @@ import { createDataLoadingThunk } from 'mastodon/store/typed_functions'; import { fetchRelationships } from './accounts'; import { importFetchedAccounts } from './importer'; +const DIRECTORY_FETCH_LIMIT = 20; + export const fetchDirectory = createDataLoadingThunk( 'directory/fetch', async (params: Parameters[0]) => - apiGetDirectory(params), + apiGetDirectory(params, DIRECTORY_FETCH_LIMIT), (data, { dispatch }) => { dispatch(importFetchedAccounts(data)); dispatch(fetchRelationships(data.map((x) => x.id))); - return { accounts: data }; + return { accounts: data, isLast: data.length < DIRECTORY_FETCH_LIMIT }; }, ); @@ -26,12 +28,15 @@ export const expandDirectory = createDataLoadingThunk( 'items', ]) as ImmutableList; - return apiGetDirectory({ ...params, offset: loadedItems.size }, 20); + return apiGetDirectory( + { ...params, offset: loadedItems.size }, + DIRECTORY_FETCH_LIMIT, + ); }, (data, { dispatch }) => { dispatch(importFetchedAccounts(data)); dispatch(fetchRelationships(data.map((x) => x.id))); - return { accounts: data }; + return { accounts: data, isLast: data.length < DIRECTORY_FETCH_LIMIT }; }, ); diff --git a/app/javascript/mastodon/features/directory/index.tsx b/app/javascript/mastodon/features/directory/index.tsx index 0fe140b4eb4..54317a6c76d 100644 --- a/app/javascript/mastodon/features/directory/index.tsx +++ b/app/javascript/mastodon/features/directory/index.tsx @@ -83,6 +83,9 @@ export const Directory: React.FC<{ (state) => state.user_lists.getIn(['directory', 'isLoading'], true) as boolean, ); + const hasMore = useAppSelector( + (state) => !!state.user_lists.getIn(['directory', 'next']), + ); useEffect(() => { void dispatch(fetchDirectory({ order, local })); @@ -182,7 +185,7 @@ export const Directory: React.FC<{
diff --git a/app/javascript/mastodon/reducers/user_lists.js b/app/javascript/mastodon/reducers/user_lists.js index 466bfe54d65..0393c06763a 100644 --- a/app/javascript/mastodon/reducers/user_lists.js +++ b/app/javascript/mastodon/reducers/user_lists.js @@ -204,9 +204,9 @@ export default function userLists(state = initialState, action) { else if (fetchFeaturedTags.rejected.match(action)) return state.setIn(['featured_tags', action.meta.arg.accountId, 'isLoading'], false); else if (fetchDirectory.fulfilled.match(action)) - return normalizeList(state, ['directory'], action.payload.accounts, undefined); + return normalizeList(state, ['directory'], action.payload.accounts, action.payload.isLast ? null : true); else if (expandDirectory.fulfilled.match(action)) - return appendToList(state, ['directory'], action.payload.accounts, undefined); + return appendToList(state, ['directory'], action.payload.accounts, action.payload.isLast ? null : true); else if (fetchDirectory.pending.match(action) || expandDirectory.pending.match(action)) return state.setIn(['directory', 'isLoading'], true); From 0d9fcb59a497902373c6293c8a193e46d307f2ff Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 10:17:56 +0100 Subject: [PATCH 50/89] New Crowdin Translations (automated) (#37517) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/be.json | 1 + app/javascript/mastodon/locales/da.json | 1 + app/javascript/mastodon/locales/de.json | 1 + app/javascript/mastodon/locales/el.json | 1 + app/javascript/mastodon/locales/en-GB.json | 1 + app/javascript/mastodon/locales/es-AR.json | 1 + app/javascript/mastodon/locales/es-MX.json | 1 + app/javascript/mastodon/locales/es.json | 1 + app/javascript/mastodon/locales/fo.json | 1 + app/javascript/mastodon/locales/hu.json | 1 + app/javascript/mastodon/locales/is.json | 1 + app/javascript/mastodon/locales/lt.json | 6 +++--- app/javascript/mastodon/locales/pt-PT.json | 1 + app/javascript/mastodon/locales/sq.json | 1 + app/javascript/mastodon/locales/vi.json | 1 + app/javascript/mastodon/locales/zh-CN.json | 1 + app/javascript/mastodon/locales/zh-TW.json | 1 + config/locales/cy.yml | 1 + config/locales/lt.yml | 2 +- 19 files changed, 21 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index da4aa544093..caf49397770 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}", "loading_indicator.label": "Ідзе загрузка…", "media_gallery.hide": "Схаваць", + "minicard.more_items": "+ яшчэ {count}", "moved_to_account_banner.text": "Ваш уліковы запіс {disabledAccount} зараз адключаны, таму што Вы перайшлі на {movedToAccount}.", "mute_modal.hide_from_notifications": "Схаваць з апавяшчэнняў", "mute_modal.hide_options": "Схаваць опцыі", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 035037fccbb..a95303c0f2c 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# nyt element} other {# nye elementer}}", "loading_indicator.label": "Indlæser…", "media_gallery.hide": "Skjul", + "minicard.more_items": "+ {count} mere", "moved_to_account_banner.text": "Din konto {disabledAccount} er i øjeblikket deaktiveret, fordi du er flyttet til {movedToAccount}.", "mute_modal.hide_from_notifications": "Skjul fra notifikationer", "mute_modal.hide_options": "Skjul valgmuligheder", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 9e63460159b..f475865203d 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}", "loading_indicator.label": "Lädt …", "media_gallery.hide": "Ausblenden", + "minicard.more_items": "+ {count} weitere", "moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.", "mute_modal.hide_from_notifications": "Auch aus den Benachrichtigungen entfernen", "mute_modal.hide_options": "Optionen ausblenden", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index e31e6f3bde0..9903fed3b43 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}", "loading_indicator.label": "Φόρτωση…", "media_gallery.hide": "Απόκρυψη", + "minicard.more_items": "+ {count} ακόμα", "moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.", "mute_modal.hide_from_notifications": "Απόκρυψη από ειδοποιήσεις", "mute_modal.hide_options": "Απόκρυψη επιλογών", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 0863df66995..58912dd035d 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading…", "media_gallery.hide": "Hide", + "minicard.more_items": "+ {count} more", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "mute_modal.hide_from_notifications": "Hide from notifications", "mute_modal.hide_options": "Hide options", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 670cc70055e..197dc7a92ae 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", + "minicard.more_items": "y {count} más", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar en las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index db87e024631..48eaf1dc6fb 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", + "minicard.more_items": "y {count} más", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar de las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 2b7d1905350..aa2b7588ddf 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", + "minicard.more_items": "y {count} más", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar de las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 52ef3946c51..1f421614597 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# nýtt evni} other {# nýggj evni}}", "loading_indicator.label": "Innlesur…", "media_gallery.hide": "Fjal", + "minicard.more_items": "+ {count} meira", "moved_to_account_banner.text": "Konta tín {disabledAccount} er í løtuni óvirkin, tí tú flutti til {movedToAccount}.", "mute_modal.hide_from_notifications": "Fjal boð", "mute_modal.hide_options": "Fjal valmøguleikar", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index 0f53d1b18eb..edcdd2b9f35 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# új elem} other {# új elem}}", "loading_indicator.label": "Betöltés…", "media_gallery.hide": "Elrejtés", + "minicard.more_items": "+ {count} további", "moved_to_account_banner.text": "A(z) {disabledAccount} fiókod jelenleg le van tiltva, mert átköltöztél ide: {movedToAccount}.", "mute_modal.hide_from_notifications": "Elrejtés az értesítések közül", "mute_modal.hide_options": "Beállítások elrejtése", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index 0f255dfa977..e2efff0a47d 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# nýtt atriði} other {# ný atriði}}", "loading_indicator.label": "Hleð inn…", "media_gallery.hide": "Fela", + "minicard.more_items": "+ {count} í viðbót", "moved_to_account_banner.text": "Aðgangurinn þinn {disabledAccount} er óvirkur í augnablikinu vegna þess að þú fluttir þig yfir á {movedToAccount}.", "mute_modal.hide_from_notifications": "Fela úr tilkynningum", "mute_modal.hide_options": "Fela valkosti", diff --git a/app/javascript/mastodon/locales/lt.json b/app/javascript/mastodon/locales/lt.json index cfbb355bb30..3b6957b0c26 100644 --- a/app/javascript/mastodon/locales/lt.json +++ b/app/javascript/mastodon/locales/lt.json @@ -69,7 +69,7 @@ "account.mute_short": "Nutildyti", "account.muted": "Nutildytas", "account.muting": "Užtildymas", - "account.mutual": "Jūs sekate vienas kitą", + "account.mutual": "Sekate vienas kitą", "account.no_bio": "Nėra pateikto aprašymo.", "account.open_original_page": "Atidaryti originalų puslapį", "account.posts": "Įrašai", @@ -685,7 +685,7 @@ "notifications.filter.follows": "Sekimai", "notifications.filter.mentions": "Paminėjimai", "notifications.filter.polls": "Balsavimo rezultatai", - "notifications.filter.statuses": "Naujinimai iš žmonių, kuriuos seki", + "notifications.filter.statuses": "Naujienos iš žmonių, kuriuos sekate", "notifications.grant_permission": "Suteikti leidimą.", "notifications.group": "{count} pranešimai", "notifications.mark_as_read": "Pažymėti kiekvieną pranešimą kaip perskaitytą", @@ -821,7 +821,7 @@ "report.thanks.title": "Nenori to matyti?", "report.thanks.title_actionable": "Ačiū, kad pranešei, mes tai išnagrinėsime.", "report.unfollow": "Nebesekti @{name}", - "report.unfollow_explanation": "Tu seki šią paskyrą. Jei nori nebematyti jų įrašų savo pagrindiniame sraute, nebesek jų.", + "report.unfollow_explanation": "Jūs sekate šią paskyrą. Kad nebematytumėte jų įrašų savo pagrindiniame sraute, nebesekite.", "report_notification.attached_statuses": "Pridėti {count, plural, one {{count} įrašas} few {{count} įrašai} many {{count} įrašo} other {{count} įrašų}}", "report_notification.categories.legal": "Teisinės", "report_notification.categories.legal_sentence": "nelegalus turinys", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 66710d1172b..72b97b22cb9 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# novo item} other {# novos itens}}", "loading_indicator.label": "A carregar…", "media_gallery.hide": "Esconder", + "minicard.more_items": "+ {count} mais", "moved_to_account_banner.text": "A tua conta {disabledAccount} está neste momento desativada porque migraste para {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar das notificações", "mute_modal.hide_options": "Ocultar opções", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 300966b2093..897cbd8722b 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -587,6 +587,7 @@ "load_pending": "{count, plural,one {# objekt i ri }other {# objekte të rinj }}", "loading_indicator.label": "Po ngarkohet…", "media_gallery.hide": "Fshihe", + "minicard.more_items": "+ {count} më tepër", "moved_to_account_banner.text": "Llogaria juaj {disabledAccount} aktualisht është e çaktivizuar, ngaqë kaluat te {movedToAccount}.", "mute_modal.hide_from_notifications": "Fshihe prej njoftimeve", "mute_modal.hide_options": "Fshihi mundësitë", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 4a9158e5b94..6b1138ec087 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, one {# tút mới} other {# tút mới}}", "loading_indicator.label": "Đang tải…", "media_gallery.hide": "Ẩn", + "minicard.more_items": "+ {count} khác", "moved_to_account_banner.text": "Tài khoản {disabledAccount} của bạn hiện không khả dụng vì bạn đã chuyển sang {movedToAccount}.", "mute_modal.hide_from_notifications": "Ẩn thông báo", "mute_modal.hide_options": "Ẩn tùy chọn", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index d69f490b5b4..92c0eaa3f40 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -589,6 +589,7 @@ "load_pending": "{count} 项", "loading_indicator.label": "加载中…", "media_gallery.hide": "隐藏", + "minicard.more_items": "+ 还有 {count} 项", "moved_to_account_banner.text": "你的账号 {disabledAccount} 已禁用,因为你已迁移到 {movedToAccount}。", "mute_modal.hide_from_notifications": "从通知中隐藏", "mute_modal.hide_options": "隐藏选项", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index 78d191efa38..f6e4e3967b1 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -589,6 +589,7 @@ "load_pending": "{count, plural, other {# 個新項目}}", "loading_indicator.label": "正在載入...", "media_gallery.hide": "隱藏", + "minicard.more_items": "+ 其餘 {count} 個", "moved_to_account_banner.text": "您的帳號 {disabledAccount} 目前已停用,因為您已搬家至 {movedToAccount}。", "mute_modal.hide_from_notifications": "於推播通知中隱藏", "mute_modal.hide_options": "隱藏選項", diff --git a/config/locales/cy.yml b/config/locales/cy.yml index a74a51d31f0..1f38ea6eeea 100644 --- a/config/locales/cy.yml +++ b/config/locales/cy.yml @@ -904,6 +904,7 @@ cy: publish_statistics: Cyhoeddi ystadegau title: Darganfod trends: Tueddiadau + wrapstodon: Wrapstodon domain_blocks: all: I bawb disabled: I neb diff --git a/config/locales/lt.yml b/config/locales/lt.yml index 33028404502..6666a4d7bd8 100644 --- a/config/locales/lt.yml +++ b/config/locales/lt.yml @@ -1033,7 +1033,7 @@ lt: merge_long: Išsaugoti esančius įrašus ir pridėti naujus overwrite: Perrašyti overwrite_long: Pakeisti senus įrašus naujais - preface: Gali importuoti duomenis, kuriuos eksportavai iš kito serverio, pavyzdžiui, sekamų arba blokuojamų žmonių sąrašą. + preface: Galite importuoti duomenis, kuriuos eksportavote iš kito serverio kaip sekamų arba blokuojamų žmonių sąrašą. success: Jūsų informacija sėkmingai įkelta ir bus apdorota kaip įmanoma greičiau types: blocking: Blokuojamų sąrašas From be00db4fa3164f89090f60676231ecb0792e2f77 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 16 Jan 2026 10:49:53 +0100 Subject: [PATCH 51/89] Use snowflake ids for collections and their items (#37514) --- ...53219_use_snowflake_ids_for_collections.rb | 25 +++++++++++++++++++ db/schema.rb | 6 ++--- 2 files changed, 28 insertions(+), 3 deletions(-) create mode 100644 db/migrate/20260115153219_use_snowflake_ids_for_collections.rb diff --git a/db/migrate/20260115153219_use_snowflake_ids_for_collections.rb b/db/migrate/20260115153219_use_snowflake_ids_for_collections.rb new file mode 100644 index 00000000000..25aad0732ed --- /dev/null +++ b/db/migrate/20260115153219_use_snowflake_ids_for_collections.rb @@ -0,0 +1,25 @@ +# frozen_string_literal: true + +class UseSnowflakeIdsForCollections < ActiveRecord::Migration[8.0] + def up + safety_assured do + execute(<<~SQL.squish) + ALTER TABLE collections ALTER COLUMN id SET DEFAULT timestamp_id('collections'); + ALTER TABLE collection_items ALTER COLUMN id SET DEFAULT timestamp_id('collection_items'); + SQL + end + + Mastodon::Snowflake.ensure_id_sequences_exist + end + + def down + execute(<<~SQL.squish) + LOCK collections; + SELECT setval('collections_id_seq', (SELECT MAX(id) FROM collections)); + ALTER TABLE collections ALTER COLUMN id SET DEFAULT nextval('collections_id_seq'); + LOCK collection_items; + SELECT setval('collection_items_id_seq', (SELECT MAX(id) FROM collection_items)); + ALTER TABLE collection_items ALTER COLUMN id SET DEFAULT nextval('collection_items_id_seq'); + SQL + end +end diff --git a/db/schema.rb b/db/schema.rb index 78d7ef68261..8dc1e88fd17 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_12_17_091936) do +ActiveRecord::Schema[8.0].define(version: 2026_01_15_153219) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -352,7 +352,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_17_091936) do t.index ["reference_account_id"], name: "index_canonical_email_blocks_on_reference_account_id" end - create_table "collection_items", force: :cascade do |t| + create_table "collection_items", id: :bigint, default: -> { "timestamp_id('collection_items'::text)" }, force: :cascade do |t| t.bigint "collection_id", null: false t.bigint "account_id" t.integer "position", default: 1, null: false @@ -369,7 +369,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_17_091936) do t.index ["object_uri"], name: "index_collection_items_on_object_uri", unique: true, where: "(activity_uri IS NOT NULL)" end - create_table "collections", force: :cascade do |t| + create_table "collections", id: :bigint, default: -> { "timestamp_id('collections'::text)" }, force: :cascade do |t| t.bigint "account_id", null: false t.string "name", null: false t.text "description", null: false From e58084a85f6ccf64325c9af6f71ed7ef030fed65 Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 16 Jan 2026 11:00:04 +0100 Subject: [PATCH 52/89] Update `theme_color_tags` helper for new theme infrastructure (#37519) --- app/helpers/theme_helper.rb | 15 +++++++-------- app/views/layouts/application.html.haml | 2 +- spec/helpers/theme_helper_spec.rb | 18 +++++++++--------- 3 files changed, 17 insertions(+), 18 deletions(-) diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index 1d642056809..e7655414145 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -28,14 +28,17 @@ module ThemeHelper end end - def theme_color_tags(theme) - if theme == 'system' + def theme_color_tags(color_scheme) + case color_scheme + when 'auto' ''.html_safe.tap do |tags| tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:dark], media: '(prefers-color-scheme: dark)') tags << tag.meta(name: 'theme-color', content: Themes::THEME_COLORS[:light], media: '(prefers-color-scheme: light)') end - else - tag.meta name: 'theme-color', content: theme_color_for(theme) + when 'light' + tag.meta name: 'theme-color', content: Themes::THEME_COLORS[:light] + when 'dark' + tag.meta name: 'theme-color', content: Themes::THEME_COLORS[:dark] end end @@ -65,8 +68,4 @@ module ThemeHelper Setting.custom_css&.then { |content| Digest::SHA256.hexdigest(content) } end end - - def theme_color_for(theme) - theme == 'mastodon-light' ? Themes::THEME_COLORS[:light] : Themes::THEME_COLORS[:dark] - end end diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml index 0cbd51c85b0..15f1e961ed6 100755 --- a/app/views/layouts/application.html.haml +++ b/app/views/layouts/application.html.haml @@ -21,7 +21,7 @@ %link{ rel: 'mask-icon', href: frontend_asset_path('images/logo-symbol-icon.svg'), color: '#6364FF' }/ %link{ rel: 'manifest', href: manifest_path(format: :json) }/ = javascript_inline_tag 'theme-selection.js' - = theme_color_tags current_theme + = theme_color_tags color_scheme %meta{ name: 'mobile-web-app-capable', content: 'yes' }/ %title= html_title diff --git a/spec/helpers/theme_helper_spec.rb b/spec/helpers/theme_helper_spec.rb index 9eefa01f902..9e8119a7429 100644 --- a/spec/helpers/theme_helper_spec.rb +++ b/spec/helpers/theme_helper_spec.rb @@ -49,12 +49,12 @@ RSpec.describe ThemeHelper do end describe 'theme_color_tags' do - let(:result) { helper.theme_color_tags(theme) } + let(:result) { helper.theme_color_tags(color_scheme) } context 'when using system theme' do - let(:theme) { 'system' } + let(:color_scheme) { 'auto' } - it 'returns the mastodon-light and default stylesheets with correct color schemes' do + it 'returns both color schemes with appropriate media queries' do expect(html_theme_colors.first.attributes.symbolize_keys) .to include( content: have_attributes(value: Themes::THEME_COLORS[:dark]), @@ -68,10 +68,10 @@ RSpec.describe ThemeHelper do end end - context 'when using mastodon-light theme' do - let(:theme) { 'mastodon-light' } + context 'when light color scheme' do + let(:color_scheme) { 'light' } - it 'returns the theme stylesheet without color scheme information' do + it 'returns the light color' do expect(html_theme_colors.first.attributes.symbolize_keys) .to include( content: have_attributes(value: Themes::THEME_COLORS[:light]) @@ -79,10 +79,10 @@ RSpec.describe ThemeHelper do end end - context 'when using other theme' do - let(:theme) { 'contrast' } + context 'when using dark color scheme' do + let(:color_scheme) { 'dark' } - it 'returns the theme stylesheet without color scheme information' do + it 'returns the dark color' do expect(html_theme_colors.first.attributes.symbolize_keys) .to include( content: have_attributes(value: Themes::THEME_COLORS[:dark]) From 918563704f5ff5eeb1efa31a412ac38f97781e9e Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 16 Jan 2026 11:28:50 +0100 Subject: [PATCH 53/89] =?UTF-8?q?Update=20the=20=E2=80=9Cembedded=E2=80=9D?= =?UTF-8?q?=20styling=20to=20the=20new=20theme=20infrastructure=20(#37520)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/views/layouts/embedded.html.haml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/views/layouts/embedded.html.haml b/app/views/layouts/embedded.html.haml index b802854ae72..d815b59140e 100644 --- a/app/views/layouts/embedded.html.haml +++ b/app/views/layouts/embedded.html.haml @@ -1,5 +1,5 @@ !!! 5 -%html{ lang: I18n.locale } +%html{ lang: I18n.locale, 'data-contrast': 'auto', 'data-color-scheme': 'light' } %head %meta{ charset: 'utf-8' }/ %meta{ name: 'robots', content: 'noindex' }/ @@ -11,10 +11,11 @@ - if storage_host? %link{ rel: 'dns-prefetch', href: storage_host }/ + = javascript_inline_tag 'theme-selection.js' = vite_client_tag = vite_react_refresh_tag = vite_polyfills_tag - = theme_style_tags 'mastodon-light' + = theme_style_tags 'system' = vite_preload_file_tag "mastodon/locales/#{I18n.locale}.json" = render_initial_state = vite_typescript_tag 'embed.tsx', integrity: true, crossorigin: 'anonymous' From 047338e684a3b8fb89080b3ecbbd2f7c831e9c16 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 16 Jan 2026 13:44:49 +0100 Subject: [PATCH 54/89] Profile fields redesign (#37513) --- .../mastodon/components/mini_card/list.tsx | 36 ++++--- .../mini_card/mini_card.stories.tsx | 35 ++++--- .../components/mini_card/styles.module.css | 14 +-- .../features/account_timeline/common.ts | 5 + .../components/account_header.tsx | 33 +------ .../account_timeline/components/fields.tsx | 97 +++++++++++++++++++ .../components/fields_modal.tsx | 80 +++++++++++++++ .../account_timeline/components/links.tsx | 58 ----------- .../components/number_fields.tsx | 94 ++++++++++++++++++ .../components/redesign.module.scss | 47 +++++++++ .../features/ui/components/modal_root.jsx | 1 + app/javascript/mastodon/locales/en.json | 5 +- 12 files changed, 386 insertions(+), 119 deletions(-) create mode 100644 app/javascript/mastodon/features/account_timeline/common.ts create mode 100644 app/javascript/mastodon/features/account_timeline/components/fields.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx delete mode 100644 app/javascript/mastodon/features/account_timeline/components/links.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/number_fields.tsx create mode 100644 app/javascript/mastodon/features/account_timeline/components/redesign.module.scss diff --git a/app/javascript/mastodon/components/mini_card/list.tsx b/app/javascript/mastodon/components/mini_card/list.tsx index b5b8fbc2c80..f775e70aac8 100644 --- a/app/javascript/mastodon/components/mini_card/list.tsx +++ b/app/javascript/mastodon/components/mini_card/list.tsx @@ -11,11 +11,13 @@ import classes from './styles.module.css'; interface MiniCardListProps { cards?: (Pick & { key?: Key })[]; + className?: string; onOverflowClick?: MouseEventHandler; } export const MiniCardList: FC = ({ cards = [], + className, onOverflowClick, }) => { const { @@ -27,29 +29,37 @@ export const MiniCardList: FC = ({ maxWidth, } = useOverflow(); + if (!cards.length) { + return null; + } + return ( -
+
{cards.map((card, index) => (
- + {cards.length > 1 && ( +
+ +
+ )}
); }; diff --git a/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx index ada76011b27..60534f05f6b 100644 --- a/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx +++ b/app/javascript/mastodon/components/mini_card/mini_card.stories.tsx @@ -7,18 +7,6 @@ const meta = { title: 'Components/MiniCard', component: MiniCardList, args: { - cards: [ - { label: 'Pronouns', value: 'they/them' }, - { - label: 'Website', - value: bowie-the-db.meow, - }, - { - label: 'Free playlists', - value: soundcloud.com, - }, - { label: 'Location', value: 'Purris, France' }, - ], onOverflowClick: action('Overflow clicked'), }, render(args) { @@ -43,7 +31,22 @@ export default meta; type Story = StoryObj; -export const Default: Story = {}; +export const Default: Story = { + args: { + cards: [ + { label: 'Pronouns', value: 'they/them' }, + { + label: 'Website', + value: bowie-the-db.meow, + }, + { + label: 'Free playlists', + value: soundcloud.com, + }, + { label: 'Location', value: 'Purris, France' }, + ], + }, +}; export const LongValue: Story = { args: { @@ -60,3 +63,9 @@ export const LongValue: Story = { ], }, }; + +export const OneCard: Story = { + args: { + cards: [{ label: 'Pronouns', value: 'they/them' }], + }, +}; diff --git a/app/javascript/mastodon/components/mini_card/styles.module.css b/app/javascript/mastodon/components/mini_card/styles.module.css index d912f1e5cf5..642c08c5fac 100644 --- a/app/javascript/mastodon/components/mini_card/styles.module.css +++ b/app/javascript/mastodon/components/mini_card/styles.module.css @@ -6,7 +6,6 @@ } .list { - min-width: 0; display: flex; gap: 4px; overflow: hidden; @@ -21,16 +20,19 @@ flex-shrink: 0; } -.card { - max-width: 20vw; - overflow: hidden; -} - .more { color: var(--color-text-secondary); font-weight: 600; appearance: none; background: none; + aspect-ratio: 1; + height: 100%; + transition: all 300ms linear; +} + +.more:hover { + background-color: var(--color-bg-brand-softer); + color: var(--color-text-primary); } .hidden { diff --git a/app/javascript/mastodon/features/account_timeline/common.ts b/app/javascript/mastodon/features/account_timeline/common.ts new file mode 100644 index 00000000000..ee76c25bb74 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/common.ts @@ -0,0 +1,5 @@ +import { isClientFeatureEnabled } from '@/mastodon/utils/environment'; + +export function isRedesignEnabled() { + return isClientFeatureEnabled('profile_redesign'); +} diff --git a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx index 4244d7f7026..8fc89d206b0 100644 --- a/app/javascript/mastodon/features/account_timeline/components/account_header.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/account_header.tsx @@ -1,18 +1,16 @@ import { useCallback } from 'react'; -import { useIntl, FormattedMessage } from 'react-intl'; +import { useIntl } from 'react-intl'; import classNames from 'classnames'; import { Helmet } from 'react-helmet'; import { AccountBio } from '@/mastodon/components/account_bio'; -import { AccountFields } from '@/mastodon/components/account_fields'; import { DisplayName } from '@/mastodon/components/display_name'; import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; import LockIcon from '@/material-icons/400-24px/lock.svg?react'; import { openModal } from 'mastodon/actions/modal'; import { Avatar } from 'mastodon/components/avatar'; -import { FormattedDateWrapper } from 'mastodon/components/formatted_date'; import { Icon } from 'mastodon/components/icon'; import { AccountNote } from 'mastodon/features/account/components/account_note'; import { DomainPill } from 'mastodon/features/account/components/domain_pill'; @@ -25,10 +23,11 @@ import { useAppSelector, useAppDispatch } from 'mastodon/store'; import { AccountBadges } from './badges'; import { AccountButtons } from './buttons'; import { FamiliarFollowers } from './familiar_followers'; +import { AccountHeaderFields } from './fields'; import { AccountInfo } from './info'; -import { AccountLinks } from './links'; import { MemorialNote } from './memorial_note'; import { MovedNote } from './moved_note'; +import { AccountNumberFields } from './number_fields'; import { AccountTabs } from './tabs'; const titleFromAccount = (account: Account) => { @@ -192,32 +191,10 @@ export const AccountHeader: React.FC<{ className='account__header__content' /> -
-
-
- -
-
- -
-
- - -
+
- +
)}
diff --git a/app/javascript/mastodon/features/account_timeline/components/fields.tsx b/app/javascript/mastodon/features/account_timeline/components/fields.tsx new file mode 100644 index 00000000000..a73d92c1b69 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/fields.tsx @@ -0,0 +1,97 @@ +import { useCallback, useMemo } from 'react'; +import type { FC } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import { openModal } from '@/mastodon/actions/modal'; +import { AccountFields } from '@/mastodon/components/account_fields'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; +import { MiniCardList } from '@/mastodon/components/mini_card/list'; +import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import type { Account } from '@/mastodon/models/account'; +import { useAppDispatch } from '@/mastodon/store'; + +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + +export const AccountHeaderFields: FC<{ accountId: string }> = ({ + accountId, +}) => { + const account = useAccount(accountId); + + if (!account) { + return null; + } + + if (isRedesignEnabled()) { + return ; + } + + return ( +
+
+
+ +
+
+ +
+
+ + +
+ ); +}; + +const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => { + const htmlHandlers = useElementHandledLink(); + const cards = useMemo( + () => + account.fields.toArray().map(({ value_emojified, name_emojified }) => ({ + label: ( + + ), + value: ( + + ), + })), + [account.emojis, account.fields, htmlHandlers], + ); + + const dispatch = useAppDispatch(); + const handleOverflowClick = useCallback(() => { + dispatch( + openModal({ + modalType: 'ACCOUNT_FIELDS', + modalProps: { accountId: account.id }, + }), + ); + }, [account.id, dispatch]); + + return ( + + ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx new file mode 100644 index 00000000000..715f6097f45 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx @@ -0,0 +1,80 @@ +import type { FC } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; + +import { DisplayName } from '@/mastodon/components/display_name'; +import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; +import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { IconButton } from '@/mastodon/components/icon_button'; +import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; +import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import CloseIcon from '@/material-icons/400-24px/close.svg?react'; + +import classes from './redesign.module.scss'; + +export const AccountFieldsModal: FC<{ + accountId: string; + onClose: () => void; +}> = ({ accountId, onClose }) => { + const intl = useIntl(); + const account = useAccount(accountId); + const htmlHandlers = useElementHandledLink(); + + if (!account) { + return ( +
+ +
+ ); + } + + return ( +
+
+ + + , + }} + /> + +
+
+ +
+ {account.fields.map((field, index) => ( +
+ + +
+ ))} +
+
+
+
+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/links.tsx b/app/javascript/mastodon/features/account_timeline/components/links.tsx deleted file mode 100644 index 2e056e4e577..00000000000 --- a/app/javascript/mastodon/features/account_timeline/components/links.tsx +++ /dev/null @@ -1,58 +0,0 @@ -import type { FC } from 'react'; - -import { useIntl } from 'react-intl'; - -import { NavLink } from 'react-router-dom'; - -import { - FollowersCounter, - FollowingCounter, - StatusesCounter, -} from '@/mastodon/components/counters'; -import { ShortNumber } from '@/mastodon/components/short_number'; -import { useAccount } from '@/mastodon/hooks/useAccount'; - -export const AccountLinks: FC<{ accountId: string }> = ({ accountId }) => { - const intl = useIntl(); - const account = useAccount(accountId); - - if (!account) { - return null; - } - - return ( -
- - - - - - - - - - - -
- ); -}; diff --git a/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx b/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx new file mode 100644 index 00000000000..20df6a0125f --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/number_fields.tsx @@ -0,0 +1,94 @@ +import type { FC } from 'react'; + +import { FormattedMessage, useIntl } from 'react-intl'; + +import classNames from 'classnames'; +import { NavLink } from 'react-router-dom'; + +import { + FollowersCounter, + FollowingCounter, + StatusesCounter, +} from '@/mastodon/components/counters'; +import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; +import { ShortNumber } from '@/mastodon/components/short_number'; +import { useAccount } from '@/mastodon/hooks/useAccount'; + +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + +export const AccountNumberFields: FC<{ accountId: string }> = ({ + accountId, +}) => { + const intl = useIntl(); + const account = useAccount(accountId); + + if (!account) { + return null; + } + + return ( +
+ {!isRedesignEnabled() && ( + + + + )} + + + + + + + + + + {isRedesignEnabled() && ( + + + + + ), + }} + /> + + )} +
+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss new file mode 100644 index 00000000000..dd09f199e56 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -0,0 +1,47 @@ +.fieldList { + margin-top: 16px; +} + +.fieldNumbersWrapper { + a { + font-weight: unset; + } +} + +.modalCloseButton { + padding: 8px; + border-radius: 50%; + border: 1px solid var(--color-border-primary); +} + +.modalTitle { + flex-grow: 1; + text-align: center; +} + +.modalFieldsList { + padding: 24px; +} + +.modalFieldItem { + &:not(:first-child) { + padding-top: 12px; + } + + &:not(:last-child)::after { + content: ''; + display: block; + border-bottom: 1px solid var(--color-border-primary); + margin-top: 12px; + } + + dt { + color: var(--color-text-secondary); + font-size: 13px; + } + + dd { + font-weight: 600; + font-size: 15px; + } +} diff --git a/app/javascript/mastodon/features/ui/components/modal_root.jsx b/app/javascript/mastodon/features/ui/components/modal_root.jsx index 6f9b23042d7..0e718747d9c 100644 --- a/app/javascript/mastodon/features/ui/components/modal_root.jsx +++ b/app/javascript/mastodon/features/ui/components/modal_root.jsx @@ -85,6 +85,7 @@ export const MODAL_COMPONENTS = { 'IGNORE_NOTIFICATIONS': IgnoreNotificationsModal, 'ANNUAL_REPORT': AnnualReportModal, 'COMPOSE_PRIVACY': () => Promise.resolve({ default: VisibilityModal }), + 'ACCOUNT_FIELDS': () => import('mastodon/features/account_timeline/components/fields_modal.tsx').then(module => ({ default: module.AccountFieldsModal })), }; export default class ModalRoot extends PureComponent { diff --git a/app/javascript/mastodon/locales/en.json b/app/javascript/mastodon/locales/en.json index 77dd9538871..afccb038570 100644 --- a/app/javascript/mastodon/locales/en.json +++ b/app/javascript/mastodon/locales/en.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Go to profile", "account.hide_reblogs": "Hide boosts from @{name}", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "Joined on {date}", "account.joined_short": "Joined", "account.languages": "Change subscribed languages", "account.link_verified_on": "Ownership of this link was checked on {date}", @@ -90,6 +91,8 @@ "account.unmute": "Unmute @{name}", "account.unmute_notifications_short": "Unmute notifications", "account.unmute_short": "Unmute", + "account_fields_modal.close": "Close", + "account_fields_modal.title": "{name}'s info", "account_note.placeholder": "Click to add note", "admin.dashboard.daily_retention": "User retention rate by day after sign-up", "admin.dashboard.monthly_retention": "User retention rate by month after sign-up", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading…", "media_gallery.hide": "Hide", - "minicard.more_items": "+ {count} more", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "mute_modal.hide_from_notifications": "Hide from notifications", "mute_modal.hide_options": "Hide options", From e715bb50758687ade8cb574f4eebd6ad3918fc7e Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 16 Jan 2026 14:57:48 +0100 Subject: [PATCH 55/89] Add high-contrast styles to common stylesheet (#37523) --- app/javascript/styles/contrast.scss | 1 - .../styles/mastodon/components.scss | 41 +++++++++++++++---- .../styles/mastodon/high-contrast.scss | 40 ------------------ spec/helpers/theme_helper_spec.rb | 2 +- 4 files changed, 34 insertions(+), 50 deletions(-) delete mode 100644 app/javascript/styles/mastodon/high-contrast.scss diff --git a/app/javascript/styles/contrast.scss b/app/javascript/styles/contrast.scss index 93bbfed0805..cc23627a15a 100644 --- a/app/javascript/styles/contrast.scss +++ b/app/javascript/styles/contrast.scss @@ -1,2 +1 @@ @use 'common'; -@use 'mastodon/high-contrast'; diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index 95460764030..6526b380df3 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -39,8 +39,8 @@ color: var(--color-text-error); } - &:hover, - &:active { + &:not(:disabled):hover, + &:not(:disabled):active { text-decoration: underline; } @@ -1081,20 +1081,36 @@ body > [data-popper-placement] { } a { + --text-decoration-default: none; + --text-decoration-hover: underline; + + [data-contrast='high'] & { + --text-decoration-default: underline; + --text-decoration-hover: none; + } + color: var(--color-text-status-links); - text-decoration: none; + text-decoration: var(--text-decoration-default); unicode-bidi: isolate; - &:hover { - text-decoration: underline; + &:hover, + &:focus, + &:active { + text-decoration: var(--text-decoration-hover); } &.mention { - &:hover { - text-decoration: none; + text-decoration: none; + span { + text-decoration: var(--text-decoration-default); + } + + &:hover, + &:focus, + &:active { span { - text-decoration: underline; + text-decoration: var(--text-decoration-hover); } } } @@ -1356,6 +1372,15 @@ body > [data-popper-placement] { text-decoration: underline; } + [data-contrast='high'] & { + text-decoration: underline; + + &:hover, + &:active { + text-decoration: none; + } + } + .icon { width: 15px; height: 15px; diff --git a/app/javascript/styles/mastodon/high-contrast.scss b/app/javascript/styles/mastodon/high-contrast.scss deleted file mode 100644 index f55e7fae3b8..00000000000 --- a/app/javascript/styles/mastodon/high-contrast.scss +++ /dev/null @@ -1,40 +0,0 @@ -.status__content a, -.reply-indicator__content a, -.edit-indicator__content a, -.link-footer a, -.status__content__read-more-button, -.status__content__translate-button { - text-decoration: underline; - - &:hover, - &:focus, - &:active { - text-decoration: none; - } - - &.mention { - text-decoration: none; - - span { - text-decoration: underline; - } - - &:hover, - &:focus, - &:active { - span { - text-decoration: none; - } - } - } -} - -.link-button:disabled { - cursor: not-allowed; - - &:hover, - &:focus, - &:active { - text-decoration: none !important; - } -} diff --git a/spec/helpers/theme_helper_spec.rb b/spec/helpers/theme_helper_spec.rb index 9e8119a7429..35798cced6e 100644 --- a/spec/helpers/theme_helper_spec.rb +++ b/spec/helpers/theme_helper_spec.rb @@ -41,7 +41,7 @@ RSpec.describe ThemeHelper do it 'returns the theme stylesheet without color scheme information' do expect(html_links.first.attributes.symbolize_keys) .to include( - href: have_attributes(value: match(/contrast/)), + href: have_attributes(value: match(/default/)), media: have_attributes(value: 'all') ) end From 5b5b00055ec54f3949991662823b10c8a87c534a Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:35:42 +0100 Subject: [PATCH 56/89] Update dependency pino to v10.2.0 (#37507) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index c93adccaa13..8309b1e4f55 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10669,8 +10669,8 @@ __metadata: linkType: hard "pino@npm:^10.0.0": - version: 10.1.1 - resolution: "pino@npm:10.1.1" + version: 10.2.0 + resolution: "pino@npm:10.2.0" dependencies: "@pinojs/redact": "npm:^0.4.0" atomic-sleep: "npm:^1.0.0" @@ -10685,7 +10685,7 @@ __metadata: thread-stream: "npm:^4.0.0" bin: pino: bin.js - checksum: 10c0/c0a78ae4209b81879e3b2c72abd523c6045412066d214b7577f4073b23369751bbb7a1c6abb691bdf347ac81fab5e9cad9d8db0612fe2e5a9607ef895651a1bb + checksum: 10c0/8f88a2e205508d47ef04d2a6ec26ec450abb4b344d2d998d2e24b9e624e1a1ef7184f260ca5be06bc3733aa1ad76704657e373b359c7b71489a11709227e26da languageName: node linkType: hard From a9bdf5eef9b8b6b1742f172e02128776d9bf9dfd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 15:35:51 +0100 Subject: [PATCH 57/89] Update dependency pg-connection-string to v2.10.0 (#37495) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 8309b1e4f55..ccec52e40a9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10507,9 +10507,9 @@ __metadata: linkType: hard "pg-connection-string@npm:^2.6.0, pg-connection-string@npm:^2.9.1": - version: 2.9.1 - resolution: "pg-connection-string@npm:2.9.1" - checksum: 10c0/9a646529bbc0843806fc5de98ce93735a4612b571f11867178a85665d11989a827e6fd157388ca0e34ec948098564fce836c178cfd499b9f0e8cd9972b8e2e5c + version: 2.10.0 + resolution: "pg-connection-string@npm:2.10.0" + checksum: 10c0/6639d3e12f66bc3cc5fa1e5794b4b22a4212e30424cbf001422e041c77c598ee0cd19395a9d79cb99a962da612b03c021e8148c3437cf01a29a18437bad193eb languageName: node linkType: hard From 5727b866b14d29d696f2ef20e92ad4c22ff43c1d Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 16 Jan 2026 15:44:58 +0100 Subject: [PATCH 58/89] Fix error pages not using the new theming infrastructure (#37524) --- app/javascript/entrypoints/theme-selection.ts | 1 + app/views/layouts/error.html.haml | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 app/javascript/entrypoints/theme-selection.ts diff --git a/app/javascript/entrypoints/theme-selection.ts b/app/javascript/entrypoints/theme-selection.ts new file mode 100644 index 00000000000..76e46e15f19 --- /dev/null +++ b/app/javascript/entrypoints/theme-selection.ts @@ -0,0 +1 @@ +import '../inline/theme-selection'; diff --git a/app/views/layouts/error.html.haml b/app/views/layouts/error.html.haml index f85a671a531..03cb93dba7b 100644 --- a/app/views/layouts/error.html.haml +++ b/app/views/layouts/error.html.haml @@ -1,5 +1,5 @@ !!! -%html{ lang: I18n.locale } +%html{ lang: I18n.locale, 'data-contrast': 'auto', 'data-color-scheme': 'auto' } %head %meta{ 'content' => 'text/html; charset=UTF-8', 'http-equiv' => 'Content-Type' }/ %meta{ charset: 'utf-8' }/ @@ -8,6 +8,7 @@ = vite_client_tag = vite_react_refresh_tag = vite_polyfills_tag + = vite_typescript_tag 'theme-selection.ts', crossorigin: 'anonymous', blocking: 'render' = theme_style_tags Setting.default_settings['theme'] = vite_typescript_tag 'error.ts', crossorigin: 'anonymous' %body.error From e21cb9f0d08778000af4ae291e41fad633dff292 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 16:43:36 +0100 Subject: [PATCH 59/89] Update dependency pg to v8.17.1 (#37494) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/yarn.lock b/yarn.lock index ccec52e40a9..7cfdb5e4d30 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10499,14 +10499,14 @@ __metadata: languageName: node linkType: hard -"pg-cloudflare@npm:^1.2.7": - version: 1.2.7 - resolution: "pg-cloudflare@npm:1.2.7" - checksum: 10c0/8a52713dbdecc9d389dc4e65e3b7ede2e199ec3715f7491ee80a15db171f2d75677a102e9c2cef0cb91a2f310e91f976eaec0dd6ef5d8bf357de0b948f9d9431 +"pg-cloudflare@npm:^1.3.0": + version: 1.3.0 + resolution: "pg-cloudflare@npm:1.3.0" + checksum: 10c0/b0866c88af8e54c7b3ed510719d92df37714b3af5e3a3a10d9f761fcec99483e222f5b78a1f2de590368127648087c45c01aaf66fadbe46edb25673eedc4f8fc languageName: node linkType: hard -"pg-connection-string@npm:^2.6.0, pg-connection-string@npm:^2.9.1": +"pg-connection-string@npm:^2.10.0, pg-connection-string@npm:^2.6.0": version: 2.10.0 resolution: "pg-connection-string@npm:2.10.0" checksum: 10c0/6639d3e12f66bc3cc5fa1e5794b4b22a4212e30424cbf001422e041c77c598ee0cd19395a9d79cb99a962da612b03c021e8148c3437cf01a29a18437bad193eb @@ -10520,19 +10520,19 @@ __metadata: languageName: node linkType: hard -"pg-pool@npm:^3.10.1": - version: 3.10.1 - resolution: "pg-pool@npm:3.10.1" +"pg-pool@npm:^3.11.0": + version: 3.11.0 + resolution: "pg-pool@npm:3.11.0" peerDependencies: pg: ">=8.0" - checksum: 10c0/a00916b7df64226cc597fe769e3a757ff9b11562dc87ce5b0a54101a18c1fe282daaa2accaf27221e81e1e4cdf4da6a33dab09614734d32904d6c4e11c44a079 + checksum: 10c0/4b104b48a47257a0edad0c62e5ea1908b72cb79386270264b452e69895e9e4c589d00cdbf6e46d4e9c05bc7e7d191656b66814b5282d65f33b12648a21df3c7f languageName: node linkType: hard -"pg-protocol@npm:*, pg-protocol@npm:^1.10.3": - version: 1.10.3 - resolution: "pg-protocol@npm:1.10.3" - checksum: 10c0/f7ef54708c93ee6d271e37678296fc5097e4337fca91a88a3d99359b78633dbdbf6e983f0adb34b7cdd261b7ec7266deb20c3233bf3dfdb498b3e1098e8750b9 +"pg-protocol@npm:*, pg-protocol@npm:^1.11.0": + version: 1.11.0 + resolution: "pg-protocol@npm:1.11.0" + checksum: 10c0/93e83581781418c9173eba4e4545f73392cfe66b78dd1d3624d7339fbd37e7f4abebaf2615e68e0701a9bf0edf5b81a4ad533836f388f775fe25fa24a691c464 languageName: node linkType: hard @@ -10550,13 +10550,13 @@ __metadata: linkType: hard "pg@npm:^8.5.0": - version: 8.16.3 - resolution: "pg@npm:8.16.3" + version: 8.17.1 + resolution: "pg@npm:8.17.1" dependencies: - pg-cloudflare: "npm:^1.2.7" - pg-connection-string: "npm:^2.9.1" - pg-pool: "npm:^3.10.1" - pg-protocol: "npm:^1.10.3" + pg-cloudflare: "npm:^1.3.0" + pg-connection-string: "npm:^2.10.0" + pg-pool: "npm:^3.11.0" + pg-protocol: "npm:^1.11.0" pg-types: "npm:2.2.0" pgpass: "npm:1.0.5" peerDependencies: @@ -10567,7 +10567,7 @@ __metadata: peerDependenciesMeta: pg-native: optional: true - checksum: 10c0/a6a407ff0efb7599760d72ffdcda47a74c34c0fd71d896623caac45cf2cfb0f49a10973cce23110f182b9810639a1e9f6904454d7358c7001574ee0ffdcbce2a + checksum: 10c0/39a92391adfc73f793d195b4062bc2d21aa3537073e3973f3979d72901d92a59e129f31f42577ff916038a6c3f9fe423b6024717529609ae8548fda21248cfe7 languageName: node linkType: hard From cc3c7ba532f75617505bb41297651a5c14f71bdd Mon Sep 17 00:00:00 2001 From: Claire Date: Fri, 16 Jan 2026 16:55:16 +0100 Subject: [PATCH 60/89] Fix `system` theme being included twice (#37526) --- app/helpers/theme_helper.rb | 12 ++++-------- spec/helpers/theme_helper_spec.rb | 14 +++----------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/app/helpers/theme_helper.rb b/app/helpers/theme_helper.rb index e7655414145..f651a495ffe 100644 --- a/app/helpers/theme_helper.rb +++ b/app/helpers/theme_helper.rb @@ -18,14 +18,10 @@ module ThemeHelper end def theme_style_tags(theme) - if theme == 'system' - ''.html_safe.tap do |tags| - tags << vite_stylesheet_tag('themes/mastodon-light', type: :virtual, media: 'not all and (prefers-color-scheme: dark)', crossorigin: 'anonymous') - tags << vite_stylesheet_tag('themes/default', type: :virtual, media: '(prefers-color-scheme: dark)', crossorigin: 'anonymous') - end - else - vite_stylesheet_tag "themes/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous' - end + # TODO: get rid of that when we retire the themes and perform the settings migration + theme = 'default' if %w(mastodon-light contrast system).include?(theme) + + vite_stylesheet_tag "themes/#{theme}", type: :virtual, media: 'all', crossorigin: 'anonymous' end def theme_color_tags(color_scheme) diff --git a/spec/helpers/theme_helper_spec.rb b/spec/helpers/theme_helper_spec.rb index 35798cced6e..a4028857114 100644 --- a/spec/helpers/theme_helper_spec.rb +++ b/spec/helpers/theme_helper_spec.rb @@ -9,17 +9,10 @@ RSpec.describe ThemeHelper do context 'when using "system" theme' do let(:theme) { 'system' } - it 'returns the mastodon-light and application stylesheets with correct color schemes' do + it 'returns the default theme' do expect(html_links.first.attributes.symbolize_keys) .to include( - # This is now identical to the default theme & will be unified very soon - href: have_attributes(value: match(/default/)), - media: have_attributes(value: 'not all and (prefers-color-scheme: dark)') - ) - expect(html_links.last.attributes.symbolize_keys) - .to include( - href: have_attributes(value: match(/default/)), - media: have_attributes(value: '(prefers-color-scheme: dark)') + href: have_attributes(value: match(/default/)) ) end end @@ -41,8 +34,7 @@ RSpec.describe ThemeHelper do it 'returns the theme stylesheet without color scheme information' do expect(html_links.first.attributes.symbolize_keys) .to include( - href: have_attributes(value: match(/default/)), - media: have_attributes(value: 'all') + href: have_attributes(value: match(/default/)) ) end end From f05e76e591781149ee1aeb68c93a5574cbc954e5 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:22:35 +0100 Subject: [PATCH 61/89] Update dependency postcss-preset-env to v11.1.1 (#37508) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 39 ++++++++++++++++++++++++++------------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/yarn.lock b/yarn.lock index 7cfdb5e4d30..82ce70acfd9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1386,9 +1386,9 @@ __metadata: languageName: node linkType: hard -"@csstools/postcss-alpha-function@npm:^2.0.0": - version: 2.0.0 - resolution: "@csstools/postcss-alpha-function@npm:2.0.0" +"@csstools/postcss-alpha-function@npm:^2.0.1": + version: 2.0.1 + resolution: "@csstools/postcss-alpha-function@npm:2.0.1" dependencies: "@csstools/css-color-parser": "npm:^4.0.0" "@csstools/css-parser-algorithms": "npm:^4.0.0" @@ -1397,7 +1397,7 @@ __metadata: "@csstools/utilities": "npm:^3.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/b267a4d48c132a2e0a9160a17811bf6bda09e4ff4b393d6d7643cdd8c9335a2db37d956abe8960545139cf503ed5c1c57e433456a19d7361a0f206b96291b7f7 + checksum: 10c0/7e38e245eb4a4d76dcaafd9ee1f6d1cdfc8fceabf835dd1615fbb9fb0944224d29c8d601e55db4b3ba1dd37a541c8568fd130106171ca11b012eb512ff1ec7a9 languageName: node linkType: hard @@ -1695,6 +1695,18 @@ __metadata: languageName: node linkType: hard +"@csstools/postcss-mixins@npm:^1.0.0": + version: 1.0.0 + resolution: "@csstools/postcss-mixins@npm:1.0.0" + dependencies: + "@csstools/css-parser-algorithms": "npm:^4.0.0" + "@csstools/css-tokenizer": "npm:^4.0.0" + peerDependencies: + postcss: ^8.4 + checksum: 10c0/3160f87f8c307ccdb97fe9ef3507c36e59bec11c0cfa7e486fbe7b0df96cb41b071d7e475d8ef52974bbcc4bf8a2243d5c2c849c0ba8a171aa384ba4b6c0b768 + languageName: node + linkType: hard + "@csstools/postcss-nested-calc@npm:^5.0.0": version: 5.0.0 resolution: "@csstools/postcss-nested-calc@npm:5.0.0" @@ -6341,10 +6353,10 @@ __metadata: languageName: node linkType: hard -"cssdb@npm:^8.6.0": - version: 8.6.0 - resolution: "cssdb@npm:8.6.0" - checksum: 10c0/4bb7b77ba24902e8d481e9514ec0be56e205186a2b7d9f5027fedfe718952c559c62acfd2859f92869f8090da7c2170f83d68170db5058a6ba8d9d5e8ded3b3e +"cssdb@npm:^8.7.0": + version: 8.7.0 + resolution: "cssdb@npm:8.7.0" + checksum: 10c0/8db722877a68732d378e6f734dcf343f5d62670a1df630a74f5ae4a4c1ec482223730128825c5bcd99307fcd2128160b3705a6b1508d446e80e6ab2eebc7e8bc languageName: node linkType: hard @@ -11056,10 +11068,10 @@ __metadata: linkType: hard "postcss-preset-env@npm:^11.0.0": - version: 11.0.1 - resolution: "postcss-preset-env@npm:11.0.1" + version: 11.1.1 + resolution: "postcss-preset-env@npm:11.1.1" dependencies: - "@csstools/postcss-alpha-function": "npm:^2.0.0" + "@csstools/postcss-alpha-function": "npm:^2.0.1" "@csstools/postcss-cascade-layers": "npm:^6.0.0" "@csstools/postcss-color-function": "npm:^5.0.0" "@csstools/postcss-color-function-display-p3-linear": "npm:^2.0.0" @@ -11083,6 +11095,7 @@ __metadata: "@csstools/postcss-logical-viewport-units": "npm:^4.0.0" "@csstools/postcss-media-minmax": "npm:^3.0.0" "@csstools/postcss-media-queries-aspect-ratio-number-values": "npm:^4.0.0" + "@csstools/postcss-mixins": "npm:^1.0.0" "@csstools/postcss-nested-calc": "npm:^5.0.0" "@csstools/postcss-normalize-display-values": "npm:^5.0.0" "@csstools/postcss-oklab-function": "npm:^5.0.0" @@ -11104,7 +11117,7 @@ __metadata: css-blank-pseudo: "npm:^8.0.1" css-has-pseudo: "npm:^8.0.0" css-prefers-color-scheme: "npm:^11.0.0" - cssdb: "npm:^8.6.0" + cssdb: "npm:^8.7.0" postcss-attribute-case-insensitive: "npm:^8.0.0" postcss-clamp: "npm:^4.1.0" postcss-color-functional-notation: "npm:^8.0.0" @@ -11132,7 +11145,7 @@ __metadata: postcss-selector-not: "npm:^9.0.0" peerDependencies: postcss: ^8.4 - checksum: 10c0/f4f3a01b1e20fd784c9e4233ed84718293adbf040db78f6af6f703fce0c320c921bd4131657df01099e37bd7082a815b9387ca92c4406dccd4808ceb0c7ab9ca + checksum: 10c0/ed9e0b07c82368defe1ab7f776e8b163610c9db3a79e36cf95291f65959d912261aa8c4b94734bba047fe875fcf3eb6e0dd71dec93640dfd94578cba33833c5a languageName: node linkType: hard From 4339f0ea719dd54d550c79f00a5112c34b753356 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:22:42 +0100 Subject: [PATCH 62/89] Update dependency ioredis to v5.9.2 (#37510) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 82ce70acfd9..f37750a4cc7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8563,8 +8563,8 @@ __metadata: linkType: hard "ioredis@npm:^5.3.2": - version: 5.9.1 - resolution: "ioredis@npm:5.9.1" + version: 5.9.2 + resolution: "ioredis@npm:5.9.2" dependencies: "@ioredis/commands": "npm:1.5.0" cluster-key-slot: "npm:^1.1.0" @@ -8575,7 +8575,7 @@ __metadata: redis-errors: "npm:^1.2.0" redis-parser: "npm:^3.0.0" standard-as-callback: "npm:^2.1.0" - checksum: 10c0/0993372ac380332d487e329882f247254a77dbf2d6f746e67c2211b50dda8067c0bce16c120b77ffe1eb693766dc4f36fac8cf5679231ab0c27671e34ef13449 + checksum: 10c0/9732a3adab56c63a0e76bd1a1c891e4201448c44ab31e7e9aee8e83005be07597a4969199593c0d59642fb3fcd64ce00e9a8fad6c92d80e648df20b4b2b184ce languageName: node linkType: hard From fd78aa9eab2d5dde2481a398f9c3127bc7adcde6 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:23:01 +0100 Subject: [PATCH 63/89] Update dependency haml_lint to v0.69.0 (#37531) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- Gemfile.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index f920acecbf8..3ee1f77aa0b 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -282,7 +282,7 @@ GEM rake (>= 13) googleapis-common-protos-types (1.22.0) google-protobuf (~> 4.26) - haml (7.1.0) + haml (7.2.0) temple (>= 0.8.2) thor tilt @@ -291,7 +291,7 @@ GEM activesupport (>= 5.1) haml (>= 4.0.6) railties (>= 5.1) - haml_lint (0.68.0) + haml_lint (0.69.0) haml (>= 5.0) parallel (~> 1.10) rainbow @@ -589,7 +589,7 @@ GEM ox (2.14.23) bigdecimal (>= 3.0) parallel (1.27.0) - parser (3.3.10.0) + parser (3.3.10.1) ast (~> 2.4.1) racc parslet (2.0.0) @@ -613,7 +613,7 @@ GEM net-smtp premailer (~> 1.7, >= 1.7.9) prettyprint (0.2.0) - prism (1.7.0) + prism (1.8.0) prometheus_exporter (2.3.1) webrick propshaft (1.3.1) @@ -863,7 +863,7 @@ GEM climate_control test-prof (1.5.0) thor (1.5.0) - tilt (2.6.1) + tilt (2.7.0) timeout (0.6.0) tpm-key_attestation (0.14.1) bindata (~> 2.4) From e4c6130f778c2d9c070feccbbfbcb7db275a13dc Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 19 Jan 2026 10:40:47 +0100 Subject: [PATCH 64/89] New Crowdin Translations (automated) (#37532) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/be.json | 5 ++- app/javascript/mastodon/locales/cs.json | 4 +++ app/javascript/mastodon/locales/da.json | 5 ++- app/javascript/mastodon/locales/de.json | 5 ++- app/javascript/mastodon/locales/el.json | 5 ++- app/javascript/mastodon/locales/en-GB.json | 5 ++- app/javascript/mastodon/locales/es-AR.json | 5 ++- app/javascript/mastodon/locales/es-MX.json | 5 ++- app/javascript/mastodon/locales/es.json | 1 - app/javascript/mastodon/locales/fi.json | 4 +++ app/javascript/mastodon/locales/fo.json | 5 ++- app/javascript/mastodon/locales/fr-CA.json | 10 ++++-- app/javascript/mastodon/locales/fr.json | 18 ++++++---- app/javascript/mastodon/locales/ga.json | 4 +++ app/javascript/mastodon/locales/gl.json | 6 +++- app/javascript/mastodon/locales/he.json | 4 +++ app/javascript/mastodon/locales/hu.json | 5 ++- app/javascript/mastodon/locales/is.json | 5 ++- app/javascript/mastodon/locales/it.json | 4 +++ app/javascript/mastodon/locales/nn.json | 4 +++ app/javascript/mastodon/locales/pa.json | 17 +++++++++ app/javascript/mastodon/locales/pt-PT.json | 5 ++- app/javascript/mastodon/locales/sl.json | 31 ++++++++++++++++ app/javascript/mastodon/locales/sq.json | 5 ++- app/javascript/mastodon/locales/vi.json | 5 ++- app/javascript/mastodon/locales/zh-CN.json | 5 ++- app/javascript/mastodon/locales/zh-TW.json | 5 ++- config/locales/activerecord.sl.yml | 6 ++++ config/locales/ko.yml | 2 +- config/locales/nn.yml | 1 + config/locales/simple_form.sl.yml | 5 +++ config/locales/sl.yml | 41 ++++++++++++++++++++++ 32 files changed, 209 insertions(+), 28 deletions(-) diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index caf49397770..02f88f1513a 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Перайсці да профілю", "account.hide_reblogs": "Схаваць пашырэнні ад @{name}", "account.in_memoriam": "У памяць.", + "account.joined_long": "Далучыў(-ла)ся {date}", "account.joined_short": "Далучыўся", "account.languages": "Змяніць выбраныя мовы", "account.link_verified_on": "Права ўласнасці на гэтую спасылку праверана {date}", @@ -90,6 +91,8 @@ "account.unmute": "Не ігнараваць @{name}", "account.unmute_notifications_short": "Апавяшчаць", "account.unmute_short": "Не ігнараваць", + "account_fields_modal.close": "Закрыць", + "account_fields_modal.title": "Інфармацыя пра {name}", "account_note.placeholder": "Націсніце, каб дадаць нататку", "admin.dashboard.daily_retention": "Штодзённы паказчык утрымання карыстальнікаў пасля рэгістрацыі", "admin.dashboard.monthly_retention": "Штомесячны паказчык утрымання карыстальнікаў пасля рэгістрацыі", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# новы элемент} few {# новыя элементы} many {# новых элементаў} other {# новых элементаў}}", "loading_indicator.label": "Ідзе загрузка…", "media_gallery.hide": "Схаваць", - "minicard.more_items": "+ яшчэ {count}", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Ваш уліковы запіс {disabledAccount} зараз адключаны, таму што Вы перайшлі на {movedToAccount}.", "mute_modal.hide_from_notifications": "Схаваць з апавяшчэнняў", "mute_modal.hide_options": "Схаваць опцыі", diff --git a/app/javascript/mastodon/locales/cs.json b/app/javascript/mastodon/locales/cs.json index 447bbf56ea0..b29ca9c3521 100644 --- a/app/javascript/mastodon/locales/cs.json +++ b/app/javascript/mastodon/locales/cs.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Přejít na profil", "account.hide_reblogs": "Skrýt boosty od @{name}", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "Přidali se {date}", "account.joined_short": "Připojen/a", "account.languages": "Změnit odebírané jazyky", "account.link_verified_on": "Vlastnictví tohoto odkazu bylo zkontrolováno {date}", @@ -90,6 +91,8 @@ "account.unmute": "Zrušit skrytí @{name}", "account.unmute_notifications_short": "Zrušit ztlumení oznámení", "account.unmute_short": "Zrušit skrytí", + "account_fields_modal.close": "Zavřít", + "account_fields_modal.title": "info o {name}", "account_note.placeholder": "Klikněte pro přidání poznámky", "admin.dashboard.daily_retention": "Míra udržení uživatelů podle dne po registraci", "admin.dashboard.monthly_retention": "Míra udržení uživatelů podle měsíce po registraci", @@ -589,6 +592,7 @@ "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", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Váš účet {disabledAccount} je momentálně deaktivován, protože jste se přesunul/a na {movedToAccount}.", "mute_modal.hide_from_notifications": "Skrýt z oznámení", "mute_modal.hide_options": "Skrýt možnosti", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index a95303c0f2c..bc738a0ee5f 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Skjul fremhævelser fra @{name}", "account.in_memoriam": "Til minde om.", + "account.joined_long": "Tilmeldt {date}", "account.joined_short": "Oprettet", "account.languages": "Skift abonnementssprog", "account.link_verified_on": "Ejerskab af dette link blev tjekket {date}", @@ -90,6 +91,8 @@ "account.unmute": "Vis @{name} igen", "account.unmute_notifications_short": "Vis notifikationer igen", "account.unmute_short": "Vis igen", + "account_fields_modal.close": "Luk", + "account_fields_modal.title": "Information om {name}", "account_note.placeholder": "Klik for at tilføje notat", "admin.dashboard.daily_retention": "Brugerfastholdelsesrate pr. dag efter tilmelding", "admin.dashboard.monthly_retention": "Brugerfastholdelsesrate pr. måned efter tilmelding", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# nyt element} other {# nye elementer}}", "loading_indicator.label": "Indlæser…", "media_gallery.hide": "Skjul", - "minicard.more_items": "+ {count} mere", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Din konto {disabledAccount} er i øjeblikket deaktiveret, fordi du er flyttet til {movedToAccount}.", "mute_modal.hide_from_notifications": "Skjul fra notifikationer", "mute_modal.hide_options": "Skjul valgmuligheder", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index f475865203d..52fa21bead1 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Profil aufrufen", "account.hide_reblogs": "Geteilte Beiträge von @{name} ausblenden", "account.in_memoriam": "Zum Andenken.", + "account.joined_long": "Registriert am {date}", "account.joined_short": "Registriert am", "account.languages": "Sprachen verwalten", "account.link_verified_on": "Das Profil mit dieser E-Mail-Adresse wurde bereits am {date} bestätigt", @@ -90,6 +91,8 @@ "account.unmute": "Stummschaltung von @{name} aufheben", "account.unmute_notifications_short": "Stummschaltung der Benachrichtigungen aufheben", "account.unmute_short": "Stummschaltung aufheben", + "account_fields_modal.close": "Schließen", + "account_fields_modal.title": "Informationen über {name}", "account_note.placeholder": "Klicken, um private Anmerkung hinzuzufügen", "admin.dashboard.daily_retention": "Verweildauer der Nutzer*innen pro Tag seit der Registrierung", "admin.dashboard.monthly_retention": "Verweildauer der Nutzer*innen pro Monat seit der Registrierung", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# neuer Beitrag} other {# neue Beiträge}}", "loading_indicator.label": "Lädt …", "media_gallery.hide": "Ausblenden", - "minicard.more_items": "+ {count} weitere", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Dein Konto {disabledAccount} ist derzeit deaktiviert, weil du zu {movedToAccount} umgezogen bist.", "mute_modal.hide_from_notifications": "Auch aus den Benachrichtigungen entfernen", "mute_modal.hide_options": "Optionen ausblenden", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 9903fed3b43..c67074ea2da 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Μετάβαση στο προφίλ", "account.hide_reblogs": "Απόκρυψη ενισχύσεων από @{name}", "account.in_memoriam": "Εις μνήμην.", + "account.joined_long": "Έγινε μέλος {date}", "account.joined_short": "Έγινε μέλος", "account.languages": "Αλλαγή εγγεγραμμένων γλωσσών", "account.link_verified_on": "Η ιδιοκτησία αυτού του συνδέσμου ελέχθηκε στις {date}", @@ -90,6 +91,8 @@ "account.unmute": "Άρση σίγασης @{name}", "account.unmute_notifications_short": "Σίγαση ειδοποιήσεων", "account.unmute_short": "Κατάργηση σίγασης", + "account_fields_modal.close": "Κλείσιμο", + "account_fields_modal.title": "Πληροφορίες {name}", "account_note.placeholder": "Κάνε κλικ για να προσθέσεις σημείωση", "admin.dashboard.daily_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά ημέρα", "admin.dashboard.monthly_retention": "Ποσοστό χρηστών που παραμένουν μετά την εγγραφή, ανά μήνα", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# νέο στοιχείο} other {# νέα στοιχεία}}", "loading_indicator.label": "Φόρτωση…", "media_gallery.hide": "Απόκρυψη", - "minicard.more_items": "+ {count} ακόμα", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Ο λογαριασμός σου {disabledAccount} είναι προσωρινά απενεργοποιημένος επειδή μεταφέρθηκες στον {movedToAccount}.", "mute_modal.hide_from_notifications": "Απόκρυψη από ειδοποιήσεις", "mute_modal.hide_options": "Απόκρυψη επιλογών", diff --git a/app/javascript/mastodon/locales/en-GB.json b/app/javascript/mastodon/locales/en-GB.json index 58912dd035d..91937f69233 100644 --- a/app/javascript/mastodon/locales/en-GB.json +++ b/app/javascript/mastodon/locales/en-GB.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Go to profile", "account.hide_reblogs": "Hide boosts from @{name}", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "Joined on {date}", "account.joined_short": "Joined", "account.languages": "Change subscribed languages", "account.link_verified_on": "Ownership of this link was checked on {date}", @@ -90,6 +91,8 @@ "account.unmute": "Unmute @{name}", "account.unmute_notifications_short": "Unmute notifications", "account.unmute_short": "Unmute", + "account_fields_modal.close": "Close", + "account_fields_modal.title": "{name}'s info", "account_note.placeholder": "Click to add note", "admin.dashboard.daily_retention": "User retention rate by day after sign-up", "admin.dashboard.monthly_retention": "User retention rate by month after sign-up", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# new item} other {# new items}}", "loading_indicator.label": "Loading…", "media_gallery.hide": "Hide", - "minicard.more_items": "+ {count} more", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Your account {disabledAccount} is currently disabled because you moved to {movedToAccount}.", "mute_modal.hide_from_notifications": "Hide from notifications", "mute_modal.hide_options": "Hide options", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 197dc7a92ae..fd4e291f171 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar adhesiones de @{name}", "account.in_memoriam": "Cuenta conmemorativa.", + "account.joined_long": "En este servidor desde el {date}", "account.joined_short": "En este servidor desde el", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "La propiedad de este enlace fue verificada el {date}", @@ -90,6 +91,8 @@ "account.unmute": "Dejar de silenciar a @{name}", "account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_short": "Dejar de silenciar", + "account_fields_modal.close": "Cerrar", + "account_fields_modal.title": "Información de {name}", "account_note.placeholder": "Hacé clic par agregar una nota", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día, después del registro", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes, después del registro", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# elemento nuevo} other {# elementos nuevos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", - "minicard.more_items": "y {count} más", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te mudaste a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar en las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index 48eaf1dc6fb..9aafae6f236 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "En memoria.", + "account.joined_long": "Se unión el {date}", "account.joined_short": "Se unió", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "El proprietario de este enlace fue comprobado el {date}", @@ -90,6 +91,8 @@ "account.unmute": "Dejar de silenciar a @{name}", "account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_short": "Dejar de silenciar", + "account_fields_modal.close": "Cerrar", + "account_fields_modal.title": "Información de {name}", "account_note.placeholder": "Haz clic para agregar una nota", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después de unirse", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después de unirse", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", - "minicard.more_items": "y {count} más", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar de las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index aa2b7588ddf..2b7d1905350 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -589,7 +589,6 @@ "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", - "minicard.more_items": "y {count} más", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar de las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index c0cd9a77cd5..db3da7bf2e7 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Siirry profiiliin", "account.hide_reblogs": "Piilota käyttäjän @{name} tehostukset", "account.in_memoriam": "Muistoissamme.", + "account.joined_long": "Liittynyt {date}", "account.joined_short": "Liittynyt", "account.languages": "Vaihda tilattuja kieliä", "account.link_verified_on": "Linkin omistus tarkistettiin {date}", @@ -90,6 +91,8 @@ "account.unmute": "Poista käyttäjän @{name} mykistys", "account.unmute_notifications_short": "Poista ilmoitusten mykistys", "account.unmute_short": "Poista mykistys", + "account_fields_modal.close": "Sulje", + "account_fields_modal.title": "Käyttäjän {name} tiedot", "account_note.placeholder": "Lisää muistiinpano napsauttamalla", "admin.dashboard.daily_retention": "Käyttäjien pysyvyys päivittäin rekisteröitymisen jälkeen", "admin.dashboard.monthly_retention": "Käyttäjien pysyvyys kuukausittain rekisteröitymisen jälkeen", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# uusi kohde} other {# uutta kohdetta}}", "loading_indicator.label": "Ladataan…", "media_gallery.hide": "Piilota", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tilisi {disabledAccount} on tällä hetkellä poissa käytöstä, koska teit siirron tiliin {movedToAccount}.", "mute_modal.hide_from_notifications": "Piilota ilmoituksista", "mute_modal.hide_options": "Piilota vaihtoehdot", diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json index 1f421614597..aac6797b046 100644 --- a/app/javascript/mastodon/locales/fo.json +++ b/app/javascript/mastodon/locales/fo.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Far til vanga", "account.hide_reblogs": "Fjal stimbran frá @{name}", "account.in_memoriam": "In memoriam.", + "account.joined_long": "Meldaði til {date}", "account.joined_short": "Gjørdist limur", "account.languages": "Broyt fylgd mál", "account.link_verified_on": "Ognarskapur av hesum leinki var eftirkannaður {date}", @@ -90,6 +91,8 @@ "account.unmute": "Doyv ikki @{name}", "account.unmute_notifications_short": "Tendra fráboðanir", "account.unmute_short": "Doyv ikki", + "account_fields_modal.close": "Lat aftur", + "account_fields_modal.title": "Upplýsingarnar hjá {name}", "account_note.placeholder": "Klikka fyri at leggja viðmerking afturat", "admin.dashboard.daily_retention": "Hvussu nógvir brúkarar eru eftir, síðani tey skrásettu seg, roknað í døgum", "admin.dashboard.monthly_retention": "Hvussu nógvir brúkarar eru eftir síðani tey skrásettu seg, roknað í mánaðum", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# nýtt evni} other {# nýggj evni}}", "loading_indicator.label": "Innlesur…", "media_gallery.hide": "Fjal", - "minicard.more_items": "+ {count} meira", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Konta tín {disabledAccount} er í løtuni óvirkin, tí tú flutti til {movedToAccount}.", "mute_modal.hide_from_notifications": "Fjal boð", "mute_modal.hide_options": "Fjal valmøguleikar", diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json index 5cf1c8b4b9f..7c8fc6358b7 100644 --- a/app/javascript/mastodon/locales/fr-CA.json +++ b/app/javascript/mastodon/locales/fr-CA.json @@ -21,7 +21,7 @@ "account.block_domain": "Bloquer le domaine {domain}", "account.block_short": "Bloquer", "account.blocked": "Bloqué·e", - "account.blocking": "Bloqué", + "account.blocking": "Bloqué·e", "account.cancel_follow_request": "Retirer cette demande d'abonnement", "account.copy": "Copier le lien du profil", "account.direct": "Mention privée @{name}", @@ -32,8 +32,8 @@ "account.enable_notifications": "Me notifier quand @{name} publie", "account.endorse": "Inclure sur profil", "account.familiar_followers_many": "Suivi par {name1}, {name2}, et {othersCount, plural, one {une autre personne que vous suivez} other {# autres personnes que vous suivez}}", - "account.familiar_followers_one": "Suivi par {name1}", - "account.familiar_followers_two": "Suivi par {name1} et {name2}", + "account.familiar_followers_one": "Suivi·e par {name1}", + "account.familiar_followers_two": "Suivi·e par {name1} et {name2}", "account.featured": "En vedette", "account.featured.accounts": "Profils", "account.featured.hashtags": "Hashtags", @@ -57,6 +57,7 @@ "account.go_to_profile": "Voir ce profil", "account.hide_reblogs": "Masquer les boosts de @{name}", "account.in_memoriam": "En souvenir de", + "account.joined_long": "Ici depuis le {date}", "account.joined_short": "Inscrit·e", "account.languages": "Changer les langues abonnées", "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}", @@ -90,6 +91,8 @@ "account.unmute": "Ne plus masquer @{name}", "account.unmute_notifications_short": "Ne plus masquer les notifications", "account.unmute_short": "Ne plus masquer", + "account_fields_modal.close": "Fermer", + "account_fields_modal.title": "Infos de {name}", "account_note.placeholder": "Cliquez pour ajouter une note", "admin.dashboard.daily_retention": "Taux de rétention des comptes par jour après inscription", "admin.dashboard.monthly_retention": "Taux de rétention des comptes par mois après inscription", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}", "loading_indicator.label": "Chargement…", "media_gallery.hide": "Masquer", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous avez déménagé sur {movedToAccount}.", "mute_modal.hide_from_notifications": "Cacher des notifications", "mute_modal.hide_options": "Masquer les options", diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json index df03661c9df..456258ca2ea 100644 --- a/app/javascript/mastodon/locales/fr.json +++ b/app/javascript/mastodon/locales/fr.json @@ -4,7 +4,7 @@ "about.default_locale": "Défaut", "about.disclaimer": "Mastodon est un logiciel libre, open-source et une marque déposée de Mastodon gGmbH.", "about.domain_blocks.no_reason_available": "Raison non disponible", - "about.domain_blocks.preamble": "Mastodon vous permet de visualiser le contenu et d'interagir avec les utilisateurs et utilisatrices de généralement n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur.", + "about.domain_blocks.preamble": "Mastodon vous permet généralement de visualiser le contenu et d'interagir avec les utilisateurs et utilisatrices de n'importe quel autre serveur dans le fédivers. Voici les exceptions qui ont été faites sur ce serveur.", "about.domain_blocks.silenced.explanation": "Vous ne verrez généralement pas les profils et le contenu de ce serveur, à moins que vous ne les recherchiez explicitement ou que vous ne choisissiez de les suivre.", "about.domain_blocks.silenced.title": "Limité", "about.domain_blocks.suspended.explanation": "Aucune donnée de ce serveur ne sera traitée, enregistrée ou échangée, rendant impossible toute interaction ou communication avec les comptes de ce serveur.", @@ -21,19 +21,19 @@ "account.block_domain": "Bloquer le domaine {domain}", "account.block_short": "Bloquer", "account.blocked": "Bloqué·e", - "account.blocking": "Bloqué", + "account.blocking": "Bloqué·e", "account.cancel_follow_request": "Annuler l'abonnement", "account.copy": "Copier le lien du profil", - "account.direct": "Mention privée @{name}", - "account.disable_notifications": "Ne plus me notifier quand @{name} publie quelque chose", + "account.direct": "Mentionner @{name} en privé", + "account.disable_notifications": "Ne plus me notifier les publications de @{name}", "account.domain_blocking": "Domaine bloqué", "account.edit_profile": "Modifier le profil", "account.edit_profile_short": "Modifier", - "account.enable_notifications": "Me notifier quand @{name} publie quelque chose", + "account.enable_notifications": "Me notifier les publications de @{name}", "account.endorse": "Recommander sur votre profil", "account.familiar_followers_many": "Suivi par {name1}, {name2}, et {othersCount, plural, one {une autre personne que vous suivez} other {# autres personnes que vous suivez}}", - "account.familiar_followers_one": "Suivi par {name1}", - "account.familiar_followers_two": "Suivi par {name1} et {name2}", + "account.familiar_followers_one": "Suivi·e par {name1}", + "account.familiar_followers_two": "Suivi·e par {name1} et {name2}", "account.featured": "En vedette", "account.featured.accounts": "Profils", "account.featured.hashtags": "Hashtags", @@ -57,6 +57,7 @@ "account.go_to_profile": "Voir le profil", "account.hide_reblogs": "Masquer les partages de @{name}", "account.in_memoriam": "En mémoire de.", + "account.joined_long": "Ici depuis le {date}", "account.joined_short": "Ici depuis", "account.languages": "Modifier les langues d'abonnements", "account.link_verified_on": "La propriété de ce lien a été vérifiée le {date}", @@ -90,6 +91,8 @@ "account.unmute": "Ne plus masquer @{name}", "account.unmute_notifications_short": "Réactiver les notifications", "account.unmute_short": "Ne plus masquer", + "account_fields_modal.close": "Fermer", + "account_fields_modal.title": "Infos de {name}", "account_note.placeholder": "Cliquez pour ajouter une note", "admin.dashboard.daily_retention": "Taux de rétention des utilisateur·rice·s par jour après inscription", "admin.dashboard.monthly_retention": "Taux de rétention des utilisateur·rice·s par mois après inscription", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nouvel élément} other {# nouveaux éléments}}", "loading_indicator.label": "Chargement…", "media_gallery.hide": "Masquer", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Votre compte {disabledAccount} est actuellement désactivé parce que vous l'avez déplacé à {movedToAccount}.", "mute_modal.hide_from_notifications": "Cacher des notifications", "mute_modal.hide_options": "Masquer les options", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index c3a57d6e09d..111a0450fff 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Téigh go dtí próifíl", "account.hide_reblogs": "Folaigh moltaí ó @{name}", "account.in_memoriam": "Ón tseanaimsir.", + "account.joined_long": "Chuaigh isteach ar {date}", "account.joined_short": "Cláraithe", "account.languages": "Athraigh teangacha foscríofa", "account.link_verified_on": "Seiceáladh úinéireacht an naisc seo ar {date}", @@ -90,6 +91,8 @@ "account.unmute": "Díbhalbhaigh @{name}", "account.unmute_notifications_short": "Díbhalbhaigh fógraí", "account.unmute_short": "Díbhalbhaigh", + "account_fields_modal.close": "Dún", + "account_fields_modal.title": "Eolas {name}", "account_note.placeholder": "Cliceáil chun nóta a chuir leis", "admin.dashboard.daily_retention": "Ráta coinneála an úsáideora de réir an lae tar éis clárú", "admin.dashboard.monthly_retention": "Ráta coinneála na n-úsáideoirí de réir na míosa tar éis dóibh clárú", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# mír nua} two {# mír nua} few {# mír nua} many {# mír nua} other {# mír nua}}", "loading_indicator.label": "Á lódáil…", "media_gallery.hide": "Folaigh", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tá do chuntas {disabledAccount} díchumasaithe faoi láthair toisc gur bhog tú go {movedToAccount}.", "mute_modal.hide_from_notifications": "Folaigh ó fhógraí", "mute_modal.hide_options": "Folaigh roghanna", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index 03a4e4acf94..430687916df 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -18,7 +18,7 @@ "account.badges.bot": "Automatizada", "account.badges.group": "Grupo", "account.block": "Bloquear @{name}", - "account.block_domain": "Agochar todo de {domain}", + "account.block_domain": "Bloquear o dominio {domain}", "account.block_short": "Bloquear", "account.blocked": "Bloqueada", "account.blocking": "Bloqueos", @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir ao perfil", "account.hide_reblogs": "Agochar promocións de @{name}", "account.in_memoriam": "Lembranzas.", + "account.joined_long": "Uníuse o {date}", "account.joined_short": "Uniuse", "account.languages": "Modificar os idiomas subscritos", "account.link_verified_on": "A propiedade desta ligazón foi verificada o {date}", @@ -90,6 +91,8 @@ "account.unmute": "Deixar de silenciar a @{name}", "account.unmute_notifications_short": "Reactivar notificacións", "account.unmute_short": "Non silenciar", + "account_fields_modal.close": "Fechar", + "account_fields_modal.title": "Info sobre {name}", "account_note.placeholder": "Preme para engadir nota", "admin.dashboard.daily_retention": "Ratio de retención de usuarias diaria após rexistrarse", "admin.dashboard.monthly_retention": "Ratio de retención de usuarias mensual após o rexistro", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# novo elemento} other {# novos elementos}}", "loading_indicator.label": "Estase a cargar…", "media_gallery.hide": "Agochar", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "A túa conta {disabledAccount} está actualmente desactivada porque movéchela a {movedToAccount}.", "mute_modal.hide_from_notifications": "Agochar nas notificacións", "mute_modal.hide_options": "Opcións ao ocultar", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 3cee7adc5e1..f01e43968ce 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -57,6 +57,7 @@ "account.go_to_profile": "מעבר לפרופיל", "account.hide_reblogs": "להסתיר הידהודים מאת @{name}", "account.in_memoriam": "פרופיל זכרון.", + "account.joined_long": "הצטרפו ב־{date}", "account.joined_short": "תאריך הצטרפות", "account.languages": "שנה רישום לשפות", "account.link_verified_on": "בעלות על הקישור הזה נבדקה לאחרונה ב{date}", @@ -90,6 +91,8 @@ "account.unmute": "הפסקת השתקת @{name}", "account.unmute_notifications_short": "הפעלת הודעות", "account.unmute_short": "ביטול השתקה", + "account_fields_modal.close": "סגירה", + "account_fields_modal.title": "הפרטים של {name}", "account_note.placeholder": "יש ללחוץ כדי להוסיף הערות", "admin.dashboard.daily_retention": "קצב שימור משתמשים יומי אחרי ההרשמה", "admin.dashboard.monthly_retention": "קצב שימור משתמשים (פר חודש) אחרי ההרשמה", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# פריט חדש} other {# פריטים חדשים}}", "loading_indicator.label": "בטעינה…", "media_gallery.hide": "להסתיר", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "חשבונך {disabledAccount} אינו פעיל כרגע עקב מעבר ל{movedToAccount}.", "mute_modal.hide_from_notifications": "להסתיר מהתראות", "mute_modal.hide_options": "הסתרת אפשרויות", diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json index edcdd2b9f35..26d9658dbdf 100644 --- a/app/javascript/mastodon/locales/hu.json +++ b/app/javascript/mastodon/locales/hu.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ugrás a profilhoz", "account.hide_reblogs": "@{name} megtolásainak elrejtése", "account.in_memoriam": "Emlékünkben.", + "account.joined_long": "Csatlakozás ideje: {date}", "account.joined_short": "Csatlakozott", "account.languages": "Feliratkozott nyelvek módosítása", "account.link_verified_on": "A linket eredetiségét ebben az időpontban ellenőriztük: {date}", @@ -90,6 +91,8 @@ "account.unmute": "@{name} némításának feloldása", "account.unmute_notifications_short": "Értesítések némításának feloldása", "account.unmute_short": "Némitás feloldása", + "account_fields_modal.close": "Bezárás", + "account_fields_modal.title": "{name} információi", "account_note.placeholder": "Kattintás jegyzet hozzáadásához", "admin.dashboard.daily_retention": "Napi regisztráció utáni felhasználómegtartási arány", "admin.dashboard.monthly_retention": "Havi regisztráció utáni felhasználómegtartási arány", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# új elem} other {# új elem}}", "loading_indicator.label": "Betöltés…", "media_gallery.hide": "Elrejtés", - "minicard.more_items": "+ {count} további", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "A(z) {disabledAccount} fiókod jelenleg le van tiltva, mert átköltöztél ide: {movedToAccount}.", "mute_modal.hide_from_notifications": "Elrejtés az értesítések közül", "mute_modal.hide_options": "Beállítások elrejtése", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index e2efff0a47d..efdce412ca2 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Fara í notandasnið", "account.hide_reblogs": "Fela endurbirtingar fyrir @{name}", "account.in_memoriam": "Minning.", + "account.joined_long": "Skáði sig {date}", "account.joined_short": "Gerðist þátttakandi", "account.languages": "Breyta tungumálum í áskrift", "account.link_verified_on": "Eignarhald á þessum tengli var athugað þann {date}", @@ -90,6 +91,8 @@ "account.unmute": "Hætta að þagga niður í @{name}", "account.unmute_notifications_short": "Hætta að þagga í tilkynningum", "account.unmute_short": "Hætta að þagga niður", + "account_fields_modal.close": "Loka", + "account_fields_modal.title": "Upplýsingar um {name}", "account_note.placeholder": "Smelltu til að bæta við minnispunkti", "admin.dashboard.daily_retention": "Hlutfall virkra notenda eftir nýskráningu eftir dögum", "admin.dashboard.monthly_retention": "Hlutfall virkra notenda eftir nýskráningu eftir mánuðum", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# nýtt atriði} other {# ný atriði}}", "loading_indicator.label": "Hleð inn…", "media_gallery.hide": "Fela", - "minicard.more_items": "+ {count} í viðbót", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Aðgangurinn þinn {disabledAccount} er óvirkur í augnablikinu vegna þess að þú fluttir þig yfir á {movedToAccount}.", "mute_modal.hide_from_notifications": "Fela úr tilkynningum", "mute_modal.hide_options": "Fela valkosti", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 8b6c70bd140..364b6227c0d 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Vai al profilo", "account.hide_reblogs": "Nascondi condivisioni da @{name}", "account.in_memoriam": "In memoria.", + "account.joined_long": "Su questa istanza dal {date}", "account.joined_short": "Iscritto", "account.languages": "Modifica le lingue d'iscrizione", "account.link_verified_on": "La proprietà di questo link è stata controllata il {date}", @@ -90,6 +91,8 @@ "account.unmute": "Riattiva @{name}", "account.unmute_notifications_short": "Riattiva notifiche", "account.unmute_short": "Attiva audio", + "account_fields_modal.close": "Chiudi", + "account_fields_modal.title": "Informazioni su {name}", "account_note.placeholder": "Clicca per aggiungere una nota", "admin.dashboard.daily_retention": "Tasso di ritenzione dell'utente per giorno, dopo la registrazione", "admin.dashboard.monthly_retention": "Tasso di ritenzione dell'utente per mese, dopo la registrazione", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nuovo oggetto} other {# nuovi oggetti}}", "loading_indicator.label": "Caricamento…", "media_gallery.hide": "Nascondi", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Il tuo profilo {disabledAccount} è correntemente disabilitato perché ti sei spostato a {movedToAccount}.", "mute_modal.hide_from_notifications": "Nascondi dalle notifiche", "mute_modal.hide_options": "Nascondi le opzioni", diff --git a/app/javascript/mastodon/locales/nn.json b/app/javascript/mastodon/locales/nn.json index e22d0f0b524..7bf3e42234e 100644 --- a/app/javascript/mastodon/locales/nn.json +++ b/app/javascript/mastodon/locales/nn.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Gå til profil", "account.hide_reblogs": "Gøym framhevingar frå @{name}", "account.in_memoriam": "Til minne om.", + "account.joined_long": "Vart med {date}", "account.joined_short": "Vart med", "account.languages": "Endre språktingingar", "account.link_verified_on": "Eigarskap for denne lenkja vart sist sjekka {date}", @@ -90,6 +91,8 @@ "account.unmute": "Opphev demping av @{name}", "account.unmute_notifications_short": "Opphev demping av varslingar", "account.unmute_short": "Opphev demping", + "account_fields_modal.close": "Lukk", + "account_fields_modal.title": "{name} sine opplysingar", "account_note.placeholder": "Klikk for å leggja til merknad", "admin.dashboard.daily_retention": "Mengda brukarar aktive ved dagar etter registrering", "admin.dashboard.monthly_retention": "Mengda brukarar aktive ved månader etter registrering", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nytt element} other {# nye element}}", "loading_indicator.label": "Lastar…", "media_gallery.hide": "Gøym", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Kontoen din, {disabledAccount} er for tida deaktivert fordi du har flytta til {movedToAccount}.", "mute_modal.hide_from_notifications": "Ikkje vis varslingar", "mute_modal.hide_options": "Gøym val", diff --git a/app/javascript/mastodon/locales/pa.json b/app/javascript/mastodon/locales/pa.json index 2520939eb77..e0ced02cbd3 100644 --- a/app/javascript/mastodon/locales/pa.json +++ b/app/javascript/mastodon/locales/pa.json @@ -49,6 +49,7 @@ "account.follows_you": "ਤੁਹਾਨੂੰ ਫ਼ਾਲੋ ਕਰਦੇ ਹਨ", "account.go_to_profile": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਜਾਓ", "account.hide_reblogs": "{name} ਵਲੋਂ ਬੂਸਟ ਨੂੰ ਲੁਕਾਓ", + "account.joined_long": "{date} ਨੂੰ ਜੁਆਇਨ ਕੀਤਾ", "account.joined_short": "ਜੁਆਇਨ ਕੀਤਾ", "account.media": "ਮੀਡੀਆ", "account.mention": "@{name} ਦਾ ਜ਼ਿਕਰ", @@ -76,6 +77,8 @@ "account.unmute": "@{name} ਲਈ ਮੌਨ ਹਟਾਓ", "account.unmute_notifications_short": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਅਣ-ਮੌਨ ਕਰੋ", "account.unmute_short": "ਮੌਨ-ਰਹਿਤ ਕਰੋ", + "account_fields_modal.close": "ਬੰਦ ਕਰੋ", + "account_fields_modal.title": "{name} ਦੀ ਜਾਣਕਾਰੀ", "account_note.placeholder": "Click to add a note", "admin.dashboard.retention.average": "ਔਸਤ", "admin.dashboard.retention.cohort_size": "ਨਵੇਂ ਵਰਤੋਂਕਾਰ", @@ -85,6 +88,13 @@ "alt_text_modal.cancel": "ਰੱਦ ਕਰੋ", "alt_text_modal.done": "ਮੁਕੰਮਲ", "announcement.announcement": "ਹੋਕਾ", + "annual_report.announcement.action_dismiss": "ਨਹੀਂ, ਧੰਨਵਾਦ", + "annual_report.nav_item.badge": "ਨਵਾਂ", + "annual_report.shared_page.donate": "ਦਾਨ ਕਰੋ", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.close": "ਬੰਦ ਕਰੋ", + "annual_report.summary.copy_link": "ਲਿੰਕ ਨੂੰ ਕਾਪੀ ਕਰੋ", + "annual_report.summary.highlighted_post.title": "ਸਭ ਤੋਂ ਵੱਧ ਹਰਮਨਪਿਆਰੀ ਪੋਸਟ", "annual_report.summary.most_used_app.most_used_app": "ਸਭ ਤੋਂ ਵੱਧ ਵਰਤੀ ਐਪ", "annual_report.summary.new_posts.new_posts": "ਨਵੀਆਂ ਪੋਸਟਾਂ", "audio.hide": "ਆਡੀਓ ਨੂੰ ਲੁਕਾਓ", @@ -273,6 +283,8 @@ "follow_suggestions.who_to_follow": "ਕਿਸ ਨੂੰ ਫ਼ਾਲੋ ਕਰੀਏ", "followed_tags": "ਫ਼ਾਲੋ ਕੀਤੇ ਹੈਸ਼ਟੈਗ", "footer.about": "ਸਾਡੇ ਬਾਰੇ", + "footer.about_mastodon": "Mastodon ਬਾਰੇ", + "footer.about_server": "{domain} ਬਾਰੇ", "footer.about_this_server": "ਇਸ ਬਾਰੇ", "footer.directory": "ਪਰੋਫਾਇਲ ਡਾਇਰੈਕਟਰੀ", "footer.get_app": "ਐਪ ਲਵੋ", @@ -293,6 +305,7 @@ "hashtag.column_settings.tag_mode.any": "ਇਹਨਾਂ ਵਿੱਚੋਂ ਕੋਈ", "hashtag.column_settings.tag_mode.none": "ਇਹਨਾਂ ਵਿੱਚੋਂ ਕੋਈ ਨਹੀਂ", "hashtag.column_settings.tag_toggle": "Include additional tags in this column", + "hashtag.feature": "ਪਰੋਫਾਇਲ ਉੱਤੇ ਫ਼ੀਚਰ", "hashtag.follow": "ਹੈਸ਼ਟੈਗ ਨੂੰ ਫ਼ਾਲੋ ਕਰੋ", "hashtag.mute": "#{hashtag} ਨੂੰ ਮੌਨ ਕਰੋ", "hashtag.unfollow": "ਹੈਸ਼ਟੈਗ ਨੂੰ ਅਣ-ਫ਼ਾਲੋ ਕਰੋ", @@ -438,6 +451,7 @@ "notification_requests.exit_selection": "ਮੁਕੰਮਲ", "notification_requests.notifications_from": "{name} ਵਲੋਂ ਨੋਟੀਫਿਕੇਸ਼ਨ", "notification_requests.title": "ਫਿਲਟਰ ਕੀਤੇ ਨੋਟੀਫਿਕੇਸ਼ਨ", + "notification_requests.view": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਵੇਖੋ", "notifications.clear": "ਸੂਚਨਾਵਾਂ ਨੂੰ ਮਿਟਾਓ", "notifications.clear_confirmation": "ਕੀ ਤੁਸੀਂ ਆਪਣੇ ਸਾਰੇ ਨੋਟੀਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਪੱਕੇ ਤੌਰ ਉੱਤੇ ਹਟਾਉਣਾ ਚਾਹੁੰਦੇ ਹੋ?", "notifications.clear_title": "ਨੋਟਫਿਕੇਸ਼ਨਾਂ ਨੂੰ ਮਿਟਾਉਣਾ ਹੈ?", @@ -481,6 +495,7 @@ "onboarding.follows.back": "ਪਿੱਛੇ", "onboarding.follows.done": "ਮੁਕੰਮਲ", "onboarding.follows.search": "ਖੋਜੋ", + "onboarding.profile.display_name": "ਦਿਖਾਇਆ ਜਾਣ ਵਾਲਾ ਨਾਂ", "onboarding.profile.note": "ਜਾਣਕਾਰੀ", "onboarding.profile.save_and_continue": "ਸੰਭਾਲੋ ਅਤੇ ਜਾਰੀ ਰੱਖੋ", "onboarding.profile.title": "ਪਰੋਫਾਈਲ ਸੈਟਅੱਪ", @@ -584,6 +599,7 @@ "status.edited": "ਆਖਰੀ ਸੋਧ ਦੀ ਤਾਰੀਖ {date}", "status.edited_x_times": "Edited {count, plural, one {# time} other {# times}}", "status.favourite": "ਪਸੰਦ", + "status.filter": "ਇਸ ਪੋਸਟ ਨੂੰ ਫਿਲਟਰ ਕਰੋ", "status.history.created": "{name} ਨੇ {date} ਨੂੰ ਬਣਾਇਆ", "status.history.edited": "{name} ਨੇ {date} ਨੂੰ ਸੋਧਿਆ", "status.load_more": "ਹੋਰ ਦਿਖਾਓ", @@ -598,6 +614,7 @@ "status.pin": "ਪਰੋਫਾਈਲ ਉੱਤੇ ਟੰਗੋ", "status.quote": "ਹਵਾਲਾ", "status.quote.cancel": "ਹਵਾਲੇ ਨੂੰ ਰੱਦ ਕਰੋ", + "status.quote_noun": "ਹਵਾਲਾ", "status.quotes_count": "{count, plural, one {{counter} ਹਵਾਲਾ} other {{counter} ਹਵਾਲੇ}}", "status.read_more": "ਹੋਰ ਪੜ੍ਹੋ", "status.reblog": "ਬੂਸਟ", diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json index 72b97b22cb9..2738e364100 100644 --- a/app/javascript/mastodon/locales/pt-PT.json +++ b/app/javascript/mastodon/locales/pt-PT.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir para o perfil", "account.hide_reblogs": "Esconder partilhas de @{name}", "account.in_memoriam": "Em Memória.", + "account.joined_long": "Juntou-se em {date}", "account.joined_short": "Juntou-se a", "account.languages": "Alterar idiomas subscritos", "account.link_verified_on": "O proprietário desta hiperligação foi verificado em {date}", @@ -90,6 +91,8 @@ "account.unmute": "Desocultar @{name}", "account.unmute_notifications_short": "Desocultar notificações", "account.unmute_short": "Desocultar", + "account_fields_modal.close": "Fechar", + "account_fields_modal.title": "Info de {name}", "account_note.placeholder": "Clicar para adicionar nota", "admin.dashboard.daily_retention": "Taxa de retenção de utilizadores por dia após a inscrição", "admin.dashboard.monthly_retention": "Taxa de retenção de utilizadores por mês após a inscrição", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# novo item} other {# novos itens}}", "loading_indicator.label": "A carregar…", "media_gallery.hide": "Esconder", - "minicard.more_items": "+ {count} mais", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "A tua conta {disabledAccount} está neste momento desativada porque migraste para {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar das notificações", "mute_modal.hide_options": "Ocultar opções", diff --git a/app/javascript/mastodon/locales/sl.json b/app/javascript/mastodon/locales/sl.json index 4e401c5c882..c7f2b15d12d 100644 --- a/app/javascript/mastodon/locales/sl.json +++ b/app/javascript/mastodon/locales/sl.json @@ -38,8 +38,10 @@ "account.follow": "Sledi", "account.follow_back": "Sledi nazaj", "account.follow_back_short": "Sledi nazaj", + "account.follow_request": "Zaprosi za sledenje", "account.follow_request_cancel": "Prekliči zahtevo", "account.follow_request_cancel_short": "Prekliči", + "account.follow_request_short": "Zaprosi", "account.followers": "Sledilci", "account.followers.empty": "Nihče še ne sledi temu uporabniku.", "account.followers_counter": "{count, plural, one {{counter} sledilec} two {{counter} sledilca} few {{counter} sledilci} other {{counter} sledilcev}}", @@ -50,6 +52,7 @@ "account.go_to_profile": "Pojdi na profil", "account.hide_reblogs": "Skrij izpostavitve od @{name}", "account.in_memoriam": "V spomin.", + "account.joined_long": "Pridružen/a {date}", "account.joined_short": "Pridružil/a", "account.languages": "Spremeni naročene jezike", "account.link_verified_on": "Lastništvo te povezave je bilo preverjeno {date}", @@ -61,13 +64,16 @@ "account.mute_notifications_short": "Utišaj obvestila", "account.mute_short": "Utišaj", "account.muted": "Utišan", + "account.muting": "Izklop zvoka", "account.mutual": "Drug drugemu sledita", "account.no_bio": "Ni opisa.", "account.open_original_page": "Odpri izvirno stran", "account.posts": "Objave", "account.posts_with_replies": "Objave in odgovori", + "account.remove_from_followers": "Odstrani {name} iz sledilcev", "account.report": "Prijavi @{name}", "account.requested_follow": "{name} vam želi slediti", + "account.requests_to_follow_you": "Vas prosi za sledenje", "account.share": "Deli profil osebe @{name}", "account.show_reblogs": "Pokaži izpostavitve osebe @{name}", "account.statuses_counter": "{count, plural, one {{counter} objava} two {{counter} objavi} few {{counter} objave} other {{counter} objav}}", @@ -80,6 +86,7 @@ "account.unmute": "Povrni glas @{name}", "account.unmute_notifications_short": "Izklopi utišanje obvestil", "account.unmute_short": "Povrni glas", + "account_fields_modal.close": "Zapri", "account_note.placeholder": "Kliknite, da dodate opombo", "admin.dashboard.daily_retention": "Mera ohranjanja uporabnikov po dnevih od registracije", "admin.dashboard.monthly_retention": "Mera ohranjanja uporabnikov po mesecih od registracije", @@ -103,11 +110,24 @@ "alt_text_modal.describe_for_people_with_visual_impairments": "Podaj opis za slabovidne ...", "alt_text_modal.done": "Opravljeno", "announcement.announcement": "Oznanilo", + "annual_report.announcement.action_dismiss": "Ne, hvala", + "annual_report.nav_item.badge": "Nov", + "annual_report.shared_page.donate": "Prispevaj", + "annual_report.summary.archetype.replier.name": "Metulj", + "annual_report.summary.archetype.reveal": "Razkrij moj arhetip", + "annual_report.summary.archetype.title_public": "Arhetip {name}", + "annual_report.summary.archetype.title_self": "Vaš arhetip", + "annual_report.summary.close": "Zapri", + "annual_report.summary.copy_link": "Kopiraj povezavo", + "annual_report.summary.followers.new_followers": "{count, plural, one {{counter} nov sledilec} two {{counter} nova sledilca} few {{counter} novi sledilci} other {{counter} novih sledilcev}}", + "annual_report.summary.highlighted_post.title": "Najbolj priljubljena objava", "annual_report.summary.most_used_app.most_used_app": "najpogosteje uporabljena aplikacija", "annual_report.summary.most_used_hashtag.most_used_hashtag": "največkrat uporabljen ključnik", "annual_report.summary.new_posts.new_posts": "nove objave", "annual_report.summary.percentile.text": "S tem ste se uvrstili med zgornjih uporabnikov domene {domain}.", "annual_report.summary.percentile.we_wont_tell_bernie": "Živi duši ne bomo povedali.", + "annual_report.summary.share_elsewhere": "Deli drugje", + "annual_report.summary.share_on_mastodon": "Deli na Mastodonu", "attachments_list.unprocessed": "(neobdelano)", "audio.hide": "Skrij zvok", "block_modal.remote_users_caveat": "Strežnik {domain} bomo pozvali, naj spoštuje vašo odločitev. Kljub temu pa ni gotovo, da bo strežnik prošnjo upošteval, saj nekateri strežniki blokiranja obravnavajo drugače. Javne objave bodo morda še vedno vidne neprijavljenim uporabnikom.", @@ -368,6 +388,8 @@ "follow_suggestions.who_to_follow": "Komu slediti", "followed_tags": "Sledeni ključniki", "footer.about": "O Mastodonu", + "footer.about_mastodon": "O Mastodonu", + "footer.about_server": "O {domain}", "footer.directory": "Imenik profilov", "footer.get_app": "Prenesite aplikacijo", "footer.keyboard_shortcuts": "Tipkovne bližnjice", @@ -812,6 +834,10 @@ "status.cancel_reblog_private": "Prekliči izpostavitev", "status.cannot_reblog": "Te objave ni mogoče izpostaviti", "status.contains_quote": "Vsebuje citat", + "status.context.loading": "Nalaganje več odgovorov", + "status.context.loading_error": "Novih odgovorov ni bilo možno naložiti", + "status.context.loading_success": "Novi odgovori naloženi", + "status.context.more_replies_found": "Najdenih več odgovorov", "status.context.retry": "Poskusi znova", "status.context.show": "Pokaži", "status.continued_thread": "Nadaljevanje niti", @@ -847,6 +873,9 @@ "status.quote_followers_only": "Samo sledilci lahko citirajo to objavo", "status.quote_policy_change": "Spremenite, kdo lahko citira", "status.quote_private": "Zasebnih objav ni možno citirati", + "status.quotes.empty": "Nihče še ni citiral te objave. Ko se bo to zgodilo, se bodo pojavile tukaj.", + "status.quotes.local_other_disclaimer": "Citati, ki jih je avtor zavrnil, ne bodo prikazani.", + "status.quotes_count": "{count, plural, one {{counter} citat} two {{counter} citata} few {{counter} citati} other {{counter} citatov}}", "status.read_more": "Preberi več", "status.reblog": "Izpostavi", "status.reblogged_by": "{name} je izpostavil/a", @@ -920,7 +949,9 @@ "video.unmute": "Odtišaj", "video.volume_down": "Zmanjšaj glasnost", "video.volume_up": "Povečaj glasnost", + "visibility_modal.button_title": "Določi vidnost", "visibility_modal.header": "Vidnost in interakcija", + "visibility_modal.helper.privacy_editing": "Vidnosti ni moč spremeniti, ko je objava objavljena.", "visibility_modal.privacy_label": "Vidnost", "visibility_modal.quote_followers": "Samo sledilci", "visibility_modal.quote_label": "Kdo lahko citira", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index 897cbd8722b..b1bf08af1bc 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Kalo te profili", "account.hide_reblogs": "Fshih përforcime nga @{name}", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "U bë pjesë më {date}", "account.joined_short": "U bë pjesë", "account.languages": "Ndryshoni gjuhë pajtimesh", "account.link_verified_on": "Pronësia e kësaj lidhjeje qe kontrolluar më {date}", @@ -90,6 +91,8 @@ "account.unmute": "Ktheji zërin @{name}", "account.unmute_notifications_short": "Shfaqi njoftimet", "account.unmute_short": "Çheshtoje", + "account_fields_modal.close": "Mbylle", + "account_fields_modal.title": "Hollësi për {name}", "account_note.placeholder": "Klikoni për të shtuar shënim", "admin.dashboard.daily_retention": "Shkallë mbajtjeje përdoruesi, në ditë, pas regjistrimit", "admin.dashboard.monthly_retention": "Shkallë mbajtjeje përdoruesi, në muaj, pas regjistrimit", @@ -587,7 +590,7 @@ "load_pending": "{count, plural,one {# objekt i ri }other {# objekte të rinj }}", "loading_indicator.label": "Po ngarkohet…", "media_gallery.hide": "Fshihe", - "minicard.more_items": "+ {count} më tepër", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Llogaria juaj {disabledAccount} aktualisht është e çaktivizuar, ngaqë kaluat te {movedToAccount}.", "mute_modal.hide_from_notifications": "Fshihe prej njoftimeve", "mute_modal.hide_options": "Fshihi mundësitë", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 6b1138ec087..59bd543b9d2 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Xem hồ sơ", "account.hide_reblogs": "Ẩn tút @{name} đăng lại", "account.in_memoriam": "Tưởng Niệm.", + "account.joined_long": "Tham gia {date}", "account.joined_short": "Tham gia", "account.languages": "Đổi ngôn ngữ mong muốn", "account.link_verified_on": "Liên kết này đã được xác minh vào {date}", @@ -90,6 +91,8 @@ "account.unmute": "Bỏ ẩn @{name}", "account.unmute_notifications_short": "Mở lại thông báo", "account.unmute_short": "Bỏ ẩn", + "account_fields_modal.close": "Đóng", + "account_fields_modal.title": "Thông tin {name}", "account_note.placeholder": "Nhấn để thêm", "admin.dashboard.daily_retention": "Tỉ lệ người dùng sau đăng ký ở lại theo ngày", "admin.dashboard.monthly_retention": "Tỉ lệ người dùng ở lại sau khi đăng ký", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, one {# tút mới} other {# tút mới}}", "loading_indicator.label": "Đang tải…", "media_gallery.hide": "Ẩn", - "minicard.more_items": "+ {count} khác", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tài khoản {disabledAccount} của bạn hiện không khả dụng vì bạn đã chuyển sang {movedToAccount}.", "mute_modal.hide_from_notifications": "Ẩn thông báo", "mute_modal.hide_options": "Ẩn tùy chọn", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 92c0eaa3f40..fecfde2c375 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -57,6 +57,7 @@ "account.go_to_profile": "前往个人资料页", "account.hide_reblogs": "隐藏来自 @{name} 的转嘟", "account.in_memoriam": "谨此悼念。", + "account.joined_long": "加入于 {date}", "account.joined_short": "加入于", "account.languages": "更改订阅语言", "account.link_verified_on": "此链接的所有权已在 {date} 检查", @@ -90,6 +91,8 @@ "account.unmute": "不再隐藏 @{name}", "account.unmute_notifications_short": "恢复通知", "account.unmute_short": "取消隐藏", + "account_fields_modal.close": "关闭", + "account_fields_modal.title": "{name} 的信息", "account_note.placeholder": "点击添加备注", "admin.dashboard.daily_retention": "注册后用户留存率(按日计算)", "admin.dashboard.monthly_retention": "注册后用户留存率(按月计算)", @@ -589,7 +592,7 @@ "load_pending": "{count} 项", "loading_indicator.label": "加载中…", "media_gallery.hide": "隐藏", - "minicard.more_items": "+ 还有 {count} 项", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "你的账号 {disabledAccount} 已禁用,因为你已迁移到 {movedToAccount}。", "mute_modal.hide_from_notifications": "从通知中隐藏", "mute_modal.hide_options": "隐藏选项", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index f6e4e3967b1..d9799ee4393 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -57,6 +57,7 @@ "account.go_to_profile": "前往個人檔案", "account.hide_reblogs": "隱藏來自 @{name} 之轉嘟", "account.in_memoriam": "謹此悼念。", + "account.joined_long": "加入於 {date}", "account.joined_short": "加入時間", "account.languages": "變更訂閱的語言", "account.link_verified_on": "已於 {date} 檢查此連結的擁有者權限", @@ -90,6 +91,8 @@ "account.unmute": "解除靜音 @{name}", "account.unmute_notifications_short": "解除靜音推播通知", "account.unmute_short": "解除靜音", + "account_fields_modal.close": "關閉", + "account_fields_modal.title": "{name} 之資訊", "account_note.placeholder": "點擊以新增備註", "admin.dashboard.daily_retention": "註冊後使用者存留率(日)", "admin.dashboard.monthly_retention": "註冊後使用者存留率(月)", @@ -589,7 +592,7 @@ "load_pending": "{count, plural, other {# 個新項目}}", "loading_indicator.label": "正在載入...", "media_gallery.hide": "隱藏", - "minicard.more_items": "+ 其餘 {count} 個", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "您的帳號 {disabledAccount} 目前已停用,因為您已搬家至 {movedToAccount}。", "mute_modal.hide_from_notifications": "於推播通知中隱藏", "mute_modal.hide_options": "隱藏選項", diff --git a/config/locales/activerecord.sl.yml b/config/locales/activerecord.sl.yml index e4c4fe598f9..cecd0444d72 100644 --- a/config/locales/activerecord.sl.yml +++ b/config/locales/activerecord.sl.yml @@ -32,6 +32,12 @@ sl: attributes: url: invalid: ni veljaven URL + collection: + attributes: + collection_items: + too_many: je preveč, dovoljenih je največ %{count} + tag: + unusable: ni možno uporabiti doorkeeper/application: attributes: website: diff --git a/config/locales/ko.yml b/config/locales/ko.yml index 62346c68597..f0ede70b9d5 100644 --- a/config/locales/ko.yml +++ b/config/locales/ko.yml @@ -1748,7 +1748,7 @@ ko: unrecognized_emoji: 인식 되지 않은 에모지입니다 redirects: prompt: 이 링크를 믿을 수 있다면, 클릭해서 계속하세요. - title: "%{instance}를 떠나려고 합니다." + title: "%{instance}을(를) 떠나려고 합니다." relationships: activity: 계정 활동 confirm_follow_selected_followers: 정말로 선택된 팔로워들을 팔로우하시겠습니까? diff --git a/config/locales/nn.yml b/config/locales/nn.yml index 9d92e42a541..792a4e28666 100644 --- a/config/locales/nn.yml +++ b/config/locales/nn.yml @@ -2190,6 +2190,7 @@ nn: error: Det oppsto et problem med å slette sikkerhetsnøkkelen. Prøv igjen. success: Sikkerhetsnøkkelen din ble vellykket slettet. invalid_credential: Ugyldig sikkerhetsnøkkel + nickname: Kallenamn nickname_hint: Skriv inn kallenavnet til din nye sikkerhetsnøkkel not_enabled: Du har ikke aktivert WebAuthn ennå not_supported: Denne nettleseren støtter ikke sikkerhetsnøkler diff --git a/config/locales/simple_form.sl.yml b/config/locales/simple_form.sl.yml index e87dd799d2a..7cddf46aaf8 100644 --- a/config/locales/simple_form.sl.yml +++ b/config/locales/simple_form.sl.yml @@ -73,6 +73,7 @@ sl: featured_tag: name: 'Tukaj je nekaj ključnikov, ki ste jih nedavno uporabili:' filters: + action: Izberite, kako naj se program vede, ko se objava sklada s filtrom actions: hide: Povsem skrij filtrirano vsebino, kot da ne bi obstajala warn: Skrij filtrirano vsebino za opozorilom, ki pomenja naslov filtra @@ -220,6 +221,8 @@ sl: setting_always_send_emails: Vedno pošlji e-obvestila setting_auto_play_gif: Samodejno predvajanje animiranih GIF-ov setting_boost_modal: Nadziraj vidnost objav + setting_color_scheme: Način + setting_contrast: Kontrast setting_default_language: Jezik objavljanja setting_default_privacy: Vidnost objav setting_default_quote_policy: Kdo lahko citira @@ -231,6 +234,7 @@ sl: setting_display_media_default: Privzeto setting_display_media_hide_all: Skrij vse setting_display_media_show_all: Prikaži vse + setting_emoji_style: Slog čustvenih simbolov setting_expand_spoilers: Vedno razširi objave, označene z opozorili o vsebini setting_hide_network: Skrij svoje omrežje setting_reduce_motion: Zmanjšanje premikanja v animacijah @@ -285,6 +289,7 @@ sl: thumbnail: Sličica strežnika trendable_by_default: Dovoli trende brez predhodnega pregleda trends: Omogoči trende + wrapstodon: Omogoči Wrapstodon interactions: must_be_follower: Blokiraj obvestila nesledilcev must_be_following: Blokiraj obvestila oseb, ki jim ne sledite diff --git a/config/locales/sl.yml b/config/locales/sl.yml index 2fc26113588..2abd3600e70 100644 --- a/config/locales/sl.yml +++ b/config/locales/sl.yml @@ -7,6 +7,8 @@ sl: hosted_on: Mastodon gostuje na %{domain} title: O programu accounts: + errors: + cannot_be_added_to_collections: Tega računa ni možno dodati v zbirke. followers: few: Sledilci one: Sledilec @@ -196,6 +198,7 @@ sl: create_relay: Ustvari rele create_unavailable_domain: Ustvari domeno, ki ni na voljo create_user_role: Ustvari vlogo + create_username_block: Ustvari pravilo uporabniškega imena demote_user: Ponižaj uporabnika destroy_announcement: Izbriši obvestilo destroy_canonical_email_block: Izbriši blokado e-naslova @@ -209,6 +212,7 @@ sl: destroy_status: Izbriši objavo destroy_unavailable_domain: Izbriši nedosegljivo domeno destroy_user_role: Uniči vlogo + destroy_username_block: Izbriši pravilo uporabniškega imena disable_2fa_user: Onemogoči disable_custom_emoji: Onemogoči emotikon po meri disable_relay: Onemogoči rele @@ -243,6 +247,7 @@ sl: update_report: Posodobi poročilo update_status: Posodobi objavo update_user_role: Posodobi vlogo + update_username_block: Posodobi pravilo uporabniškega imena actions: approve_appeal_html: "%{name} je ugodil pritožbi uporabnika %{target} na moderatorsko odločitev" approve_user_html: "%{name} je odobril/a registracijo iz %{target}" @@ -498,21 +503,30 @@ sl: fasp: debug: callbacks: + created_at: Ustvarjeno delete: Izbriši ip: Naslov IP + request_body: Telo zahteve providers: + active: Dejaven base_url: Osnovna povezava callback: Povratni klic delete: Izbriši edit: Uredi ponudnika finish_registration: Dokončaj registracijo name: Ime + providers: Ponudniki + public_key_fingerprint: Prstni odtis javnega ključa + registration_requested: Registracija je obvezna registrations: confirm: Potrdi reject: Zavrni + title: Potrdri registracijo FASP save: Shrani sign_in: Prijava status: Stanje + title: Ponudniki dodatnih storitev Fediverse + title: FASP follow_recommendations: description_html: "Sledi priporočilom pomaga novim uporabnikom, da hitro najdejo zanimivo vsebino. Če uporabnik ni dovolj komuniciral z drugimi, da bi oblikoval prilagojena priporočila za sledenje, se namesto tega priporočajo ti računi. Dnevno se ponovno izračunajo iz kombinacije računov z najvišjimi nedavnimi angažiranostmi in najvišjim številom krajevnih sledilcev za določen jezik." language: Za jezik @@ -844,10 +858,15 @@ sl: publish_statistics: Objavi statistiko title: Razkrivanje trends: Trendi + wrapstodon: Wrapstodon domain_blocks: all: Vsem disabled: Nikomur users: Prijavljenim krajevnim uporabnikom + feed_access: + modes: + authenticated: Samo overjeni uporabniki + public: Vsi landing_page: values: trends: Trendi @@ -1103,13 +1122,23 @@ sl: trending: V porastu username_blocks: add_new: Dodaj novo + block_registrations: Blokiraj registracije comparison: contains: Vsebuje + equals: Je enako contains_html: Vsebuje %{string} + created_msg: Pravilo uporabniškega imena uspešno ustvarjeno delete: Izbriši + edit: + title: Uredi pravilo uporabniškega imena + matches_exactly_html: Je enako %{string} new: create: Ustvari pravilo + title: Ustvari novo pravilo uporabniškega imena + no_username_block_selected: Nobeno pravilo uporabniškega imena ni bilo spremenjeno, ker nobeno ni bilo izbrano not_permitted: Ni dovoljeno + title: Pravila uporabniškega imena + updated_msg: Pravilo uporabniškega imena uspešno posodobljeno warning_presets: add_new: Dodaj novo delete: Izbriši @@ -1302,6 +1331,10 @@ sl: hint_html: "Namig: naslednjo uro vas ne bomo več vprašali po vašem geslu." invalid_password: Neveljavno geslo prompt: Potrdite geslo za nadaljevanje + color_scheme: + auto: Samodejno + contrast: + auto: Samodejno crypto: errors: invalid_key: ni veljaven ključ Ed25519 ali Curve25519 @@ -1629,6 +1662,7 @@ sl: author_html: Avtor/ica %{name} potentially_sensitive_content: action: Kliknite za prikaz + confirm_visit: Ali ste prepričani, da želite odpreti to povezavo? hide_button: Skrij lists: errors: @@ -1727,10 +1761,13 @@ sl: body: "%{name} vas je omenil/a v:" subject: "%{name} vas je omenil/a" title: Nova omemba + moderation_warning: + subject: Prejeli ste opozorilo moderatorjev poll: subject: Anketa, ki jo je pripravil/a %{name}, se je iztekla quote: body: 'Vašo objavo je citiral/a %{name}:' + subject: "%{name} je citiral/a vašo objavo" title: Nov citat reblog: body: 'Vašo objavo je izpostavil/a %{name}:' @@ -1962,6 +1999,7 @@ sl: reblog: Izpostavitev ne more biti pripeta quote_error: not_available: Objava ni na voljo + revoked: Avtor je umaknil objavo quote_policies: followers: Samo sledilci nobody: Samo jaz @@ -2191,8 +2229,11 @@ sl: error: Pri brisanju vašega varnostnega ključa je prišlo do težav. Poskusite znova. success: Vaš varnostni ključ je bil uspešno izbrisan. invalid_credential: Neveljaven varnostni ključ + nickname: Vzdevek nickname_hint: Vnesite vzdevek svojega novega varnostnega ključa not_enabled: Niste še omogočili WebAuthn not_supported: Ta brskalnik ne podpira varnostnih ključev otp_required: Za uporabo varnostnih ključev morate najprej omogočiti 2FA (dvostopenjsko overjanje). registered_on: Datum registracije %{date} + wrapstodon: + title: Wrapstodon %{year} za %{name} From 9b6500f74a391c8de80c67c2227bf99091cac762 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 19 Jan 2026 11:36:58 +0100 Subject: [PATCH 65/89] Skip tombstone creation on deleting from 404 (#37533) --- app/services/activitypub/fetch_remote_status_service.rb | 1 - 1 file changed, 1 deletion(-) diff --git a/app/services/activitypub/fetch_remote_status_service.rb b/app/services/activitypub/fetch_remote_status_service.rb index 0473bb5939f..e08f82f7d9a 100644 --- a/app/services/activitypub/fetch_remote_status_service.rb +++ b/app/services/activitypub/fetch_remote_status_service.rb @@ -92,7 +92,6 @@ class ActivityPub::FetchRemoteStatusService < BaseService existing_status = Status.remote.find_by(uri: uri) if existing_status&.distributable? Rails.logger.debug { "FetchRemoteStatusService - Got 404 for orphaned status with URI #{uri}, deleting" } - Tombstone.find_or_create_by(uri: uri, account: existing_status.account) RemoveStatusService.new.call(existing_status, redraft: false) end end From 220115757d45f1bb3f520df150023af8b5e61071 Mon Sep 17 00:00:00 2001 From: Claire Date: Mon, 19 Jan 2026 14:47:27 +0100 Subject: [PATCH 66/89] Fix potential duplicate handling of quote accept/reject/delete (#37537) --- app/lib/activitypub/activity/accept.rb | 2 +- app/lib/activitypub/activity/delete.rb | 2 +- app/models/quote.rb | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/app/lib/activitypub/activity/accept.rb b/app/lib/activitypub/activity/accept.rb index 144ba9645c5..92a8190c038 100644 --- a/app/lib/activitypub/activity/accept.rb +++ b/app/lib/activitypub/activity/accept.rb @@ -46,7 +46,7 @@ class ActivityPub::Activity::Accept < ActivityPub::Activity def accept_quote!(quote) approval_uri = value_or_id(first_of_value(@json['result'])) - return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local? + return if unsupported_uri_scheme?(approval_uri) || quote.quoted_account != @account || !quote.status.local? || !quote.pending? # NOTE: we are not going through `ActivityPub::VerifyQuoteService` as the `Accept` is as authoritative # as the stamp, but this means we are not checking the stamp, which may lead to inconsistencies diff --git a/app/lib/activitypub/activity/delete.rb b/app/lib/activitypub/activity/delete.rb index 3e77f9b9556..f606d9520f0 100644 --- a/app/lib/activitypub/activity/delete.rb +++ b/app/lib/activitypub/activity/delete.rb @@ -56,7 +56,7 @@ class ActivityPub::Activity::Delete < ActivityPub::Activity end def revoke_quote - @quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account) + @quote = Quote.find_by(approval_uri: object_uri, quoted_account: @account, state: [:pending, :accepted]) return if @quote.nil? ActivityPub::Forwarder.new(@account, @json, @quote.status).forward! if @quote.status.present? diff --git a/app/models/quote.rb b/app/models/quote.rb index e81d427089d..4ad393e3a57 100644 --- a/app/models/quote.rb +++ b/app/models/quote.rb @@ -51,9 +51,9 @@ class Quote < ApplicationRecord def reject! if accepted? - update!(state: :revoked) + update!(state: :revoked, approval_uri: nil) elsif !revoked? - update!(state: :rejected) + update!(state: :rejected, approval_uri: nil) end end From ad77ee7f8ba6aa25ee36e3e21c38d9bbc252b097 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Mon, 19 Jan 2026 10:53:52 -0500 Subject: [PATCH 67/89] Add coverage for misc "policy" classes (#37525) --- spec/policies/announcement_policy_spec.rb | 37 +++++++++++++--- .../instance_moderation_note_policy_spec.rb | 42 +++++++++++++++++++ spec/policies/media_attachment_policy_spec.rb | 36 ++++++++++++++++ spec/policies/quote_policy_spec.rb | 29 +++++++++++++ spec/policies/username_block_policy_spec.rb | 20 +++++++++ 5 files changed, 158 insertions(+), 6 deletions(-) create mode 100644 spec/policies/instance_moderation_note_policy_spec.rb create mode 100644 spec/policies/media_attachment_policy_spec.rb create mode 100644 spec/policies/quote_policy_spec.rb create mode 100644 spec/policies/username_block_policy_spec.rb diff --git a/spec/policies/announcement_policy_spec.rb b/spec/policies/announcement_policy_spec.rb index 2fec34f8e4b..a78b016a61c 100644 --- a/spec/policies/announcement_policy_spec.rb +++ b/spec/policies/announcement_policy_spec.rb @@ -3,20 +3,45 @@ require 'rails_helper' RSpec.describe AnnouncementPolicy do - let(:policy) { described_class } + subject { described_class } + let(:admin) { Fabricate(:admin_user).account } let(:john) { Fabricate(:account) } permissions :index?, :create?, :update?, :destroy? do context 'with an admin' do - it 'permits' do - expect(policy).to permit(admin, Announcement) - end + it { is_expected.to permit(admin, Announcement) } end context 'with a non-admin' do - it 'denies' do - expect(policy).to_not permit(john, Announcement) + it { is_expected.to_not permit(john, Announcement) } + end + end + + permissions :distribute? do + let(:announcement) { Fabricate :announcement } + + context 'with non admin role' do + it { is_expected.to_not permit(john, announcement) } + end + + context 'with admin role' do + context 'with unpublished announcement' do + let(:announcement) { Fabricate :announcement, published: false, scheduled_at: 5.days.from_now } + + it { is_expected.to_not permit(admin, announcement) } + end + + context 'with published already sent announcement' do + let(:announcement) { Fabricate :announcement, notification_sent_at: 3.days.ago } + + it { is_expected.to_not permit(admin, announcement) } + end + + context 'with published not sent announcement' do + let(:announcement) { Fabricate :announcement } + + it { is_expected.to permit(admin, announcement) } end end end diff --git a/spec/policies/instance_moderation_note_policy_spec.rb b/spec/policies/instance_moderation_note_policy_spec.rb new file mode 100644 index 00000000000..66b4e7f937f --- /dev/null +++ b/spec/policies/instance_moderation_note_policy_spec.rb @@ -0,0 +1,42 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe InstanceModerationNotePolicy do + subject { described_class } + + let(:admin) { Fabricate(:admin_user).account } + let(:account) { Fabricate(:account) } + + permissions :create? do + context 'when admin' do + it { is_expected.to permit(admin, InstanceModerationNote.new) } + end + + context 'when not admin' do + it { is_expected.to_not permit(account, InstanceModerationNote.new) } + end + end + + permissions :destroy? do + context 'when owner of note' do + let(:note) { Fabricate :instance_moderation_note, account: account } + + it { is_expected.to permit(account, note) } + end + + context 'when not owner of note' do + context 'when admin and overrides' do + let(:note) { Fabricate :instance_moderation_note } + + it { is_expected.to permit(admin, note) } + end + + context 'when admin and does not override' do + let(:note) { Fabricate :instance_moderation_note, account: Fabricate(:admin_user).account } + + it { is_expected.to_not permit(admin, note) } + end + end + end +end diff --git a/spec/policies/media_attachment_policy_spec.rb b/spec/policies/media_attachment_policy_spec.rb new file mode 100644 index 00000000000..d194cd819c6 --- /dev/null +++ b/spec/policies/media_attachment_policy_spec.rb @@ -0,0 +1,36 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe MediaAttachmentPolicy do + subject { described_class } + + let(:admin) { Fabricate(:admin_user).account } + let(:account) { Fabricate(:account) } + + permissions :download? do + context 'when attachment is on private discarded status' do + let(:media_attachment) { Fabricate.build :media_attachment, status: Fabricate.build(:status, deleted_at: 5.days.ago, visibility: :private) } + + context 'when admin' do + it { is_expected.to permit(admin, media_attachment) } + end + + context 'when not admin' do + it { is_expected.to_not permit(account, media_attachment) } + end + end + + context 'when attachment is on public status' do + let(:media_attachment) { Fabricate.build :media_attachment, status: Fabricate.build(:status, visibility: :public) } + + context 'when admin' do + it { is_expected.to permit(admin, media_attachment) } + end + + context 'when not admin' do + it { is_expected.to permit(account, media_attachment) } + end + end + end +end diff --git a/spec/policies/quote_policy_spec.rb b/spec/policies/quote_policy_spec.rb new file mode 100644 index 00000000000..71708a9b715 --- /dev/null +++ b/spec/policies/quote_policy_spec.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe QuotePolicy do + subject { described_class } + + let(:account) { Fabricate(:account) } + + permissions :revoke? do + context 'when quote matches the revoking account' do + let(:quote) { Fabricate.build :quote, quoted_account_id: account.id } + + it { is_expected.to permit(account, quote) } + end + + context 'when quote does not match the revoking account' do + let(:quote) { Fabricate.build :quote, quoted_account_id: Fabricate(:account).id } + + it { is_expected.to_not permit(account, quote) } + end + + context 'when quote does not have quoted account id' do + let(:quote) { Fabricate.build :quote } + + it { is_expected.to_not permit(account, quote) } + end + end +end diff --git a/spec/policies/username_block_policy_spec.rb b/spec/policies/username_block_policy_spec.rb new file mode 100644 index 00000000000..5092f71274d --- /dev/null +++ b/spec/policies/username_block_policy_spec.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe UsernameBlockPolicy do + subject { described_class } + + let(:admin) { Fabricate(:admin_user).account } + let(:account) { Fabricate(:account) } + + permissions :index?, :create?, :update?, :destroy? do + context 'when admin' do + it { is_expected.to permit(admin, UsernameBlock.new) } + end + + context 'when not admin' do + it { is_expected.to_not permit(account, UsernameBlock.new) } + end + end +end From 51224bb437f195922d9bfa0680e011dcc5a54220 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Tue, 20 Jan 2026 09:27:05 +0100 Subject: [PATCH 68/89] Add "featured collections" collection to actors (#37512) --- .../featured_collections_controller.rb | 77 +++++++++ app/models/concerns/account/interactions.rb | 7 + app/policies/account_policy.rb | 4 + app/policies/collection_policy.rb | 14 +- .../activitypub/actor_serializer.rb | 7 + .../activitypub/collection_serializer.rb | 2 + config/routes.rb | 2 + .../concerns/account/interactions_spec.rb | 38 +++++ spec/policies/account_policy_spec.rb | 20 +++ .../activitypub/featured_collections_spec.rb | 153 ++++++++++++++++++ .../activitypub/collection_serializer_spec.rb | 6 + 11 files changed, 317 insertions(+), 13 deletions(-) create mode 100644 app/controllers/activitypub/featured_collections_controller.rb create mode 100644 spec/requests/activitypub/featured_collections_spec.rb diff --git a/app/controllers/activitypub/featured_collections_controller.rb b/app/controllers/activitypub/featured_collections_controller.rb new file mode 100644 index 00000000000..872d03423d2 --- /dev/null +++ b/app/controllers/activitypub/featured_collections_controller.rb @@ -0,0 +1,77 @@ +# frozen_string_literal: true + +class ActivityPub::FeaturedCollectionsController < ApplicationController + include SignatureAuthentication + include Authorization + include AccountOwnedConcern + + PER_PAGE = 5 + + vary_by -> { public_fetch_mode? ? 'Accept, Accept-Language, Cookie' : 'Accept, Accept-Language, Cookie, Signature' } + + before_action :check_feature_enabled + before_action :require_account_signature!, if: -> { authorized_fetch_mode? } + before_action :set_collections + + skip_around_action :set_locale + skip_before_action :require_functional!, unless: :limited_federation_mode? + + def index + respond_to do |format| + format.json do + expires_in(page_requested? ? 0 : 3.minutes, public: public_fetch_mode?) + + render json: collection_presenter, + serializer: ActivityPub::CollectionSerializer, + adapter: ActivityPub::Adapter, + content_type: 'application/activity+json' + end + end + end + + private + + def set_collections + authorize @account, :index_collections? + @collections = @account.collections.page(params[:page]).per(PER_PAGE) + rescue Mastodon::NotPermittedError + not_found + end + + def page_requested? + params[:page].present? + end + + def next_page_url + ap_account_featured_collections_url(@account, page: @collections.next_page) if @collections.respond_to?(:next_page) + end + + def prev_page_url + ap_account_featured_collections_url(@account, page: @collections.prev_page) if @collections.respond_to?(:prev_page) + end + + def collection_presenter + if page_requested? + ActivityPub::CollectionPresenter.new( + id: ap_account_featured_collections_url(@account, page: params.fetch(:page, 1)), + type: :unordered, + size: @account.collections.count, + items: @collections, + part_of: ap_account_featured_collections_url(@account), + next: next_page_url, + prev: prev_page_url + ) + else + ActivityPub::CollectionPresenter.new( + id: ap_account_featured_collections_url(@account), + type: :unordered, + size: @account.collections.count, + first: ap_account_featured_collections_url(@account, page: 1) + ) + end + end + + def check_feature_enabled + raise ActionController::RoutingError unless Mastodon::Feature.collections_enabled? + end +end diff --git a/app/models/concerns/account/interactions.rb b/app/models/concerns/account/interactions.rb index c51ccf1229e..d4a415ee31b 100644 --- a/app/models/concerns/account/interactions.rb +++ b/app/models/concerns/account/interactions.rb @@ -164,6 +164,13 @@ module Account::Interactions end end + def blocking_or_domain_blocking?(other_account) + return true if blocking?(other_account) + return false if other_account.domain.blank? + + domain_blocking?(other_account.domain) + end + def muting?(other_account) other_id = other_account.is_a?(Account) ? other_account.id : other_account diff --git a/app/policies/account_policy.rb b/app/policies/account_policy.rb index ab3b41d6280..1fef35714cd 100644 --- a/app/policies/account_policy.rb +++ b/app/policies/account_policy.rb @@ -68,4 +68,8 @@ class AccountPolicy < ApplicationPolicy def feature? record.featureable? && !current_account.blocking?(record) && !current_account.blocked_by?(record) end + + def index_collections? + current_account.nil? || !record.blocking_or_domain_blocking?(current_account) + end end diff --git a/app/policies/collection_policy.rb b/app/policies/collection_policy.rb index 70a869d16ad..4d100c0e32f 100644 --- a/app/policies/collection_policy.rb +++ b/app/policies/collection_policy.rb @@ -6,7 +6,7 @@ class CollectionPolicy < ApplicationPolicy end def show? - current_account.nil? || (!owner_blocking? && !owner_blocking_domain?) + current_account.nil? || !owner.blocking_or_domain_blocking?(current_account) end def create? @@ -27,18 +27,6 @@ class CollectionPolicy < ApplicationPolicy current_account == owner end - def owner_blocking_domain? - return false if current_account.nil? || current_account.domain.nil? - - owner.domain_blocking?(current_account.domain) - end - - def owner_blocking? - return false if current_account.nil? - - current_account.blocked_by?(owner) - end - def owner record.account end diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index c19d42bfb43..ff1a70104b8 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -19,6 +19,7 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer :discoverable, :indexable, :published, :memorial attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? } + attribute :featured_collections, if: -> { Mastodon::Feature.collections_enabled? } has_one :public_key, serializer: ActivityPub::PublicKeySerializer @@ -177,6 +178,12 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer } end + def featured_collections + return nil if instance_actor? + + ap_account_featured_collections_url(object.id) + end + class CustomEmojiSerializer < ActivityPub::EmojiSerializer end diff --git a/app/serializers/activitypub/collection_serializer.rb b/app/serializers/activitypub/collection_serializer.rb index 1b410cecaef..ba0d17f5408 100644 --- a/app/serializers/activitypub/collection_serializer.rb +++ b/app/serializers/activitypub/collection_serializer.rb @@ -18,6 +18,8 @@ class ActivityPub::CollectionSerializer < ActivityPub::Serializer ActivityPub::HashtagSerializer when 'ActivityPub::CollectionPresenter' ActivityPub::CollectionSerializer + when 'Collection' + ActivityPub::FeaturedCollectionSerializer when 'String' StringSerializer else diff --git a/config/routes.rb b/config/routes.rb index b3338a725eb..bf50b67fe13 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -124,6 +124,8 @@ Rails.application.routes.draw do scope path: 'ap', as: 'ap' do resources :accounts, path: 'users', only: [:show], param: :id, concerns: :account_resources do + resources :featured_collections, only: [:index], module: :activitypub + resources :statuses, only: [:show] do member do get :activity diff --git a/spec/models/concerns/account/interactions_spec.rb b/spec/models/concerns/account/interactions_spec.rb index cc50c465517..5bca7959080 100644 --- a/spec/models/concerns/account/interactions_spec.rb +++ b/spec/models/concerns/account/interactions_spec.rb @@ -450,6 +450,44 @@ RSpec.describe Account::Interactions do end end + describe '#blocking_or_domain_blocking?' do + subject { account.blocking_or_domain_blocking?(target_account) } + + context 'when blocking target_account' do + before do + account.block_relationships.create(target_account: target_account) + end + + it 'returns true' do + result = nil + expect { result = subject }.to execute_queries + + expect(result).to be true + end + end + + context 'when blocking the domain' do + let(:target_account) { Fabricate(:remote_account) } + + before do + account_domain_block = Fabricate(:account_domain_block, domain: target_account.domain) + account.domain_blocks << account_domain_block + end + + it 'returns true' do + result = nil + expect { result = subject }.to execute_queries + expect(result).to be true + end + end + + context 'when blocking neither target_account nor its domain' do + it 'returns false' do + expect(subject).to be false + end + end + end + describe '#muting?' do subject { account.muting?(target_account) } diff --git a/spec/policies/account_policy_spec.rb b/spec/policies/account_policy_spec.rb index f877bded252..96fcbdb4d82 100644 --- a/spec/policies/account_policy_spec.rb +++ b/spec/policies/account_policy_spec.rb @@ -188,4 +188,24 @@ RSpec.describe AccountPolicy do end end end + + permissions :index_collections? do + it 'permits when no user is given' do + expect(subject).to permit(nil, john) + end + + it 'permits unblocked users' do + expect(subject).to permit(john, john) + expect(subject).to permit(alice, john) + end + + it 'denies blocked users' do + domain_blocked_user = Fabricate(:remote_account) + john.block_domain!(domain_blocked_user.domain) + john.block!(alice) + + expect(subject).to_not permit(domain_blocked_user, john) + expect(subject).to_not permit(alice, john) + end + end end diff --git a/spec/requests/activitypub/featured_collections_spec.rb b/spec/requests/activitypub/featured_collections_spec.rb new file mode 100644 index 00000000000..09a17c53bea --- /dev/null +++ b/spec/requests/activitypub/featured_collections_spec.rb @@ -0,0 +1,153 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe 'Collections' do + describe 'GET /ap/users/@:account_id/featured_collections', feature: :collections do + subject { get ap_account_featured_collections_path(account.id, format: :json) } + + let(:collection) { Fabricate(:collection) } + let(:account) { collection.account } + + context 'when signed out' do + context 'when account is permanently suspended' do + before do + account.suspend! + account.deletion_request.destroy + end + + it 'returns http gone' do + subject + + expect(response) + .to have_http_status(410) + end + end + + context 'when account is temporarily suspended' do + before { account.suspend! } + + it 'returns http forbidden' do + subject + + expect(response) + .to have_http_status(403) + end + end + + context 'when account is accessible' do + it 'renders ActivityPub Collection successfully', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie') + + expect(response.headers).to include( + 'Content-Type' => include('application/activity+json') + ) + expect(response.parsed_body) + .to include({ + 'type' => 'Collection', + 'totalItems' => 1, + 'first' => match(%r{^https://.*page=1.*$}), + }) + end + + context 'when requesting the first page' do + subject { get ap_account_featured_collections_path(account.id, page: 1, format: :json) } + + context 'when account has many collections' do + before do + Fabricate.times(5, :collection, account:) + end + + it 'includes a link to the next page', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + + expect(response.parsed_body) + .to include({ + 'type' => 'CollectionPage', + 'totalItems' => 6, + 'next' => match(%r{^https://.*page=2.*$}), + }) + end + end + end + end + end + + context 'when signed in' do + let(:user) { Fabricate(:user) } + + before do + post user_session_path, params: { user: { email: user.email, password: user.password } } + end + + context 'when account blocks user' do + before { account.block!(user.account) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + end + + context 'with "HTTP Signature" access signed by a remote account' do + subject do + get ap_account_featured_collections_path(account.id, format: :json), + headers: nil, + sign_with: remote_account + end + + let(:remote_account) { Fabricate(:account, domain: 'host.example') } + + context 'when account blocks the remote account' do + before { account.block!(remote_account) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + + context 'when account domain blocks the domain of the remote account' do + before { account.block_domain!(remote_account.domain) } + + it 'returns http not found' do + subject + + expect(response) + .to have_http_status(404) + end + end + + context 'with JSON' do + it 'renders ActivityPub FeaturedCollection object successfully', :aggregate_failures do + subject + + expect(response) + .to have_http_status(200) + .and have_cacheable_headers.with_vary('Accept, Accept-Language, Cookie') + + expect(response.headers).to include( + 'Content-Type' => include('application/activity+json') + ) + expect(response.parsed_body) + .to include({ + 'type' => 'Collection', + 'totalItems' => 1, + }) + end + end + end + end +end diff --git a/spec/serializers/activitypub/collection_serializer_spec.rb b/spec/serializers/activitypub/collection_serializer_spec.rb index 7726df914f2..d7099ba3d5a 100644 --- a/spec/serializers/activitypub/collection_serializer_spec.rb +++ b/spec/serializers/activitypub/collection_serializer_spec.rb @@ -35,5 +35,11 @@ RSpec.describe ActivityPub::CollectionSerializer do it { is_expected.to eq(ActiveModel::Serializer::CollectionSerializer) } end + + context 'with a Collection' do + let(:model) { Collection.new } + + it { is_expected.to eq(ActivityPub::FeaturedCollectionSerializer) } + end end end From 8ff7662918dbe33ffe414a7bcbc3eac42dac11cd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Tue, 20 Jan 2026 08:31:49 +0000 Subject: [PATCH 69/89] New Crowdin Translations (automated) (#37545) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/et.json | 46 ++++++++++++++----------- app/javascript/mastodon/locales/tr.json | 4 +++ config/locales/devise.et.yml | 12 +++---- config/locales/et.yml | 18 +++++----- config/locales/simple_form.et.yml | 8 ++--- 5 files changed, 48 insertions(+), 40 deletions(-) diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 854d70b0ce1..e4c303153d8 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -2,15 +2,15 @@ "about.blocks": "Modereeritavad serverid", "about.contact": "Kontakt:", "about.default_locale": "Vaikimisi", - "about.disclaimer": "Mastodon on tasuta ja vaba tarkvara ning Mastodon gGmbH kaubamärk.", + "about.disclaimer": "Mastodon on vaba, tasuta ja avatud lähtekoodiga tarkvara ning Mastodon gGmbH kaubamärk.", "about.domain_blocks.no_reason_available": "Põhjus on teadmata", - "about.domain_blocks.preamble": "Mastodon lubab tavaliselt vaadata sisu ning suhelda kasutajatega ükskõik millisest teisest fediversumi serverist. Need on erandid, mis on paika pandud sellel kindlal serveril.", - "about.domain_blocks.silenced.explanation": "Sa ei näe üldiselt profiile ja sisu sellelt serverilt, kui sa just tahtlikult seda ei otsi või jälgimise moel nõusolekut ei anna.", + "about.domain_blocks.preamble": "Mastodon lubab üldiselt vaadata sisu ning suhelda kasutajatega ükskõik millisest teisest födiversumi serverist. Need on erandid, mis kehtivad selles kindlas serveris.", + "about.domain_blocks.silenced.explanation": "Sa üldjuhul ei näe profiile ja sisu sellest serverist, kui sa just tahtlikult neid ei otsi või jälgimise moel nõusolekut ei anna.", "about.domain_blocks.silenced.title": "Piiratud", - "about.domain_blocks.suspended.explanation": "Mitte mingeid andmeid sellelt serverilt ei töödelda, salvestata ega vahetata, tehes igasuguse interaktsiooni või kirjavahetuse selle serveri kasutajatega võimatuks.", + "about.domain_blocks.suspended.explanation": "Mitte mingeid andmeid sellelt serverilt ei töödelda, salvestata ega vahetata, tehes igasuguse suhestumise või infovahetuse selle serveri kasutajatega võimatuks.", "about.domain_blocks.suspended.title": "Peatatud", "about.language_label": "Keel", - "about.not_available": "See info ei ole sellel serveril saadavaks tehtud.", + "about.not_available": "See info ei ole selles serveris saadavaks tehtud.", "about.powered_by": "Hajutatud sotsiaalmeedia, mille taga on {mastodon}", "about.rules": "Serveri reeglid", "account.account_note_header": "Isiklik märge", @@ -18,19 +18,19 @@ "account.badges.bot": "Robot", "account.badges.group": "Grupp", "account.block": "Blokeeri @{name}", - "account.block_domain": "Peida kõik domeenist {domain}", + "account.block_domain": "Blokeeri kõik domeenist {domain}", "account.block_short": "Blokeerimine", "account.blocked": "Blokeeritud", "account.blocking": "Blokeeritud kasutaja", "account.cancel_follow_request": "Võta jälgimistaotlus tagasi", "account.copy": "Kopeeri profiili link", "account.direct": "Maini privaatselt @{name}", - "account.disable_notifications": "Peata teavitused @{name} postitustest", + "account.disable_notifications": "Ära teavita, kui @{name} postitab", "account.domain_blocking": "Blokeeritud domeen", "account.edit_profile": "Muuda profiili", "account.edit_profile_short": "Muuda", - "account.enable_notifications": "Teavita mind @{name} postitustest", - "account.endorse": "Too profiilil esile", + "account.enable_notifications": "Teavita mind, kui {name} postitab", + "account.endorse": "Too profiilis esile", "account.familiar_followers_many": "Jälgijateks {name1}, {name2} ja veel {othersCount, plural, one {üks kasutaja, keda tead} other {# kasutajat, keda tead}}", "account.familiar_followers_one": "Jälgijaks {name1}", "account.familiar_followers_two": "Jälgijateks {name1} ja {name2}", @@ -57,11 +57,12 @@ "account.go_to_profile": "Mine profiilile", "account.hide_reblogs": "Peida @{name} jagamised", "account.in_memoriam": "In Memoriam.", + "account.joined_long": "Liitus {date}", "account.joined_short": "Liitus", "account.languages": "Muuda tellitud keeli", "account.link_verified_on": "Selle lingi autorsust kontrolliti {date}", "account.locked_info": "Selle konto privaatsussätteks on lukustatud. Omanik vaatab käsitsi üle, kes teda jälgida saab.", - "account.media": "Meedia", + "account.media": "Meedium", "account.mention": "Maini @{name}", "account.moved_to": "{name} on teada andnud, et ta uus konto on nüüd:", "account.mute": "Summuta @{name}", @@ -81,20 +82,22 @@ "account.share": "Jaga @{name} profiili", "account.show_reblogs": "Näita @{name} jagamisi", "account.statuses_counter": "{count, plural, one {{counter} postitus} other {{counter} postitust}}", - "account.unblock": "Eemalda blokeering @{name}", - "account.unblock_domain": "Tee {domain} nähtavaks", + "account.unblock": "Lõpeta {name} kasutaja blokeerimine", + "account.unblock_domain": "Lõpeta {domain} domeeni blokeerimine", "account.unblock_domain_short": "Lõpeta blokeerimine", - "account.unblock_short": "Eemalda blokeering", + "account.unblock_short": "Lõpeta blokeerimine", "account.unendorse": "Ära kuva profiilil", - "account.unfollow": "Jälgid", + "account.unfollow": "Ära jälgi", "account.unmute": "Lõpeta {name} kasutaja summutamine", "account.unmute_notifications_short": "Lõpeta teavituste summutamine", "account.unmute_short": "Lõpeta summutamine", + "account_fields_modal.close": "Sulge", + "account_fields_modal.title": "Kasutaja teave: {name}", "account_note.placeholder": "Klõpsa märke lisamiseks", "admin.dashboard.daily_retention": "Kasutajate päevane allesjäämine peale registreerumist", "admin.dashboard.monthly_retention": "Kasutajate kuine allesjäämine peale registreerumist", "admin.dashboard.retention.average": "Keskmine", - "admin.dashboard.retention.cohort": "Registreerumiskuu", + "admin.dashboard.retention.cohort": "Liitumiskuu", "admin.dashboard.retention.cohort_size": "Uued kasutajad", "admin.impact_report.instance_accounts": "Kontode profiilid, mille see kustutaks", "admin.impact_report.instance_followers": "Jälgijad, kelle meie kasutajad kaotaks", @@ -103,11 +106,11 @@ "alert.rate_limited.message": "Palun proovi uuesti pärast {retry_time, time, medium}.", "alert.rate_limited.title": "Kiiruspiirang", "alert.unexpected.message": "Tekkis ootamatu viga.", - "alert.unexpected.title": "Oih!", - "alt_text_badge.title": "Alternatiivtekst", - "alt_text_modal.add_alt_text": "Lisa alt-tekst", + "alert.unexpected.title": "Vaat kus lops!", + "alt_text_badge.title": "Selgitustekst", + "alt_text_modal.add_alt_text": "Lisa selgitustekst", "alt_text_modal.add_text_from_image": "Lisa tekst pildilt", - "alt_text_modal.cancel": "Tühista", + "alt_text_modal.cancel": "Katkesta", "alt_text_modal.change_thumbnail": "Muuda pisipilti", "alt_text_modal.describe_for_people_with_hearing_impairments": "Kirjelda seda kuulmispuudega inimeste jaoks…", "alt_text_modal.describe_for_people_with_visual_impairments": "Kirjelda seda nägemispuudega inimeste jaoks…", @@ -119,7 +122,7 @@ "annual_report.announcement.description": "Vaata teavet oma suhestumise kohta Mastodonis eelmisel aastal.", "annual_report.announcement.title": "{year}. aasta Mastodoni kokkuvõte on valmis", "annual_report.nav_item.badge": "Uus", - "annual_report.shared_page.donate": "Anneta", + "annual_report.shared_page.donate": "Toeta rahaliselt", "annual_report.shared_page.footer": "Loodud {heart} Mastodoni meeskonna poolt", "annual_report.shared_page.footer_server_info": "{username} kasutab {domain}-i, üht paljudest kogukondadest, mis toimivad Mastodonil.", "annual_report.summary.archetype.booster.desc_public": "{name} jätkas postituste otsimist, et neid edendada, tugevdades teisi loojaid täiusliku täpsusega.", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# uus kirje} other {# uut kirjet}}", "loading_indicator.label": "Laadimine…", "media_gallery.hide": "Peida", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Kontot {disabledAccount} ei ole praegu võimalik kasutada, sest kolisid kontole {movedToAccount}.", "mute_modal.hide_from_notifications": "Peida teavituste hulgast", "mute_modal.hide_options": "Peida valikud", @@ -767,7 +771,7 @@ "onboarding.profile.upload_avatar": "Laadi üles profiilipilt", "onboarding.profile.upload_header": "Laadi üles profiili päis", "password_confirmation.exceeds_maxlength": "Salasõnakinnitus on pikem kui salasõna maksimumpikkus", - "password_confirmation.mismatching": "Salasõnakinnitus ei sobi kokku", + "password_confirmation.mismatching": "Salasõnad ei klapi", "picture_in_picture.restore": "Pane tagasi", "poll.closed": "Suletud", "poll.refresh": "Värskenda", diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json index 4356fce6c5d..1d8a0a247f4 100644 --- a/app/javascript/mastodon/locales/tr.json +++ b/app/javascript/mastodon/locales/tr.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Profile git", "account.hide_reblogs": "@{name} kişisinin yeniden paylaşımlarını gizle", "account.in_memoriam": "Hatırasına.", + "account.joined_long": "{date} tarihinde katıldı", "account.joined_short": "Katıldı", "account.languages": "Abone olunan dilleri değiştir", "account.link_verified_on": "Bu bağlantının sahipliği {date} tarihinde denetlendi", @@ -90,6 +91,8 @@ "account.unmute": "@{name} adlı kişinin sesini aç", "account.unmute_notifications_short": "Bildirimlerin sesini aç", "account.unmute_short": "Susturmayı kaldır", + "account_fields_modal.close": "Kapat", + "account_fields_modal.title": "{name} bilgileri", "account_note.placeholder": "Not eklemek için tıklayın", "admin.dashboard.daily_retention": "Kayıttan sonra günlük kullanıcı saklama oranı", "admin.dashboard.monthly_retention": "Kayıttan sonra aylık kullanıcı saklama oranı", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# yeni öğe} other {# yeni öğe}}", "loading_indicator.label": "Yükleniyor…", "media_gallery.hide": "Gizle", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "{disabledAccount} hesabınız, {movedToAccount} hesabına taşıdığınız için şu an devre dışı.", "mute_modal.hide_from_notifications": "Bildirimlerde gizle", "mute_modal.hide_options": "Seçenekleri gizle", diff --git a/config/locales/devise.et.yml b/config/locales/devise.et.yml index 5843761ddba..6ed4c2dd701 100644 --- a/config/locales/devise.et.yml +++ b/config/locales/devise.et.yml @@ -29,23 +29,23 @@ et: title: Kinnita e-postiaadress email_changed: explanation: 'Sinu konto e-postiaadress muudetakse:' - extra: Kui sa ei muutnud oma e-posti, on tõenäoline, et kellelgi on ligipääs su kontole. Palun muuda koheselt oma salasõna. Kui oled aga oma kontost välja lukustatud, võta ühendust oma serveri administraatoriga. + extra: Kui sa ei muutnud oma e-posti, on tõenäoline, et kellelgi on ligipääs su kontole. Palun muuda koheselt oma salasõna. Kui oled aga oma kontole ligipääsu kaotanud, palun võta kohe ühendust oma serveri haldajaga. subject: 'Mastodon: e-post muudetud' title: Uus e-postiaadress password_change: explanation: Konto salasõna on vahetatud. extra: Kui sa ei muutnud oma salasõna, on tõenäoline, et keegi on su kontole ligi pääsenud. Palun muuda viivitamata oma salasõna. Kui sa oma kontole ligi ei pääse, võta ühendust serveri haldajaga. - subject: 'Mastodon: salasõna muudetud' - title: Salasõna muudetud + subject: 'Mastodon: salasõna on muudetud' + title: Salasõna on muudetud reconfirmation_instructions: explanation: Kinnita uus aadress, et oma e-posti aadress muuta. extra: Kui see muudatus pole sinu poolt algatatud, palun eira seda kirja. Selle Mastodoni konto e-postiaadress ei muutu enne, kui vajutad üleval olevale lingile. subject: 'Mastodon: kinnita e-postiaadress %{instance} jaoks' title: Kinnita e-postiaadress reset_password_instructions: - action: Salasõna muutmine - explanation: Kontole on küsitud uut salasõna. - extra: Kui see tuleb üllatusena, võib seda kirja eirata. Salasõna ei muutu enne ülaoleva lingi külastamist ja uue salasõna määramist. + action: Muuda salasõna + explanation: Sa palusid oma kasutajakontole luua uus salasõna. + extra: Kui see tuleb üllatusena, võid seda kirja eirata. Salasõna ei muutu enne ülaoleva lingi külastamist ja uue salasõna sisestamist. subject: 'Mastodon: salasõna lähtestamisjuhendid' title: Salasõna lähtestamine two_factor_disabled: diff --git a/config/locales/et.yml b/config/locales/et.yml index 508329e996f..58df54c8dfb 100644 --- a/config/locales/et.yml +++ b/config/locales/et.yml @@ -142,7 +142,7 @@ et: security: Turvalisus security_measures: only_password: Ainult salasõna - password_and_2fa: Salasõna ja 2-etapine autentimine (2FA) + password_and_2fa: Salasõna ja kahefaktoriline autentimine (2FA) sensitive: Tundlik sisu sensitized: Märgitud kui tundlik sisu shared_inbox_url: Jagatud sisendkausta URL @@ -292,7 +292,7 @@ et: remove_avatar_user_html: "%{name} eemaldas %{target} avatari" reopen_report_html: "%{name} taasavas raporti %{target}" resend_user_html: "%{name} lähtestas %{target} kinnituskirja e-posti" - reset_password_user_html: "%{name} lähtestas %{target} salasõna" + reset_password_user_html: "%{name} lähtestas %{target} kasutaja salasõna" resolve_report_html: "%{name} lahendas raporti %{target}" sensitive_account_html: "%{name} märkis %{target} meedia kui tundlik sisu" silence_account_html: "%{name} piiras %{target} konto" @@ -787,7 +787,7 @@ et: manage_taxonomies: Halda taksonoomiaid manage_taxonomies_description: Luba kasutajatel populaarset sisu üle vaadata ning uuendada teemaviidete seadistusi manage_user_access: Halda kasutajate ligipääsu - manage_user_access_description: Võimaldab kasutajatel keelata teiste kasutajate kaheastmelise autentimise, muuta oma e-posti aadressi ja lähtestada oma parooli + manage_user_access_description: Võimaldab kasutajatel keelata teiste kasutajate kaheastmelise autentimise, muuta nende e-posti aadressi ja lähtestada oma salasõna manage_users: Kasutajate haldamine manage_users_description: Lubab kasutajail näha teiste kasutajate üksikasju ja teha nende suhtes modereerimisotsuseid manage_webhooks: Halda webhook'e @@ -1249,7 +1249,7 @@ et: suffix: Kasutajakontoga saad jälgida inimesi, postitada uudiseid ning pidada kirjavahetust ükskõik millise Mastodoni serveri kasutajatega ja muudki! didnt_get_confirmation: Ei saanud kinnituslinki? dont_have_your_security_key: Pole turvavõtit? - forgot_password: Salasõna ununenud? + forgot_password: Kas unustasid oma salasõna? invalid_reset_password_token: Salasõna lähtestusvõti on vale või aegunud. Palun taotle uus. link_to_otp: Kaheastmeline kood telefonist või taastekood link_to_webauth: Turvavõtmete seadme kasutamine @@ -1270,7 +1270,7 @@ et: register: Loo konto registration_closed: "%{instance} ei võta vastu uusi liikmeid" resend_confirmation: Saada kinnituslink uuesti - reset_password: Salasõna lähtestamine + reset_password: Lähtesta salasõna rules: accept: Nõus back: Tagasi @@ -1280,7 +1280,7 @@ et: title: Mõned põhireeglid. title_invited: Oled kutsutud. security: Turvalisus - set_new_password: Uue salasõna määramine + set_new_password: Sisesta uus salasõna setup: email_below_hint_html: Kontrolli rämpsposti kausta või palu uue kirja saatmist. Kui sinu e-posti aadress on vale, siis saad seda parandada. email_settings_hint_html: Klõpsa aadressile %{email} saadetud linki, et alustada Mastodoni kasutamist. Me oleme ootel. @@ -1316,9 +1316,9 @@ et: title: Autori tunnustamine challenge: confirm: Jätka - hint_html: "Nõuanne: Me ei küsi salasõna uuesti järgmise tunni jooksul." + hint_html: "Nõuanne: Me ei küsi sinu salasõna uuesti järgmise tunni jooksul." invalid_password: Vigane salasõna - prompt: Jätkamiseks salasõna veelkord + prompt: Jätkamiseks korda salasõna color_scheme: auto: Auto dark: Tume @@ -1627,7 +1627,7 @@ et: password: salasõna sign_in_token: e-posti turvvakood webauthn: turvavõtmed - description_html: Kui paistab tundmatuid tegevusi, tuleks vahetada salasõna ja aktiveerida kaheastmeline autentimine. + description_html: Kui paistab tundmatuid tegevusi, palun vaheta salasõna ja aktiveeri kaheastmeline autentimine. empty: Autentimisajalugu pole saadaval failed_sign_in_html: Nurjunud sisenemine meetodiga %{method} aadressilt %{ip} (%{browser}) successful_sign_in_html: Edukas sisenemine meetodiga %{method} aadressilt %{ip} (%{browser}) diff --git a/config/locales/simple_form.et.yml b/config/locales/simple_form.et.yml index d51e816d2e5..2e45c6ffa07 100644 --- a/config/locales/simple_form.et.yml +++ b/config/locales/simple_form.et.yml @@ -51,7 +51,7 @@ et: inbox_url: Kopeeri soovitud sõnumivahendusserveri avalehe võrguaadress irreversible: Filtreeritud postitused kaovad taastamatult, isegi kui filter on hiljem eemaldatud locale: Kasutajaliidese, e-kirjade ja tõuketeadete keel - password: Vajalik on vähemalt 8 märki + password: Vajalik on vähemalt 8 tähemärki phrase: Kattub olenemata postituse teksti suurtähtedest või sisuhoiatusest scopes: Milliseid API-sid see rakendus tohib kasutada. Kui valid kõrgeima taseme, ei pea üksikuid eraldi valima. setting_advanced_layout: Näita Mastodoni mitme veeruga paigutuses, mispuhul näed korraga nii ajajoont, teavitusi, kui sinu valitud kolmandat veergu. Ei sobi kasutamiseks väikeste ekraanide puhul. @@ -113,7 +113,7 @@ et: trends: Trendid näitavad, millised postitused, teemaviited ja uudislood koguvad sinu serveris tähelepanu. wrapstodon: Paku kohalikele kasutajatele luua nende Mastodoni kasutamise aastast mänguline kokkuvõte. See võimalus on saadaval igal aastal 10. ja 31. detsembri vahel ja seda pakutakse kasutajatele, kes tegid vähemalt ühe avaliku või vaikse avaliku postituse ja kes kasutas aasta jooksul vähemalt ühte silti. form_challenge: - current_password: Turvalisse alasse sisenemine + current_password: Sisened turvalisse alasse imports: data: CSV fail eksporditi teisest Mastodoni serverist invite_request: @@ -214,8 +214,8 @@ et: avatar: Profiilipilt bot: See konto on robot chosen_languages: Keelte filtreerimine - confirm_new_password: Uue salasõna kinnitamine - confirm_password: Salasõna kinnitamine + confirm_new_password: Korda uut salasõna + confirm_password: Korda salasõna context: Filtreeri kontekste current_password: Kehtiv salasõna data: Andmed From fa9b905fdf110afc2e0e710283bea8e81c1161aa Mon Sep 17 00:00:00 2001 From: diondiondion Date: Tue, 20 Jan 2026 12:04:06 +0100 Subject: [PATCH 70/89] Improve `apiRequest` helper types (#37544) --- app/javascript/mastodon/api.ts | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/app/javascript/mastodon/api.ts b/app/javascript/mastodon/api.ts index 1820e00a537..2af29c783e0 100644 --- a/app/javascript/mastodon/api.ts +++ b/app/javascript/mastodon/api.ts @@ -128,15 +128,18 @@ export default function api(withAuthorization = true) { } type ApiUrl = `v${1 | '1_alpha' | 2}/${string}`; -type RequestParamsOrData = Record; +type RequestParamsOrData = T | Record; -export async function apiRequest( +export async function apiRequest< + ApiResponse = unknown, + ApiParamsOrData = unknown, +>( method: Method, url: string, args: { signal?: AbortSignal; - params?: RequestParamsOrData; - data?: RequestParamsOrData; + params?: RequestParamsOrData; + data?: RequestParamsOrData; timeout?: number; } = {}, ) { @@ -149,30 +152,30 @@ export async function apiRequest( return data; } -export async function apiRequestGet( +export async function apiRequestGet( url: ApiUrl, - params?: RequestParamsOrData, + params?: RequestParamsOrData, ) { return apiRequest('GET', url, { params }); } -export async function apiRequestPost( +export async function apiRequestPost( url: ApiUrl, - data?: RequestParamsOrData, + data?: RequestParamsOrData, ) { return apiRequest('POST', url, { data }); } -export async function apiRequestPut( +export async function apiRequestPut( url: ApiUrl, - data?: RequestParamsOrData, + data?: RequestParamsOrData, ) { return apiRequest('PUT', url, { data }); } -export async function apiRequestDelete( - url: ApiUrl, - params?: RequestParamsOrData, -) { +export async function apiRequestDelete< + ApiResponse = unknown, + ApiParams = unknown, +>(url: ApiUrl, params?: RequestParamsOrData) { return apiRequest('DELETE', url, { params }); } From a1c17fef3aa863fc18eaa0941fa32f15222f859c Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 20 Jan 2026 12:10:46 +0100 Subject: [PATCH 71/89] Profile redesign: Account name (#37527) --- .../account/components/domain_pill.tsx | 10 ++- .../components/account_header.tsx | 62 +++++++---------- .../components/account_name.tsx | 68 +++++++++++++++++++ .../components/redesign.module.scss | 41 +++++++++++ 4 files changed, 140 insertions(+), 41 deletions(-) create mode 100644 app/javascript/mastodon/features/account_timeline/components/account_name.tsx diff --git a/app/javascript/mastodon/features/account/components/domain_pill.tsx b/app/javascript/mastodon/features/account/components/domain_pill.tsx index 13f5ebacf1e..1f334bc004a 100644 --- a/app/javascript/mastodon/features/account/components/domain_pill.tsx +++ b/app/javascript/mastodon/features/account/components/domain_pill.tsx @@ -1,3 +1,4 @@ +import type { ReactNode } from 'react'; import { useState, useRef, useCallback, useId } from 'react'; import { FormattedMessage } from 'react-intl'; @@ -15,7 +16,9 @@ export const DomainPill: React.FC<{ domain: string; username: string; isSelf: boolean; -}> = ({ domain, username, isSelf }) => { + children?: ReactNode; + className?: string; +}> = ({ domain, username, isSelf, children, className }) => { const accessibilityId = useId(); const [open, setOpen] = useState(false); const [expanded, setExpanded] = useState(false); @@ -32,7 +35,9 @@ export const DomainPill: React.FC<{ return ( <> { @@ -47,7 +45,6 @@ export const AccountHeader: React.FC<{ hideTabs?: boolean; }> = ({ accountId, hideTabs }) => { const dispatch = useAppDispatch(); - const intl = useIntl(); const account = useAppSelector((state) => state.accounts.get(accountId)); const relationship = useAppSelector((state) => state.relationships.get(accountId), @@ -85,8 +82,6 @@ export const AccountHeader: React.FC<{ const suspendedOrHidden = hidden || account.suspended; const isLocal = !account.acct.includes('@'); - const username = account.acct.split('@')[0]; - const domain = isLocal ? localDomain : account.acct.split('@')[1]; return (
@@ -133,38 +128,27 @@ export const AccountHeader: React.FC<{ /> - + {!isRedesignEnabled() && ( + + )}
-
-

- - - - @{username} - @{domain} - - - {account.locked && ( - - )} - -

+
+ + {isRedesignEnabled() && }
diff --git a/app/javascript/mastodon/features/account_timeline/components/account_name.tsx b/app/javascript/mastodon/features/account_timeline/components/account_name.tsx new file mode 100644 index 00000000000..90ccf7486d3 --- /dev/null +++ b/app/javascript/mastodon/features/account_timeline/components/account_name.tsx @@ -0,0 +1,68 @@ +import type { FC } from 'react'; + +import { useIntl } from 'react-intl'; + +import { DisplayName } from '@/mastodon/components/display_name'; +import { Icon } from '@/mastodon/components/icon'; +import { useAccount } from '@/mastodon/hooks/useAccount'; +import { useAppSelector } from '@/mastodon/store'; +import InfoIcon from '@/material-icons/400-24px/info.svg?react'; +import LockIcon from '@/material-icons/400-24px/lock.svg?react'; + +import { DomainPill } from '../../account/components/domain_pill'; +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + +export const AccountName: FC<{ accountId: string; className?: string }> = ({ + accountId, + className, +}) => { + const intl = useIntl(); + const account = useAccount(accountId); + const me = useAppSelector((state) => state.meta.get('me') as string); + const localDomain = useAppSelector( + (state) => state.meta.get('domain') as string, + ); + + if (!account) { + return null; + } + + const [username = '', domain = localDomain] = account.acct.split('@'); + + return ( +

+ + + + @{username} + {isRedesignEnabled() && '@'} + + {!isRedesignEnabled() && '@'} + {domain} + + + + {isRedesignEnabled() && } + + {!isRedesignEnabled() && account.locked && ( + + )} + +

+ ); +}; diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss index dd09f199e56..757f5a42314 100644 --- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -1,3 +1,44 @@ +.nameWrapper { + display: flex; + gap: 16px; +} + +.name { + flex-grow: 1; + font-size: 22px; + white-space: initial; + text-overflow: initial; + line-height: normal; + + :global(.icon-info) { + margin-left: 2px; + width: 1em; + height: 1em; + align-self: center; + } +} + +// Overrides .account__header__tabs__name h1 small +h1.name > small { + gap: 0; +} + +.domainPill { + appearance: none; + border: none; + background: none; + padding: 0; + text-decoration: underline; + color: inherit; + font-size: 1em; + font-weight: initial; + + &:global(.active) { + background: none; + color: inherit; + } +} + .fieldList { margin-top: 16px; } From 7cdc05b9a8b098ca0d369d1ba75396e4d6150c33 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Tue, 20 Jan 2026 14:22:54 +0100 Subject: [PATCH 72/89] Add `language` attribute to collections (#37549) --- .../api/v1_alpha/collections_controller.rb | 4 ++-- app/models/collection.rb | 2 ++ .../featured_collection_serializer.rb | 13 ++++++++++++- .../rest/base_collection_serializer.rb | 2 +- ...20260119153538_add_language_to_collections.rb | 7 +++++++ db/schema.rb | 3 ++- spec/models/collection_spec.rb | 6 ++++++ spec/requests/api/v1_alpha/collections_spec.rb | 1 + .../featured_collection_serializer_spec.rb | 16 ++++++++++++++++ .../rest/collection_serializer_spec.rb | 2 ++ 10 files changed, 51 insertions(+), 5 deletions(-) create mode 100644 db/migrate/20260119153538_add_language_to_collections.rb diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1_alpha/collections_controller.rb index d0c4e0f3f04..9d6b2f9a381 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1_alpha/collections_controller.rb @@ -81,11 +81,11 @@ class Api::V1Alpha::CollectionsController < Api::BaseController end def collection_creation_params - params.permit(:name, :description, :sensitive, :discoverable, :tag_name, account_ids: []) + params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name, account_ids: []) end def collection_update_params - params.permit(:name, :description, :sensitive, :discoverable, :tag_name) + params.permit(:name, :description, :language, :sensitive, :discoverable, :tag_name) end def check_feature_enabled diff --git a/app/models/collection.rb b/app/models/collection.rb index b732a3d220b..334318b73d3 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -8,6 +8,7 @@ # description :text not null # discoverable :boolean not null # item_count :integer default(0), not null +# language :string # local :boolean not null # name :string not null # original_number_of_items :integer @@ -36,6 +37,7 @@ class Collection < ApplicationRecord presence: true, numericality: { greater_than_or_equal: 0 }, if: :remote? + validates :language, language: { if: :local?, allow_nil: true } validate :tag_is_usable validate :items_do_not_exceed_limit diff --git a/app/serializers/activitypub/featured_collection_serializer.rb b/app/serializers/activitypub/featured_collection_serializer.rb index e70d155a1a0..af4c5548514 100644 --- a/app/serializers/activitypub/featured_collection_serializer.rb +++ b/app/serializers/activitypub/featured_collection_serializer.rb @@ -17,9 +17,12 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer end end - attributes :id, :type, :total_items, :name, :summary, :attributed_to, + attributes :id, :type, :total_items, :name, :attributed_to, :sensitive, :discoverable, :published, :updated + attribute :summary, unless: :language_present? + attribute :summary_map, if: :language_present? + has_one :tag, key: :topic, serializer: ActivityPub::NoteSerializer::TagSerializer has_many :collection_items, key: :ordered_items, serializer: FeaturedItemSerializer @@ -36,6 +39,10 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer object.description end + def summary_map + { object.language => object.description } + end + def attributed_to ActivityPub::TagManager.instance.uri_for(object.account) end @@ -51,4 +58,8 @@ class ActivityPub::FeaturedCollectionSerializer < ActivityPub::Serializer def updated object.updated_at.iso8601 end + + def language_present? + object.language.present? + end end diff --git a/app/serializers/rest/base_collection_serializer.rb b/app/serializers/rest/base_collection_serializer.rb index be26aac6fe2..6bb75e99a39 100644 --- a/app/serializers/rest/base_collection_serializer.rb +++ b/app/serializers/rest/base_collection_serializer.rb @@ -1,7 +1,7 @@ # frozen_string_literal: true class REST::BaseCollectionSerializer < ActiveModel::Serializer - attributes :id, :uri, :name, :description, :local, :sensitive, + attributes :id, :uri, :name, :description, :language, :local, :sensitive, :discoverable, :item_count, :created_at, :updated_at belongs_to :tag, serializer: REST::StatusSerializer::TagSerializer diff --git a/db/migrate/20260119153538_add_language_to_collections.rb b/db/migrate/20260119153538_add_language_to_collections.rb new file mode 100644 index 00000000000..066288b070a --- /dev/null +++ b/db/migrate/20260119153538_add_language_to_collections.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddLanguageToCollections < ActiveRecord::Migration[8.0] + def change + add_column :collections, :language, :string + end +end diff --git a/db/schema.rb b/db/schema.rb index 8dc1e88fd17..8801882808d 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2026_01_15_153219) do +ActiveRecord::Schema[8.0].define(version: 2026_01_19_153538) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -382,6 +382,7 @@ ActiveRecord::Schema[8.0].define(version: 2026_01_15_153219) do t.datetime "created_at", null: false t.datetime "updated_at", null: false t.integer "item_count", default: 0, null: false + t.string "language" t.index ["account_id"], name: "index_collections_on_account_id" t.index ["tag_id"], name: "index_collections_on_tag_id" end diff --git a/spec/models/collection_spec.rb b/spec/models/collection_spec.rb index bcc31fd0877..b50969b68a9 100644 --- a/spec/models/collection_spec.rb +++ b/spec/models/collection_spec.rb @@ -16,12 +16,18 @@ RSpec.describe Collection do it { is_expected.to_not allow_value(nil).for(:discoverable) } + it { is_expected.to allow_value('en').for(:language) } + + it { is_expected.to_not allow_value('randomstuff').for(:language) } + context 'when collection is remote' do subject { Fabricate.build :collection, local: false } it { is_expected.to validate_presence_of(:uri) } it { is_expected.to validate_presence_of(:original_number_of_items) } + + it { is_expected.to allow_value('randomstuff').for(:language) } end context 'when using a hashtag as category' do diff --git a/spec/requests/api/v1_alpha/collections_spec.rb b/spec/requests/api/v1_alpha/collections_spec.rb index 3921fabfde8..b529fc2d92f 100644 --- a/spec/requests/api/v1_alpha/collections_spec.rb +++ b/spec/requests/api/v1_alpha/collections_spec.rb @@ -115,6 +115,7 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do { name: 'Low-traffic bots', description: 'Really nice bots, please follow', + language: 'en', sensitive: '0', discoverable: '1', } diff --git a/spec/serializers/activitypub/featured_collection_serializer_spec.rb b/spec/serializers/activitypub/featured_collection_serializer_spec.rb index b01cce12d81..e6bb4ea4b0f 100644 --- a/spec/serializers/activitypub/featured_collection_serializer_spec.rb +++ b/spec/serializers/activitypub/featured_collection_serializer_spec.rb @@ -45,4 +45,20 @@ RSpec.describe ActivityPub::FeaturedCollectionSerializer do 'updated' => match_api_datetime_format, }) end + + context 'when a language is set' do + before do + collection.language = 'en' + end + + it 'uses "summaryMap" to include the language' do + expect(subject).to include({ + 'summaryMap' => { + 'en' => 'These are really amazing', + }, + }) + + expect(subject).to_not have_key('summary') + end + end end diff --git a/spec/serializers/rest/collection_serializer_spec.rb b/spec/serializers/rest/collection_serializer_spec.rb index f0baf7dff87..80ed6a559ed 100644 --- a/spec/serializers/rest/collection_serializer_spec.rb +++ b/spec/serializers/rest/collection_serializer_spec.rb @@ -18,6 +18,7 @@ RSpec.describe REST::CollectionSerializer do id: 2342, name: 'Exquisite follows', description: 'Always worth a follow', + language: 'en', local: true, sensitive: true, discoverable: false, @@ -31,6 +32,7 @@ RSpec.describe REST::CollectionSerializer do 'id' => '2342', 'name' => 'Exquisite follows', 'description' => 'Always worth a follow', + 'language' => 'en', 'local' => true, 'sensitive' => true, 'discoverable' => false, From c2f776b74704409d8ba299f67f140367e3f1c4ad Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jan 2026 15:10:38 +0100 Subject: [PATCH 73/89] Merge commit from fork --- app/controllers/api/web/push_subscriptions_controller.rb | 2 +- spec/requests/api/web/push_subscriptions_spec.rb | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/app/controllers/api/web/push_subscriptions_controller.rb b/app/controllers/api/web/push_subscriptions_controller.rb index ced68d39fc7..2edd92dbc7b 100644 --- a/app/controllers/api/web/push_subscriptions_controller.rb +++ b/app/controllers/api/web/push_subscriptions_controller.rb @@ -62,7 +62,7 @@ class Api::Web::PushSubscriptionsController < Api::Web::BaseController end def set_push_subscription - @push_subscription = ::Web::PushSubscription.find(params[:id]) + @push_subscription = ::Web::PushSubscription.where(user_id: active_session.user_id).find(params[:id]) end def subscription_params diff --git a/spec/requests/api/web/push_subscriptions_spec.rb b/spec/requests/api/web/push_subscriptions_spec.rb index 21830d1b1c1..88c0302f860 100644 --- a/spec/requests/api/web/push_subscriptions_spec.rb +++ b/spec/requests/api/web/push_subscriptions_spec.rb @@ -163,9 +163,10 @@ RSpec.describe 'API Web Push Subscriptions' do end describe 'PUT /api/web/push_subscriptions/:id' do - before { sign_in Fabricate :user } + before { sign_in user } - let(:subscription) { Fabricate :web_push_subscription } + let(:user) { Fabricate(:user) } + let(:subscription) { Fabricate(:web_push_subscription, user: user) } it 'gracefully handles invalid nested params' do put api_web_push_subscription_path(subscription), params: { data: 'invalid' } From 7a0d3d3bf851429c044d07cee250b367d809b496 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jan 2026 15:13:10 +0100 Subject: [PATCH 74/89] Merge commit from fork --- app/models/custom_filter.rb | 3 +++ app/models/custom_filter_keyword.rb | 4 +++- app/models/list.rb | 3 ++- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/app/models/custom_filter.rb b/app/models/custom_filter.rb index 07bbfd43733..1151c7de985 100644 --- a/app/models/custom_filter.rb +++ b/app/models/custom_filter.rb @@ -30,6 +30,8 @@ class CustomFilter < ApplicationRecord EXPIRATION_DURATIONS = [30.minutes, 1.hour, 6.hours, 12.hours, 1.day, 1.week].freeze + TITLE_LENGTH_LIMIT = 256 + include Expireable include Redisable @@ -41,6 +43,7 @@ class CustomFilter < ApplicationRecord accepts_nested_attributes_for :keywords, reject_if: :all_blank, allow_destroy: true validates :title, :context, presence: true + validates :title, length: { maximum: TITLE_LENGTH_LIMIT } validate :context_must_be_valid normalizes :context, with: ->(context) { context.map(&:strip).filter_map(&:presence) } diff --git a/app/models/custom_filter_keyword.rb b/app/models/custom_filter_keyword.rb index 112798b10a9..1abec4ddc4d 100644 --- a/app/models/custom_filter_keyword.rb +++ b/app/models/custom_filter_keyword.rb @@ -17,7 +17,9 @@ class CustomFilterKeyword < ApplicationRecord belongs_to :custom_filter - validates :keyword, presence: true + KEYWORD_LENGTH_LIMIT = 512 + + validates :keyword, presence: true, length: { maximum: KEYWORD_LENGTH_LIMIT } alias_attribute :phrase, :keyword diff --git a/app/models/list.rb b/app/models/list.rb index 8fd1953ab31..49ead642ac9 100644 --- a/app/models/list.rb +++ b/app/models/list.rb @@ -17,6 +17,7 @@ class List < ApplicationRecord include Paginable PER_ACCOUNT_LIMIT = 50 + TITLE_LENGTH_LIMIT = 256 enum :replies_policy, { list: 0, followed: 1, none: 2 }, prefix: :show, validate: true @@ -26,7 +27,7 @@ class List < ApplicationRecord has_many :accounts, through: :list_accounts has_many :active_accounts, -> { merge(ListAccount.active) }, through: :list_accounts, source: :account - validates :title, presence: true + validates :title, presence: true, length: { maximum: TITLE_LENGTH_LIMIT } validate :validate_account_lists_limit, on: :create From 841dfce9f76ef5ff6542cc6f98ec9c6df3fab5a1 Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jan 2026 15:13:43 +0100 Subject: [PATCH 75/89] Merge commit from fork --- app/lib/activitypub/activity/update.rb | 3 ++- app/services/fan_out_on_write_service.rb | 2 ++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/app/lib/activitypub/activity/update.rb b/app/lib/activitypub/activity/update.rb index d94f8767618..e22bea2c64f 100644 --- a/app/lib/activitypub/activity/update.rb +++ b/app/lib/activitypub/activity/update.rb @@ -30,7 +30,8 @@ class ActivityPub::Activity::Update < ActivityPub::Activity @status = Status.find_by(uri: object_uri, account_id: @account.id) # Ignore updates for old unknown objects, since those are updates we are not interested in - return if @status.nil? && object_too_old? + # Also ignore unknown objects from suspended users for the same reasons + return if @status.nil? && (@account.suspended? || object_too_old?) # We may be getting `Create` and `Update` out of order @status ||= ActivityPub::Activity::Create.new(@json, @account, **@options).perform diff --git a/app/services/fan_out_on_write_service.rb b/app/services/fan_out_on_write_service.rb index 64769230b7c..428077b11fb 100644 --- a/app/services/fan_out_on_write_service.rb +++ b/app/services/fan_out_on_write_service.rb @@ -14,6 +14,8 @@ class FanOutOnWriteService < BaseService @account = status.account @options = options + return if @status.proper.account.suspended? + check_race_condition! warm_payload_cache! From 028e76f7703eecbbbf6e953c7d8d38184969e5dc Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jan 2026 15:14:45 +0100 Subject: [PATCH 76/89] Merge commit from fork * Add limit on inbox payload size The 1MB limit is consistent with the limit we use when fetching remote resources * Add limit to number of options from federated polls * Add a limit to the number of federated profile fields * Add limit on federated username length * Add hard limits for federated display name and account bio * Add hard limits for `alsoKnownAs` and `attributionDomains` * Add hard limit on federated custom emoji shortcode * Highlight most destructive limits and expand on their reasoning --- FEDERATION.md | 19 +++++++++++++++++++ .../activitypub/inboxes_controller.rb | 5 +++++ app/lib/activitypub/activity.rb | 1 + app/lib/activitypub/parser/poll_parser.rb | 6 +++++- app/models/account.rb | 9 ++++++++- app/models/custom_emoji.rb | 5 ++++- .../activitypub/process_account_service.rb | 14 +++++++++----- 7 files changed, 51 insertions(+), 8 deletions(-) diff --git a/FEDERATION.md b/FEDERATION.md index 03ea5449de3..eb91d9545fe 100644 --- a/FEDERATION.md +++ b/FEDERATION.md @@ -48,3 +48,22 @@ Mastodon requires all `POST` requests to be signed, and MAY require `GET` reques ### Additional documentation - [Mastodon documentation](https://docs.joinmastodon.org/) + +## Size limits + +Mastodon imposes a few hard limits on federated content. +These limits are intended to be very generous and way above what the Mastodon user experience is optimized for, so as to accomodate future changes and unusual or unforeseen usage patterns, while still providing some limits for performance reasons. +The following table attempts to summary those limits. + +| Limited property | Size limit | Consequence of exceeding the limit | +| ------------------------------------------------------------- | ---------- | ---------------------------------- | +| Serialized JSON-LD | 1MB | **Activity is rejected/dropped** | +| Profile fields (actor `PropertyValue` attachments) name/value | 2047 | Field name/value is truncated | +| Number of profile fields (actor `PropertyValue` attachments) | 50 | Fields list is truncated | +| Poll options (number of `anyOf`/`oneOf` in a `Question`) | 500 | Items list is truncated | +| Account username (actor `preferredUsername`) length | 2048 | **Actor will be rejected** | +| Account display name (actor `name`) length | 2048 | Display name will be truncated | +| Account note (actor `summary`) length | 20kB | Account note will be truncated | +| Account `attributionDomains` | 256 | List will be truncated | +| Account aliases (actor `alsoKnownAs`) | 256 | List will be truncated | +| Custom emoji shortcode (`Emoji` `name`) | 2048 | Emoji will be rejected | diff --git a/app/controllers/activitypub/inboxes_controller.rb b/app/controllers/activitypub/inboxes_controller.rb index 1f7abb97fa5..cf46bf21b5e 100644 --- a/app/controllers/activitypub/inboxes_controller.rb +++ b/app/controllers/activitypub/inboxes_controller.rb @@ -3,6 +3,7 @@ class ActivityPub::InboxesController < ActivityPub::BaseController include JsonLdHelper + before_action :skip_large_payload before_action :skip_unknown_actor_activity before_action :require_actor_signature! skip_before_action :authenticate_user! @@ -16,6 +17,10 @@ class ActivityPub::InboxesController < ActivityPub::BaseController private + def skip_large_payload + head 413 if request.content_length > ActivityPub::Activity::MAX_JSON_SIZE + end + def skip_unknown_actor_activity head 202 if unknown_affected_account? end diff --git a/app/lib/activitypub/activity.rb b/app/lib/activitypub/activity.rb index d07d1c2f241..eab345ce457 100644 --- a/app/lib/activitypub/activity.rb +++ b/app/lib/activitypub/activity.rb @@ -5,6 +5,7 @@ class ActivityPub::Activity include Redisable include Lockable + MAX_JSON_SIZE = 1.megabyte SUPPORTED_TYPES = %w(Note Question).freeze CONVERTED_TYPES = %w(Image Audio Video Article Page Event).freeze diff --git a/app/lib/activitypub/parser/poll_parser.rb b/app/lib/activitypub/parser/poll_parser.rb index 758c03f07ec..d43eaf6cfb4 100644 --- a/app/lib/activitypub/parser/poll_parser.rb +++ b/app/lib/activitypub/parser/poll_parser.rb @@ -3,6 +3,10 @@ class ActivityPub::Parser::PollParser include JsonLdHelper + # Limit the number of items for performance purposes. + # We truncate rather than error out to avoid missing the post entirely. + MAX_ITEMS = 500 + def initialize(json) @json = json end @@ -48,6 +52,6 @@ class ActivityPub::Parser::PollParser private def items - @json['anyOf'] || @json['oneOf'] + (@json['anyOf'] || @json['oneOf'])&.take(MAX_ITEMS) end end diff --git a/app/models/account.rb b/app/models/account.rb index deb1589a090..32f6e39840f 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -81,6 +81,13 @@ class Account < ApplicationRecord DISPLAY_NAME_LENGTH_LIMIT = 30 NOTE_LENGTH_LIMIT = 500 + # Hard limits for federated content + USERNAME_LENGTH_HARD_LIMIT = 2048 + DISPLAY_NAME_LENGTH_HARD_LIMIT = 2048 + NOTE_LENGTH_HARD_LIMIT = 20.kilobytes + ATTRIBUTION_DOMAINS_HARD_LIMIT = 256 + ALSO_KNOWN_AS_HARD_LIMIT = 256 + AUTOMATED_ACTOR_TYPES = %w(Application Service).freeze include Attachmentable # Load prior to Avatar & Header concerns @@ -114,7 +121,7 @@ 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 }, length: { maximum: USERNAME_LENGTH_HARD_LIMIT }, if: -> { (remote? || actor_type_application?) && will_save_change_to_username? } # Remote user validations validates :uri, presence: true, unless: :local?, on: :create diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb index 65a2faa9fd5..5c39e053be1 100644 --- a/app/models/custom_emoji.rb +++ b/app/models/custom_emoji.rb @@ -26,6 +26,8 @@ class CustomEmoji < ApplicationRecord LIMIT = 256.kilobytes MINIMUM_SHORTCODE_SIZE = 2 + MAX_SHORTCODE_SIZE = 128 + MAX_FEDERATED_SHORTCODE_SIZE = 2048 SHORTCODE_RE_FRAGMENT = '[a-zA-Z0-9_]{2,}' @@ -45,7 +47,8 @@ class CustomEmoji < ApplicationRecord normalizes :domain, with: ->(domain) { domain.downcase.strip } validates_attachment :image, content_type: { content_type: IMAGE_MIME_TYPES }, presence: true, size: { less_than: LIMIT } - validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE } + validates :shortcode, uniqueness: { scope: :domain }, format: { with: SHORTCODE_ONLY_RE }, length: { minimum: MINIMUM_SHORTCODE_SIZE, maximum: MAX_FEDERATED_SHORTCODE_SIZE } + validates :shortcode, length: { maximum: MAX_SHORTCODE_SIZE }, if: :local? scope :local, -> { where(domain: nil) } scope :remote, -> { where.not(domain: nil) } diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index f133fbc84ae..6f4aa2fdb6e 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -6,6 +6,7 @@ class ActivityPub::ProcessAccountService < BaseService include Redisable include Lockable + MAX_PROFILE_FIELDS = 50 SUBDOMAINS_RATELIMIT = 10 DISCOVERIES_PER_REQUEST = 400 @@ -124,15 +125,15 @@ class ActivityPub::ProcessAccountService < BaseService def set_immediate_attributes! @account.featured_collection_url = valid_collection_uri(@json['featured']) - @account.display_name = @json['name'] || '' - @account.note = @json['summary'] || '' + @account.display_name = (@json['name'] || '')[0...(Account::DISPLAY_NAME_LENGTH_HARD_LIMIT)] + @account.note = (@json['summary'] || '')[0...(Account::NOTE_LENGTH_HARD_LIMIT)] @account.locked = @json['manuallyApprovesFollowers'] || false @account.fields = property_values || {} - @account.also_known_as = as_array(@json['alsoKnownAs'] || []).map { |item| value_or_id(item) } + @account.also_known_as = as_array(@json['alsoKnownAs'] || []).take(Account::ALSO_KNOWN_AS_HARD_LIMIT).map { |item| value_or_id(item) } @account.discoverable = @json['discoverable'] || false @account.indexable = @json['indexable'] || false @account.memorial = @json['memorial'] || false - @account.attribution_domains = as_array(@json['attributionDomains'] || []).map { |item| value_or_id(item) } + @account.attribution_domains = as_array(@json['attributionDomains'] || []).take(Account::ATTRIBUTION_DOMAINS_HARD_LIMIT).map { |item| value_or_id(item) } end def set_fetchable_key! @@ -253,7 +254,10 @@ class ActivityPub::ProcessAccountService < BaseService def property_values return unless @json['attachment'].is_a?(Array) - as_array(@json['attachment']).select { |attachment| attachment['type'] == 'PropertyValue' }.map { |attachment| attachment.slice('name', 'value') } + as_array(@json['attachment']) + .select { |attachment| attachment['type'] == 'PropertyValue' } + .take(MAX_PROFILE_FIELDS) + .map { |attachment| attachment.slice('name', 'value') } end def mismatching_origin?(url) From 400c1f3e8ec0ffd33ad30d9334b9210cdb89b14c Mon Sep 17 00:00:00 2001 From: Claire Date: Tue, 20 Jan 2026 16:00:10 +0100 Subject: [PATCH 77/89] Bump version to v4.5.5 (#37551) --- CHANGELOG.md | 27 +++++++++++++++++++++++++++ docker-compose.yml | 6 +++--- lib/mastodon/version.rb | 2 +- 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c5ec67d854..39e975479e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,33 @@ All notable changes to this project will be documented in this file. +## [4.5.5] - 2026-01-20 + +### Security + +- Fix missing limits on various federated properties [GHSA-gg8q-rcg7-p79g](https://github.com/mastodon/mastodon/security/advisories/GHSA-gg8q-rcg7-p79g) +- Fix remote user suspension bypass [GHSA-5h2f-wg8j-xqwp](https://github.com/mastodon/mastodon/security/advisories/GHSA-5h2f-wg8j-xqwp) +- Fix missing length limits on some user-provided fields [GHSA-6x3w-9g92-gvf3](https://github.com/mastodon/mastodon/security/advisories/GHSA-6x3w-9g92-gvf3) +- Fix missing access check for push notification settings update [GHSA-f3q8-7vw3-69v4](https://github.com/mastodon/mastodon/security/advisories/GHSA-f3q8-7vw3-69v4) + +### Changed + +- Skip tombstone creation on deleting from 404 (#37533 by @ClearlyClaire) + +### Fixed + +- Fix potential duplicate handling of quote accept/reject/delete (#37537 by @ClearlyClaire) +- Fix `FeedManager#filter_from_home` error when handling a reblog of a deleted status (#37486 by @ClearlyClaire) +- Fix needlessly complicated SQL query in status batch removal (#37469 by @ClearlyClaire) +- Fix `quote_approval_policy` being reset to user defaults when omitted in status update (#37436 and #37474 by @mjankowski and @shleeable) +- Fix `Vary` parsing in cache control enforcement (#37426 by @MegaManSec) +- Fix missing URI scheme test in `QuoteRequest` handling (#37425 by @MegaManSec) +- Fix thread-unsafe ActivityPub activity dispatch (#37423 by @MegaManSec) +- Fix URI generation for reblogs by accounts with numerical ActivityPub identifiers (#37415 by @oneiros) +- Fix SignatureParser accepting duplicate parameters in HTTP Signature header (#37375 by @shleeable) +- Fix emoji with variant selector not being rendered properly (#37320 by @ChaosExAnima) +- Fix mobile admin sidebar displaying under batch table toolbar (#37307 by @diondiondion) + ## [4.5.4] - 2026-01-07 ### Security diff --git a/docker-compose.yml b/docker-compose.yml index d4974eb1bdd..52d2a83f443 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -59,7 +59,7 @@ services: web: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.5.4 + image: ghcr.io/mastodon/mastodon:v4.5.5 restart: always env_file: .env.production command: bundle exec puma -C config/puma.rb @@ -83,7 +83,7 @@ services: # build: # dockerfile: ./streaming/Dockerfile # context: . - image: ghcr.io/mastodon/mastodon-streaming:v4.5.4 + image: ghcr.io/mastodon/mastodon-streaming:v4.5.5 restart: always env_file: .env.production command: node ./streaming/index.js @@ -102,7 +102,7 @@ services: sidekiq: # You can uncomment the following line if you want to not use the prebuilt image, for example if you have local code changes # build: . - image: ghcr.io/mastodon/mastodon:v4.5.4 + image: ghcr.io/mastodon/mastodon:v4.5.5 restart: always env_file: .env.production command: bundle exec sidekiq diff --git a/lib/mastodon/version.rb b/lib/mastodon/version.rb index 235ac92cbd6..f532276f85e 100644 --- a/lib/mastodon/version.rb +++ b/lib/mastodon/version.rb @@ -17,7 +17,7 @@ module Mastodon end def default_prerelease - 'alpha.2' + 'alpha.3' end def prerelease From 22ec36857415db40c3c0af843bda5217f893494a Mon Sep 17 00:00:00 2001 From: Echo Date: Tue, 20 Jan 2026 16:15:49 +0100 Subject: [PATCH 78/89] Profile redesign: Badges (#37550) --- app/javascript/images/icons/icon_admin.svg | 10 ++++ app/javascript/mastodon/components/badge.jsx | 31 ------------- app/javascript/mastodon/components/badge.tsx | 46 +++++++++++++++++++ .../account_timeline/components/badges.tsx | 38 +++++++++++---- .../components/redesign.module.scss | 21 +++++++++ 5 files changed, 107 insertions(+), 39 deletions(-) create mode 100644 app/javascript/images/icons/icon_admin.svg delete mode 100644 app/javascript/mastodon/components/badge.jsx create mode 100644 app/javascript/mastodon/components/badge.tsx diff --git a/app/javascript/images/icons/icon_admin.svg b/app/javascript/images/icons/icon_admin.svg new file mode 100644 index 00000000000..7e40dc46437 --- /dev/null +++ b/app/javascript/images/icons/icon_admin.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/javascript/mastodon/components/badge.jsx b/app/javascript/mastodon/components/badge.jsx deleted file mode 100644 index 2a335d7f506..00000000000 --- a/app/javascript/mastodon/components/badge.jsx +++ /dev/null @@ -1,31 +0,0 @@ -import PropTypes from 'prop-types'; - -import { FormattedMessage } from 'react-intl'; - -import GroupsIcon from '@/material-icons/400-24px/group.svg?react'; -import PersonIcon from '@/material-icons/400-24px/person.svg?react'; -import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react'; - - -export const Badge = ({ icon = , label, domain, roleId }) => ( -
- {icon} - {label} - {domain && {domain}} -
-); - -Badge.propTypes = { - icon: PropTypes.node, - label: PropTypes.node, - domain: PropTypes.node, - roleId: PropTypes.string -}; - -export const GroupBadge = () => ( - } label={} /> -); - -export const AutomatedBadge = () => ( - } label={} /> -); diff --git a/app/javascript/mastodon/components/badge.tsx b/app/javascript/mastodon/components/badge.tsx new file mode 100644 index 00000000000..b7dc169edbc --- /dev/null +++ b/app/javascript/mastodon/components/badge.tsx @@ -0,0 +1,46 @@ +import type { FC, ReactNode } from 'react'; + +import { FormattedMessage } from 'react-intl'; + +import classNames from 'classnames'; + +import GroupsIcon from '@/material-icons/400-24px/group.svg?react'; +import PersonIcon from '@/material-icons/400-24px/person.svg?react'; +import SmartToyIcon from '@/material-icons/400-24px/smart_toy.svg?react'; + +export const Badge: FC<{ + label: ReactNode; + icon?: ReactNode; + className?: string; + domain?: ReactNode; + roleId?: string; +}> = ({ icon = , label, className, domain, roleId }) => ( +
+ {icon} + {label} + {domain && {domain}} +
+); + +export const GroupBadge: FC<{ className?: string }> = ({ className }) => ( + } + label={ + + } + className={className} + /> +); + +export const AutomatedBadge: FC<{ className?: string }> = ({ className }) => ( + } + label={ + + } + className={className} + /> +); diff --git a/app/javascript/mastodon/features/account_timeline/components/badges.tsx b/app/javascript/mastodon/features/account_timeline/components/badges.tsx index cc6c456e9c1..1c5942d90d3 100644 --- a/app/javascript/mastodon/features/account_timeline/components/badges.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/badges.tsx @@ -1,9 +1,16 @@ -import type { FC } from 'react'; +import type { FC, ReactNode } from 'react'; +import IconAdmin from '@/images/icons/icon_admin.svg?react'; import { AutomatedBadge, Badge, GroupBadge } from '@/mastodon/components/badge'; +import { Icon } from '@/mastodon/components/icon'; import { useAccount } from '@/mastodon/hooks/useAccount'; +import type { AccountRole } from '@/mastodon/models/account'; import { useAppSelector } from '@/mastodon/store'; +import { isRedesignEnabled } from '../common'; + +import classes from './redesign.module.scss'; + export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { const account = useAccount(accountId); const localDomain = useAppSelector( @@ -15,22 +22,32 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { return null; } + const className = isRedesignEnabled() ? classes.badge : ''; + if (account.bot) { - badges.push(); + badges.push(); } else if (account.group) { - badges.push(); + badges.push(); } const domain = account.acct.includes('@') ? account.acct.split('@')[1] : localDomain; account.roles.forEach((role) => { + let icon: ReactNode = undefined; + if (isAdminBadge(role)) { + icon = ( + + ); + } badges.push( {role.get('name')}} - domain={domain} - roleId={role.get('id')} + key={role.id} + label={role.name} + className={className} + domain={isRedesignEnabled() ? `(${domain})` : domain} + roleId={role.id} + icon={icon} />, ); }); @@ -39,5 +56,10 @@ export const AccountBadges: FC<{ accountId: string }> = ({ accountId }) => { return null; } - return
{badges}
; + return
{badges}
; }; + +function isAdminBadge(role: AccountRole) { + const name = role.name.toLowerCase(); + return isRedesignEnabled() && (name === 'admin' || name === 'owner'); +} diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss index 757f5a42314..5ccdb1f3100 100644 --- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -39,6 +39,27 @@ h1.name > small { } } +.badge { + background-color: var(--color-bg-secondary); + border: none; + color: var(--color-text-secondary); + font-weight: 600; + + > span { + font-weight: unset; + opacity: 1; + } +} + +svg.badgeIcon { + opacity: 1; + fill: revert-layer; + + path { + fill: revert-layer; + } +} + .fieldList { margin-top: 16px; } From e65103bd3de034b89196f7f0d86a6e9eb157971e Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 21 Jan 2026 10:18:16 +0100 Subject: [PATCH 79/89] Fix rendering of initial state when `collections` feature is enabled (#37556) --- app/serializers/initial_state_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/serializers/initial_state_serializer.rb b/app/serializers/initial_state_serializer.rb index fe2a857d509..a8e4b1d7f79 100644 --- a/app/serializers/initial_state_serializer.rb +++ b/app/serializers/initial_state_serializer.rb @@ -140,7 +140,7 @@ class InitialStateSerializer < ActiveModel::Serializer end def serialized_account(account) - ActiveModelSerializers::SerializableResource.new(account, serializer: REST::AccountSerializer) + ActiveModelSerializers::SerializableResource.new(account, serializer: REST::AccountSerializer, scope_name: :current_user, scope: object.current_account&.user) end def instance_presenter From e7c6600d83d043a91769142e249a8818cba855d6 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 21 Jan 2026 13:02:41 +0100 Subject: [PATCH 80/89] Fix cross-server conversation tracking (#37559) --- app/lib/activitypub/activity/create.rb | 1 + app/lib/activitypub/tag_manager.rb | 16 +++++++----- app/lib/ostatus/tag_manager.rb | 10 +++----- spec/lib/activitypub/activity/create_spec.rb | 26 +++++++++++++++++++- spec/lib/activitypub/tag_manager_spec.rb | 8 ------ 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/app/lib/activitypub/activity/create.rb b/app/lib/activitypub/activity/create.rb index 43c7bb1fe71..a7d2be35ed0 100644 --- a/app/lib/activitypub/activity/create.rb +++ b/app/lib/activitypub/activity/create.rb @@ -379,6 +379,7 @@ class ActivityPub::Activity::Create < ActivityPub::Activity def conversation_from_uri(uri) return nil if uri.nil? return Conversation.find_by(id: OStatus::TagManager.instance.unique_tag_to_local_id(uri, 'Conversation')) if OStatus::TagManager.instance.local_id?(uri) + return ActivityPub::TagManager.instance.uri_to_resource(uri, Conversation) if ActivityPub::TagManager.instance.local_uri?(uri) begin Conversation.find_or_create_by!(uri: uri) diff --git a/app/lib/activitypub/tag_manager.rb b/app/lib/activitypub/tag_manager.rb index e6714c51abb..f9cb90f548c 100644 --- a/app/lib/activitypub/tag_manager.rb +++ b/app/lib/activitypub/tag_manager.rb @@ -243,12 +243,6 @@ class ActivityPub::TagManager !host.nil? && (::TagManager.instance.local_domain?(host) || ::TagManager.instance.web_domain?(host)) end - def uri_to_local_id(uri, param = :id) - path_params = Rails.application.routes.recognize_path(uri) - path_params[:username] = Rails.configuration.x.local_domain if path_params[:controller] == 'instance_actors' - path_params[param] - end - def uris_to_local_accounts(uris) usernames = [] ids = [] @@ -266,6 +260,14 @@ class ActivityPub::TagManager uri_to_resource(uri, Account) end + def uri_to_local_conversation(uri) + path_params = Rails.application.routes.recognize_path(uri) + return unless path_params[:controller] == 'activitypub/contexts' + + account_id, conversation_id = path_params[:id].split('-') + Conversation.find_by(parent_account_id: account_id, id: conversation_id) + end + def uri_to_resource(uri, klass) return if uri.nil? @@ -273,6 +275,8 @@ class ActivityPub::TagManager case klass.name when 'Account' uris_to_local_accounts([uri]).first + when 'Conversation' + uri_to_local_conversation(uri) else StatusFinder.new(uri).status end diff --git a/app/lib/ostatus/tag_manager.rb b/app/lib/ostatus/tag_manager.rb index cb0c9f89668..7d0f23c4dc1 100644 --- a/app/lib/ostatus/tag_manager.rb +++ b/app/lib/ostatus/tag_manager.rb @@ -11,16 +11,12 @@ class OStatus::TagManager def unique_tag_to_local_id(tag, expected_type) return nil unless local_id?(tag) - if ActivityPub::TagManager.instance.local_uri?(tag) - ActivityPub::TagManager.instance.uri_to_local_id(tag) - else - matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) - matches[1] unless matches.nil? - end + matches = Regexp.new("objectId=([\\d]+):objectType=#{expected_type}").match(tag) + matches[1] unless matches.nil? end def local_id?(id) - id.start_with?("tag:#{Rails.configuration.x.local_domain}") || ActivityPub::TagManager.instance.local_uri?(id) + id.start_with?("tag:#{Rails.configuration.x.local_domain}") end def uri_for(target) diff --git a/spec/lib/activitypub/activity/create_spec.rb b/spec/lib/activitypub/activity/create_spec.rb index 1e8a2a29db4..19b6014af15 100644 --- a/spec/lib/activitypub/activity/create_spec.rb +++ b/spec/lib/activitypub/activity/create_spec.rb @@ -471,7 +471,7 @@ RSpec.describe ActivityPub::Activity::Create do end end - context 'with a reply' do + context 'with a reply without explicitly setting a conversation' do let(:original_status) { Fabricate(:status) } let(:object_json) do @@ -493,6 +493,30 @@ RSpec.describe ActivityPub::Activity::Create do end end + context 'with a reply explicitly setting a conversation' do + let(:original_status) { Fabricate(:status) } + + let(:object_json) do + build_object( + inReplyTo: ActivityPub::TagManager.instance.uri_for(original_status), + conversation: ActivityPub::TagManager.instance.uri_for(original_status.conversation), + context: ActivityPub::TagManager.instance.uri_for(original_status.conversation) + ) + end + + it 'creates status' do + expect { subject.perform }.to change(sender.statuses, :count).by(1) + + status = sender.statuses.first + + expect(status).to_not be_nil + expect(status.thread).to eq original_status + expect(status.reply?).to be true + expect(status.in_reply_to_account).to eq original_status.account + expect(status.conversation).to eq original_status.conversation + end + end + context 'with mentions' do let(:recipient) { Fabricate(:account) } diff --git a/spec/lib/activitypub/tag_manager_spec.rb b/spec/lib/activitypub/tag_manager_spec.rb index 55e54ede5e1..a15529057cb 100644 --- a/spec/lib/activitypub/tag_manager_spec.rb +++ b/spec/lib/activitypub/tag_manager_spec.rb @@ -629,14 +629,6 @@ RSpec.describe ActivityPub::TagManager do end end - describe '#uri_to_local_id' do - let(:account) { Fabricate(:account, id_scheme: :username_ap_id) } - - it 'returns the local ID' do - expect(subject.uri_to_local_id(subject.uri_for(account), :username)).to eq account.username - end - end - describe '#uris_to_local_accounts' do it 'returns the expected local accounts' do account = Fabricate(:account) From 783504f36a394a7eaa4e34552116cfeb0c7cd1c6 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 21 Jan 2026 13:30:07 +0100 Subject: [PATCH 81/89] Do not return undiscoverable collections (#37560) --- .../api/v1_alpha/collections_controller.rb | 1 + app/models/collection.rb | 1 + .../requests/api/v1_alpha/collections_spec.rb | 26 +++++++++++++++++++ 3 files changed, 28 insertions(+) diff --git a/app/controllers/api/v1_alpha/collections_controller.rb b/app/controllers/api/v1_alpha/collections_controller.rb index 9d6b2f9a381..4b07b5012a2 100644 --- a/app/controllers/api/v1_alpha/collections_controller.rb +++ b/app/controllers/api/v1_alpha/collections_controller.rb @@ -74,6 +74,7 @@ class Api::V1Alpha::CollectionsController < Api::BaseController .order(created_at: :desc) .offset(offset_param) .limit(limit_param(DEFAULT_COLLECTIONS_LIMIT)) + @collections = @collections.discoverable unless @account == current_account end def set_collection diff --git a/app/models/collection.rb b/app/models/collection.rb index 334318b73d3..3681c41d84f 100644 --- a/app/models/collection.rb +++ b/app/models/collection.rb @@ -43,6 +43,7 @@ class Collection < ApplicationRecord scope :with_items, -> { includes(:collection_items).merge(CollectionItem.with_accounts) } scope :with_tag, -> { includes(:tag) } + scope :discoverable, -> { where(discoverable: true) } def remote? !local? diff --git a/spec/requests/api/v1_alpha/collections_spec.rb b/spec/requests/api/v1_alpha/collections_spec.rb index b529fc2d92f..de79dcf7230 100644 --- a/spec/requests/api/v1_alpha/collections_spec.rb +++ b/spec/requests/api/v1_alpha/collections_spec.rb @@ -55,6 +55,32 @@ RSpec.describe 'Api::V1Alpha::Collections', feature: :collections do ) end end + + context 'when some collections are not discoverable' do + before do + Fabricate(:collection, account:, discoverable: false) + end + + context 'when requesting user is a third party' do + it 'hides the collections that are not discoverable' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body.size).to eq 3 + end + end + + context 'when requesting user owns the collection' do + let(:account) { user.account } + + it 'returns all collections, including the ones that are not discoverable' do + subject + + expect(response).to have_http_status(200) + expect(response.parsed_body.size).to eq 4 + end + end + end end describe 'GET /api/v1_alpha/collections/:id' do From 24ffa00bca165e8c180a5e45ba0380a2d2b37f81 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:31:19 +0000 Subject: [PATCH 82/89] Update dependency pino to v10.2.1 (#37543) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index f37750a4cc7..d5efe483cba 100644 --- a/yarn.lock +++ b/yarn.lock @@ -10681,8 +10681,8 @@ __metadata: linkType: hard "pino@npm:^10.0.0": - version: 10.2.0 - resolution: "pino@npm:10.2.0" + version: 10.2.1 + resolution: "pino@npm:10.2.1" dependencies: "@pinojs/redact": "npm:^0.4.0" atomic-sleep: "npm:^1.0.0" @@ -10697,7 +10697,7 @@ __metadata: thread-stream: "npm:^4.0.0" bin: pino: bin.js - checksum: 10c0/8f88a2e205508d47ef04d2a6ec26ec450abb4b344d2d998d2e24b9e624e1a1ef7184f260ca5be06bc3733aa1ad76704657e373b359c7b71489a11709227e26da + checksum: 10c0/2eaed48bb7fb8865e27ac6d6709383f5c117f1e59c818734c7cc22b362e9aa5846a0547e7fd9cde64088a3b48aa314e1dab07ee16da8dc3b87897970eb56843e languageName: node linkType: hard From 5d82d48af3e6cdeabe2559e1e17ca48c4d716837 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 12:31:35 +0000 Subject: [PATCH 83/89] Update dependency stylelint-config-standard-scss to v17 (#37511) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 89 ++++++++++++++++++++++++---------------------------- 2 files changed, 42 insertions(+), 49 deletions(-) diff --git a/package.json b/package.json index 701719d7cff..987467458d3 100644 --- a/package.json +++ b/package.json @@ -186,7 +186,7 @@ "storybook": "^10.0.5", "stylelint": "^16.19.1", "stylelint-config-prettier-scss": "^1.0.0", - "stylelint-config-standard-scss": "^16.0.0", + "stylelint-config-standard-scss": "^17.0.0", "typescript": "~5.9.0", "typescript-eslint": "^8.45.0", "typescript-plugin-css-modules": "^5.2.0", diff --git a/yarn.lock b/yarn.lock index d5efe483cba..e36a0cca262 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2990,7 +2990,7 @@ __metadata: stringz: "npm:^2.1.0" stylelint: "npm:^16.19.1" stylelint-config-prettier-scss: "npm:^1.0.0" - stylelint-config-standard-scss: "npm:^16.0.0" + stylelint-config-standard-scss: "npm:^17.0.0" substring-trie: "npm:^1.0.2" tesseract.js: "npm:^7.0.0" tiny-queue: "npm:^0.2.1" @@ -9311,13 +9311,6 @@ __metadata: languageName: node linkType: hard -"known-css-properties@npm:^0.36.0": - version: 0.36.0 - resolution: "known-css-properties@npm:0.36.0" - checksum: 10c0/098c8f956408a7ce26a639c2354e0184fb2bb2772bb7d1ba23192b6b6cf5818cbb8a0acfb4049705ea103d9916065703bc540fa084a6349fdb41bf745aada4bc - languageName: node - linkType: hard - "known-css-properties@npm:^0.37.0": version: 0.37.0 resolution: "known-css-properties@npm:0.37.0" @@ -9679,10 +9672,10 @@ __metadata: languageName: node linkType: hard -"mdn-data@npm:^2.21.0": - version: 2.21.0 - resolution: "mdn-data@npm:2.21.0" - checksum: 10c0/cd26902551af2cc29f06f130893cb04bca9ee278939fce3ffbcb759497cc80d53a6f4abdef2ae2f3ed3c95ac8d651f53fc141defd580ebf4ae2f93aea325957b +"mdn-data@npm:^2.25.0": + version: 2.26.0 + resolution: "mdn-data@npm:2.26.0" + checksum: 10c0/e5f17f4dac247f3e260c081761628d371e23659a7ff13413f83f5bd7fd0f2d8317e72277bb77f0e13115041334ff728a5363db64aabaf376c0e1b0b31016d0b8 languageName: node linkType: hard @@ -13185,74 +13178,74 @@ __metadata: languageName: node linkType: hard -"stylelint-config-recommended-scss@npm:^16.0.1": - version: 16.0.2 - resolution: "stylelint-config-recommended-scss@npm:16.0.2" +"stylelint-config-recommended-scss@npm:^17.0.0": + version: 17.0.0 + resolution: "stylelint-config-recommended-scss@npm:17.0.0" dependencies: postcss-scss: "npm:^4.0.9" - stylelint-config-recommended: "npm:^17.0.0" - stylelint-scss: "npm:^6.12.1" + stylelint-config-recommended: "npm:^18.0.0" + stylelint-scss: "npm:^7.0.0" peerDependencies: postcss: ^8.3.3 - stylelint: ^16.24.0 + stylelint: ^17.0.0 peerDependenciesMeta: postcss: optional: true - checksum: 10c0/d4e30a881e248d8b039347bf967526f6afe6d6a07f18e2747e14568de32273e819ba478be7a61a0dd63178931b4e891050a34e73d296ab533aa434209a7f3146 + checksum: 10c0/05b2e8d4316c2a8cc66eed0a2a8f01237e0ee8966a2e73d0b3c6706694f7630be165daa5a0cef511bc51f7e3fcb07a84c55d948c15fe6193a7e13cf9bb67c913 languageName: node linkType: hard -"stylelint-config-recommended@npm:^17.0.0": +"stylelint-config-recommended@npm:^18.0.0": + version: 18.0.0 + resolution: "stylelint-config-recommended@npm:18.0.0" + peerDependencies: + stylelint: ^17.0.0 + checksum: 10c0/c7f8ff45c76ec23f4c8c0438894726976fd5e872c59d489f959b728d9879bba20dbf0040cd29ad3bbc00eb32befd95f5b6ca150002bb8aea74b0797bc42ccc17 + languageName: node + linkType: hard + +"stylelint-config-standard-scss@npm:^17.0.0": version: 17.0.0 - resolution: "stylelint-config-recommended@npm:17.0.0" - peerDependencies: - stylelint: ^16.23.0 - checksum: 10c0/49e5d1c0f58197b2c5585b85fad814fed9bdec44c9870368c46a762664c5ff158c1145b6337456ae194409d692992b5b87421d62880422f71d8a3360417f5ad1 - languageName: node - linkType: hard - -"stylelint-config-standard-scss@npm:^16.0.0": - version: 16.0.0 - resolution: "stylelint-config-standard-scss@npm:16.0.0" + resolution: "stylelint-config-standard-scss@npm:17.0.0" dependencies: - stylelint-config-recommended-scss: "npm:^16.0.1" - stylelint-config-standard: "npm:^39.0.0" + stylelint-config-recommended-scss: "npm:^17.0.0" + stylelint-config-standard: "npm:^40.0.0" peerDependencies: postcss: ^8.3.3 - stylelint: ^16.23.1 + stylelint: ^17.0.0 peerDependenciesMeta: postcss: optional: true - checksum: 10c0/eb77f23824c5d649b193cb71d7f9b538b32b8cc1769451b2993270361127243d4011baf891ec265711b8e34e69ce28acb57ab6c3947b51fa3713ac26f4276439 + checksum: 10c0/0506537ba896f3d5e0fb002608090fcb41aa8ba7b65f1de8533702ce7c70e3f92b275782788a8356b5b687c86c53468c223e082226dda62780294b1cba324a36 languageName: node linkType: hard -"stylelint-config-standard@npm:^39.0.0": - version: 39.0.1 - resolution: "stylelint-config-standard@npm:39.0.1" +"stylelint-config-standard@npm:^40.0.0": + version: 40.0.0 + resolution: "stylelint-config-standard@npm:40.0.0" dependencies: - stylelint-config-recommended: "npm:^17.0.0" + stylelint-config-recommended: "npm:^18.0.0" peerDependencies: - stylelint: ^16.23.0 - checksum: 10c0/70a9862a2cedcc2a1807bd92fc91c40877270cf8a39576b91ae056d6de51d3b68104b26f71056ff22461b4319e9ec988d009abf10ead513b2ec15569d82e865a + stylelint: ^17.0.0 + checksum: 10c0/d8942552d53a3afda59b64d0c49503bb626fe5cef39a9e8c9583fcd60869f21431125ef4480ff27a59f7f2cf0da8af810d377129ef1d670ddc5def4defe2880c languageName: node linkType: hard -"stylelint-scss@npm:^6.12.1": - version: 6.12.1 - resolution: "stylelint-scss@npm:6.12.1" +"stylelint-scss@npm:^7.0.0": + version: 7.0.0 + resolution: "stylelint-scss@npm:7.0.0" dependencies: css-tree: "npm:^3.0.1" is-plain-object: "npm:^5.0.0" - known-css-properties: "npm:^0.36.0" - mdn-data: "npm:^2.21.0" + known-css-properties: "npm:^0.37.0" + mdn-data: "npm:^2.25.0" postcss-media-query-parser: "npm:^0.2.3" postcss-resolve-nested-selector: "npm:^0.1.6" - postcss-selector-parser: "npm:^7.1.0" + postcss-selector-parser: "npm:^7.1.1" postcss-value-parser: "npm:^4.2.0" peerDependencies: - stylelint: ^16.0.2 - checksum: 10c0/9a0903d34be3c75a72bef32402899db5f6b94c0823c5944fdf1acb2c3dc61c1f70fbb322558f8cb7e42dd01ed5e0dec22ed298f03b7bacc9f467c28330acae71 + stylelint: ^16.8.2 || ^17.0.0 + checksum: 10c0/07d0f20c6bcb34b8b0b6bfb1d4367b4825b52a7eef7dde2adfbaec11ebc67242e6b99dccf70dfbef1eb0a9bf8712fe0ab49d183ff6e4cca9c7f89752f7e27027 languageName: node linkType: hard From e79d51ce19274504dc85f4bb3fbf8502c0cfdc58 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 21 Jan 2026 14:08:08 +0100 Subject: [PATCH 84/89] Profile redesign: verified badges (#37538) --- app/javascript/images/icons/icon_verified.svg | 10 ++++ .../mastodon/components/mini_card/list.tsx | 5 +- .../account_timeline/components/fields.tsx | 57 ++++++++++++------- .../components/fields_modal.tsx | 28 ++++++--- .../components/redesign.module.scss | 38 +++++++++++++ .../styles/mastodon/theme/_dark.scss | 1 + .../styles/mastodon/theme/_light.scss | 1 + 7 files changed, 113 insertions(+), 27 deletions(-) create mode 100644 app/javascript/images/icons/icon_verified.svg diff --git a/app/javascript/images/icons/icon_verified.svg b/app/javascript/images/icons/icon_verified.svg new file mode 100644 index 00000000000..65873b9dc43 --- /dev/null +++ b/app/javascript/images/icons/icon_verified.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/app/javascript/mastodon/components/mini_card/list.tsx b/app/javascript/mastodon/components/mini_card/list.tsx index f775e70aac8..318c5849538 100644 --- a/app/javascript/mastodon/components/mini_card/list.tsx +++ b/app/javascript/mastodon/components/mini_card/list.tsx @@ -10,7 +10,9 @@ import type { MiniCardProps } from '.'; import classes from './styles.module.css'; interface MiniCardListProps { - cards?: (Pick & { key?: Key })[]; + cards?: (Pick & { + key?: Key; + })[]; className?: string; onOverflowClick?: MouseEventHandler; } @@ -42,6 +44,7 @@ export const MiniCardList: FC = ({ label={card.label} value={card.value} hidden={hasOverflow && index >= hiddenIndex} + className={card.className} /> ))} diff --git a/app/javascript/mastodon/features/account_timeline/components/fields.tsx b/app/javascript/mastodon/features/account_timeline/components/fields.tsx index a73d92c1b69..ab29a8299e4 100644 --- a/app/javascript/mastodon/features/account_timeline/components/fields.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/fields.tsx @@ -3,10 +3,14 @@ import type { FC } from 'react'; import { FormattedMessage } from 'react-intl'; +import classNames from 'classnames'; + +import IconVerified from '@/images/icons/icon_verified.svg?react'; import { openModal } from '@/mastodon/actions/modal'; import { AccountFields } from '@/mastodon/components/account_fields'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; import { FormattedDateWrapper } from '@/mastodon/components/formatted_date'; +import { Icon } from '@/mastodon/components/icon'; import { MiniCardList } from '@/mastodon/components/mini_card/list'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; import { useAccount } from '@/mastodon/hooks/useAccount'; @@ -55,25 +59,40 @@ const RedesignAccountHeaderFields: FC<{ account: Account }> = ({ account }) => { const htmlHandlers = useElementHandledLink(); const cards = useMemo( () => - account.fields.toArray().map(({ value_emojified, name_emojified }) => ({ - label: ( - - ), - value: ( - - ), - })), + account.fields + .toArray() + .map(({ value_emojified, name_emojified, verified_at }) => ({ + label: ( + <> + + {!!verified_at && ( + + )} + + ), + value: ( + + ), + className: classNames( + classes.fieldCard, + !!verified_at && classes.fieldCardVerified, + ), + })), [account.emojis, account.fields, htmlHandlers], ); diff --git a/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx index 715f6097f45..103fffca505 100644 --- a/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx +++ b/app/javascript/mastodon/features/account_timeline/components/fields_modal.tsx @@ -2,9 +2,11 @@ import type { FC } from 'react'; import { FormattedMessage, useIntl } from 'react-intl'; +import IconVerified from '@/images/icons/icon_verified.svg?react'; import { DisplayName } from '@/mastodon/components/display_name'; import { AnimateEmojiProvider } from '@/mastodon/components/emoji/context'; import { EmojiHTML } from '@/mastodon/components/emoji/html'; +import { Icon } from '@/mastodon/components/icon'; import { IconButton } from '@/mastodon/components/icon_button'; import { LoadingIndicator } from '@/mastodon/components/loading_indicator'; import { useElementHandledLink } from '@/mastodon/components/status/handled_link'; @@ -56,7 +58,10 @@ export const AccountFieldsModal: FC<{
{account.fields.map((field, index) => ( -
+
- +
+ + {!!field.verified_at && ( + + )} +
))}
diff --git a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss index 5ccdb1f3100..4bc64d05a98 100644 --- a/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss +++ b/app/javascript/mastodon/features/account_timeline/components/redesign.module.scss @@ -64,6 +64,39 @@ svg.badgeIcon { margin-top: 16px; } +.fieldCard { + position: relative; + + a { + color: var(--color-text-brand); + text-decoration: none; + } +} + +.fieldCardVerified { + background-color: var(--color-bg-brand-softer); + + dt { + padding-right: 1rem; + } + + .fieldIconVerified { + position: absolute; + top: 4px; + right: 4px; + } +} + +.fieldIconVerified { + width: 1rem; + height: 1rem; + + // Need to override .icon path. + path { + fill: revert-layer; + } +} + .fieldNumbersWrapper { a { font-weight: unset; @@ -106,4 +139,9 @@ svg.badgeIcon { font-weight: 600; font-size: 15px; } + + .fieldIconVerified { + vertical-align: middle; + margin-left: 4px; + } } diff --git a/app/javascript/styles/mastodon/theme/_dark.scss b/app/javascript/styles/mastodon/theme/_dark.scss index e6fd6d3cc14..9485464e099 100644 --- a/app/javascript/styles/mastodon/theme/_dark.scss +++ b/app/javascript/styles/mastodon/theme/_dark.scss @@ -142,6 +142,7 @@ var(--border-strength-primary) )}; --color-border-media: rgb(252 248 255 / 15%); + --color-border-verified: rgb(220, 3, 240); --color-border-on-bg-secondary: #{utils.css-alpha( var(--color-indigo-200), calc(var(--border-strength-primary) / 1.5) diff --git a/app/javascript/styles/mastodon/theme/_light.scss b/app/javascript/styles/mastodon/theme/_light.scss index f0dc1bdfbc3..534a18367ca 100644 --- a/app/javascript/styles/mastodon/theme/_light.scss +++ b/app/javascript/styles/mastodon/theme/_light.scss @@ -140,6 +140,7 @@ var(--color-grey-950) var(--border-strength-primary) ); --color-border-media: rgb(252 248 255 / 15%); + --color-border-verified: rgb(220, 3, 240); --color-border-on-bg-secondary: var(--color-grey-200); --color-border-on-bg-brand-softer: var(--color-indigo-200); --color-border-on-bg-error-softer: #{utils.css-alpha( From 1468f945093671f67c7444fcc2c56fc42858653d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 13:08:16 +0000 Subject: [PATCH 85/89] New Crowdin Translations (automated) (#37555) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/es.json | 4 ++++ config/locales/ca.yml | 1 + config/locales/simple_form.de.yml | 2 +- 3 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 2b7d1905350..8a3672ad4c9 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -57,6 +57,7 @@ "account.go_to_profile": "Ir al perfil", "account.hide_reblogs": "Ocultar impulsos de @{name}", "account.in_memoriam": "Cuenta conmemorativa.", + "account.joined_long": "Se unió el {date}", "account.joined_short": "Se unió", "account.languages": "Cambiar idiomas suscritos", "account.link_verified_on": "La propiedad de este enlace fue verificada el {date}", @@ -90,6 +91,8 @@ "account.unmute": "Dejar de silenciar a @{name}", "account.unmute_notifications_short": "Dejar de silenciar notificaciones", "account.unmute_short": "Dejar de silenciar", + "account_fields_modal.close": "Cerrar", + "account_fields_modal.title": "Información de {name}", "account_note.placeholder": "Haz clic para añadir nota", "admin.dashboard.daily_retention": "Tasa de retención de usuarios por día después del registro", "admin.dashboard.monthly_retention": "Tasa de retención de usuarios por mes después del registro", @@ -589,6 +592,7 @@ "load_pending": "{count, plural, one {# nuevo elemento} other {# nuevos elementos}}", "loading_indicator.label": "Cargando…", "media_gallery.hide": "Ocultar", + "minicard.more_items": "+{count}", "moved_to_account_banner.text": "Tu cuenta {disabledAccount} está actualmente deshabilitada porque te has mudado a {movedToAccount}.", "mute_modal.hide_from_notifications": "Ocultar de las notificaciones", "mute_modal.hide_options": "Ocultar opciones", diff --git a/config/locales/ca.yml b/config/locales/ca.yml index 0cbbb08f83e..6adcf1871c7 100644 --- a/config/locales/ca.yml +++ b/config/locales/ca.yml @@ -2162,6 +2162,7 @@ ca: error: Hi ha hagut un problema al esborrar la teva clau de seguretat. Tornau-ho a provar. success: La teva clau de seguretat s'ha esborrat correctament. invalid_credential: Clau de seguretat invàlida + nickname: Sobrenom nickname_hint: Introdueix el sobrenom de la teva clau de seguretat nova not_enabled: Encara no has activat WebAuthn not_supported: Aquest navegador no suporta claus de seguretat diff --git a/config/locales/simple_form.de.yml b/config/locales/simple_form.de.yml index 98defb79cdb..a39cf443594 100644 --- a/config/locales/simple_form.de.yml +++ b/config/locales/simple_form.de.yml @@ -175,7 +175,7 @@ de: labels: account: attribution_domains: Websites, die auf dich verweisen dürfen - discoverable: Profil und Beiträge in Suchalgorithmen berücksichtigen + discoverable: Profil und Beiträge in Empfehlungsalgorithmen berücksichtigen fields: name: Beschriftung value: Inhalt From 22e438d7bdefdb467ddb759acf2c5a5ef584af26 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 21 Jan 2026 15:14:48 +0100 Subject: [PATCH 86/89] Update dependency @csstools/stylelint-formatter-github to v2 (#37515) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- package.json | 2 +- yarn.lock | 12 ++++++------ 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/package.json b/package.json index 987467458d3..acdd57e3fd6 100644 --- a/package.json +++ b/package.json @@ -40,7 +40,7 @@ }, "private": true, "dependencies": { - "@csstools/stylelint-formatter-github": "^1.0.0", + "@csstools/stylelint-formatter-github": "^2.0.0", "@dnd-kit/core": "^6.1.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", diff --git a/yarn.lock b/yarn.lock index e36a0cca262..9c14c9e85e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1926,12 +1926,12 @@ __metadata: languageName: node linkType: hard -"@csstools/stylelint-formatter-github@npm:^1.0.0": - version: 1.0.0 - resolution: "@csstools/stylelint-formatter-github@npm:1.0.0" +"@csstools/stylelint-formatter-github@npm:^2.0.0": + version: 2.0.0 + resolution: "@csstools/stylelint-formatter-github@npm:2.0.0" peerDependencies: - stylelint: ^16.6.0 - checksum: 10c0/2052c4e4d89656b2b4176a6d07508ef73278d33c24a7408a3555d07f26ec853f85da95525590c51751fb3150a2ebb5e3083d8200dc6597af2cd8e93198695269 + stylelint: ^17.0.0 + checksum: 10c0/1eddcb749eb93efff2e2d7edb4405459bf558ceaa6d90e792408802f30c55e3482a4cead9e69fd651f04a927e863782fc6cf813c37433da9ff1f068910080a06 languageName: node linkType: hard @@ -2861,7 +2861,7 @@ __metadata: version: 0.0.0-use.local resolution: "@mastodon/mastodon@workspace:." dependencies: - "@csstools/stylelint-formatter-github": "npm:^1.0.0" + "@csstools/stylelint-formatter-github": "npm:^2.0.0" "@dnd-kit/core": "npm:^6.1.0" "@dnd-kit/sortable": "npm:^10.0.0" "@dnd-kit/utilities": "npm:^3.2.2" From 6897475f9be80d9f3f81d06087dec0a3b6d7cc44 Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 21 Jan 2026 16:54:52 +0100 Subject: [PATCH 87/89] Adds theming to Storybook (#37562) --- .storybook/preview.tsx | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/.storybook/preview.tsx b/.storybook/preview.tsx index abbd193c681..10d45acfe65 100644 --- a/.storybook/preview.tsx +++ b/.storybook/preview.tsx @@ -50,9 +50,19 @@ const preview: Preview = { dynamicTitle: true, }, }, + theme: { + description: 'Theme for the story', + toolbar: { + title: 'Theme', + icon: 'circlehollow', + items: [{ value: 'light' }, { value: 'dark' }], + dynamicTitle: true, + }, + }, }, initialGlobals: { locale: 'en', + theme: 'light', }, decorators: [ (Story, { parameters, globals, args, argTypes }) => { @@ -135,6 +145,13 @@ const preview: Preview = { ); }, + (Story, { globals }) => { + const theme = (globals.theme as string) || 'light'; + useEffect(() => { + document.body.setAttribute('data-color-scheme', theme); + }, [theme]); + return ; + }, (Story) => ( From 3219373d5600ee5c6f38b947eb0bae6a56647643 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?nicole=20miko=C5=82ajczyk?= Date: Wed, 21 Jan 2026 17:01:33 +0100 Subject: [PATCH 88/89] Add profile field limits to instance serializer (#37535) --- app/serializers/rest/instance_serializer.rb | 3 +++ 1 file changed, 3 insertions(+) diff --git a/app/serializers/rest/instance_serializer.rb b/app/serializers/rest/instance_serializer.rb index 75d3acfea58..1900330475c 100644 --- a/app/serializers/rest/instance_serializer.rb +++ b/app/serializers/rest/instance_serializer.rb @@ -71,6 +71,9 @@ class REST::InstanceSerializer < ActiveModel::Serializer accounts: { max_featured_tags: FeaturedTag::LIMIT, max_pinned_statuses: StatusPinValidator::PIN_LIMIT, + max_profile_fields: Account::DEFAULT_FIELDS_SIZE, + profile_field_name_limit: Account::Field::MAX_CHARACTERS_LOCAL, + profile_field_value_limit: Account::Field::MAX_CHARACTERS_LOCAL, }, statuses: { From 562ea656f495f0619e393b7d93bd07c5abd28e5a Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 21 Jan 2026 11:11:38 -0500 Subject: [PATCH 89/89] Add coverage for `TagManager#normalize_domain` (#35994) --- app/lib/tag_manager.rb | 2 +- spec/lib/tag_manager_spec.rb | 40 ++++++++++++++++++-- spec/models/instance_moderation_note_spec.rb | 2 +- 3 files changed, 38 insertions(+), 6 deletions(-) diff --git a/app/lib/tag_manager.rb b/app/lib/tag_manager.rb index c1bd2973ed1..5a6284cc5b6 100644 --- a/app/lib/tag_manager.rb +++ b/app/lib/tag_manager.rb @@ -18,7 +18,7 @@ class TagManager return if domain.nil? uri = Addressable::URI.new - uri.host = domain.delete_suffix('/') + uri.host = domain.strip.delete_suffix('/') uri.normalized_host end diff --git a/spec/lib/tag_manager_spec.rb b/spec/lib/tag_manager_spec.rb index 38203a55f70..927214bb40c 100644 --- a/spec/lib/tag_manager_spec.rb +++ b/spec/lib/tag_manager_spec.rb @@ -54,12 +54,44 @@ RSpec.describe TagManager do end describe '#normalize_domain' do - it 'returns nil if the given parameter is nil' do - expect(described_class.instance.normalize_domain(nil)).to be_nil + subject { described_class.instance.normalize_domain(domain) } + + context 'with a nil value' do + let(:domain) { nil } + + it { is_expected.to be_nil } end - it 'returns normalized domain' do - expect(described_class.instance.normalize_domain('DoMaIn.Example.com/')).to eq 'domain.example.com' + context 'with a blank value' do + let(:domain) { '' } + + it { is_expected.to be_blank } + end + + context 'with a mixed case string' do + let(:domain) { 'DoMaIn.Example.com' } + + it { is_expected.to eq('domain.example.com') } + end + + context 'with a trailing slash string' do + let(:domain) { 'domain.example.com/' } + + it { is_expected.to eq('domain.example.com') } + end + + context 'with a space padded string' do + let(:domain) { ' domain.example.com ' } + + it { is_expected.to eq('domain.example.com') } + end + + context 'with an invalid domain string' do + let(:domain) { ' !@#$@#$@$@# ' } + + it 'raises invalid uri error' do + expect { subject }.to raise_error(Addressable::URI::InvalidURIError) + end end end diff --git a/spec/models/instance_moderation_note_spec.rb b/spec/models/instance_moderation_note_spec.rb index 4d77d497122..011b001cc72 100644 --- a/spec/models/instance_moderation_note_spec.rb +++ b/spec/models/instance_moderation_note_spec.rb @@ -5,7 +5,7 @@ require 'rails_helper' RSpec.describe InstanceModerationNote do describe 'chronological' do it 'returns the instance notes sorted by oldest first' do - instance = Instance.find_or_initialize_by(domain: TagManager.instance.normalize_domain('mastodon.example')) + instance = Instance.find_or_initialize_by(domain: 'mastodon.example') note1 = Fabricate(:instance_moderation_note, domain: instance.domain) note2 = Fabricate(:instance_moderation_note, domain: instance.domain)