mirror of
https://github.com/glitch-soc/mastodon.git
synced 2026-03-29 03:00:33 +02:00
Merge commit '8a0261c51caf76b6d12e3801da471759c31c9608' into glitch-soc/merge-upstream
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
.filterSelectButton {
|
||||
appearance: none;
|
||||
border: none;
|
||||
color: inherit;
|
||||
background: none;
|
||||
padding: 8px 0;
|
||||
font-size: 15px;
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -1074,7 +1074,7 @@
|
||||
"sign_in_banner.sign_in": "Kirjaudu",
|
||||
"sign_in_banner.sso_redirect": "Kirjaudu tai rekisteröidy",
|
||||
"skip_links.hotkey": "<span>Pikanäppäin</span> {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ä",
|
||||
|
||||
@@ -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": "<span>Snarknappur</span> {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",
|
||||
|
||||
@@ -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": "<span>Raccourci</span> {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",
|
||||
|
||||
@@ -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": "<span>Raccourci</span> {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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "가입 후 월별 사용자 유지율",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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": "<span>Kısayol tuşu</span> {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",
|
||||
|
||||
@@ -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": "<span>快捷键</span> {hotkey}",
|
||||
"skip_links.skip_to_content": "跳转到主内容",
|
||||
"skip_links.skip_to_navigation": "跳转到主导航",
|
||||
"status.admin_account": "打开 @{name} 的管理界面",
|
||||
"status.admin_domain": "打开 {domain} 的管理界面",
|
||||
"status.admin_status": "在管理界面查看此嘟文",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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],
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
23
app/workers/update_link_card_attribution_worker.rb
Normal file
23
app/workers/update_link_card_attribution_worker.rb
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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) }
|
||||
|
||||
@@ -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 }
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
22
spec/workers/update_link_card_attribution_worker_spec.rb
Normal file
22
spec/workers/update_link_card_attribution_worker_spec.rb
Normal file
@@ -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
|
||||
Reference in New Issue
Block a user