From e697d441448453fb6b33b8a7aebca39149581cb3 Mon Sep 17 00:00:00 2001 From: Claire Date: Wed, 10 Jun 2026 10:46:36 +0200 Subject: [PATCH 01/17] Fix avatar and profile header descriptions not being serialized over ActivityPub (#39352) --- app/serializers/activitypub/actor_serializer.rb | 14 ++++++++++++-- app/serializers/activitypub/image_serializer.rb | 9 +++++++++ .../activitypub/actor_serializer_spec.rb | 10 ++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index 38b0878b1a..4244f4f177 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -44,6 +44,16 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer end end + class ImageWithDescription < SimpleDelegator + attr_reader :description + + def initialize(object, description) + super(object) + + @description = description + end + end + has_one :endpoints, serializer: EndpointsSerializer has_one :icon, serializer: ActivityPub::ImageSerializer, if: :avatar_exists? @@ -120,11 +130,11 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer end def icon - object.avatar + ImageWithDescription.new(object.avatar, object.avatar_description) end def image - object.header + ImageWithDescription.new(object.header, object.header_description) end def public_key diff --git a/app/serializers/activitypub/image_serializer.rb b/app/serializers/activitypub/image_serializer.rb index 1060f96914..c6513c4dc4 100644 --- a/app/serializers/activitypub/image_serializer.rb +++ b/app/serializers/activitypub/image_serializer.rb @@ -7,6 +7,7 @@ class ActivityPub::ImageSerializer < ActivityPub::Serializer attributes :type, :media_type, :url attribute :focal_point, if: :focal_point? + attribute :summary, if: :summary? def type 'Image' @@ -27,4 +28,12 @@ class ActivityPub::ImageSerializer < ActivityPub::Serializer def focal_point [object.meta['focus']['x'], object.meta['focus']['y']] end + + def summary? + object.respond_to?(:description) && object.description.present? + end + + def summary + object.description + end end diff --git a/spec/serializers/activitypub/actor_serializer_spec.rb b/spec/serializers/activitypub/actor_serializer_spec.rb index 661890f33b..1c8542a7d3 100644 --- a/spec/serializers/activitypub/actor_serializer_spec.rb +++ b/spec/serializers/activitypub/actor_serializer_spec.rb @@ -74,4 +74,14 @@ RSpec.describe ActivityPub::ActorSerializer do end end end + + describe 'avatar description' do + let(:record) { Fabricate(:account, avatar: attachment_fixture('avatar.gif'), avatar_description: 'test') } + + it 'includes an `icon` with the appropraite `summary`' do + expect(subject).to include('icon' => a_hash_including( + 'summary' => 'test' + )) + end + end end From 6432fa6649ddb99121225d26654b62b127f3aa06 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:03:15 +0200 Subject: [PATCH 02/17] New Crowdin Translations (automated) (#39351) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/be.json | 3 +++ app/javascript/mastodon/locales/da.json | 3 +++ app/javascript/mastodon/locales/de.json | 3 +++ app/javascript/mastodon/locales/el.json | 27 ++++++++++++---------- app/javascript/mastodon/locales/es-AR.json | 3 +++ app/javascript/mastodon/locales/es-MX.json | 3 +++ app/javascript/mastodon/locales/es.json | 3 +++ app/javascript/mastodon/locales/fi.json | 3 +++ app/javascript/mastodon/locales/ga.json | 3 +++ app/javascript/mastodon/locales/gl.json | 4 ++++ app/javascript/mastodon/locales/he.json | 4 ++++ app/javascript/mastodon/locales/is.json | 3 +++ app/javascript/mastodon/locales/it.json | 3 +++ app/javascript/mastodon/locales/pl.json | 13 +++++++++++ app/javascript/mastodon/locales/pt-BR.json | 3 +++ app/javascript/mastodon/locales/sq.json | 1 + app/javascript/mastodon/locales/vi.json | 4 ++++ app/javascript/mastodon/locales/zh-CN.json | 3 +++ app/javascript/mastodon/locales/zh-TW.json | 3 +++ config/locales/be.yml | 18 +++++++++++++++ config/locales/gl.yml | 17 ++++++++++++++ config/locales/he.yml | 17 ++++++++++++++ config/locales/is.yml | 5 ++++ config/locales/sq.yml | 17 ++++++++++++++ config/locales/vi.yml | 17 ++++++++++++++ 25 files changed, 171 insertions(+), 12 deletions(-) diff --git a/app/javascript/mastodon/locales/be.json b/app/javascript/mastodon/locales/be.json index cfef805f01..2b824becb5 100644 --- a/app/javascript/mastodon/locales/be.json +++ b/app/javascript/mastodon/locales/be.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Прагледзець абнаўленні", "home.pending_critical_update.title": "Даступна крытычнае абнаўленне бяспекі!", "home.show_announcements": "Паказаць аб'явы", + "ignore_notifications_modal.bots_title": "Ігнараваць апавяшчэнні ад ботаў?", "ignore_notifications_modal.disclaimer": "Mastodon не можа паведамляць карыстальнікам, што Вы праігнаравалі апавяшчэнні ад іх. Ігнараванне апавяшчэнняў не спыніць адпраўку саміх паведамленняў.", "ignore_notifications_modal.filter_instead": "Замест гэтага адфільтраваць", "ignore_notifications_modal.filter_to_act_users": "Вы па-ранейшаму зможаце прымаць, адхіляць ці скардзіцца на карыстальнікаў", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Iгнараваць", "notifications.policy.drop_hint": "Адправіць у бездань, адкуль больш ніколі не ўбачыце", "notifications.policy.filter": "Фільтраваць", + "notifications.policy.filter_bots_hint": "Уліковыя запісы, пазначаныя як аўтаматычныя", + "notifications.policy.filter_bots_title": "Боты", "notifications.policy.filter_hint": "Адправіць у скрыню адфільтраваных апавяшчэнняў", "notifications.policy.filter_limited_accounts_hint": "Абмежавана мадэратарамі сервера", "notifications.policy.filter_limited_accounts_title": "Уліковыя запісы пад мадэрацыяй", diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json index 172923513f..df249a9d19 100644 --- a/app/javascript/mastodon/locales/da.json +++ b/app/javascript/mastodon/locales/da.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Se opdateringer", "home.pending_critical_update.title": "Kritisk sikkerhedsopdatering tilgængelig!", "home.show_announcements": "Vis bekendtgørelser", + "ignore_notifications_modal.bots_title": "Ignorér notifikationer fra bots?", "ignore_notifications_modal.disclaimer": "Mastodon kan ikke informere brugere om, at du har ignoreret deres notifikationer. At ignorere notifikationer forhindrer ikke selve beskederne i at blive sendt.", "ignore_notifications_modal.filter_instead": "Filtrér i stedet", "ignore_notifications_modal.filter_to_act_users": "Du vil stadig kunne acceptere, afvise eller anmelde brugere", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Ignorér", "notifications.policy.drop_hint": "Send til intetheden, for aldrig at blive set igen", "notifications.policy.filter": "Filter", + "notifications.policy.filter_bots_hint": "Konti markeret som automatiseret", + "notifications.policy.filter_bots_title": "Bots", "notifications.policy.filter_hint": "Send til filtrerede notifikationsindbakke", "notifications.policy.filter_limited_accounts_hint": "Begrænset af servermoderatorer", "notifications.policy.filter_limited_accounts_title": "Modererede konti", diff --git a/app/javascript/mastodon/locales/de.json b/app/javascript/mastodon/locales/de.json index 214f34ffa6..6efabc91e0 100644 --- a/app/javascript/mastodon/locales/de.json +++ b/app/javascript/mastodon/locales/de.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Updates ansehen", "home.pending_critical_update.title": "Kritisches Sicherheitsupdate verfügbar!", "home.show_announcements": "Ankündigungen anzeigen", + "ignore_notifications_modal.bots_title": "Benachrichtigungen von Bots ignorieren?", "ignore_notifications_modal.disclaimer": "Mastodon kann anderen Nutzer*innen nicht mitteilen, dass du deren Benachrichtigungen ignorierst. Das Ignorieren von Benachrichtigungen wird nicht das Absenden der Nachricht selbst unterbinden.", "ignore_notifications_modal.filter_instead": "Stattdessen filtern", "ignore_notifications_modal.filter_to_act_users": "Du wirst weiterhin die Möglichkeit haben, andere Nutzer*innen zu akzeptieren, abzulehnen oder zu melden", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Ignorieren", "notifications.policy.drop_hint": "Ins Nirwana befördern und auf Nimmerwiedersehen!", "notifications.policy.filter": "Filtern", + "notifications.policy.filter_bots_hint": "Als automatisiert gekennzeichnete Konten", + "notifications.policy.filter_bots_title": "Bots", "notifications.policy.filter_hint": "Im separaten Feed „Gefilterte Benachrichtigungen“ anzeigen", "notifications.policy.filter_limited_accounts_hint": "Durch Server-Moderator*innen eingeschränkte Profile", "notifications.policy.filter_limited_accounts_title": "eingeschränkten Konten", diff --git a/app/javascript/mastodon/locales/el.json b/app/javascript/mastodon/locales/el.json index 0d7850f09c..ed53be4201 100644 --- a/app/javascript/mastodon/locales/el.json +++ b/app/javascript/mastodon/locales/el.json @@ -577,7 +577,7 @@ "content_warning.show_more": "Εμφάνιση περισσότερων", "content_warning.show_short": "Εμφάνιση", "conversation.delete": "Διαγραφή συνομιλίας", - "conversation.mark_as_read": "Σήμανση ως αναγνωσμένη", + "conversation.mark_as_read": "Σήμανση ως διαβασμένη", "conversation.open": "Προβολή συνομιλίας", "conversation.with": "Με {names}", "copy_icon_button.copied": "Αντιγράφηκε στο πρόχειρο", @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Δείτε ενημερώσεις", "home.pending_critical_update.title": "Κρίσιμη ενημέρωση ασφαλείας διαθέσιμη!", "home.show_announcements": "Εμφάνιση ανακοινώσεων", + "ignore_notifications_modal.bots_title": "Αγνόηση ειδοποιήσεων από bots;", "ignore_notifications_modal.disclaimer": "Το Mastodon δε μπορεί να ενημερώσει τους χρήστες ότι αγνόησες τις ειδοποιήσεις του. Η αγνόηση ειδοποιήσεων δεν θα εμποδίσει την αποστολή των ίδιων των μηνυμάτων.", "ignore_notifications_modal.filter_instead": "Φίλτρο αντ' αυτού", "ignore_notifications_modal.filter_to_act_users": "Θα μπορείς ακόμη να αποδεχθείς, να απορρίψεις ή να αναφέρεις χρήστες", @@ -1011,41 +1012,43 @@ "notifications.column_settings.filter_bar.category": "Μπάρα γρήγορου φίλτρου", "notifications.column_settings.follow": "Νέοι ακόλουθοι:", "notifications.column_settings.follow_request": "Νέο αίτημα ακολούθησης:", - "notifications.column_settings.group": "Ομάδα", + "notifications.column_settings.group": "Ομαδοποίηση", "notifications.column_settings.mention": "Επισημάνσεις:", "notifications.column_settings.poll": "Αποτελέσματα δημοσκόπησης:", "notifications.column_settings.push": "Ειδοποιήσεις Push", "notifications.column_settings.quote": "Παραθέσεις:", "notifications.column_settings.reblog": "Ενισχύσεις:", - "notifications.column_settings.show": "Εμφάνισε σε στήλη", + "notifications.column_settings.show": "Εμφάνιση σε στήλη", "notifications.column_settings.sound": "Αναπαραγωγή ήχου", "notifications.column_settings.status": "Νέες αναρτήσεις:", - "notifications.column_settings.unread_notifications.category": "Μη αναγνωσμένες ειδοποιήσεις", - "notifications.column_settings.unread_notifications.highlight": "Επισήμανση μη αναγνωσμένων ειδοποιήσεων", + "notifications.column_settings.unread_notifications.category": "Αδιάβαστες ειδοποιήσεις", + "notifications.column_settings.unread_notifications.highlight": "Επισήμανση αδιάβαστων ειδοποιήσεων", "notifications.column_settings.update": "Επεξεργασίες:", "notifications.filter.all": "Όλες", "notifications.filter.boosts": "Προωθήσεις", "notifications.filter.collections": "Συλλογές", "notifications.filter.favourites": "Αγαπημένα", - "notifications.filter.follows": "Ακολουθείς", + "notifications.filter.follows": "Νέοι ακόλουθοι", "notifications.filter.mentions": "Επισημάνσεις", "notifications.filter.polls": "Αποτελέσματα δημοσκόπησης", "notifications.filter.statuses": "Ενημερώσεις από όσους ακολουθείς", "notifications.grant_permission": "Χορήγηση άδειας.", "notifications.group": "{count} ειδοποιήσεις", - "notifications.mark_as_read": "Σήμανε όλες τις ειδοποιήσεις ως αναγνωσμένες", + "notifications.mark_as_read": "Σήμανε όλες τις ειδοποιήσεις ως διαβασμένες", "notifications.permission_denied": "Οι ειδοποιήσεις για υπολογιστή δεν είναι διαθέσιμες διότι έχει απορριφθεί κάποιο προηγούμενο αίτημα άδειας", - "notifications.permission_denied_alert": "Δεν είναι δυνατή η ενεργοποίηση των ειδοποιήσεων για υπολογιστή, καθώς η άδεια του προγράμματος περιήγησης έχει απορριφθεί προηγουμένων", + "notifications.permission_denied_alert": "Δεν είναι δυνατή η ενεργοποίηση των ειδοποιήσεων για υπολογιστή, καθώς η άδεια του προγράμματος περιήγησης έχει απορριφθεί προηγουμένως", "notifications.permission_required": "Οι ειδοποιήσεις για υπολογιστή δεν είναι διαθέσιμες επειδή δεν έχει δοθεί η απαιτούμενη άδεια.", "notifications.policy.accept": "Αποδοχή", "notifications.policy.accept_hint": "Εμφάνιση στις ειδοποιήσεις", "notifications.policy.drop": "Αγνόηση", - "notifications.policy.drop_hint": "Στείλε τες στο υπερπέραν, για να μην τις ξαναδείτε", - "notifications.policy.filter": "Φίλτρο", + "notifications.policy.drop_hint": "Στείλε τες στο υπερπέραν, για να μην τις ξαναδείς", + "notifications.policy.filter": "Φιλτράρισμα", + "notifications.policy.filter_bots_hint": "Λογαριασμοί σημασμένοι ως αυτοματοποιημένοι", + "notifications.policy.filter_bots_title": "Bots", "notifications.policy.filter_hint": "Αποστολή στα εισερχόμενα φιλτραρισμένων ειδοποιήσεων", - "notifications.policy.filter_limited_accounts_hint": "Περιορισμένη από συντονιστές διακομιστή", + "notifications.policy.filter_limited_accounts_hint": "Περιορισμένοι από συντονιστές διακομιστή", "notifications.policy.filter_limited_accounts_title": "Συντονισμένοι λογαριασμοί", - "notifications.policy.filter_new_accounts.hint": "Δημιουργήθηκε εντός {days, plural, one {της τελευταίας ημέρας} other {των τελευταίων # ημερών}}", + "notifications.policy.filter_new_accounts.hint": "Δημιουργημένοι εντός {days, plural, one {της τελευταίας ημέρας} other {των τελευταίων # ημερών}}", "notifications.policy.filter_new_accounts_title": "Νέοι λογαριασμοί", "notifications.policy.filter_not_followers_hint": "Συμπεριλαμβανομένων των ατόμων που σας έχουν ακολουθήσει λιγότερο από {days, plural, one {μια ημέρα} other {# ημέρες}} πριν", "notifications.policy.filter_not_followers_title": "Άτομα που δε σε ακολουθούν", diff --git a/app/javascript/mastodon/locales/es-AR.json b/app/javascript/mastodon/locales/es-AR.json index 30540fa0a9..3a68fb4731 100644 --- a/app/javascript/mastodon/locales/es-AR.json +++ b/app/javascript/mastodon/locales/es-AR.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Ver actualizaciones", "home.pending_critical_update.title": "¡Actualización de seguridad crítica disponible!", "home.show_announcements": "Mostrar anuncios", + "ignore_notifications_modal.bots_title": "¿Ignorar notificaciones de bots?", "ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios que ignoraste sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.", "ignore_notifications_modal.filter_instead": "Filtrar en vez de ignorar", "ignore_notifications_modal.filter_to_act_users": "Aún podrás aceptar, rechazar o denunciar a usuarios", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Ignorar", "notifications.policy.drop_hint": "Enviar al vacío, no volver a mostrar nunca", "notifications.policy.filter": "Filtrar", + "notifications.policy.filter_bots_hint": "Cuentas marcadas como automatizadas", + "notifications.policy.filter_bots_title": "Botes", "notifications.policy.filter_hint": "Enviar a la bandeja de entrada de notificaciones filtradas", "notifications.policy.filter_limited_accounts_hint": "Limitada por los moderadores del servidor", "notifications.policy.filter_limited_accounts_title": "Cuentas moderadas", diff --git a/app/javascript/mastodon/locales/es-MX.json b/app/javascript/mastodon/locales/es-MX.json index e09e8edf58..0d35307a8d 100644 --- a/app/javascript/mastodon/locales/es-MX.json +++ b/app/javascript/mastodon/locales/es-MX.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Ver actualizaciones", "home.pending_critical_update.title": "¡Actualización de seguridad crítica disponible!", "home.show_announcements": "Mostrar anuncios", + "ignore_notifications_modal.bots_title": "¿Ignorar notificaciones de bots?", "ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios que has ignorado sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.", "ignore_notifications_modal.filter_instead": "Filtrar en su lugar", "ignore_notifications_modal.filter_to_act_users": "Aún podrás aceptar, rechazar o reportar usuarios", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Ignorar", "notifications.policy.drop_hint": "Enviar al vacío, no volver a mostrar nunca", "notifications.policy.filter": "Filtrar", + "notifications.policy.filter_bots_hint": "Cuentas etiquetadas como automatizadas", + "notifications.policy.filter_bots_title": "Bots", "notifications.policy.filter_hint": "Enviar a la bandeja de entrada de notificaciones filtradas", "notifications.policy.filter_limited_accounts_hint": "Limitadas por los moderadores del servidor", "notifications.policy.filter_limited_accounts_title": "Cuentas moderadas", diff --git a/app/javascript/mastodon/locales/es.json b/app/javascript/mastodon/locales/es.json index 2b9aca3c47..948e4f52cd 100644 --- a/app/javascript/mastodon/locales/es.json +++ b/app/javascript/mastodon/locales/es.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Ver actualizaciones", "home.pending_critical_update.title": "¡Actualización de seguridad crítica disponible!", "home.show_announcements": "Mostrar comunicaciones", + "ignore_notifications_modal.bots_title": "¿Ignorar notificaciones de bots?", "ignore_notifications_modal.disclaimer": "Mastodon no puede informar a los usuarios de que has ignorado sus notificaciones. Ignorar notificaciones no impedirá que se sigan enviando los mensajes.", "ignore_notifications_modal.filter_instead": "Filtrar en vez de ignorar", "ignore_notifications_modal.filter_to_act_users": "Aún podrás aceptar, rechazar o reportar usuarios", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Ignorar", "notifications.policy.drop_hint": "Enviar al vacío, no volver a mostrar nunca", "notifications.policy.filter": "Filtrar", + "notifications.policy.filter_bots_hint": "Cuentas etiquetadas como automatizadas", + "notifications.policy.filter_bots_title": "Bots", "notifications.policy.filter_hint": "Enviar a la bandeja de entrada de notificaciones filtradas", "notifications.policy.filter_limited_accounts_hint": "Limitadas por los moderadores del servidor", "notifications.policy.filter_limited_accounts_title": "Cuentas moderadas", diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json index d3940571da..11785a2050 100644 --- a/app/javascript/mastodon/locales/fi.json +++ b/app/javascript/mastodon/locales/fi.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Tutustu päivityssisältöihin", "home.pending_critical_update.title": "Kriittinen tietoturvapäivitys saatavilla!", "home.show_announcements": "Näytä tiedotteet", + "ignore_notifications_modal.bots_title": "Sivuutetaanko ilmoitukset boteilta?", "ignore_notifications_modal.disclaimer": "Mastodon ei voi ilmoittaa käyttäjille, että olet sivuuttanut heidän ilmoituksensa. Ilmoitusten sivuuttaminen ei lopeta itse viestien lähetystä.", "ignore_notifications_modal.filter_instead": "Suodata sen sijaan", "ignore_notifications_modal.filter_to_act_users": "Voit kuitenkin yhä hyväksyä, hylätä tai raportoida käyttäjiä", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Sivuuta", "notifications.policy.drop_hint": "Lähetä tyhjyyteen, jotta et näe niitä enää koskaan", "notifications.policy.filter": "Suodata", + "notifications.policy.filter_bots_hint": "Automatisoiduiksi merkityt tilit", + "notifications.policy.filter_bots_title": "Botit", "notifications.policy.filter_hint": "Lähetä suodatettuihin ilmoituksiin", "notifications.policy.filter_limited_accounts_hint": "Palvelimen moderaattorien rajoittamat", "notifications.policy.filter_limited_accounts_title": "Moderoidut tilit", diff --git a/app/javascript/mastodon/locales/ga.json b/app/javascript/mastodon/locales/ga.json index 8ef61e60df..0f06aabfcc 100644 --- a/app/javascript/mastodon/locales/ga.json +++ b/app/javascript/mastodon/locales/ga.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Féach nuashonruithe", "home.pending_critical_update.title": "Nuashonrú slándála ríthábhachtach ar fáil!", "home.show_announcements": "Taispeáin fógraí", + "ignore_notifications_modal.bots_title": "Neamhaird a dhéanamh d’fhógraí ó bhotaí?", "ignore_notifications_modal.disclaimer": "Ní féidir le Mastodon úsáideoirí a chur ar an eolas gur thug tú neamhaird dá bhfógraí. Má dhéantar neamhaird de fhógraí, ní stopfar na teachtaireachtaí iad féin a sheoladh.", "ignore_notifications_modal.filter_instead": "Scag ina ionad sin", "ignore_notifications_modal.filter_to_act_users": "Beidh tú fós in ann glacadh le húsáideoirí, iad a dhiúltú nó a thuairisciú", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Déan neamhaird de", "notifications.policy.drop_hint": "Seol chuig an neamhní, gan a bheith le feiceáil arís", "notifications.policy.filter": "Scagaire", + "notifications.policy.filter_bots_hint": "Cuntais atá marcáilte mar uathoibrithe", + "notifications.policy.filter_bots_title": "Botanna", "notifications.policy.filter_hint": "Seol chuig an mbosca isteach fógraí scagtha", "notifications.policy.filter_limited_accounts_hint": "Teoranta ag modhnóirí freastalaí", "notifications.policy.filter_limited_accounts_title": "Cuntais mhodhnaithe", diff --git a/app/javascript/mastodon/locales/gl.json b/app/javascript/mastodon/locales/gl.json index b4da525cf9..7982e694ca 100644 --- a/app/javascript/mastodon/locales/gl.json +++ b/app/javascript/mastodon/locales/gl.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Mira as actualizacións", "home.pending_critical_update.title": "Hai una actualización crítica de seguridade!", "home.show_announcements": "Amosar anuncios", + "ignore_notifications_modal.bots_title": "Ignorar as notificacións dos robots?", "ignore_notifications_modal.disclaimer": "Mastodon non pode informar ás usuarias de que ignoraches as súas notificacións. Ao ignorar as notificacións non evitarás que as mensaxes sexan enviadas igualmente.", "ignore_notifications_modal.filter_instead": "Filtrar igualmente", "ignore_notifications_modal.filter_to_act_users": "Poderás seguir aceptando, rexeitando e denunciando usuarias", @@ -846,6 +847,7 @@ "keyboard_shortcuts.toggle_sensitivity": "Para amosar/agochar contido multimedia", "keyboard_shortcuts.toot": "Para escribir unha nova publicación", "keyboard_shortcuts.top": "Mover arriba de todo", + "keyboard_shortcuts.translate": "Traducir unha publicación", "keyboard_shortcuts.unfocus": "Para deixar de destacar a área de escritura/procura", "keyboard_shortcuts.up": "Para mover cara arriba na listaxe", "learn_more_link.got_it": "Entendo", @@ -1041,6 +1043,8 @@ "notifications.policy.drop": "Ignorar", "notifications.policy.drop_hint": "Esquecer isto, non volver a velo", "notifications.policy.filter": "Filtrar", + "notifications.policy.filter_bots_hint": "Contas marcadas como automatizadas", + "notifications.policy.filter_bots_title": "Robots", "notifications.policy.filter_hint": "Enviar á caixa de notificacións filtradas", "notifications.policy.filter_limited_accounts_hint": "Limitada pola moderación do servidor", "notifications.policy.filter_limited_accounts_title": "Contas moderadas", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index 2cb0b07697..e26b6e5f46 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "צפיה בעדכונים", "home.pending_critical_update.title": "יצא עדכון אבטחה חשוב!", "home.show_announcements": "הצג הכרזות", + "ignore_notifications_modal.bots_title": "להתעלם מהתראות מחשבונות רובוטיים?", "ignore_notifications_modal.disclaimer": "מסטודון אינו יכול ליידע משתמשים שהתעלמתם מהתראותיהם. התעלמות מהתראות לא תחסום את ההודעות עצמן מלהשלח.", "ignore_notifications_modal.filter_instead": "לסנן במקום", "ignore_notifications_modal.filter_to_act_users": "עדיין ביכולתך לקבל, לדחות ולדווח על משתמשים אחרים", @@ -846,6 +847,7 @@ "keyboard_shortcuts.toggle_sensitivity": "הצגת/הסתרת מדיה", "keyboard_shortcuts.toot": "להתחיל חיצרוץ חדש", "keyboard_shortcuts.top": "העברה לראש הרשימה", + "keyboard_shortcuts.translate": "לתרגם הודעה", "keyboard_shortcuts.unfocus": "לצאת מתיבת חיבור/חיפוש", "keyboard_shortcuts.up": "לנוע במעלה הרשימה", "learn_more_link.got_it": "הבנתי", @@ -1041,6 +1043,8 @@ "notifications.policy.drop": "להתעלם", "notifications.policy.drop_hint": "שליחה אל מצולות הנשיה, ולא יוודעו אודותיה לעולם", "notifications.policy.filter": "מסנן", + "notifications.policy.filter_bots_hint": "חשבונות המסומנים כאוטומטיים", + "notifications.policy.filter_bots_title": "בוטים", "notifications.policy.filter_hint": "שליחה לתיבה נכנסת מסוננת", "notifications.policy.filter_limited_accounts_hint": "הוגבל על ידי מנהלי הדיונים", "notifications.policy.filter_limited_accounts_title": "חשבומות תחת ניהול תוכן", diff --git a/app/javascript/mastodon/locales/is.json b/app/javascript/mastodon/locales/is.json index ed5fc9bffb..a5fbd1b845 100644 --- a/app/javascript/mastodon/locales/is.json +++ b/app/javascript/mastodon/locales/is.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Skoða uppfærslur", "home.pending_critical_update.title": "Áríðandi öryggisuppfærsla er tiltæk!", "home.show_announcements": "Birta auglýsingar", + "ignore_notifications_modal.bots_title": "Hunsa tilkynningar frá yrkjum?", "ignore_notifications_modal.disclaimer": "Mastodon getur ekki upplýst notendur um að þú hunsir tilkynningar frá þeim. Hunsun tilkynninga kemur ekki í veg fyrir að sjálf skilaboðin verði send.", "ignore_notifications_modal.filter_instead": "Sía frekar", "ignore_notifications_modal.filter_to_act_users": "Þú munt áfram geta samþykkt, hafnað eða kært notendur", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Hunsa", "notifications.policy.drop_hint": "Senda út í tómið, svo það sjáist aldrei framar", "notifications.policy.filter": "Sía", + "notifications.policy.filter_bots_hint": "Aðgangar merktir fyrir yrki", + "notifications.policy.filter_bots_title": "Yrki", "notifications.policy.filter_hint": "Senda í pósthólf fyrir síaðar tilkynningar", "notifications.policy.filter_limited_accounts_hint": "Takmarkað af umsjónarmönnum netþjóns", "notifications.policy.filter_limited_accounts_title": "Aðgangar í umsjón", diff --git a/app/javascript/mastodon/locales/it.json b/app/javascript/mastodon/locales/it.json index 3382a02b92..8667f342bb 100644 --- a/app/javascript/mastodon/locales/it.json +++ b/app/javascript/mastodon/locales/it.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Visualizza aggiornamenti", "home.pending_critical_update.title": "Aggiornamento critico di sicurezza disponibile!", "home.show_announcements": "Mostra annunci", + "ignore_notifications_modal.bots_title": "Ignorare le notifiche dai bot?", "ignore_notifications_modal.disclaimer": "Mastodon non può informare gli utenti che hai ignorato le loro notifiche. Ignorare le notifiche non impedirà l'invio dei messaggi stessi.", "ignore_notifications_modal.filter_instead": "Filtra invece", "ignore_notifications_modal.filter_to_act_users": "Potrai comunque accettare, rifiutare o segnalare gli utenti", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Ignora", "notifications.policy.drop_hint": "Scarta definitivamente, per non essere mai più visto", "notifications.policy.filter": "Filtrare", + "notifications.policy.filter_bots_hint": "Account contrassegnati come automatizzati", + "notifications.policy.filter_bots_title": "Bot", "notifications.policy.filter_hint": "Invia alla casella in arrivo delle notifiche filtrate", "notifications.policy.filter_limited_accounts_hint": "Limitato dai moderatori del server", "notifications.policy.filter_limited_accounts_title": "Account moderati", diff --git a/app/javascript/mastodon/locales/pl.json b/app/javascript/mastodon/locales/pl.json index 39f141965c..dd88b2e587 100644 --- a/app/javascript/mastodon/locales/pl.json +++ b/app/javascript/mastodon/locales/pl.json @@ -43,6 +43,7 @@ "account.featured": "Wyróżnione", "account.featured.accounts": "Profile", "account.featured.collections": "Kolekcje", + "account.featured.new_collection": "Nowa kolekcja", "account.field_overflow": "Pokaż całą zawartość", "account.filters.all": "Wszystkie aktywności", "account.filters.boosts_toggle": "Pokaż ulepszenia", @@ -68,9 +69,13 @@ "account.go_to_profile": "Przejdź do profilu", "account.hide_reblogs": "Ukryj podbicia od @{name}", "account.in_memoriam": "Ku pamięci.", + "account.join_modal.day": "Dzień", "account.join_modal.me": "Dołączyłeś(aś) na {server}", "account.join_modal.me_today": "To Twój pierwszy dzień na {server}!", "account.join_modal.other": "{name} dołączył(a) na {server}", + "account.join_modal.share.celebrate": "Udostępnij uroczysty post", + "account.join_modal.share.intro": "Udostępnij post wprowadzający", + "account.join_modal.share.welcome": "Udostępnij post powitalny", "account.joined_short": "Dołączył(a)", "account.languages": "Zmień subskrybowane języki", "account.last_active": "Ostatnia aktywność", @@ -120,6 +125,7 @@ "account.note.edit_button": "Edytuj", "account.note.title": "Osobista notatka (widoczna tylko dla Ciebie)", "account.open_original_page": "Otwórz stronę oryginalną", + "account.pending": "Oczekuje", "account.posts": "Wpisy", "account.remove_from_followers": "Usuń {name} z obserwujących", "account.report": "Zgłoś @{name}", @@ -173,9 +179,12 @@ "account_edit.field_edit_modal.discard_confirm": "Odrzuć", "account_edit.field_edit_modal.discard_message": "Masz niezapisane zmiany. Czy na pewno chcesz je odrzucić?", "account_edit.field_edit_modal.edit_title": "Edytuj dodatkowe pole", + "account_edit.field_edit_modal.name_hint": "Np. Strona osobista", "account_edit.field_edit_modal.name_label": "Etykieta", "account_edit.field_edit_modal.value_label": "Wartość", "account_edit.image_alt_modal.add_title": "Dodaj tekst alternatywny", + "account_edit.image_alt_modal.edit_title": "Edytuj tekst alternatywny", + "account_edit.image_alt_modal.text_hint": "Tekst alternatywny pomaga użytkownikom czytników ekranu zrozumieć twoją treść.", "account_edit.image_alt_modal.text_label": "Tekst alternatywny", "account_edit.image_delete_modal.delete_button": "Usuń", "account_edit.image_delete_modal.title": "Usunąć obraz?", @@ -185,6 +194,8 @@ "account_edit.image_edit.remove_button": "Usuń obraz", "account_edit.image_edit.replace_button": "Zastąp obraz", "account_edit.item_list.delete": "Usuń {name}", + "account_edit.name_modal.add_title": "Dodaj nazwę wyświetlaną", + "account_edit.name_modal.edit_title": "Edytuj nazwę wyświetlaną", "account_edit.profile_tab.button_label": "Dostosuj", "account_edit.profile_tab.hint.description": "Te ustawienia wpływają na to, co użytkownicy widzą na {server} w oficjalnych aplikacjach, ale mogą nie obowiązywać na innych serwerach i w aplikacjach zewnętrznych.", "account_edit.profile_tab.hint.title": "Wyświetlanie może się różnić", @@ -204,12 +215,14 @@ "account_edit.upload_modal.next": "Następne", "account_edit.upload_modal.step_crop.zoom": "Powiększenie", "account_edit.upload_modal.step_upload.button": "Wybierz pliki", + "account_edit.upload_modal.step_upload.dragging": "Upuść, aby przesłać", "account_edit.upload_modal.step_upload.header": "Wybierz obraz", "account_edit.upload_modal.step_upload.hint": "WEBP, PNG, GIF lub JPG, maksymalnie {limit} MB.{br}Obraz zostanie przeskalowany do {width}x{height} px.", "account_edit.upload_modal.title_add.avatar": "Dodaj zdjęcie profilowe", "account_edit.upload_modal.title_add.header": "Dodaj zdjęcie nagłówka", "account_edit.upload_modal.title_replace.avatar": "Zmień zdjęcie profilowe", "account_edit.upload_modal.title_replace.header": "Zmień zdjęcie nagłówka", + "account_edit.verified_modal.invisible_link.summary": "Co zrobić, aby link był niewidoczny?", "account_edit_tags.add_tag": "Dodaj #{tagName}", "account_edit_tags.search_placeholder": "Dodaj hashtag...", "admin.dashboard.daily_retention": "Wskaźnik utrzymania użytkowników według dni od rejestracji", diff --git a/app/javascript/mastodon/locales/pt-BR.json b/app/javascript/mastodon/locales/pt-BR.json index 987ddc2101..cb74515a8a 100644 --- a/app/javascript/mastodon/locales/pt-BR.json +++ b/app/javascript/mastodon/locales/pt-BR.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Ver atualizações", "home.pending_critical_update.title": "Atualização de segurança crítica disponível!", "home.show_announcements": "Mostrar anúncios", + "ignore_notifications_modal.bots_title": "Ignorar notificações de robôs?", "ignore_notifications_modal.disclaimer": "O Mastodon não informa os usuários se você ignorar as notificações deles. Ignorar notificações não impedirá as mensagens de serem enviadas.", "ignore_notifications_modal.filter_instead": "Filtrar em vez disso", "ignore_notifications_modal.filter_to_act_users": "Você ainda poderá aceitar, rejeitar ou denunciar", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "Ignorar", "notifications.policy.drop_hint": "Enviar para o vácuo, para nunca mais ser visto", "notifications.policy.filter": "Filtrar", + "notifications.policy.filter_bots_hint": "Contas marcadas como automáticas", + "notifications.policy.filter_bots_title": "Robôs", "notifications.policy.filter_hint": "Enviar para a caixa de notificações filtradas", "notifications.policy.filter_limited_accounts_hint": "Limitado pelos moderadores do servidor", "notifications.policy.filter_limited_accounts_title": "Contas moderadas", diff --git a/app/javascript/mastodon/locales/sq.json b/app/javascript/mastodon/locales/sq.json index d2803adfd1..3c3d0074e3 100644 --- a/app/javascript/mastodon/locales/sq.json +++ b/app/javascript/mastodon/locales/sq.json @@ -841,6 +841,7 @@ "keyboard_shortcuts.toggle_sensitivity": "Për shfaqje/fshehje mediash", "keyboard_shortcuts.toot": "Për të filluar një mesazh të ri", "keyboard_shortcuts.top": "Shpjere në krye të listës", + "keyboard_shortcuts.translate": "Përkthe një postim", "keyboard_shortcuts.unfocus": "Për heqjen e fokusit nga fusha e hartimit të mesazheve apo kërkimeve", "keyboard_shortcuts.up": "Për ngjitje sipër nëpër listë", "learn_more_link.got_it": "E mora vesh", diff --git a/app/javascript/mastodon/locales/vi.json b/app/javascript/mastodon/locales/vi.json index 745f89d159..3f34087d52 100644 --- a/app/javascript/mastodon/locales/vi.json +++ b/app/javascript/mastodon/locales/vi.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "Xem bản cập nhật", "home.pending_critical_update.title": "Có bản cập nhật bảo mật quan trọng!", "home.show_announcements": "Xem thông báo máy chủ", + "ignore_notifications_modal.bots_title": "Bỏ qua thông báo từ các tài khoản bot?", "ignore_notifications_modal.disclaimer": "Mastodon sẽ không thông báo cho tài khoản rằng bạn đã bỏ qua thông báo của họ. Họ sẽ vẫn có thể tương tác với bạn.", "ignore_notifications_modal.filter_instead": "Lọc thay thế", "ignore_notifications_modal.filter_to_act_users": "Bạn vẫn có thể chấp nhận, từ chối hoặc báo cáo tài khoản khác", @@ -846,6 +847,7 @@ "keyboard_shortcuts.toggle_sensitivity": "ẩn/hiện ảnh hoặc video", "keyboard_shortcuts.toot": "soạn tút mới", "keyboard_shortcuts.top": "di chuyển đến đầu danh sách", + "keyboard_shortcuts.translate": "Dịch tút", "keyboard_shortcuts.unfocus": "đưa con trỏ ra khỏi ô soạn thảo hoặc ô tìm kiếm", "keyboard_shortcuts.up": "di chuyển lên trên danh sách", "learn_more_link.got_it": "Đã hiểu", @@ -1041,6 +1043,8 @@ "notifications.policy.drop": "Bỏ qua", "notifications.policy.drop_hint": "Bỏ qua vĩnh viễn", "notifications.policy.filter": "Lọc", + "notifications.policy.filter_bots_hint": "Những tài khoản đánh dấu là tự động", + "notifications.policy.filter_bots_title": "Bot", "notifications.policy.filter_hint": "Cho vào mục thông báo đã lọc", "notifications.policy.filter_limited_accounts_hint": "Chỉ dành cho kiểm duyệt viên", "notifications.policy.filter_limited_accounts_title": "Kiểm duyệt tài khoản", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index 15f0f42da2..2d57e6f49f 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "查看更新", "home.pending_critical_update.title": "有紧急安全更新!", "home.show_announcements": "显示公告", + "ignore_notifications_modal.bots_title": "是否忽略来自机器人账号的通知?", "ignore_notifications_modal.disclaimer": "Mastodon无法通知对方用户你忽略了他们的通知。忽略通知不会阻止消息本身的发送。", "ignore_notifications_modal.filter_instead": "改为过滤", "ignore_notifications_modal.filter_to_act_users": "你仍然可以接受、拒绝或举报用户", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "忽略", "notifications.policy.drop_hint": "送入虚空,再也不查看", "notifications.policy.filter": "过滤", + "notifications.policy.filter_bots_hint": "被标记为自动化的账号", + "notifications.policy.filter_bots_title": "机器人", "notifications.policy.filter_hint": "发送到被过滤通知列表", "notifications.policy.filter_limited_accounts_hint": "被服务器管理员限制的账号", "notifications.policy.filter_limited_accounts_title": "受限账号", diff --git a/app/javascript/mastodon/locales/zh-TW.json b/app/javascript/mastodon/locales/zh-TW.json index b3f4fc0321..263afc29e1 100644 --- a/app/javascript/mastodon/locales/zh-TW.json +++ b/app/javascript/mastodon/locales/zh-TW.json @@ -780,6 +780,7 @@ "home.pending_critical_update.link": "檢視更新內容", "home.pending_critical_update.title": "有可取得的重要安全性更新!", "home.show_announcements": "顯示公告", + "ignore_notifications_modal.bots_title": "是否忽略來自機器人帳號之推播通知?", "ignore_notifications_modal.disclaimer": "Mastodon 無法通知您已忽略推播通知之使用者。忽略通知不會阻止訊息本身的發送。", "ignore_notifications_modal.filter_instead": "改為過濾", "ignore_notifications_modal.filter_to_act_users": "您仍能接受、拒絕、或檢舉使用者", @@ -1042,6 +1043,8 @@ "notifications.policy.drop": "忽略", "notifications.policy.drop_hint": "送至黑洞,永不相見", "notifications.policy.filter": "過濾器", + "notifications.policy.filter_bots_hint": "自動化帳號", + "notifications.policy.filter_bots_title": "機器人", "notifications.policy.filter_hint": "送至已過濾推播通知收件夾", "notifications.policy.filter_limited_accounts_hint": "已被伺服器管理員限制", "notifications.policy.filter_limited_accounts_title": "受管制帳號", diff --git a/config/locales/be.yml b/config/locales/be.yml index 091bf2f465..5d09fbdc44 100644 --- a/config/locales/be.yml +++ b/config/locales/be.yml @@ -525,6 +525,21 @@ be: no_lists_yet: Пакуль няма спісаў last_email: Апошняя электронная пошта lead: Уліковыя запісы, якія ўключылі гэту функцыю і маюць падпісчыкаў, будуць паказаныя знізу. + show: + confirm_disable_feature: Адключыць рассылку па электроннай пошце для %{name}? Абнаўленні больш не будуць прыходзіць на пошту гэтага ўліковага запісу. Карыстальнік усё яшчэ зможа ўключыць гэту функцыю нанова ў наладах уліковага запісу. Каб назаўсёды прыбраць доступ да гэтай функцыі, змяніце дазволы ўліковага запісу ў Ролях. + confirm_remove_subscriber: "%{email} больш не будзе атрымліваць электронныя лісты ад %{name}. Гэтае дзеянне незваротнае." + consent: Падпісчыкі пагадзіліся толькі атрымліваць допісы на электронную пошту. Не карыстайцеся гэтым спісам у іншых мэтах. + date: Дата рэгістрацыі + disable_feature: Адключыць функцыю + disabled: Функцыя была адключаная і электронныя лісты больш не будуць дасылацца гэтаму спісу. + email: Адрас электроннай пошты + empty: + hint: Ніхто яшчэ не аформіў падпіску на гэты ўліковы запіс. + no_subscribers_yet: Пакуль няма падпісчыкаў + enable_feature: Уключыць функцыю + no_access_html: У гэтага ўліковага запісу больш няма дазволаў, патрэбных, каб уключыць гэту функцыю. Змяніце гэта ў Ролях. + title: Паштовая рассылка %{name} + view_account: Прагл. уліковы запіс status: Стан subscribers: Падпісчыкі title: Спісы рассылкі @@ -1541,6 +1556,7 @@ be: your_appeal_rejected: Ваша абскарджанне было адхілена edit_profile: other: Іншае + privacy_redesign_body: Уключыць ці адключыць паказ Вашых падпісчыкаў і падпісак цяпер можна прама ў Вашым профілі. redesign_body: Рэдагаванне профілю цяпер даступнае наўпрост са старонкі профілю. redesign_button: Перайсці туды redesign_title: Адбыліся змены ў рэдагаванні профілю @@ -1575,7 +1591,9 @@ be: success_html: Вы цяпер пачняце атрымліваць электронныя лісты, калі %{name} будзе рабіць новыя допісы. Дадайце %{sender} у свае кантакты, каб гэтыя допісы не траплялі ў папку са спамам. title: Вы падпісаліся праз эл. пошту unsubscribe: Адпісацца + disabled: Адключана inactive: Неактыўная + no_access: Няма доступу status: Стан subscribers: Падпісчыкі па эл.пошце emoji_styles: diff --git a/config/locales/gl.yml b/config/locales/gl.yml index 475ffa70a0..bcf6cdba8c 100644 --- a/config/locales/gl.yml +++ b/config/locales/gl.yml @@ -505,6 +505,21 @@ gl: no_lists_yet: Aínda non hai listas last_email: Último correo lead: Aquí móstranse as contas que activaron esta ferramenta e teñen subscritoras. + show: + confirm_disable_feature: Desactivar o boletín para %{name}? Non se van enviar máis novidades desta conta por correo. A usuaria poderá reactivar esta ferramenta nos axustes da súa conta. Para quitar de xeito permanente o acceso á ferramenta, edita os permisos da conta en Roles. + confirm_remove_subscriber: "%{email} non vai seguir recibindo correos de %{name}. Esta acción non é reversible." + consent: As subscritoras consentiron exclusivamente recibir no correo as publicacións. Non uses esta lista para outros propósitos. + date: Data da subscrición + disable_feature: Desactivar a ferramenta + disabled: Esta ferramenta desactivouse e non se van seguir enviando correos a esta lista. + email: Enderezo de correo + empty: + hint: Aínda non hai subscricións a esta conta. + no_subscribers_yet: Sen subscricións + enable_feature: Activar a ferramenta + no_access_html: Esta conta xa non ten os permisos requeridos para activar a ferramenta. Cambiaos en Roles. + title: Boletín por correo de %{name} + view_account: Ver conta status: Estado subscribers: Subscritoras title: Listas de correo @@ -1532,7 +1547,9 @@ gl: success_html: Vas comezar a recibir correos cando %{name} publique novas publicacións. Engade %{sender} á túa libreta de enderezos para que os correos non vaian directamente ao cartafol de Spam. title: Subscribícheste unsubscribe: Anular subscrición + disabled: Desactivado inactive: Inactiva + no_access: Sen acceso status: Estado subscribers: Subscritoras emoji_styles: diff --git a/config/locales/he.yml b/config/locales/he.yml index 59617180e2..cb0e3e7d42 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -525,6 +525,21 @@ he: no_lists_yet: אין רשימות עדיין last_email: הודעת דואל אחרונה lead: חשבונות שבהם הופעלה האפשרות ויש להם מנויים יופיעו להלן. + show: + confirm_disable_feature: להשבית מנשרי דוא"ל של %{name}? עדכוני דוא"ל לא ישלחו עוד מטעם חשבון זה. המשתמשת תוכל לאפשר מחדש את התכונה מהעדפות החשבון שלה. כדי לכבות לצמיתות גישה לתכונה הזו, יש לערוך את הרשאות החשבון תחת תפקידים. + confirm_remove_subscriber: '%{email} לא יקבלו יותר דוא"ל מאת %{name}. פעולה זו לא בלתי הפיכה.' + consent: מנויים הסכימו רק לקבלת הודעות דרך דוא"ל. אין להשתש ברשימה זו לאף מטרה אחרת. + date: תאריך ההצטרפות + disable_feature: כיבוי תכונה + disabled: תכונה זו הושבתה והודעות דוא"ל לא נשלחות יותר אל רשימה זו. + email: כתובת דוא"ל + empty: + hint: אין לחשבון זה נרשמים עדיין. + no_subscribers_yet: אין מנויים עדיין + enable_feature: אפשר תכונה + no_access_html: לחשבון זה אין יותר את ההרשאות הדרושות להפעלת התכונה. שנו זאת תחת תפקידים. + title: מנשרי דוא"ל של %{name} + view_account: הצג חשבון status: מצב subscribers: מנויים title: רשימות תפוצה @@ -1576,7 +1591,9 @@ he: success_html: מעתה תקבלנה דוא"ל כאשר %{name} יפרסמו הודעות חדשות. הוספנה את %{sender} לאנשי הקשר כדי שההודעות האלו לא יגיעו אל פח הספאם שלכן. title: נרשמת unsubscribe: ביטול ההרשמה + disabled: כבוי inactive: לא פעילים + no_access: אין גישה status: מצב subscribers: מנויים emoji_styles: diff --git a/config/locales/is.yml b/config/locales/is.yml index 835c3e560c..887574ecdd 100644 --- a/config/locales/is.yml +++ b/config/locales/is.yml @@ -506,13 +506,18 @@ is: last_email: Síðasti tölvupóstur lead: Aðgangar sem hafa virkjað eiginleikann og eru með áskrifendur munu birtast hér fyrir neðan. show: + confirm_disable_feature: Á að gera óvirkar fréttir í tölvupósti frá %{name}? Færslur verða ekki lengur sendar í tölvupósti frá þessum notandaaðgangi. Notandinn mun samt geta virkjað aftur þennan eiginleika í stillingunum sínum. Til að fjarlægja endanlega aðgang að þessum eiginleika skaltu breyta heimildum aðgangsins í gegnum 'Hlutverk'. + confirm_remove_subscriber: "%{email} mun ekki lengur fá tölvupósta frá %{name}. Ekki er hægt að afturkalla þessa aðgerð." + consent: Áskrifendur hafa einungis samþykkt að fá sendar færslur í tölvupósti. Ekki nota þennan lista í neinum öðrum tilgangi. date: Dagsetning skráningar disable_feature: Gera eiginleika óvirkan + disabled: Eiginleikinn var gerður óvirkur og eru tölvupóstar ekki lengur sendir á þenna lista. email: Tölvupóstfang empty: hint: Enginn hefur enn gerst áskrifandi að þessum notandaaðgangi. no_subscribers_yet: Engir áskrifendur ennþá enable_feature: Virkja eiginleika + no_access_html: Þessi aðgangur hefur ekki lengur heimild til að virkja þennan eiginleika. Breyttu því í Hlutverk. title: Tölvupóstfréttir frá %{name} view_account: Skoða notandaaðgang status: Staða diff --git a/config/locales/sq.yml b/config/locales/sq.yml index e4cdf0e993..191f74de77 100644 --- a/config/locales/sq.yml +++ b/config/locales/sq.yml @@ -505,6 +505,21 @@ sq: no_lists_yet: Ende pa lista last_email: Email-i i fundit lead: Llogaritë që kanë aktivizuar veçorinë dhe kanë pajtimtarë do të shfaqen më poshtë. + show: + confirm_disable_feature: Të çaktivizohen buletine me email për %{name}? Për këtë llogari s’do të dërgohen më përditësime me email. Përdoruesi do të jetë prapëseprapë në gjendje të riaktivizojë veçorinë që nga rregullimet e llogarisë të vet. Për të hequr përgjithnjë përdorimin e kësaj veçorie, përpunoni lejen e llogarisë te Role. + confirm_remove_subscriber: "%{email} s’do të marrë më email-e nga %{name}. Ky veprim s’mund të zhbëhet." + consent: Pajtimtarët kanë pranuar vetëm të marrin postime përmes email-i. Mos e përdorni këtë listë për qëllime të tjera. + date: Datë e regjistrimit + disable_feature: Çaktivizoje veçorinë + disabled: Veçoria qe çaktivizuar dhe te kjo listë s’dërgohen më email-e. + email: Adresë email + empty: + hint: Te kjo llogari s’është pajtuar ende dikush. + no_subscribers_yet: Ende pa pajtimtarë + enable_feature: Aktivizoje veçorinë + no_access_html: Kjo llogari s’ka më lejet e domosdoshme për aktivizimin e veçorisë. Ndryshojeni këtë që nga Role. + title: Buletine me email nga %{name} + view_account: Shiheni llogarinë status: Gjendje subscribers: Pajtimtarë title: Lista postimesh @@ -1521,7 +1536,9 @@ sq: success_html: Tani do të filloni të merrni email-e, kur %{name} boton postime të reja. Shtojeni %{sender} te kontaktet tuaj, që këto postime të mos përfundojnë te dosja juaj e Të padëshiruarve. title: U regjistruat unsubscribe: Shpajtohuni + disabled: E çaktivizuar inactive: Joaktiv + no_access: S’lejohet përdorim status: Gjendje subscribers: Pajtimtarë errors: diff --git a/config/locales/vi.yml b/config/locales/vi.yml index 44d247e2ac..f3b20dfafe 100644 --- a/config/locales/vi.yml +++ b/config/locales/vi.yml @@ -495,6 +495,21 @@ vi: no_lists_yet: Chưa có danh sách nào last_email: Email gần nhất lead: Các tài khoản đã kích hoạt tính năng này và có người đăng ký sẽ hiển thị bên dưới. + show: + confirm_disable_feature: Tắt bản tin email cho %{name}? Bản tin cập nhật qua email sẽ không còn được gửi cho tài khoản này nữa. Người dùng vẫn có thể bật lại tính năng này trong cài đặt tài khoản của họ. Để xóa vĩnh viễn quyền truy cập vào tính năng này, hãy chỉnh sửa quyền của tài khoản trong mục Vai trò. + confirm_remove_subscriber: "%{email} sẽ không còn nhận được email từ %{name} nữa. Hành động này không thể hoàn tác." + consent: Người đăng ký chỉ đồng ý nhận bài viết qua email. Vui lòng không sử dụng danh sách này cho mục đích khác. + date: Ngày đăng ký + disable_feature: Tắt tính năng + disabled: Tính năng này đã bị vô hiệu hóa và email không còn được gửi đến danh sách này nữa. + email: Địa chỉ email + empty: + hint: Hiện chưa có ai đăng ký theo dõi tài khoản này. + no_subscribers_yet: Chưa có đăng ký theo dõi + enable_feature: Bật tính năng + no_access_html: Tài khoản này không còn quyền hạn cần thiết để kích hoạt tính năng này nữa. Thay đổi trong Vai trò. + title: Bản tin email của %{name} + view_account: Xem tài khoản status: Trạng thái subscribers: Người đăng ký đọc title: Danh sách gửi thư @@ -1510,7 +1525,9 @@ vi: success_html: Bạn sẽ bắt đầu nhận được email khi %{name} đăng tút mới. Thêm %{sender} vào danh bạ của bạn để những tút này không bị chuyển vào thư mục Spam. title: Bạn đã đăng ký đọc unsubscribe: Hủy đăng ký đọc + disabled: Đã tắt inactive: Không hoạt động + no_access: Không có quyền status: Trạng thái subscribers: Người đăng ký đọc emoji_styles: From 885a9f1d84c7afb1980e33f82aac11109be688e7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Wed, 10 Jun 2026 09:04:23 +0000 Subject: [PATCH 03/17] Update dependency libvips to v8.18.3 (#39344) 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 6f44c4c3e1..0799a7b66f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -210,7 +210,7 @@ FROM media-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.18.2 +ARG VIPS_VERSION=8.18.3 # 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 c51eb56a9029f28f277fb30886cd0fb2f864ae47 Mon Sep 17 00:00:00 2001 From: Matt Jankowski Date: Wed, 10 Jun 2026 05:27:07 -0400 Subject: [PATCH 04/17] Clarify preview card CLI command handles media only (#39348) --- lib/mastodon/cli/preview_cards.rb | 17 ++++++++--------- spec/lib/mastodon/cli/preview_cards_spec.rb | 6 +++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/lib/mastodon/cli/preview_cards.rb b/lib/mastodon/cli/preview_cards.rb index c0e207ad59..c09fb9d2c7 100644 --- a/lib/mastodon/cli/preview_cards.rb +++ b/lib/mastodon/cli/preview_cards.rb @@ -11,18 +11,17 @@ module Mastodon::CLI option :verbose, type: :boolean, aliases: [:v] option :dry_run, type: :boolean, default: false option :link, type: :boolean, default: false - desc 'remove', 'Remove preview cards' + desc 'remove', 'Remove preview card media' long_desc <<-DESC Removes local thumbnails for preview cards. - The --days option specifies how old preview cards have to be before - they are removed. It defaults to 180 days. Since preview cards will - not be re-fetched unless the link is re-posted after 2 weeks from - last time, it is not recommended to delete preview cards within the - last 14 days. + The --days option sets the age a preview card must be before attached + media will be removed. Preview cards will not be re-fetched unless the + link is posted again two weeks after the last usage, so it is not + recommended to delete preview card media from within the last 14 days. - With the --link option, only link-type preview cards will be deleted, - leaving video and photo cards untouched. + With the --link option, only media from link-type preview cards will be + deleted, skipping video and photo cards. DESC def remove time_ago = options[:days].days.ago @@ -44,7 +43,7 @@ module Mastodon::CLI size end - say("Removed #{processed} #{link}preview cards (approx. #{number_to_human_size(aggregate)})#{dry_run_mode_suffix}", :green, true) + say("Removed media from #{processed} #{link}preview cards (approx. #{number_to_human_size(aggregate)})#{dry_run_mode_suffix}", :green, true) end end end diff --git a/spec/lib/mastodon/cli/preview_cards_spec.rb b/spec/lib/mastodon/cli/preview_cards_spec.rb index 949787a759..ddbb206ef8 100644 --- a/spec/lib/mastodon/cli/preview_cards_spec.rb +++ b/spec/lib/mastodon/cli/preview_cards_spec.rb @@ -26,7 +26,7 @@ RSpec.describe Mastodon::CLI::PreviewCards do it 'deletes thumbnails for local preview cards' do expect { subject } .to output_results( - 'Removed 2 preview cards', + 'Removed media from 2 preview cards', 'approx. 119 KB' ) end @@ -38,7 +38,7 @@ RSpec.describe Mastodon::CLI::PreviewCards do it 'deletes thumbnails for local preview cards' do expect { subject } .to output_results( - 'Removed 1 link-type preview cards', + 'Removed media from 1 link-type preview cards', 'approx. 59.6 KB' ) end @@ -50,7 +50,7 @@ RSpec.describe Mastodon::CLI::PreviewCards do it 'deletes thumbnails for local preview cards' do expect { subject } .to output_results( - 'Removed 1 preview cards', + 'Removed media from 1 preview cards', 'approx. 59.6 KB' ) end From f2094164c4046822dc455ae56f8d1847e88dc129 Mon Sep 17 00:00:00 2001 From: matrix07012 <8049717+matrix07012@users.noreply.github.com> Date: Wed, 10 Jun 2026 11:27:38 +0200 Subject: [PATCH 05/17] Fix being unable to unmark media as sensitive when "always mark media as sensitive" is enabled (#39339) --- .../features/compose/components/compose_form.jsx | 4 +++- app/javascript/mastodon/reducers/compose.js | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/features/compose/components/compose_form.jsx b/app/javascript/mastodon/features/compose/components/compose_form.jsx index 2cc1f7915a..f2beaed847 100644 --- a/app/javascript/mastodon/features/compose/components/compose_form.jsx +++ b/app/javascript/mastodon/features/compose/components/compose_form.jsx @@ -220,7 +220,9 @@ class ComposeForm extends ImmutablePureComponent { } else if(prevProps.isSubmitting && !this.props.isSubmitting) { this.textareaRef.current.focus(); } else if (this.props.spoiler !== prevProps.spoiler) { - if (this.props.spoiler) { + const mediaJustAdded = this.props.anyMedia && !prevProps.anyMedia; + + if (this.props.spoiler && !mediaJustAdded) { this.spoilerText.input.focus(); } else if (prevProps.spoiler) { this.textareaRef.current.focus(); diff --git a/app/javascript/mastodon/reducers/compose.js b/app/javascript/mastodon/reducers/compose.js index 3606eea4e6..ce9ef9cbda 100644 --- a/app/javascript/mastodon/reducers/compose.js +++ b/app/javascript/mastodon/reducers/compose.js @@ -155,6 +155,10 @@ function appendMedia(state, media, file) { if (prevSize === 0 && (state.get('default_sensitive') || state.get('spoiler'))) { map.set('sensitive', true); + + if (state.get('default_sensitive')) { + map.set('spoiler', true); + } } }); } @@ -400,7 +404,7 @@ export const composeReducer = (state = initialState, action) => { map.set('spoiler', !state.get('spoiler')); map.set('idempotencyKey', uuid()); - if (state.get('media_attachments').size >= 1 && !state.get('default_sensitive')) { + if (state.get('media_attachments').size >= 1) { map.set('sensitive', !state.get('spoiler')); } }); @@ -545,7 +549,7 @@ export const composeReducer = (state = initialState, action) => { map.set('spoiler', true); map.set('spoiler_text', action.status.get('spoiler_text')); } else { - map.set('spoiler', false); + map.set('spoiler', action.status.get('sensitive') && action.status.get('media_attachments').size > 0); map.set('spoiler_text', ''); } @@ -582,7 +586,7 @@ export const composeReducer = (state = initialState, action) => { map.set('spoiler', true); map.set('spoiler_text', action.spoiler_text); } else { - map.set('spoiler', false); + map.set('spoiler', action.status.get('sensitive') && action.status.get('media_attachments').size > 0); map.set('spoiler_text', ''); } From b2996dcbbc5f6bf7350dd01a2bb22a5cb97a800a Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 10 Jun 2026 11:59:03 +0200 Subject: [PATCH 06/17] Fix font size of `Callout` component actions in Safari (#39354) --- app/javascript/mastodon/components/callout/styles.module.css | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/mastodon/components/callout/styles.module.css b/app/javascript/mastodon/components/callout/styles.module.css index dd2c753525..f049f25e98 100644 --- a/app/javascript/mastodon/components/callout/styles.module.css +++ b/app/javascript/mastodon/components/callout/styles.module.css @@ -62,6 +62,7 @@ background: none; border: none; color: inherit; + font: inherit; font-weight: 500; padding: 0; text-wrap: nowrap; From 9f7e2d0002a65f9b77d32e7a12640dce0ae479ce Mon Sep 17 00:00:00 2001 From: Echo Date: Wed, 10 Jun 2026 12:40:48 +0200 Subject: [PATCH 07/17] Emoji substring search (#39353) --- .../mastodon/actions/importer/emoji.ts | 2 +- .../mastodon/features/emoji/database.test.ts | 4 ++ .../mastodon/features/emoji/database.ts | 39 ++++++++++- .../mastodon/features/emoji/db-schema.ts | 5 +- .../mastodon/features/emoji/emoji_picker.tsx | 2 + .../mastodon/features/emoji/index.ts | 68 ++++++++++--------- .../mastodon/features/emoji/normalize.test.ts | 31 ++++++++- .../mastodon/features/emoji/normalize.ts | 5 ++ .../mastodon/features/emoji/picker.ts | 17 ++++- .../mastodon/features/emoji/types.ts | 10 +++ .../mastodon/features/emoji/utils.ts | 3 + .../mastodon/features/emoji/worker.ts | 25 ++++--- .../mastodon/utils/__tests__/cache.test.ts | 8 ++- app/javascript/mastodon/utils/cache.ts | 4 +- 14 files changed, 169 insertions(+), 54 deletions(-) diff --git a/app/javascript/mastodon/actions/importer/emoji.ts b/app/javascript/mastodon/actions/importer/emoji.ts index e9356ab621..36fb04b51e 100644 --- a/app/javascript/mastodon/actions/importer/emoji.ts +++ b/app/javascript/mastodon/actions/importer/emoji.ts @@ -18,7 +18,7 @@ export async function importCustomEmoji(emojis: ApiCustomEmojiJSON[]) { ); // If there's a mismatch, re-import all custom emojis. - if (existingEmojis.length < emojis.length) { + if (existingEmojis.length > 0 && existingEmojis.length < emojis.length) { await clearCache('custom'); await loadCustomEmoji(); diff --git a/app/javascript/mastodon/features/emoji/database.test.ts b/app/javascript/mastodon/features/emoji/database.test.ts index e272ec2dea..cbbbf9f165 100644 --- a/app/javascript/mastodon/features/emoji/database.test.ts +++ b/app/javascript/mastodon/features/emoji/database.test.ts @@ -22,6 +22,10 @@ function rawEmojiFactory(data: Partial = {}): CompactEmoji { } describe('emoji database', () => { + beforeEach(async () => { + await testGet(); // Loads the database schema. + }); + afterEach(() => { testClear(); indexedDB = new IDBFactory(); diff --git a/app/javascript/mastodon/features/emoji/database.ts b/app/javascript/mastodon/features/emoji/database.ts index 3dd2f95858..2ba20029bf 100644 --- a/app/javascript/mastodon/features/emoji/database.ts +++ b/app/javascript/mastodon/features/emoji/database.ts @@ -143,7 +143,28 @@ export async function search({ return intersection; }) .values(), - ).toSorted((a, b) => a.score - b.score); + ); + + // If there are no results, try a cursor-based custom emoji search instead. + if (results.length === 0) { + const trx = db.transaction('custom', 'readonly'); + const foundEmojis = new Set(); + for await (const cursor of trx.store) { + const emoji = cursor.value; + const score = getScoreForEmoji(emoji, query, false); + if (score === null || foundEmojis.has(emoji.shortcode)) { + continue; + } + + results.push({ ...emoji, score }); + foundEmojis.add(emoji.shortcode); + } + log('cursor search found %d results for "%s"', foundEmojis.size, query); + await trx.done; + } + + // Sort by score, descending. + results.sort((a, b) => a.score - b.score); const time = performance.measure('emoji-search-end', 'emoji-search-start'); log( @@ -159,14 +180,19 @@ export async function search({ return results; } -function getScoreForEmoji(emoji: AnyEmojiData, query: string) { +function getScoreForEmoji( + emoji: AnyEmojiData, + query: string, + checkTokens = true, +) { const id = 'shortcode' in emoji ? emoji.shortcode : emoji.label; if (id === query) { return 0; } let index = 1; - for (const token of [id, ...emoji.tokens]) { + const searchTokens = checkTokens ? [id, ...emoji.tokens] : [id]; + for (const token of searchTokens) { const tokenIndex = token.indexOf(query); if (tokenIndex !== -1) { return index + tokenIndex / token.length; @@ -246,6 +272,13 @@ export async function clearCache(key: CacheKey) { log('Cleared cache for %s', key); } +export async function resetDatabase() { + const db = await loadDB(); + const storeNames = [...db.objectStoreNames]; + await Promise.all(storeNames.map((storeName) => db.clear(storeName))); + log(storeNames, 'Reset emoji database stores:'); +} + export async function loadEmojiByHexcode( hexcode: string, localeString: string, diff --git a/app/javascript/mastodon/features/emoji/db-schema.ts b/app/javascript/mastodon/features/emoji/db-schema.ts index 7f4d09fd0b..f31ac96f81 100644 --- a/app/javascript/mastodon/features/emoji/db-schema.ts +++ b/app/javascript/mastodon/features/emoji/db-schema.ts @@ -10,6 +10,7 @@ import type { StoreNames, } from 'idb'; +import { resetDatabase } from './database'; import type { CustomEmojiData, CacheKey, UnicodeEmojiData } from './types'; import { emojiLogger } from './utils'; @@ -57,7 +58,7 @@ type Transaction = export type Database = IDBPDatabase; -const SCHEMA_VERSION = 3; +const SCHEMA_VERSION = 4; export async function openEmojiDB() { const db = await openDB('mastodon-emoji', SCHEMA_VERSION, { @@ -98,6 +99,8 @@ export async function openEmojiDB() { }); deleteOldIndexes(shortcodeTable, ['hexcode']); + void resetDatabase(); + log( 'Upgraded emoji database from version %d to %d', oldVersion, diff --git a/app/javascript/mastodon/features/emoji/emoji_picker.tsx b/app/javascript/mastodon/features/emoji/emoji_picker.tsx index 233c451aad..3661a7193b 100644 --- a/app/javascript/mastodon/features/emoji/emoji_picker.tsx +++ b/app/javascript/mastodon/features/emoji/emoji_picker.tsx @@ -57,6 +57,8 @@ export const Emoji: FC = ({ const { mode } = useEmojiAppState(); return ( 0) { + log('loaded %d custom emojis', customEmojis.length); + await reloadCustomEmojis(); + } const shortcodes = await importLegacyShortcodes(); if (shortcodes?.length) { log('loaded %d legacy shortcodes', shortcodes.length); } - await loadEmojiLocale(userLocale); -} -async function loadEmojiLocale(localeString: string) { - const locale = toSupportedLocale(localeString); - const { importEmojiData } = await import('./loader'); - - if (worker) { - log('asking worker to load locale %s', locale); - messageWorker(locale); - } else { - const emojis = await importEmojiData(locale); - if (emojis) { - log('loaded %d emojis to locale %s', emojis.length, locale); - } + const emojis = await importEmojiData(userLocale); + if (emojis) { + log('loaded %d emojis to locale %s', emojis.length, userLocale); } } @@ -96,11 +97,16 @@ export async function loadCustomEmoji() { } } -function messageWorker( - locale: LocaleOrCustom | typeof EMOJI_DB_NAME_SHORTCODES, -) { +function messageWorker(data: EmojiWorkerMessage | string) { if (!worker) { return; } - worker.postMessage({ locale }); + if (typeof data === 'string') { + worker.postMessage({ + type: 'load', + storeName: data, + } satisfies EmojiWorkerMessage); + } else { + worker.postMessage(data); + } } diff --git a/app/javascript/mastodon/features/emoji/normalize.test.ts b/app/javascript/mastodon/features/emoji/normalize.test.ts index 8c6346709b..08587138c6 100644 --- a/app/javascript/mastodon/features/emoji/normalize.test.ts +++ b/app/javascript/mastodon/features/emoji/normalize.test.ts @@ -4,7 +4,7 @@ import { basename, resolve } from 'path'; import { flattenEmojiData } from 'emojibase'; import unicodeRawEmojis from 'emojibase-data/en/data.json'; -import { unicodeToTwemojiHex } from './normalize'; +import { extractTokens, unicodeToTwemojiHex } from './normalize'; const emojiSVGFiles = await readdir( // This assumes tests are run from project root @@ -33,3 +33,32 @@ describe('unicodeToTwemojiHex', () => { expect(svgFileNamesWithoutBorder).toContain(result); }); }); + +describe('extractTokens', () => { + test('returns an empty array for blank input', () => { + expect(extractTokens(' ', null)).toEqual([]); + }); + + test('check token word breaking with Intl.Segmenter', () => { + const segmenter = new Intl.Segmenter('en', { granularity: 'word' }); + + expect( + extractTokens('thumbs_up smiling-face camelCase', segmenter), + ).toEqual(['thumbs', 'up', 'smiling', 'face', 'camel', 'case']); + }); + + test('check token word breaking with regex', () => { + expect(extractTokens('Smile_face joy-test A ok 7 z', null)).toEqual([ + 'smile', + 'face', + 'joy', + 'test', + 'ok', + ]); + }); + + test('ensure +1 and -1 are preserved', () => { + expect(extractTokens('+1', null)).toEqual(['+1']); + expect(extractTokens('-1', null)).toEqual(['-1']); + }); +}); diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index 257cddfcb6..3583d178eb 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -214,6 +214,11 @@ export function extractTokens( } const tokens: string[] = []; + // Handle the edge case of thumbs up and down emoticons. + if (input === '+1' || input === '-1') { + return [input]; + } + // Prefer to use Intl.Segmenter if available for better locale support. if (segmenter) { for (const { isWordLike, segment } of segmenter.segment( diff --git a/app/javascript/mastodon/features/emoji/picker.ts b/app/javascript/mastodon/features/emoji/picker.ts index fccf6769c3..7acf878f51 100644 --- a/app/javascript/mastodon/features/emoji/picker.ts +++ b/app/javascript/mastodon/features/emoji/picker.ts @@ -3,6 +3,7 @@ import { useEffect, useState } from 'react'; import type { CategoryName, CustomEmoji } from 'emoji-mart'; import { autoPlayGif } from '@/mastodon/initial_state'; +import { createLimitedCache } from '@/mastodon/utils/cache'; import { emojiLogger } from './utils'; @@ -21,6 +22,8 @@ let customCategories = [ 'flags', ] as CategoryName[]; +const searchCache = createLimitedCache({ maxSize: 10, log }); + export async function fetchCustomEmojiData() { if (customEmojis !== null) { return customEmojis; @@ -89,6 +92,7 @@ export async function reloadCustomEmojis() { await import('@/mastodon/hooks/useCustomEmojis'); await Promise.all([fetchCustomEmojiData(), loadEmojisIntoCache()]); + searchCache.clear(); } // Replicates the old legacy search function. @@ -102,16 +106,25 @@ export async function emojiMartSearch( return []; } + const cacheKey = `${query}|${locale}|${limit}`; + const cachedResult = searchCache.get(cacheKey); + if (cachedResult) { + return cachedResult; + } + const { search } = await import('./database'); const results = await search({ query, locale, limit }); - return results.map((emoji) => + const legacyResults = results.map((emoji) => 'shortcode' in emoji - ? { id: emoji.shortcode, custom: true } + ? ({ id: emoji.shortcode, custom: true } as const) : { id: emoji.label.replaceAll(' ', '_').toLowerCase(), native: emoji.unicode, }, ); + searchCache.set(cacheKey, legacyResults); + + return legacyResults; } export function usePickerEmojis() { diff --git a/app/javascript/mastodon/features/emoji/types.ts b/app/javascript/mastodon/features/emoji/types.ts index 2eb0c0f232..5c9dc6e5c6 100644 --- a/app/javascript/mastodon/features/emoji/types.ts +++ b/app/javascript/mastodon/features/emoji/types.ts @@ -80,3 +80,13 @@ export type ExtraCustomEmojiMap = Record< string, Pick >; + +export type EmojiWorkerMessage = + | { + type: 'load'; + storeName: string; + } + | { + type: 'debug'; + debugValue: string; + }; diff --git a/app/javascript/mastodon/features/emoji/utils.ts b/app/javascript/mastodon/features/emoji/utils.ts index 670e63a3c3..1722cde08c 100644 --- a/app/javascript/mastodon/features/emoji/utils.ts +++ b/app/javascript/mastodon/features/emoji/utils.ts @@ -5,6 +5,9 @@ import { emojiRegexPolyfill } from '@/mastodon/polyfills'; import { VARIATION_SELECTOR_CODE } from './constants'; export function emojiLogger(segment: string) { + if (typeof window === 'undefined') { + return debug(`emojis:worker:${segment}`); + } return debug(`emojis:${segment}`); } diff --git a/app/javascript/mastodon/features/emoji/worker.ts b/app/javascript/mastodon/features/emoji/worker.ts index 5602577dbe..d4f002daf6 100644 --- a/app/javascript/mastodon/features/emoji/worker.ts +++ b/app/javascript/mastodon/features/emoji/worker.ts @@ -1,31 +1,36 @@ +import debug from 'debug'; + import { EMOJI_DB_NAME_SHORTCODES, EMOJI_TYPE_CUSTOM } from './constants'; import { importCustomEmojiData, importEmojiData, importLegacyShortcodes, } from './loader'; +import type { EmojiWorkerMessage } from './types'; addEventListener('message', handleMessage); self.postMessage('ready'); // After the worker is ready, notify the main thread -function handleMessage(event: MessageEvent<{ locale: string }>) { - const { - data: { locale }, - } = event; - void loadData(locale); +function handleMessage(event: MessageEvent) { + const { data } = event; + if (data.type === 'debug') { + debug.enable(data.debugValue); + } else { + void loadData(data.storeName); + } } -async function loadData(locale: string) { +async function loadData(storeName: string) { let importCount: number | undefined; - if (locale === EMOJI_TYPE_CUSTOM) { + if (storeName === EMOJI_TYPE_CUSTOM) { importCount = (await importCustomEmojiData())?.length; - } else if (locale === EMOJI_DB_NAME_SHORTCODES) { + } else if (storeName === EMOJI_DB_NAME_SHORTCODES) { importCount = (await importLegacyShortcodes())?.length; } else { - importCount = (await importEmojiData(locale))?.length; + importCount = (await importEmojiData(storeName))?.length; } if (importCount) { - self.postMessage(`loaded ${importCount} emojis into ${locale}`); + self.postMessage(`loaded ${importCount} emojis into ${storeName}`); } } diff --git a/app/javascript/mastodon/utils/__tests__/cache.test.ts b/app/javascript/mastodon/utils/__tests__/cache.test.ts index 340a51fdb4..8d9d9c78ad 100644 --- a/app/javascript/mastodon/utils/__tests__/cache.test.ts +++ b/app/javascript/mastodon/utils/__tests__/cache.test.ts @@ -40,11 +40,13 @@ describe('createCache', () => { }); test('removes oldest item cached if it exceeds a set size', () => { - const cache = createLimitedCache({ maxSize: 1 }); + const cache = createLimitedCache({ maxSize: 2 }); cache.set('test1', 1); cache.set('test2', 2); + cache.set('test3', 3); expect(cache.get('test1')).toBeUndefined(); expect(cache.get('test2')).toBe(2); + expect(cache.get('test3')).toBe(3); }); test('retrieving a value bumps up last access', () => { @@ -63,13 +65,13 @@ describe('createCache', () => { const cache = createLimitedCache({ maxSize: 1, log }); cache.set('test1', 1); expect(log).toHaveBeenLastCalledWith( - 'Added %s to cache, now size %d', + 'Added %o to cache, now size %d', 'test1', 1, ); cache.set('test2', 1); expect(log).toHaveBeenLastCalledWith( - 'Added %s and deleted %s from cache, now size %d', + 'Added %o and deleted %o from cache, now size %d', 'test2', 'test1', 1, diff --git a/app/javascript/mastodon/utils/cache.ts b/app/javascript/mastodon/utils/cache.ts index 2e3d21bfed..d00b6fc9e0 100644 --- a/app/javascript/mastodon/utils/cache.ts +++ b/app/javascript/mastodon/utils/cache.ts @@ -43,13 +43,13 @@ export function createLimitedCache({ cacheMap.delete(lastKey); cacheKeys.delete(lastKey); log( - 'Added %s and deleted %s from cache, now size %d', + 'Added %o and deleted %o from cache, now size %d', key, lastKey, cacheMap.size, ); } else { - log('Added %s to cache, now size %d', key, cacheMap.size); + log('Added %o to cache, now size %d', key, cacheMap.size); } }, clear: () => { From bff9f0eb6ddfec989a7f316dc7e9b0a7cc9b5086 Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 10 Jun 2026 14:03:47 +0200 Subject: [PATCH 08/17] Fix uncontained text overflow in column header (#39356) --- app/javascript/mastodon/components/column_header.tsx | 2 +- app/javascript/styles/mastodon/components.scss | 11 ++++++++--- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/javascript/mastodon/components/column_header.tsx b/app/javascript/mastodon/components/column_header.tsx index 36ed239a47..676ae01919 100644 --- a/app/javascript/mastodon/components/column_header.tsx +++ b/app/javascript/mastodon/components/column_header.tsx @@ -272,7 +272,7 @@ export const ColumnHeader: React.FC = ({ {!backButton && hasIcon && ( )} - {title} + {title} ); diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index cb2e6e9eb2..badbcba385 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -4641,6 +4641,7 @@ a.status-card { &__title-wrapper { display: flex; flex-grow: 1; + min-width: 0; } &__title { @@ -4656,9 +4657,7 @@ a.status-card { background: transparent; font: inherit; text-align: start; - text-overflow: ellipsis; - overflow: hidden; - white-space: nowrap; + min-width: 0; &--with-back-button { padding-inline-start: 0; @@ -4673,6 +4672,12 @@ a.status-card { } } + &__text { + text-overflow: ellipsis; + overflow: hidden; + white-space: nowrap; + } + .column-header__back-button { flex: 1; color: var(--color-text-brand); From bcb8553e01270bdb56827a701c4359d39643aa2c Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 10 Jun 2026 14:34:48 +0200 Subject: [PATCH 09/17] [Accessibility] Manage focus on navigation (#39350) --- app/javascript/mastodon/actions/modal.ts | 1 + .../mastodon/components/column_header.tsx | 8 +- .../mastodon/components/modal_root.jsx | 12 +- .../navigation_focus_target/index.tsx | 145 ++++++++++++++++++ app/javascript/mastodon/components/router.tsx | 5 + app/javascript/mastodon/components/status.jsx | 5 +- .../mastodon/containers/mastodon.jsx | 5 +- .../mastodon/features/about/index.jsx | 7 +- .../account_edit/components/field_actions.tsx | 1 + .../mastodon/features/account_edit/index.tsx | 20 ++- .../account_timeline/modals/field_modal.tsx | 15 +- .../closed_registrations_modal/index.jsx | 9 +- .../collections/components/share_modal.tsx | 5 +- .../mastodon/features/collections/index.tsx | 5 +- .../features/interaction_modal/index.tsx | 5 +- .../components/embedded_status.tsx | 3 +- .../mastodon/features/status/index.jsx | 11 +- .../ui/components/__tests__/column-test.jsx | 15 +- .../confirmation_modal.tsx | 9 +- app/javascript/mastodon/features/ui/index.jsx | 2 +- app/javascript/mastodon/reducers/modal.ts | 8 +- .../styles/mastodon/components.scss | 6 + 22 files changed, 265 insertions(+), 37 deletions(-) create mode 100644 app/javascript/mastodon/components/navigation_focus_target/index.tsx diff --git a/app/javascript/mastodon/actions/modal.ts b/app/javascript/mastodon/actions/modal.ts index 49af176a11..0978c32658 100644 --- a/app/javascript/mastodon/actions/modal.ts +++ b/app/javascript/mastodon/actions/modal.ts @@ -10,6 +10,7 @@ interface OpenModalPayload { modalType: ModalType; modalProps: ModalProps; previousModalProps?: ModalProps; + ignoreFocus?: boolean; } export const openModal = createAction('MODAL_OPEN'); diff --git a/app/javascript/mastodon/components/column_header.tsx b/app/javascript/mastodon/components/column_header.tsx index 676ae01919..77cba09fbe 100644 --- a/app/javascript/mastodon/components/column_header.tsx +++ b/app/javascript/mastodon/components/column_header.tsx @@ -19,6 +19,7 @@ import { useIdentity } from 'mastodon/identity_context'; import { useColumnIndexContext } from '../features/ui/components/columns_area'; import { getColumnSkipLinkId } from '../features/ui/components/skip_links'; +import { NavigationFocusTarget } from './navigation_focus_target'; import { useAppHistory } from './router'; export const messages = defineMessages({ @@ -285,7 +286,10 @@ export const ColumnHeader: React.FC = ({
{backButton} {hasTitle && ( -

+ {onClick ? (

+ )}
diff --git a/app/javascript/mastodon/components/modal_root.jsx b/app/javascript/mastodon/components/modal_root.jsx index 61ff19256f..c2f51a5673 100644 --- a/app/javascript/mastodon/components/modal_root.jsx +++ b/app/javascript/mastodon/components/modal_root.jsx @@ -7,6 +7,7 @@ import { multiply } from 'color-blend'; import { createBrowserHistory } from 'history'; import { WithOptionalRouterPropTypes, withOptionalRouter } from 'mastodon/utils/react_router'; +import { IGNORE_FOCUS_ON_OPEN } from '../reducers/modal'; class ModalRoot extends PureComponent { @@ -21,7 +22,10 @@ class ModalRoot extends PureComponent { b: PropTypes.number, }), ]), - ignoreFocus: PropTypes.bool, + ignoreFocus: PropTypes.oneOfType([ + PropTypes.bool, + PropTypes.string, // 'on-open', see IGNORE_FOCUS_ON_OPEN + ]), ...WithOptionalRouterPropTypes, }; @@ -118,7 +122,11 @@ class ModalRoot extends PureComponent { _ensureHistoryBuffer () { const { pathname, search, hash, state } = this.history.location; if (!state || state.mastodonModalKey !== this._modalHistoryKey) { - this.history.push({ pathname, search, hash }, { ...state, mastodonModalKey: this._modalHistoryKey }); + this.history.push({ pathname, search, hash }, { + ...state, + focusTarget: this.props.ignoreFocus !== IGNORE_FOCUS_ON_OPEN, + mastodonModalKey: this._modalHistoryKey, + }); } } diff --git a/app/javascript/mastodon/components/navigation_focus_target/index.tsx b/app/javascript/mastodon/components/navigation_focus_target/index.tsx new file mode 100644 index 0000000000..f4b42926a8 --- /dev/null +++ b/app/javascript/mastodon/components/navigation_focus_target/index.tsx @@ -0,0 +1,145 @@ +import { + createContext, + useContext, + useRef, + useLayoutEffect, + useCallback, +} from 'react'; + +import { useLocation } from 'react-router-dom'; + +import { polymorphicForwardRef } from '@/types/polymorphic'; + +import type { MastodonLocation } from '../router'; + +export const FOCUS_TARGET = { + POST: 'detailed-status', +} as const; + +export type FocusTarget = + | boolean + | (typeof FOCUS_TARGET)[keyof typeof FOCUS_TARGET]; + +const FocusTargetContext = + createContext | null>(null); + +/** + * `FocusTargetProvider` keeps track of whether focus should be + * set after a navigation. By default, any navigation will set the + * current value of `focusTargetRef` to `true`, which will cause + * the `NavigationFocusTarget` component to focus itself when it mounts. + * + * To disable this behaviour for a navigation, the focus target can be + * set to `false` using location state, for example: + * ``` + * location.push(url, { focusTarget: false }); + * ``` + * + * If the target page contains multiple `NavigationFocusTarget` components + * (e.g. a main heading and a post that should be focused), give the more + * specific `NavigationFocusTarget` instance a name, and pass the same name + * via location state: + * ``` + * location.push(url, { focusTarget: 'detailed-status' }); + * ``` + */ + +export const FocusTargetProvider: React.FC<{ + children: React.ReactNode; +}> = ({ children }) => { + const focusTargetRef = useRef(false); + const previousLocationRef = useRef< + | (Pick & { + focusTarget?: FocusTarget; + }) + | null + >(null); + + const { + pathname, + search, + state = {}, + } = useLocation<{ focusTarget?: FocusTarget } | undefined>(); + + const { focusTarget } = state; + + useLayoutEffect(() => { + // We never want to set focus on page load, so we keep + // track of whether a manual navigation has occurred by comparing + // our current with the previous location: + const previous = previousLocationRef.current; + + // Bail out on the first render, populate previousLocationRef + if (previous === null) { + previousLocationRef.current = { pathname, search, focusTarget }; + return; + } + + // Bail out if location hasn't changed + if ( + previous.pathname === pathname && + previous.search === search && + previous.focusTarget === focusTarget + ) { + return; + } + + // Location has changed: + // - Set focusTarget + // – Store current location as previous + // (We store `focusTarget` as `false` to allow overriding it.) + previousLocationRef.current = { pathname, search, focusTarget: false }; + focusTargetRef.current = focusTarget ?? true; + }, [pathname, search, focusTarget]); + + return ( + + {children} + + ); +}; + +export function useFocusOnNavigation(targetName?: string) { + const focusTargetRef = useContext(FocusTargetContext); + + if (focusTargetRef === null) { + throw Error( + 'useFocusTargetContext must be used inside of a FocusTargetProvider', + ); + } + + return useCallback( + (element: HTMLElement | null) => { + const focusTarget = focusTargetRef.current; + + // Bail out if focusTarget was set to `false` + if (!element || !focusTarget) { + return; + } + + if (focusTarget === true || focusTarget === targetName) { + setTimeout(() => { + element.focus({ preventScroll: true }); + }, 0); + } + }, + [focusTargetRef, targetName], + ); +} + +interface FocusTargetElementProps extends React.ComponentPropsWithoutRef<'h1'> { + focusTargetName?: string; +} + +export const NavigationFocusTarget = polymorphicForwardRef< + 'h1', + FocusTargetElementProps +>(({ as: Component = 'h1', focusTargetName, children, ...otherProps }) => { + const focusOnNavigation = useFocusOnNavigation(focusTargetName); + + return ( + + {children} + + ); +}); diff --git a/app/javascript/mastodon/components/router.tsx b/app/javascript/mastodon/components/router.tsx index bd6e4b568f..2a53d6dff4 100644 --- a/app/javascript/mastodon/components/router.tsx +++ b/app/javascript/mastodon/components/router.tsx @@ -14,9 +14,14 @@ import { createBrowserHistory } from 'history'; import { layoutFromWindow } from 'mastodon/is_mobile'; import { isDevelopment } from 'mastodon/utils/environment'; +import type { FocusTarget } from './navigation_focus_target'; + interface MastodonLocationState { fromMastodon?: boolean; mastodonModalKey?: string; + // Controls which element is focused after a navigation. + // Set to `false` to prevent navigation focus. + focusTarget?: FocusTarget; // Prevent the rightmost column in advanced UI from scrolling // into view on location changes preventMultiColumnAutoScroll?: string; diff --git a/app/javascript/mastodon/components/status.jsx b/app/javascript/mastodon/components/status.jsx index 808f4d73bf..2f66f15405 100644 --- a/app/javascript/mastodon/components/status.jsx +++ b/app/javascript/mastodon/components/status.jsx @@ -33,6 +33,7 @@ import StatusContent from './status_content'; import { StatusThreadLabel } from './status_thread_label'; import { CollectionPreviewCard } from '../features/collections/components/collection_preview_card'; import { compareUrls } from '../utils/compare_urls'; +import { FOCUS_TARGET } from './navigation_focus_target'; const domParser = new DOMParser(); @@ -311,9 +312,9 @@ class Status extends ImmutablePureComponent { window.open(path, '_blank', 'noopener'); } else { if (history.location.pathname.replace('/deck/', '/') === path) { - history.replace(path); + history.replace(path, {focusTarget: FOCUS_TARGET.POST}); } else { - history.push(path); + history.push(path, {focusTarget: FOCUS_TARGET.POST}); } } }; diff --git a/app/javascript/mastodon/containers/mastodon.jsx b/app/javascript/mastodon/containers/mastodon.jsx index d6df09db49..4f87a1b823 100644 --- a/app/javascript/mastodon/containers/mastodon.jsx +++ b/app/javascript/mastodon/containers/mastodon.jsx @@ -8,6 +8,7 @@ import { Provider as ReduxProvider } from 'react-redux'; import { hydrateStore } from 'mastodon/actions/store'; import { connectUserStream } from 'mastodon/actions/streaming'; import ErrorBoundary from 'mastodon/components/error_boundary'; +import { FocusTargetProvider } from '@/mastodon/components/navigation_focus_target'; import { Router } from 'mastodon/components/router'; import UI from 'mastodon/features/ui'; import { IdentityContext, createIdentityContext } from 'mastodon/identity_context'; @@ -49,7 +50,9 @@ export default class Mastodon extends PureComponent { - + + + diff --git a/app/javascript/mastodon/features/about/index.jsx b/app/javascript/mastodon/features/about/index.jsx index 83f5bf6025..d60ef9e610 100644 --- a/app/javascript/mastodon/features/about/index.jsx +++ b/app/javascript/mastodon/features/about/index.jsx @@ -8,10 +8,13 @@ import { Helmet } from '@unhead/react/helmet'; import ImmutablePropTypes from 'react-immutable-proptypes'; import { connect } from 'react-redux'; +import { domain } from 'mastodon/initial_state'; + import { injectIntl } from '@/mastodon/components/intl'; import { fetchServer, fetchExtendedDescription, fetchDomainBlocks } from 'mastodon/actions/server'; import { Account } from 'mastodon/components/account'; import Column from 'mastodon/components/column'; +import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target'; import { ServerHeroImage } from 'mastodon/components/server_hero_image'; import { Skeleton } from 'mastodon/components/skeleton'; import { LinkFooter} from 'mastodon/features/ui/components/link_footer'; @@ -91,7 +94,9 @@ class About extends PureComponent { srcSet={Object.keys(server.item?.thumbnail.versions ?? {}).map((key) => `${server.item?.thumbnail.versions && server.item.thumbnail.versions[key]} ${key.replace('@', '')}`).join(', ')} className='about__header__hero' /> -

{isLoading ? : server.domain}

+ + {isLoading ? : domain} +

Mastodon }} />

diff --git a/app/javascript/mastodon/features/account_edit/components/field_actions.tsx b/app/javascript/mastodon/features/account_edit/components/field_actions.tsx index fecdcb8eff..028f4dc803 100644 --- a/app/javascript/mastodon/features/account_edit/components/field_actions.tsx +++ b/app/javascript/mastodon/features/account_edit/components/field_actions.tsx @@ -26,6 +26,7 @@ export const AccountFieldActions: FC<{ id: string }> = ({ id }) => { openModal({ modalType: 'ACCOUNT_EDIT_FIELD_EDIT', modalProps: { fieldKey: id }, + ignoreFocus: true, }), ); }, [dispatch, id]); diff --git a/app/javascript/mastodon/features/account_edit/index.tsx b/app/javascript/mastodon/features/account_edit/index.tsx index d00473043e..6ef8997f7b 100644 --- a/app/javascript/mastodon/features/account_edit/index.tsx +++ b/app/javascript/mastodon/features/account_edit/index.tsx @@ -137,16 +137,28 @@ export const AccountEdit: FC = () => { ); const handleOpenModal = useCallback( - (type: ModalType, props?: Record) => { - dispatch(openModal({ modalType: type, modalProps: props ?? {} })); + ( + type: ModalType, + { + modalProps = {}, + ignoreFocus = false, + }: { modalProps?: Record; ignoreFocus?: boolean } = {}, + ) => { + dispatch( + openModal({ + modalType: type, + modalProps, + ignoreFocus, + }), + ); }, [dispatch], ); const handleNameEdit = useCallback(() => { - handleOpenModal('ACCOUNT_EDIT_NAME'); + handleOpenModal('ACCOUNT_EDIT_NAME', { ignoreFocus: true }); }, [handleOpenModal]); const handleBioEdit = useCallback(() => { - handleOpenModal('ACCOUNT_EDIT_BIO'); + handleOpenModal('ACCOUNT_EDIT_BIO', { ignoreFocus: true }); }, [handleOpenModal]); const handleCustomFieldAdd = useCallback(() => { handleOpenModal('ACCOUNT_EDIT_FIELD_EDIT'); diff --git a/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx b/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx index baf0c70f76..abb0a28ee3 100644 --- a/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx +++ b/app/javascript/mastodon/features/account_timeline/modals/field_modal.tsx @@ -10,6 +10,7 @@ import { ModalShellActions, ModalShellBody, } from '@/mastodon/components/modal_shell'; +import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target'; import { useFieldHtml } from '../hooks/useFieldHtml'; @@ -24,12 +25,14 @@ export const AccountFieldModal: FC<{ return ( - + + + ({ message: state.getIn(['server', 'server', 'item', 'registrations', 'message']), @@ -42,7 +43,9 @@ class ClosedRegistrationsModal extends ImmutablePureComponent { return (
-

+ + +

-

+

{closedRegistrationsMessage}
-

+

-

+ {isNew ? ( )} -

+
-

{pageTitleHtml}

+ + {pageTitleHtml} + {intl.formatMessage(createdByTabMessage, { diff --git a/app/javascript/mastodon/features/interaction_modal/index.tsx b/app/javascript/mastodon/features/interaction_modal/index.tsx index ef1d3e1111..128fb3ff5e 100644 --- a/app/javascript/mastodon/features/interaction_modal/index.tsx +++ b/app/javascript/mastodon/features/interaction_modal/index.tsx @@ -8,6 +8,7 @@ import { escapeRegExp } from 'lodash'; import { useDebouncedCallback } from 'use-debounce'; import { DisplayName } from '@/mastodon/components/display_name'; +import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target'; import { openModal, closeModal } from 'mastodon/actions/modal'; import { apiRequest } from 'mastodon/api'; import { Button } from 'mastodon/components/button'; @@ -474,12 +475,12 @@ const InteractionModal: React.FC<{ return (
-

+ -

+

{intent === 'follow' ? ( = ({ const path = `/@${account.acct}/${statusId}`; if (button === 0 && !(ctrlKey || metaKey)) { - history.push(path); + history.push(path, { focusTarget: FOCUS_TARGET.POST }); } else if (button === 1 || (button === 0 && (ctrlKey || metaKey))) { window.open(path, '_blank', 'noopener'); } diff --git a/app/javascript/mastodon/features/status/index.jsx b/app/javascript/mastodon/features/status/index.jsx index db18964a3b..69a62a3663 100644 --- a/app/javascript/mastodon/features/status/index.jsx +++ b/app/javascript/mastodon/features/status/index.jsx @@ -72,6 +72,7 @@ import ActionBar from './components/action_bar'; import { DetailedStatus } from './components/detailed_status'; import { RefreshController } from './components/refresh_controller'; import { quoteComposeById } from '@/mastodon/actions/compose_typed'; +import { FOCUS_TARGET, NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target'; const messages = defineMessages({ revealAll: { id: 'status.show_more_all', defaultMessage: 'Show more for all' }, @@ -585,7 +586,13 @@ class Status extends ImmutablePureComponent { {ancestors} -

+ -
+ {descendants} diff --git a/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx b/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx index d4e248f443..1a63c75612 100644 --- a/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx +++ b/app/javascript/mastodon/features/ui/components/__tests__/column-test.jsx @@ -1,6 +1,7 @@ import { render, fireEvent, screen } from '@/testing/rendering'; import Column from '../column'; +import { FocusTargetProvider } from '@/mastodon/components/navigation_focus_target'; const fakeIcon = () => ; @@ -9,9 +10,11 @@ describe('', () => { it('runs the scroll animation if the column contains scrollable content', () => { const scrollToMock = vi.fn(); const { container } = render( - -
- , + + +
+ + , ); container.querySelector('.scrollable').scrollTo = scrollToMock; fireEvent.click(screen.getByText('notifications')); @@ -19,7 +22,11 @@ describe('', () => { }); it('does not try to scroll if there is no scrollable content', () => { - render(); + render( + + + + ); fireEvent.click(screen.getByText('notifications')); }); }); diff --git a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx index 1dbc19ea3b..3dfd3124d1 100644 --- a/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx +++ b/app/javascript/mastodon/features/ui/components/confirmation_modals/confirmation_modal.tsx @@ -2,6 +2,7 @@ import { useCallback } from 'react'; import { FormattedMessage } from 'react-intl'; +import { NavigationFocusTarget } from '@/mastodon/components/navigation_focus_target'; import { Button } from 'mastodon/components/button'; import { ModalShell, @@ -67,7 +68,13 @@ export const ConfirmationModal: React.FC< return ( -

{title}

+ {noFocusButton ? ( + + {title} + + ) : ( +

{title}

+ )} {message &&

{message}

} {extraContent ?? children} diff --git a/app/javascript/mastodon/features/ui/index.jsx b/app/javascript/mastodon/features/ui/index.jsx index 73ff427e6f..06692d54ee 100644 --- a/app/javascript/mastodon/features/ui/index.jsx +++ b/app/javascript/mastodon/features/ui/index.jsx @@ -187,7 +187,7 @@ class SwitchingColumnsArea extends PureComponent { - + {singleColumn ? : null} {singleColumn && pathName.startsWith('/deck/') ? : null} diff --git a/app/javascript/mastodon/reducers/modal.ts b/app/javascript/mastodon/reducers/modal.ts index dfdff7cf03..a8d771f4bf 100644 --- a/app/javascript/mastodon/reducers/modal.ts +++ b/app/javascript/mastodon/reducers/modal.ts @@ -17,8 +17,10 @@ const Modal = ImmutableRecord({ modalProps: ImmutableRecord({})(), }); +export const IGNORE_FOCUS_ON_OPEN = 'on-open'; + interface ModalState { - ignoreFocus: boolean; + ignoreFocus: boolean | typeof IGNORE_FOCUS_ON_OPEN; stack: Stack>; } @@ -53,9 +55,10 @@ const pushModal = ( modalType: ModalType, modalProps: ModalProps, previousModalProps?: ModalProps, + ignoreFocusOnOpen = false, ): State => { return state.withMutations((record) => { - record.set('ignoreFocus', false); + record.set('ignoreFocus', ignoreFocusOnOpen ? IGNORE_FOCUS_ON_OPEN : false); record.update('stack', (stack) => { let tmp = stack; @@ -92,6 +95,7 @@ export const modalReducer: Reducer = (state = initialState, action) => { action.payload.modalType, action.payload.modalProps, action.payload.previousModalProps, + action.payload.ignoreFocus, ); else if (closeModal.match(action)) return popModal(state, action.payload); // TODO: type those actions diff --git a/app/javascript/styles/mastodon/components.scss b/app/javascript/styles/mastodon/components.scss index badbcba385..f228fcf0df 100644 --- a/app/javascript/styles/mastodon/components.scss +++ b/app/javascript/styles/mastodon/components.scss @@ -9146,6 +9146,8 @@ noscript { padding-bottom: 32px; } + h1, + h2, h3 { font-size: 22px; line-height: 33px; @@ -9176,6 +9178,8 @@ noscript { &__lead { margin-bottom: 20px; + h1, + h2, h3 { margin-bottom: 15px; } @@ -9251,6 +9255,8 @@ noscript { flex: 1; box-sizing: border-box; + h1, + h2, h3 { margin-bottom: 20px; } From 70bb70c2e62ce672e50ce6db823a6f75c0137027 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Wed, 10 Jun 2026 15:28:52 +0200 Subject: [PATCH 10/17] Add meta tags to Collection HTML (#39357) --- app/controllers/collections_controller.rb | 1 - app/views/collections/show.html.haml | 20 +++++++++++++++++++ spec/rails_helper.rb | 1 + spec/requests/account_show_page_spec.rb | 14 ------------- spec/requests/collections_spec.rb | 12 ++++++++++-- spec/requests/status_show_page_spec.rb | 21 -------------------- spec/support/html_head_inspection.rb | 24 +++++++++++++++++++++++ 7 files changed, 55 insertions(+), 38 deletions(-) create mode 100644 app/views/collections/show.html.haml create mode 100644 spec/support/html_head_inspection.rb diff --git a/app/controllers/collections_controller.rb b/app/controllers/collections_controller.rb index 628418557c..fe737810d5 100644 --- a/app/controllers/collections_controller.rb +++ b/app/controllers/collections_controller.rb @@ -18,7 +18,6 @@ class CollectionsController < ApplicationController respond_to do |format| format.html do expires_in expiration_duration, public: true unless user_signed_in? - render template: 'home/index' end format.json do diff --git a/app/views/collections/show.html.haml b/app/views/collections/show.html.haml new file mode 100644 index 0000000000..176d422396 --- /dev/null +++ b/app/views/collections/show.html.haml @@ -0,0 +1,20 @@ +- content_for :page_title, @collection.name + +- content_for :header_tags do + %meta{ name: 'robots', content: 'noindex, noarchive' }/ + + %link{ rel: 'alternate', type: 'application/activity+json', href: ap_account_collection_url(@collection.account_id, @collection) }/ + %link{ rel: 'alternate', type: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', href: ap_account_collection_url(@collection.account_id, @collection) }/ + + - unless @collection.sensitive? + %meta{ name: 'description', content: @collection.description }/ + = opengraph 'og:description', @collection.description + = opengraph 'og:site_name', site_title + = opengraph 'og:type', 'website' + = opengraph 'og:title', @collection.name + = opengraph 'og:url', collection_url(@collection) + - if @collection.language.present? + = opengraph 'og:locale', @collection.language + = opengraph 'twitter:card', 'summary' + += render 'shared/web_app' diff --git a/spec/rails_helper.rb b/spec/rails_helper.rb index 55ad344b80..21b56c27cd 100644 --- a/spec/rails_helper.rb +++ b/spec/rails_helper.rb @@ -92,6 +92,7 @@ RSpec.configure do |config| config.include CommandLineHelpers, type: :cli config.include SystemHelpers, type: :system config.include Shoulda::Matchers::ActiveModel, type: :validator + config.include HtmlHeadInspection, type: :request # TODO: Remove when Devise fixes https://github.com/heartcombo/devise/issues/5705 config.before do diff --git a/spec/requests/account_show_page_spec.rb b/spec/requests/account_show_page_spec.rb index 7f3ea2595f..e45c97801d 100644 --- a/spec/requests/account_show_page_spec.rb +++ b/spec/requests/account_show_page_spec.rb @@ -16,18 +16,4 @@ RSpec.describe 'The account show page' do expect(head_meta_content('og:image')).to match '.+' expect(head_meta_content('og:url')).to eq short_account_url(username: alice.username) end - - def head_link_icons - response - .parsed_body - .search('html head link[rel=icon]') - end - - def head_meta_content(property) - response - .parsed_body - .search("html head meta[property='#{property}']") - .attr('content') - .text - end end diff --git a/spec/requests/collections_spec.rb b/spec/requests/collections_spec.rb index dbdf1c16a2..72622b5cab 100644 --- a/spec/requests/collections_spec.rb +++ b/spec/requests/collections_spec.rb @@ -6,12 +6,20 @@ RSpec.describe 'Collections' do describe 'GET /collections/:id' do subject { get collection_path(collection) } - let(:collection) { Fabricate(:collection) } + let(:collection) do + Fabricate(:collection, name: 'Frequent posters', description: 'Amazing people that can quickly fill your timeline', language: 'en') + end - it 'returns success' do + it 'returns success and includes opengraph meta tags' do subject expect(response).to have_http_status(200) + + expect(head_meta_content('og:title')).to eq 'Frequent posters' + expect(head_meta_content('og:description')).to eq 'Amazing people that can quickly fill your timeline' + expect(head_meta_content('og:type')).to eq 'website' + expect(head_meta_content('og:url')).to eq collection_url(collection) + expect(head_meta_content('og:locale')).to eq 'en' end end diff --git a/spec/requests/status_show_page_spec.rb b/spec/requests/status_show_page_spec.rb index ac542e841d..03f8f5a42e 100644 --- a/spec/requests/status_show_page_spec.rb +++ b/spec/requests/status_show_page_spec.rb @@ -41,26 +41,5 @@ RSpec.describe 'Statuses' do expect(head_meta_content('og:locale')).to eq 'ca' expect(head_meta_content('og:description')).to eq status_text end - - def head_link_icons - response - .parsed_body - .search('html head link[rel=icon]') - end - - def head_meta_content(property) - response - .parsed_body - .search("html head meta[property='#{property}']") - .attr('content') - .text - end - - def head_meta_exists(property) - !response - .parsed_body - .search("html head meta[property='#{property}']") - .empty? - end end end diff --git a/spec/support/html_head_inspection.rb b/spec/support/html_head_inspection.rb new file mode 100644 index 0000000000..677bffe907 --- /dev/null +++ b/spec/support/html_head_inspection.rb @@ -0,0 +1,24 @@ +# frozen_string_literal: true + +module HtmlHeadInspection + def head_link_icons + response + .parsed_body + .search('html head link[rel=icon]') + end + + def head_meta_content(property) + response + .parsed_body + .search("html head meta[property='#{property}']") + .attr('content') + .text + end + + def head_meta_exists(property) + !response + .parsed_body + .search("html head meta[property='#{property}']") + .empty? + end +end From c6e2e2255c54770076220d63ccd921ba23bb7987 Mon Sep 17 00:00:00 2001 From: Shlee Date: Wed, 10 Jun 2026 23:10:59 +0930 Subject: [PATCH 11/17] Fix missing boolean check in Admin Reports API (#39197) Co-authored-by: Claire --- app/models/report_filter.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/models/report_filter.rb b/app/models/report_filter.rb index dab45007bc..f12aff6f3d 100644 --- a/app/models/report_filter.rb +++ b/app/models/report_filter.rb @@ -39,8 +39,8 @@ class ReportFilter end def status_scope - resolved = params.key?(:resolved) - unresolved = params.key?(:unresolved) + resolved = ActiveModel::Type::Boolean.new.cast(params[:resolved]) + unresolved = ActiveModel::Type::Boolean.new.cast(params[:unresolved]) return Report.all if resolved && unresolved return Report.resolved if resolved From 2994e8543deec8fb3c4228afd7c556a37904662f Mon Sep 17 00:00:00 2001 From: diondiondion Date: Wed, 10 Jun 2026 16:50:49 +0200 Subject: [PATCH 12/17] [Accessibility] Navigation focus handling polish (#39358) --- app/javascript/mastodon/actions/compose.js | 7 +++++-- .../mastodon/components/account_header/name.tsx | 5 +++-- .../alt_text_modal/__tests__/index-test.tsx | 12 +++++++++--- .../mastodon/features/alt_text_modal/index.tsx | 8 ++++++-- .../mastodon/features/annual_report/index.tsx | 5 ++++- .../mastodon/features/collection_adder/index.tsx | 9 +++++++-- .../mastodon/features/custom_homepage/index.tsx | 5 ++++- .../features/filters/added_to_filter.jsx | 5 ++++- .../mastodon/features/filters/select_filter.jsx | 5 ++++- .../mastodon/features/list_adder/index.tsx | 5 +++-- .../mastodon/features/privacy_policy/index.tsx | 7 +++++-- .../mastodon/features/report/category.jsx | 7 ++++--- .../mastodon/features/report/comment.tsx | 9 +++++++-- .../mastodon/features/report/rules.jsx | 5 ++++- .../mastodon/features/report/statuses.jsx | 5 ++++- .../mastodon/features/report/thanks.jsx | 5 ++++- .../mastodon/features/terms_of_service/index.tsx | 7 +++++-- .../features/ui/components/block_modal.jsx | 5 ++++- .../features/ui/components/boost_modal.tsx | 5 +++-- .../features/ui/components/dialog_modal.tsx | 5 ++++- .../ui/components/domain_block_modal.tsx | 5 +++-- .../features/ui/components/embed_modal.tsx | 5 +++-- .../ui/components/ignore_notifications_modal.jsx | 3 ++- .../features/ui/components/mute_modal.jsx | 5 ++++- .../ui/components/report_collection_modal.tsx | 5 +++-- .../features/ui/components/visibility_modal.tsx | 16 +++++++++------- app/javascript/styles/mastodon/components.scss | 2 ++ 27 files changed, 119 insertions(+), 48 deletions(-) diff --git a/app/javascript/mastodon/actions/compose.js b/app/javascript/mastodon/actions/compose.js index b4be55cbfe..abc86a3394 100644 --- a/app/javascript/mastodon/actions/compose.js +++ b/app/javascript/mastodon/actions/compose.js @@ -93,7 +93,7 @@ const messages = defineMessages({ export const ensureComposeIsVisible = (getState) => { if (!getState().getIn(['compose', 'mounted'])) { - browserHistory.push('/publish'); + browserHistory.push('/publish', { focusTarget: false }); } }; @@ -292,7 +292,10 @@ export function submitCompose(successCallback) { message: statusId === null ? messages.published : messages.saved, action: messages.open, dismissAfter: 10000, - onClick: () => browserHistory.push(`/@${response.data.account.username}/${response.data.id}`), + onClick: () => browserHistory.push( + `/@${response.data.account.username}/${response.data.id}`, + { focusTarget: 'detailed-status' } + ), })); }).catch(function (error) { dispatch(submitComposeFail(error)); diff --git a/app/javascript/mastodon/components/account_header/name.tsx b/app/javascript/mastodon/components/account_header/name.tsx index b46e849765..5bd2ed80db 100644 --- a/app/javascript/mastodon/components/account_header/name.tsx +++ b/app/javascript/mastodon/components/account_header/name.tsx @@ -19,6 +19,7 @@ import { FollowsYouBadge } from '../badge'; import { CopyButton } from '../copy_button'; import { DisplayName } from '../display_name'; import { Icon } from '../icon'; +import { NavigationFocusTarget } from '../navigation_focus_target'; import { AccountBadges } from './badges'; import classes from './styles.module.scss'; @@ -56,9 +57,9 @@ export const AccountName: FC<{ accountId: string }> = ({ accountId }) => { return (
-

+ -

+ {relationship?.followed_by && }
diff --git a/app/javascript/mastodon/features/alt_text_modal/__tests__/index-test.tsx b/app/javascript/mastodon/features/alt_text_modal/__tests__/index-test.tsx index 709e0ff6c2..bf1189d44a 100644 --- a/app/javascript/mastodon/features/alt_text_modal/__tests__/index-test.tsx +++ b/app/javascript/mastodon/features/alt_text_modal/__tests__/index-test.tsx @@ -7,6 +7,8 @@ import { List, Map } from 'immutable'; import { render } from '@testing-library/react'; import { vi } from 'vitest'; +import { FocusTargetProvider } from '@/mastodon/components/navigation_focus_target'; +import { Router } from '@/mastodon/components/router'; import type { RootState } from 'mastodon/store'; import { useAppSelector } from 'mastodon/store'; @@ -27,9 +29,13 @@ describe('', () => { const renderComponent = () => { return render( - - - , + + + + + + + , ); }; diff --git a/app/javascript/mastodon/features/alt_text_modal/index.tsx b/app/javascript/mastodon/features/alt_text_modal/index.tsx index fafd460947..3be514e1a4 100644 --- a/app/javascript/mastodon/features/alt_text_modal/index.tsx +++ b/app/javascript/mastodon/features/alt_text_modal/index.tsx @@ -22,6 +22,7 @@ import { changeUploadCompose } from 'mastodon/actions/compose_typed'; import { Button } from 'mastodon/components/button'; import { GIFV } from 'mastodon/components/gifv'; import { LoadingIndicator } from 'mastodon/components/loading_indicator'; +import { NavigationFocusTarget } from 'mastodon/components/navigation_focus_target'; import { Skeleton } from 'mastodon/components/skeleton'; import { Audio } from 'mastodon/features/audio'; import { CharacterCounter } from 'mastodon/features/compose/components/character_counter'; @@ -412,12 +413,15 @@ export const AltTextModal = forwardRef>( )} - + - +