diff --git a/Gemfile.lock b/Gemfile.lock
index 7f9f82cb39..ee036b208b 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -294,7 +294,7 @@ GEM
activesupport (>= 5.1)
haml (>= 4.0.6)
railties (>= 5.1)
- haml_lint (0.71.0)
+ haml_lint (0.72.0)
haml (>= 5.0)
parallel (~> 1.10)
rainbow
@@ -628,7 +628,7 @@ GEM
psych (5.3.1)
date
stringio
- public_suffix (7.0.2)
+ public_suffix (7.0.5)
puma (7.2.0)
nio4r (~> 2.0)
pundit (2.5.2)
@@ -892,7 +892,7 @@ GEM
unf (~> 0.1.0)
tzinfo (2.0.6)
concurrent-ruby (~> 1.0)
- tzinfo-data (1.2025.3)
+ tzinfo-data (1.2026.1)
tzinfo (>= 1.0.0)
unf (0.1.4)
unf_ext
diff --git a/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss b/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss
index 2ef62a7d25..b39892beec 100644
--- a/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss
+++ b/app/javascript/mastodon/features/account_timeline/v2/styles.module.scss
@@ -5,6 +5,7 @@
.filterSelectButton {
appearance: none;
border: none;
+ color: inherit;
background: none;
padding: 8px 0;
font-size: 15px;
diff --git a/app/javascript/mastodon/features/collections/detail/index.tsx b/app/javascript/mastodon/features/collections/detail/index.tsx
index d2317e716f..0fc13c8f79 100644
--- a/app/javascript/mastodon/features/collections/detail/index.tsx
+++ b/app/javascript/mastodon/features/collections/detail/index.tsx
@@ -105,8 +105,8 @@ const CollectionHeader: React.FC<{ collection: ApiCollectionJSON }> = ({
);
}, [collection, dispatch]);
- const location = useLocation<{ newCollection?: boolean }>();
- const wasJustCreated = location.state.newCollection;
+ const location = useLocation<{ newCollection?: boolean } | undefined>();
+ const wasJustCreated = location.state?.newCollection;
useEffect(() => {
if (wasJustCreated) {
handleShare();
diff --git a/app/javascript/mastodon/features/collections/detail/share_modal.tsx b/app/javascript/mastodon/features/collections/detail/share_modal.tsx
index 137794d95b..0f4681d077 100644
--- a/app/javascript/mastodon/features/collections/detail/share_modal.tsx
+++ b/app/javascript/mastodon/features/collections/detail/share_modal.tsx
@@ -40,8 +40,8 @@ export const CollectionShareModal: React.FC<{
}> = ({ collection, onClose }) => {
const intl = useIntl();
const dispatch = useAppDispatch();
- const location = useLocation<{ newCollection?: boolean }>();
- const isNew = !!location.state.newCollection;
+ const location = useLocation<{ newCollection?: boolean } | undefined>();
+ const isNew = !!location.state?.newCollection;
const isOwnCollection = collection.account_id === me;
const collectionLink = `${window.location.origin}/collections/${collection.id}`;
diff --git a/app/javascript/mastodon/locales/da.json b/app/javascript/mastodon/locales/da.json
index 34215c3d6d..d313e0001e 100644
--- a/app/javascript/mastodon/locales/da.json
+++ b/app/javascript/mastodon/locales/da.json
@@ -536,7 +536,7 @@
"empty_column.follow_requests": "Du har endnu ingen følgeanmodninger. Når du modtager én, vil den dukke op her.",
"empty_column.followed_tags": "Ingen hashtags følges endnu. Når det sker, vil de fremgå her.",
"empty_column.hashtag": "Der er intet med dette hashtag endnu.",
- "empty_column.home": "Din hjem-tidslinje er tom! Følg nogle personer, for at fylde den op.",
+ "empty_column.home": "Din hjem-tidslinje er tom! Følg flere personer, for at fylde den op.",
"empty_column.list": "Der er ikke noget på denne liste endnu. Når medlemmer af denne liste udgiver nye indlæg, vil de blive vist her.",
"empty_column.mutes": "Du har endnu ikke skjult nogle brugere.",
"empty_column.notification_requests": "Alt er klar! Der er intet her. Når der modtages nye notifikationer, fremgår de her jævnfør dine indstillinger.",
@@ -683,7 +683,7 @@
"keyboard_shortcuts.direct": "Åbn kolonne med private omtaler",
"keyboard_shortcuts.down": "Flyt nedad på listen",
"keyboard_shortcuts.enter": "Åbn indlæg",
- "keyboard_shortcuts.explore": "Åbn Trender-tidslinjen",
+ "keyboard_shortcuts.explore": "Åbn trender-tidslinjen",
"keyboard_shortcuts.favourite": "Føj indlæg til favoritter",
"keyboard_shortcuts.favourites": "Åbn favoritlisten",
"keyboard_shortcuts.federated": "Åbn fødereret tidslinje",
diff --git a/app/javascript/mastodon/locales/fi.json b/app/javascript/mastodon/locales/fi.json
index 544344cfa6..55bf3daee7 100644
--- a/app/javascript/mastodon/locales/fi.json
+++ b/app/javascript/mastodon/locales/fi.json
@@ -1074,7 +1074,7 @@
"sign_in_banner.sign_in": "Kirjaudu",
"sign_in_banner.sso_redirect": "Kirjaudu tai rekisteröidy",
"skip_links.hotkey": "Pikanäppäin {hotkey}",
- "skip_links.skip_to_content": "Siitty pääsisältöön",
+ "skip_links.skip_to_content": "Siirry pääsisältöön",
"skip_links.skip_to_navigation": "Siirry päänavigaatioon",
"status.admin_account": "Avaa tilin @{name} moderointinäkymä",
"status.admin_domain": "Avaa palvelimen {domain} moderointinäkymä",
diff --git a/app/javascript/mastodon/locales/fo.json b/app/javascript/mastodon/locales/fo.json
index 10f727109e..e6810d4e97 100644
--- a/app/javascript/mastodon/locales/fo.json
+++ b/app/javascript/mastodon/locales/fo.json
@@ -173,6 +173,7 @@
"account_edit.profile_tab.subtitle": "Tillaga spjøldrini á vanganum hjá tær og tað, tey vísa.",
"account_edit.profile_tab.title": "Stillingar fyri spjøldur á vanga",
"account_edit.save": "Goym",
+ "account_edit_tags.add_tag": "Legg #{tagName} afturat",
"account_edit_tags.column_title": "Rætta sermerkt frámerki",
"account_edit_tags.help_text": "Sermerkt frámerki hjálpa brúkarum at varnast og virka saman við vanga tínum. Tey síggjast sum filtur á virksemisvísingini av vanga tínum.",
"account_edit_tags.search_placeholder": "Áset eitt frámerki…",
@@ -682,6 +683,7 @@
"keyboard_shortcuts.direct": "Lat teigin við privatum umrøðum upp",
"keyboard_shortcuts.down": "Flyt niðureftir listanum",
"keyboard_shortcuts.enter": "Opna uppslag",
+ "keyboard_shortcuts.explore": "Lat rás við vælumtóktum postum upp",
"keyboard_shortcuts.favourite": "Dáma post",
"keyboard_shortcuts.favourites": "Lat listan av dámdum postum upp",
"keyboard_shortcuts.federated": "Lat felags tíðslinju upp",
@@ -1071,6 +1073,9 @@
"sign_in_banner.mastodon_is": "Mastodon er best mátin at fylgja við í tí, sum hendir.",
"sign_in_banner.sign_in": "Rita inn",
"sign_in_banner.sso_redirect": "Rita inn ella Skráset teg",
+ "skip_links.hotkey": "Snarknappur {hotkey}",
+ "skip_links.skip_to_content": "Far til høvuðsinnihald",
+ "skip_links.skip_to_navigation": "Far til høvuðs-navigatión",
"status.admin_account": "Lat kjakleiðaramarkamót upp fyri @{name}",
"status.admin_domain": "Lat umsjónarmarkamót upp fyri {domain}",
"status.admin_status": "Lat hendan postin upp í kjakleiðaramarkamótinum",
diff --git a/app/javascript/mastodon/locales/fr-CA.json b/app/javascript/mastodon/locales/fr-CA.json
index 5e753c0e5c..85970ed2fe 100644
--- a/app/javascript/mastodon/locales/fr-CA.json
+++ b/app/javascript/mastodon/locales/fr-CA.json
@@ -173,6 +173,7 @@
"account_edit.profile_tab.subtitle": "Personnaliser les onglets de votre profil et leur contenu.",
"account_edit.profile_tab.title": "Paramètres de l'onglet du profil",
"account_edit.save": "Enregistrer",
+ "account_edit_tags.add_tag": "Ajouter #{tagName}",
"account_edit_tags.column_title": "Modifier les hashtags mis en avant",
"account_edit_tags.help_text": "Les hashtags mis en avant aident les personnes à découvrir et interagir avec votre profil. Ils apparaissent comme des filtres dans la vue « Activité » de votre profil.",
"account_edit_tags.search_placeholder": "Saisir un hashtag…",
@@ -1073,6 +1074,8 @@
"sign_in_banner.sign_in": "Se connecter",
"sign_in_banner.sso_redirect": "Se connecter ou s’inscrire",
"skip_links.hotkey": "Raccourci {hotkey}",
+ "skip_links.skip_to_content": "Accéder au contenu principal",
+ "skip_links.skip_to_navigation": "Accéder à la navigation principale",
"status.admin_account": "Ouvrir l’interface de modération pour @{name}",
"status.admin_domain": "Ouvrir l’interface de modération pour {domain}",
"status.admin_status": "Ouvrir ce message dans l’interface de modération",
diff --git a/app/javascript/mastodon/locales/fr.json b/app/javascript/mastodon/locales/fr.json
index d52c9c6f34..5f32e9361c 100644
--- a/app/javascript/mastodon/locales/fr.json
+++ b/app/javascript/mastodon/locales/fr.json
@@ -173,6 +173,7 @@
"account_edit.profile_tab.subtitle": "Personnaliser les onglets de votre profil et leur contenu.",
"account_edit.profile_tab.title": "Paramètres de l'onglet du profil",
"account_edit.save": "Enregistrer",
+ "account_edit_tags.add_tag": "Ajouter #{tagName}",
"account_edit_tags.column_title": "Modifier les hashtags mis en avant",
"account_edit_tags.help_text": "Les hashtags mis en avant aident les personnes à découvrir et interagir avec votre profil. Ils apparaissent comme des filtres dans la vue « Activité » de votre profil.",
"account_edit_tags.search_placeholder": "Saisir un hashtag…",
@@ -1073,6 +1074,8 @@
"sign_in_banner.sign_in": "Se connecter",
"sign_in_banner.sso_redirect": "Se connecter ou s’inscrire",
"skip_links.hotkey": "Raccourci {hotkey}",
+ "skip_links.skip_to_content": "Accéder au contenu principal",
+ "skip_links.skip_to_navigation": "Accéder à la navigation principale",
"status.admin_account": "Ouvrir l’interface de modération pour @{name}",
"status.admin_domain": "Ouvrir l’interface de modération pour {domain}",
"status.admin_status": "Ouvrir ce message dans l’interface de modération",
diff --git a/app/javascript/mastodon/locales/hu.json b/app/javascript/mastodon/locales/hu.json
index 6e2b30eb51..50efbb4655 100644
--- a/app/javascript/mastodon/locales/hu.json
+++ b/app/javascript/mastodon/locales/hu.json
@@ -44,9 +44,11 @@
"account.familiar_followers_two": "{name1} és {name2} követi",
"account.featured": "Kiemelt",
"account.featured.accounts": "Profilok",
+ "account.featured.collections": "Gyűjtemények",
"account.featured.hashtags": "Hashtagek",
"account.featured_tags.last_status_at": "Legutolsó bejegyzés ideje: {date}",
"account.featured_tags.last_status_never": "Nincs bejegyzés",
+ "account.field_overflow": "Teljes tartalom megjelenítése",
"account.filters.all": "Összes tevékenység",
"account.filters.boosts_toggle": "Megtolások megjelenítése",
"account.filters.posts_boosts": "Bejegyzések és megtolások",
@@ -144,17 +146,30 @@
"account_edit.bio.title": "Bemutatkozás",
"account_edit.bio_modal.add_title": "Bemutatkozás hozzáadása",
"account_edit.bio_modal.edit_title": "Bemutatkozás szerkesztése",
+ "account_edit.button.add": "{item} hozzáadása",
+ "account_edit.button.delete": "{item} törlése",
+ "account_edit.button.edit": "{item} szerkesztése",
"account_edit.char_counter": "{currentLength}/{maxLength} karakter",
"account_edit.column_button": "Kész",
"account_edit.column_title": "Profil szerkesztése",
"account_edit.custom_fields.title": "Egyéni mezők",
"account_edit.display_name.placeholder": "A megjelenítendő név az, ahogy a neved megjelenik a profilodon és az idővonalakon.",
"account_edit.display_name.title": "Megjelenítendő név",
+ "account_edit.featured_hashtags.item": "hashtagek",
+ "account_edit.featured_hashtags.placeholder": "Segíts másoknak, hogy azonosíthassák a kedvenc témáid, és gyorsan elérjék azokat.",
"account_edit.featured_hashtags.title": "Kiemelt hashtagek",
"account_edit.name_modal.add_title": "Megjelenítendő név hozzáadása",
"account_edit.name_modal.edit_title": "Megjelenítendő név szerkesztése",
+ "account_edit.profile_tab.button_label": "Testreszabás",
+ "account_edit.profile_tab.hint.description": "Ezek a beállítások szabják testre, hogy a felhasználók mit látnak a(z) {server} kiszolgálón a hivatalos alkalmazásokban, de nem biztos, hogy a külső kiszolgálókon vagy alkalmazásokban is érvényesek lesznek.",
+ "account_edit.profile_tab.hint.title": "A megjelenítés eltérő lehet",
+ "account_edit.profile_tab.show_featured.description": "A „Kiemelt” egy nem kötelező lap, ahol más fiókokat mutathatsz be.",
+ "account_edit.profile_tab.show_featured.title": "„Kiemelt” lap megjelenítése",
+ "account_edit.profile_tab.show_media.description": "A „Média” egy nem kötelező lap, amely a képeket vagy videókat tartalmazó bejegyzéseidet jeleníti meg.",
+ "account_edit.profile_tab.show_media.title": "„Média” lap megjelenítése",
"account_edit.profile_tab.title": "Profil lap beállításai",
"account_edit.save": "Mentés",
+ "account_edit_tags.add_tag": "#{tagName} hozzáadása",
"account_note.placeholder": "Kattintás jegyzet hozzáadásához",
"admin.dashboard.daily_retention": "Napi regisztráció utáni felhasználómegtartási arány",
"admin.dashboard.monthly_retention": "Havi regisztráció utáni felhasználómegtartási arány",
diff --git a/app/javascript/mastodon/locales/ko.json b/app/javascript/mastodon/locales/ko.json
index b08775be97..aece536ee4 100644
--- a/app/javascript/mastodon/locales/ko.json
+++ b/app/javascript/mastodon/locales/ko.json
@@ -47,8 +47,12 @@
"account.featured.hashtags": "해시태그",
"account.featured_tags.last_status_at": "{date}에 마지막으로 게시",
"account.featured_tags.last_status_never": "게시물 없음",
+ "account.field_overflow": "내용 전체 보기",
"account.filters.all": "모든 활동",
"account.filters.boosts_toggle": "부스트 보기",
+ "account.filters.posts_boosts": "게시물과 부스트",
+ "account.filters.posts_only": "게시물",
+ "account.filters.posts_replies": "게시물과 답장",
"account.filters.replies_toggle": "답글 보기",
"account.follow": "팔로우",
"account.follow_back": "맞팔로우",
@@ -68,14 +72,23 @@
"account.go_to_profile": "프로필로 이동",
"account.hide_reblogs": "@{name}의 부스트를 숨기기",
"account.in_memoriam": "고인의 계정입니다.",
+ "account.joined_long": "{date}에 가입함",
"account.joined_short": "가입",
"account.languages": "구독한 언어 변경",
"account.link_verified_on": "{date}에 이 링크의 소유권이 확인 됨",
"account.locked_info": "이 계정의 프라이버시 설정은 잠금으로 설정되어 있습니다. 계정 소유자가 수동으로 팔로워를 승인합니다.",
"account.media": "미디어",
"account.mention": "@{name} 님에게 멘션",
+ "account.menu.add_to_list": "리스트에 추가…",
+ "account.menu.block": "계정 차단",
+ "account.menu.block_domain": "{domain} 차단",
+ "account.menu.copied": "계정 링크를 복사했습니다",
"account.menu.copy": "링크 복사하기",
"account.menu.mention": "멘션",
+ "account.menu.note.description": "나에게만 보입니다",
+ "account.menu.open_original_page": "{domain}에서 보기",
+ "account.menu.remove_follower": "팔로워 제거",
+ "account.menu.report": "계정 신고",
"account.menu.share": "공유하기…",
"account.moved_to": "{name} 님은 자신의 새 계정이 다음과 같다고 표시했습니다:",
"account.mute": "@{name} 뮤트",
@@ -85,6 +98,9 @@
"account.muting": "뮤트함",
"account.mutual": "서로 팔로우",
"account.no_bio": "제공된 설명이 없습니다.",
+ "account.node_modal.save": "저장",
+ "account.node_modal.title": "개인 메모 추가",
+ "account.note.edit_button": "편집",
"account.open_original_page": "원본 페이지 열기",
"account.posts": "게시물",
"account.posts_with_replies": "게시물과 답장",
@@ -104,6 +120,15 @@
"account.unmute": "@{name} 뮤트 해제",
"account.unmute_notifications_short": "알림 뮤트 해제",
"account.unmute_short": "뮤트 해제",
+ "account_edit.bio.title": "자기소개",
+ "account_edit.bio_modal.add_title": "자기소개 추가",
+ "account_edit.bio_modal.edit_title": "자기소개 편집",
+ "account_edit.button.add": "{item} 추가",
+ "account_edit.button.delete": "{item} 제거",
+ "account_edit.button.edit": "{item} 편집",
+ "account_edit.char_counter": "{currentLength}/{maxLength} 글자",
+ "account_edit.column_button": "완료",
+ "account_edit.column_title": "프로필 편집",
"account_note.placeholder": "클릭하여 노트 추가",
"admin.dashboard.daily_retention": "가입 후 일별 사용자 유지율",
"admin.dashboard.monthly_retention": "가입 후 월별 사용자 유지율",
diff --git a/app/javascript/mastodon/locales/pt-PT.json b/app/javascript/mastodon/locales/pt-PT.json
index 56f3149fdb..72cd874251 100644
--- a/app/javascript/mastodon/locales/pt-PT.json
+++ b/app/javascript/mastodon/locales/pt-PT.json
@@ -161,7 +161,11 @@
"account_edit.featured_hashtags.title": "Etiquetas em destaque",
"account_edit.name_modal.add_title": "Adicionar nome a mostrar",
"account_edit.name_modal.edit_title": "Editar o nome a mostrar",
+ "account_edit.profile_tab.button_label": "Personalizar",
+ "account_edit.profile_tab.hint.description": "Estas configurações personalizam o que os utilizadores veem no {server} nas aplicações oficiais, mas podem não se aplicar aos utilizadores de outros servidores nem aplicações de terceiros.",
+ "account_edit.profile_tab.hint.title": "A apresentação ainda pode variar",
"account_edit.save": "Guardar",
+ "account_edit_tags.add_tag": "Adicionar #{tagName}",
"account_edit_tags.column_title": "Editar etiquetas em destaque",
"account_edit_tags.help_text": "As etiquetas destacadas ajudam os utilizadores a descobrir e interagir com o seu perfil. Aparecem como filtros na vista de atividade da sua página de perfil.",
"account_edit_tags.search_placeholder": "Insira uma etiqueta…",
@@ -269,6 +273,12 @@
"closed_registrations_modal.find_another_server": "Procurar outro servidor",
"closed_registrations_modal.preamble": "O Mastodon é descentralizado, por isso não importa onde a tua conta é criada, pois continuarás a poder acompanhar e interagir com qualquer um neste servidor. Podes até alojar o teu próprio servidor!",
"closed_registrations_modal.title": "Criar uma conta no Mastodon",
+ "collection.share_modal.share_via_post": "Publicar no Mastodon",
+ "collection.share_modal.share_via_system": "Compartilhar com…",
+ "collection.share_modal.title": "Partilhar coleção",
+ "collection.share_modal.title_new": "Partilhe a sua nova coleção!",
+ "collection.share_template_other": "Veja esta coleção interessante: {link}",
+ "collection.share_template_own": "Veja a minha nova coleção: {link}",
"collections.account_count": "{count, plural, one {# conta} other {# contas}}",
"collections.accounts.empty_description": "Adicione até {count} contas que segue",
"collections.accounts.empty_title": "Esta coleção está vazia",
@@ -287,6 +297,8 @@
"collections.delete_collection": "Eliminar coleção",
"collections.description_length_hint": "Limite de 100 caracteres",
"collections.detail.accounts_heading": "Contas",
+ "collections.detail.curated_by_author": "Curado por {author}",
+ "collections.detail.curated_by_you": "Curado por si",
"collections.detail.loading": "A carregar a coleção…",
"collections.detail.share": "Partilhar esta coleção",
"collections.edit_details": "Editar detalhes",
diff --git a/app/javascript/mastodon/locales/tr.json b/app/javascript/mastodon/locales/tr.json
index 2ad5a23d50..55cc5daef6 100644
--- a/app/javascript/mastodon/locales/tr.json
+++ b/app/javascript/mastodon/locales/tr.json
@@ -173,6 +173,7 @@
"account_edit.profile_tab.subtitle": "Profilinizdeki sekmeleri ve bunların görüntülediği bilgileri özelleştirin.",
"account_edit.profile_tab.title": "Profil sekme ayarları",
"account_edit.save": "Kaydet",
+ "account_edit_tags.add_tag": "#{tagName} ekle",
"account_edit_tags.column_title": "Öne çıkarılmış etiketleri düzenle",
"account_edit_tags.help_text": "Öne çıkan etiketler kullanıcıların profilinizi keşfetmesine ve etkileşim kurmasına yardımcı olur. Profil sayfanızın Etkinlik görünümünde filtreler olarak görünürler.",
"account_edit_tags.search_placeholder": "Bir etiket girin…",
@@ -682,6 +683,7 @@
"keyboard_shortcuts.direct": "Özel bahsetmeler sütununu aç",
"keyboard_shortcuts.down": "Listede aşağıya inmek için",
"keyboard_shortcuts.enter": "Gönderiyi açınız",
+ "keyboard_shortcuts.explore": "Öne çıkanlar zaman çizelgesini aç",
"keyboard_shortcuts.favourite": "Gönderiyi favorilerine ekle",
"keyboard_shortcuts.favourites": "Gözde listeni aç",
"keyboard_shortcuts.federated": "Federe akışı aç",
@@ -1071,6 +1073,9 @@
"sign_in_banner.mastodon_is": "Neler olup bittiğini izlemenin en iyi aracı Mastodon'dur.",
"sign_in_banner.sign_in": "Giriş yap",
"sign_in_banner.sso_redirect": "Giriş yap veya kaydol",
+ "skip_links.hotkey": "Kısayol tuşu {hotkey}",
+ "skip_links.skip_to_content": "Ana içeriğe git",
+ "skip_links.skip_to_navigation": "Ana gezinmeye git",
"status.admin_account": "@{name} için denetim arayüzünü açın",
"status.admin_domain": "{domain} için denetim arayüzünü açın",
"status.admin_status": "Denetim arayüzünde bu gönderiyi açın",
diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json
index be8a700c03..047bc0df9a 100644
--- a/app/javascript/mastodon/locales/zh-CN.json
+++ b/app/javascript/mastodon/locales/zh-CN.json
@@ -173,6 +173,7 @@
"account_edit.profile_tab.subtitle": "自定义你个人资料的标签页及其显示的内容。",
"account_edit.profile_tab.title": "个人资料标签页设置",
"account_edit.save": "保存",
+ "account_edit_tags.add_tag": "添加 #{tagName}",
"account_edit_tags.column_title": "编辑精选话题标签",
"account_edit_tags.help_text": "精选话题标签可以帮助他人发现并与你的个人资料互动。这些标签会作为过滤器条件出现在你个人资料页面的活动视图中。",
"account_edit_tags.search_placeholder": "输入话题标签…",
@@ -682,6 +683,7 @@
"keyboard_shortcuts.direct": "打开私下提及栏",
"keyboard_shortcuts.down": "在列表中让光标下移",
"keyboard_shortcuts.enter": "展开嘟文",
+ "keyboard_shortcuts.explore": "打开当前热门时间线",
"keyboard_shortcuts.favourite": "喜欢嘟文",
"keyboard_shortcuts.favourites": "打开喜欢列表",
"keyboard_shortcuts.federated": "打开跨站时间线",
@@ -1071,6 +1073,9 @@
"sign_in_banner.mastodon_is": "Mastodon 是了解最新动态的最佳途径。",
"sign_in_banner.sign_in": "登录",
"sign_in_banner.sso_redirect": "登录或注册",
+ "skip_links.hotkey": "快捷键 {hotkey}",
+ "skip_links.skip_to_content": "跳转到主内容",
+ "skip_links.skip_to_navigation": "跳转到主导航",
"status.admin_account": "打开 @{name} 的管理界面",
"status.admin_domain": "打开 {domain} 的管理界面",
"status.admin_status": "在管理界面查看此嘟文",
diff --git a/app/lib/status_cache_hydrator.rb b/app/lib/status_cache_hydrator.rb
index b830e509bf..1f1184d42f 100644
--- a/app/lib/status_cache_hydrator.rb
+++ b/app/lib/status_cache_hydrator.rb
@@ -75,6 +75,8 @@ class StatusCacheHydrator
end
end
+ payload[:card][:missing_attribution] = status.preview_card.unverified_author_account_id == account_id if payload[:card]
+
# Nested statuses are more likely to have a stale cache
fill_status_stats(payload, status) if nested
end
diff --git a/app/models/preview_card.rb b/app/models/preview_card.rb
index 644be2671a..4c8b52a8d5 100644
--- a/app/models/preview_card.rb
+++ b/app/models/preview_card.rb
@@ -33,6 +33,7 @@
# published_at :datetime
# image_description :string default(""), not null
# author_account_id :bigint(8)
+# unverified_author_account_id :bigint(8)
#
class PreviewCard < ApplicationRecord
@@ -61,6 +62,7 @@ class PreviewCard < ApplicationRecord
has_one :trend, class_name: 'PreviewCardTrend', inverse_of: :preview_card, dependent: :destroy
belongs_to :author_account, class_name: 'Account', optional: true
+ belongs_to :unverified_author_account, class_name: 'Account', optional: true
has_attached_file :image,
processors: [:lazy_thumbnail, :blurhash_transcoder],
diff --git a/app/serializers/rest/preview_card_serializer.rb b/app/serializers/rest/preview_card_serializer.rb
index f73a051ac0..3517be619b 100644
--- a/app/serializers/rest/preview_card_serializer.rb
+++ b/app/serializers/rest/preview_card_serializer.rb
@@ -15,6 +15,8 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
has_many :authors, serializer: AuthorSerializer
+ attribute :missing_attribution, if: :current_user?
+
def url
object.original_url.presence || object.url
end
@@ -26,4 +28,12 @@ class REST::PreviewCardSerializer < ActiveModel::Serializer
def html
Sanitize.fragment(object.html, Sanitize::Config::MASTODON_OEMBED)
end
+
+ def missing_attribution
+ object.unverified_author_account_id.present? && object.unverified_author_account_id == current_user.account_id
+ end
+
+ def current_user?
+ !current_user.nil?
+ end
end
diff --git a/app/services/fetch_link_card_service.rb b/app/services/fetch_link_card_service.rb
index 84c4ba06f1..53b6861349 100644
--- a/app/services/fetch_link_card_service.rb
+++ b/app/services/fetch_link_card_service.rb
@@ -159,7 +159,14 @@ class FetchLinkCardService < BaseService
@card = PreviewCard.find_or_initialize_by(url: link_details_extractor.canonical_url) if link_details_extractor.canonical_url != @card.url
@card.assign_attributes(link_details_extractor.to_preview_card_attributes)
- @card.author_account = linked_account if linked_account&.can_be_attributed_from?(domain) || provider&.trendable?
+
+ if linked_account.present?
+ # There is an overlap in the two conditions when `provider` is trendable. This is on purpose to give users
+ # a heads-up before we remove the `provider&.trendable?` condition.
+ @card.author_account = linked_account if linked_account.can_be_attributed_from?(domain) || provider&.trendable?
+ @card.unverified_author_account = linked_account if linked_account.local? && !linked_account.can_be_attributed_from?(domain)
+ end
+
@card.save_with_optional_image! unless @card.title.blank? && @card.html.blank?
end
end
diff --git a/app/services/unfollow_service.rb b/app/services/unfollow_service.rb
index 1ea8af6992..385aa0c7b1 100644
--- a/app/services/unfollow_service.rb
+++ b/app/services/unfollow_service.rb
@@ -6,16 +6,16 @@ class UnfollowService < BaseService
include Lockable
# Unfollow and notify the remote user
- # @param [Account] source_account Where to unfollow from
- # @param [Account] target_account Which to unfollow
+ # @param [Account] follower Where to unfollow from
+ # @param [Account] followee Which to unfollow
# @param [Hash] options
# @option [Boolean] :skip_unmerge
- def call(source_account, target_account, options = {})
- @source_account = source_account
- @target_account = target_account
- @options = options
+ def call(follower, followee, options = {})
+ @follower = follower
+ @followee = followee
+ @options = options
- with_redis_lock("relationship:#{[source_account.id, target_account.id].sort.join(':')}") do
+ with_redis_lock("relationship:#{[follower.id, followee.id].sort.join(':')}") do
unfollow! || undo_follow_request!
end
end
@@ -23,19 +23,25 @@ class UnfollowService < BaseService
private
def unfollow!
- follow = Follow.find_by(account: @source_account, target_account: @target_account)
-
+ follow = Follow.find_by(account: @follower, target_account: @followee)
return unless follow
+ # List members are removed immediately with the follow relationship removal,
+ # so we need to fetch the list IDs first
+ list_ids = @follower.owned_lists.with_list_account(@followee).pluck(:list_id) unless @options[:skip_unmerge]
+
follow.destroy!
- create_notification(follow) if !@target_account.local? && @target_account.activitypub?
- create_reject_notification(follow) if @target_account.local? && !@source_account.local? && @source_account.activitypub?
+ if @followee.local? && @follower.remote? && @follower.activitypub?
+ send_reject_follow(follow)
+ elsif @followee.remote? && @followee.activitypub?
+ send_undo_follow(follow)
+ end
unless @options[:skip_unmerge]
- UnmergeWorker.perform_async(@target_account.id, @source_account.id, 'home')
- UnmergeWorker.push_bulk(@source_account.owned_lists.with_list_account(@target_account).pluck(:list_id)) do |list_id|
- [@target_account.id, list_id, 'list']
+ UnmergeWorker.perform_async(@followee.id, @follower.id, 'home')
+ UnmergeWorker.push_bulk(list_ids) do |list_id|
+ [@followee.id, list_id, 'list']
end
end
@@ -43,22 +49,21 @@ class UnfollowService < BaseService
end
def undo_follow_request!
- follow_request = FollowRequest.find_by(account: @source_account, target_account: @target_account)
-
+ follow_request = FollowRequest.find_by(account: @follower, target_account: @followee)
return unless follow_request
follow_request.destroy!
- create_notification(follow_request) unless @target_account.local?
+ send_undo_follow(follow_request) unless @followee.local?
follow_request
end
- def create_notification(follow)
+ def send_undo_follow(follow)
ActivityPub::DeliveryWorker.perform_async(build_json(follow), follow.account_id, follow.target_account.inbox_url)
end
- def create_reject_notification(follow)
+ def send_reject_follow(follow)
ActivityPub::DeliveryWorker.perform_async(build_reject_json(follow), follow.target_account_id, follow.account.inbox_url)
end
diff --git a/app/services/update_account_service.rb b/app/services/update_account_service.rb
index 78a846e03e..fed9d53009 100644
--- a/app/services/update_account_service.rb
+++ b/app/services/update_account_service.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class UpdateAccountService < BaseService
+ PREVIEW_CARD_REATTRIBUTION_LIMIT = 1_000
+
def call(account, params, raise_error: false)
was_locked = account.locked
update_method = raise_error ? :update! : :update
@@ -11,6 +13,7 @@ class UpdateAccountService < BaseService
authorize_all_follow_requests(account) if was_locked && !account.locked
check_links(account)
process_hashtags(account)
+ process_attribution_domains(account)
end
rescue Mastodon::DimensionsValidationError, Mastodon::StreamValidationError => e
account.errors.add(:avatar, e.message)
@@ -36,4 +39,22 @@ class UpdateAccountService < BaseService
def process_hashtags(account)
account.tags_as_strings = Extractor.extract_hashtags(account.note)
end
+
+ def process_attribution_domains(account)
+ return unless account.attribute_previously_changed?(:attribution_domains)
+
+ # Go through the most recent cards, and do the rest in a background job
+ preview_cards = PreviewCard.where(unverified_author_account: account).reorder(id: :desc).limit(PREVIEW_CARD_REATTRIBUTION_LIMIT).to_a
+ should_queue_worker = preview_cards.size == PREVIEW_CARD_REATTRIBUTION_LIMIT
+
+ preview_cards = preview_cards.filter do |preview_card|
+ account.can_be_attributed_from?(preview_card.domain)
+ rescue Addressable::URI::InvalidURIError
+ false
+ end
+
+ PreviewCard.where(id: preview_cards.pluck(:id), unverified_author_account: account).update_all(author_account_id: account.id, unverified_author_account_id: nil)
+
+ UpdateLinkCardAttributionWorker.perform_async(account.id) if should_queue_worker
+ end
end
diff --git a/app/workers/update_link_card_attribution_worker.rb b/app/workers/update_link_card_attribution_worker.rb
new file mode 100644
index 0000000000..9b4973aaa5
--- /dev/null
+++ b/app/workers/update_link_card_attribution_worker.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+class UpdateLinkCardAttributionWorker
+ include Sidekiq::IterableJob
+
+ def build_enumerator(account_id, cursor:)
+ @account = Account.find_by(id: account_id)
+ return if @account.blank?
+
+ scope = PreviewCard.where(unverified_author_account: @account)
+ active_record_batches_enumerator(scope, cursor:)
+ end
+
+ def each_iteration(preview_cards, account_id)
+ preview_cards = preview_cards.filter do |preview_card|
+ @account.can_be_attributed_from?(preview_card.domain)
+ rescue Addressable::URI::InvalidURIError
+ false
+ end
+
+ PreviewCard.where(id: preview_cards.pluck(:id), unverified_author_account: @account).update_all(author_account_id: account_id, unverified_author_account_id: nil)
+ end
+end
diff --git a/db/migrate/20260303144409_add_unverified_author_account_id_to_preview_cards.rb b/db/migrate/20260303144409_add_unverified_author_account_id_to_preview_cards.rb
new file mode 100644
index 0000000000..6ce2cffc6a
--- /dev/null
+++ b/db/migrate/20260303144409_add_unverified_author_account_id_to_preview_cards.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+class AddUnverifiedAuthorAccountIdToPreviewCards < ActiveRecord::Migration[8.1]
+ disable_ddl_transaction!
+
+ def change
+ safety_assured { add_reference :preview_cards, :unverified_author_account, null: true, foreign_key: { to_table: 'accounts', on_delete: :nullify }, index: false }
+ add_index :preview_cards, [:unverified_author_account_id, :id], algorithm: :concurrently, where: 'unverified_author_account_id IS NOT NULL'
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 16be435bbc..98cda7ff44 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -10,7 +10,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema[8.0].define(version: 2026_02_17_154542) do
+ActiveRecord::Schema[8.0].define(version: 2026_03_03_144409) do
# These are extensions that must be enabled in order to support this database
enable_extension "pg_catalog.plpgsql"
@@ -964,7 +964,9 @@ ActiveRecord::Schema[8.0].define(version: 2026_02_17_154542) do
t.datetime "published_at"
t.string "image_description", default: "", null: false
t.bigint "author_account_id"
+ t.bigint "unverified_author_account_id"
t.index ["author_account_id"], name: "index_preview_cards_on_author_account_id", where: "(author_account_id IS NOT NULL)"
+ t.index ["unverified_author_account_id", "id"], name: "index_preview_cards_on_unverified_author_account_id_and_id", where: "(unverified_author_account_id IS NOT NULL)"
t.index ["url"], name: "index_preview_cards_on_url", unique: true
end
diff --git a/spec/lib/status_cache_hydrator_spec.rb b/spec/lib/status_cache_hydrator_spec.rb
index a6fea36397..3eb781dfba 100644
--- a/spec/lib/status_cache_hydrator_spec.rb
+++ b/spec/lib/status_cache_hydrator_spec.rb
@@ -19,6 +19,18 @@ RSpec.describe StatusCacheHydrator do
end
end
+ context 'when handling a new status with a preview card with unverified account attribution' do
+ let(:preview_card) { Fabricate(:preview_card, unverified_author_account: account) }
+
+ before do
+ PreviewCardsStatus.create(status: status, preview_card: preview_card)
+ end
+
+ it 'renders the same attributes as a full render' do
+ expect(subject).to eql(compare_to_hash)
+ end
+ end
+
context 'when handling a new status with own poll' do
let(:poll) { Fabricate(:poll, account: account) }
let(:status) { Fabricate(:status, poll: poll, account: account) }
diff --git a/spec/serializers/rest/preview_card_serializer_spec.rb b/spec/serializers/rest/preview_card_serializer_spec.rb
index 41ba305b7c..695d02f964 100644
--- a/spec/serializers/rest/preview_card_serializer_spec.rb
+++ b/spec/serializers/rest/preview_card_serializer_spec.rb
@@ -6,10 +6,16 @@ RSpec.describe REST::PreviewCardSerializer do
subject do
serialized_record_json(
preview_card,
- described_class
+ described_class,
+ options: {
+ scope: current_user,
+ scope_name: :current_user,
+ }
)
end
+ let(:current_user) { nil }
+
context 'when preview card does not have author data' do
let(:preview_card) { Fabricate.build :preview_card }
diff --git a/spec/services/unfollow_service_spec.rb b/spec/services/unfollow_service_spec.rb
index 6cf24ca5e1..365468e432 100644
--- a/spec/services/unfollow_service_spec.rb
+++ b/spec/services/unfollow_service_spec.rb
@@ -5,54 +5,57 @@ require 'rails_helper'
RSpec.describe UnfollowService do
subject { described_class.new }
- let(:sender) { Fabricate(:account, username: 'alice') }
+ let(:follower) { Fabricate(:account) }
+ let(:followee) { Fabricate(:account) }
- describe 'local' do
- let(:bob) { Fabricate(:account, username: 'bob') }
+ before do
+ follower.follow!(followee)
+ end
- before { sender.follow!(bob) }
+ shared_examples 'when the followee is in a list' do
+ let(:list) { Fabricate(:list, account: follower) }
- it 'destroys the following relation' do
- subject.call(sender, bob)
+ before do
+ list.accounts << followee
+ end
- expect(sender)
- .to_not be_following(bob)
+ it 'schedules removal of posts from this user from the list' do
+ expect { subject.call(follower, followee) }
+ .to enqueue_sidekiq_job(UnmergeWorker).with(followee.id, list.id, 'list')
end
end
- describe 'remote ActivityPub', :inline_jobs do
- let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
-
- before do
- sender.follow!(bob)
- stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
+ describe 'a local user unfollowing another local user' do
+ it 'destroys the following relation and unmerge from home' do
+ expect { subject.call(follower, followee) }
+ .to change { follower.following?(followee) }.from(true).to(false)
+ .and enqueue_sidekiq_job(UnmergeWorker).with(followee.id, follower.id, 'home')
end
- it 'destroys the following relation and sends unfollow activity' do
- subject.call(sender, bob)
-
- expect(sender)
- .to_not be_following(bob)
- expect(a_request(:post, 'http://example.com/inbox'))
- .to have_been_made.once
- end
+ it_behaves_like 'when the followee is in a list'
end
- describe 'remote ActivityPub (reverse)', :inline_jobs do
- let(:bob) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+ describe 'a local user unfollowing a remote ActivityPub user' do
+ let(:followee) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
- before do
- bob.follow!(sender)
- stub_request(:post, 'http://example.com/inbox').to_return(status: 200)
+ it 'destroys the following relation, unmerge from home and sends undo activity' do
+ expect { subject.call(follower, followee) }
+ .to change { follower.following?(followee) }.from(true).to(false)
+ .and enqueue_sidekiq_job(UnmergeWorker).with(followee.id, follower.id, 'home')
+ .and enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Undo'), follower.id, followee.inbox_url)
end
- it 'destroys the following relation and sends a reject activity' do
- subject.call(bob, sender)
+ it_behaves_like 'when the followee is in a list'
+ end
- expect(sender)
- .to_not be_following(bob)
- expect(a_request(:post, 'http://example.com/inbox'))
- .to have_been_made.once
+ describe 'a remote ActivityPub user unfollowing a local user' do
+ let(:follower) { Fabricate(:account, username: 'bob', protocol: :activitypub, domain: 'example.com', inbox_url: 'http://example.com/inbox') }
+
+ it 'destroys the following relation, unmerge from home and sends a reject activity' do
+ expect { subject.call(follower, followee) }
+ .to change { follower.following?(followee) }.from(true).to(false)
+ .and enqueue_sidekiq_job(UnmergeWorker).with(followee.id, follower.id, 'home')
+ .and enqueue_sidekiq_job(ActivityPub::DeliveryWorker).with(match_json_values(type: 'Reject'), followee.id, follower.inbox_url)
end
end
end
diff --git a/spec/services/update_account_service_spec.rb b/spec/services/update_account_service_spec.rb
index f9059af07f..d9a66bb24f 100644
--- a/spec/services/update_account_service_spec.rb
+++ b/spec/services/update_account_service_spec.rb
@@ -33,4 +33,18 @@ RSpec.describe UpdateAccountService do
expect(eve).to_not be_requested(account)
end
end
+
+ describe 'adding domains to attribution_domains' do
+ let(:account) { Fabricate(:account) }
+ let!(:preview_card) { Fabricate(:preview_card, url: 'https://writer.example.com/article', unverified_author_account: account, author_account: nil) }
+ let!(:unattributable_preview_card) { Fabricate(:preview_card, url: 'https://otherwriter.example.com/article', unverified_author_account: account, author_account: nil) }
+ let!(:unrelated_preview_card) { Fabricate(:preview_card) }
+
+ it 'reattributes expected preview cards' do
+ expect { subject.call(account, { attribution_domains: ['writer.example.com'] }) }
+ .to change { preview_card.reload.author_account }.from(nil).to(account)
+ .and not_change { unattributable_preview_card.reload.author_account }
+ .and(not_change { unrelated_preview_card.reload.author_account })
+ end
+ end
end
diff --git a/spec/workers/update_link_card_attribution_worker_spec.rb b/spec/workers/update_link_card_attribution_worker_spec.rb
new file mode 100644
index 0000000000..e1726af6d2
--- /dev/null
+++ b/spec/workers/update_link_card_attribution_worker_spec.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+require 'rails_helper'
+
+RSpec.describe UpdateLinkCardAttributionWorker do
+ let(:worker) { described_class.new }
+
+ let(:account) { Fabricate(:account, attribution_domains: ['writer.example.com']) }
+
+ describe '#perform' do
+ let!(:preview_card) { Fabricate(:preview_card, url: 'https://writer.example.com/article', unverified_author_account: account, author_account: nil) }
+ let!(:unattributable_preview_card) { Fabricate(:preview_card, url: 'https://otherwriter.example.com/article', unverified_author_account: account, author_account: nil) }
+ let!(:unrelated_preview_card) { Fabricate(:preview_card) }
+
+ it 'reattributes expected preview cards' do
+ expect { worker.perform(account.id) }
+ .to change { preview_card.reload.author_account }.from(nil).to(account)
+ .and not_change { unattributable_preview_card.reload.author_account }
+ .and(not_change { unrelated_preview_card.reload.author_account })
+ end
+ end
+end