From 6480e16c17d66065596a367977f380a6f2b27313 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 09:13:07 +0100 Subject: [PATCH 1/9] Update dependency sass to v1.97.0 (#37266) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index eeb029e59f..35963136e2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12193,8 +12193,8 @@ __metadata: linkType: hard "sass@npm:^1.62.1, sass@npm:^1.70.0": - version: 1.96.0 - resolution: "sass@npm:1.96.0" + version: 1.97.0 + resolution: "sass@npm:1.97.0" dependencies: "@parcel/watcher": "npm:^2.4.1" chokidar: "npm:^4.0.0" @@ -12205,7 +12205,7 @@ __metadata: optional: true bin: sass: sass.js - checksum: 10c0/a932054bcee6935757417af6072d31b65ce3557798a53351b3e1369d7f06e24b0ec211e1617bdaaee998b429a44bf0f52acd240fd47f88422d5bc241eeb71672 + checksum: 10c0/4c0e2596131054089beeeb6e98f7318beb909c4775187690656cbb85a45153d04eecd970e6a4431fe47dc94f9749cf8d58c9c5e59055d2cae39f4887a4bb6104 languageName: node linkType: hard From a505c2efd8fd60e581dd8344eaff64a4b5809f2c Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 19 Dec 2025 09:39:25 +0100 Subject: [PATCH 2/9] Fix mobile admin sidebar displaying under batch table toolbar (#37307) --- app/javascript/styles/mastodon/admin.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/app/javascript/styles/mastodon/admin.scss b/app/javascript/styles/mastodon/admin.scss index eaa91936b1..7cb5406f09 100644 --- a/app/javascript/styles/mastodon/admin.scss +++ b/app/javascript/styles/mastodon/admin.scss @@ -164,6 +164,7 @@ $content-width: 840px; width: 100%; max-width: $content-width; flex: 1 1 auto; + isolation: isolate; } @media screen and (max-width: ($content-width + $sidebar-width)) { From 77b685e749610642cbc6a521f6689cdbadae223c Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:07:44 +0100 Subject: [PATCH 3/9] Update dependency vite-tsconfig-paths to v6.0.3 (#37303) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index 35963136e2..5ff0db112b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14069,8 +14069,8 @@ __metadata: linkType: hard "vite-tsconfig-paths@npm:^6.0.0": - version: 6.0.2 - resolution: "vite-tsconfig-paths@npm:6.0.2" + version: 6.0.3 + resolution: "vite-tsconfig-paths@npm:6.0.3" dependencies: debug: "npm:^4.1.1" globrex: "npm:^0.1.2" @@ -14080,7 +14080,7 @@ __metadata: peerDependenciesMeta: vite: optional: true - checksum: 10c0/878189e38a253b699998f94706b15718a03d59467b091e064f33090240f9ccfa4bf273c3b30b5f9711822c56a58b786c3e6c6cebb8859e56ec5ab49e360ff8c0 + checksum: 10c0/75cfe470f1ec0e776b2aec1d2e71316d5e1214f485fce7daaed4e4789d6f667881fb85d98129b6463a5b70c7524ef258b401c4871ed8b6318ac45cc892ee778a languageName: node linkType: hard From ff005bae0b97960998700c532a70e813f7d6c7fd Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 11:40:07 +0100 Subject: [PATCH 4/9] New Crowdin Translations (automated) (#37312) Co-authored-by: GitHub Actions --- app/javascript/mastodon/locales/et.json | 27 ++++++++++++++++++++++ app/javascript/mastodon/locales/he.json | 3 +++ app/javascript/mastodon/locales/zh-CN.json | 3 +++ config/locales/he.yml | 1 + config/locales/simple_form.he.yml | 2 ++ config/locales/simple_form.tr.yml | 2 ++ config/locales/simple_form.zh-CN.yml | 2 ++ config/locales/tr.yml | 1 + config/locales/zh-CN.yml | 1 + 9 files changed, 42 insertions(+) diff --git a/app/javascript/mastodon/locales/et.json b/app/javascript/mastodon/locales/et.json index 3ba4bd9657..d69b7235d2 100644 --- a/app/javascript/mastodon/locales/et.json +++ b/app/javascript/mastodon/locales/et.json @@ -114,9 +114,36 @@ "alt_text_modal.done": "Valmis", "announcement.announcement": "Teadaanne", "annual_report.announcement.action_build": "Koosta kokkuvõte minu tegevusest Mastodonis", + "annual_report.announcement.action_dismiss": "Tänan, ei", "annual_report.announcement.action_view": "Vaata kokkuvõtet minu tegevusest Mastodonis", "annual_report.announcement.description": "Vaata teavet oma suhestumise kohta Mastodonis eelmisel aastal.", "annual_report.announcement.title": "{year}. aasta Mastodoni kokkuvõte on valmis", + "annual_report.nav_item.badge": "Uus", + "annual_report.shared_page.donate": "Anneta", + "annual_report.shared_page.footer": "Loodud {heart} Mastodoni meeskonna poolt", + "annual_report.shared_page.footer_server_info": "{username} kasutab {domain}-i, üht paljudest kogukondadest, mis toimivad Mastodonil.", + "annual_report.summary.archetype.booster.desc_public": "{name} jätkas postituste otsimist, et neid edendada, tugevdades teisi loojaid täiusliku täpsusega.", + "annual_report.summary.archetype.booster.desc_self": "Sa jätkasid postituste otsimist, et neid edendada, tugevdades teisi loojaid täiusliku täpsusega.", + "annual_report.summary.archetype.booster.name": "Vibukütt", + "annual_report.summary.archetype.die_drei_fragezeichen": "???", + "annual_report.summary.archetype.lurker.desc_public": "Me teame, et {name} oli kusagil seal väljas ja nautis Mastodoni oma vaiksel moel.", + "annual_report.summary.archetype.lurker.desc_self": "Me teame, et sa olid kusagil seal väljas ja nautisid Mastodoni oma vaiksel moel.", + "annual_report.summary.archetype.lurker.name": "Stoiline", + "annual_report.summary.archetype.oracle.desc_public": "{name} lõi rohkem uusi postitusi kui vastuseid, hoides Mastodoni värskena ja tulevikku suunatuna.", + "annual_report.summary.archetype.oracle.desc_self": "Sa lõid rohkem uusi postitusi kui vastuseid, hoides Mastodoni värskena ja tulevikku suunatuna.", + "annual_report.summary.archetype.oracle.name": "Oraakel", + "annual_report.summary.archetype.pollster.desc_public": "{name} lõi rohkem küsitlusi kui teisi postituse tüüpe, äratades Mastodonis uudishimu.", + "annual_report.summary.archetype.pollster.desc_self": "Sa lõid rohkem küsitlusi kui teisi postituse tüüpe, äratades Mastodonis uudishimu.", + "annual_report.summary.archetype.pollster.name": "Uudishimulik", + "annual_report.summary.archetype.replier.desc_public": "{name} vastas sageli teiste inimeste postitustele, pakkudes Mastodonile uusi aruteluteemasid.", + "annual_report.summary.archetype.replier.desc_self": "Sa vastasid sageli teiste inimeste postitustele, pakkudes Mastodonile uusi aruteluteemasid.", + "annual_report.summary.archetype.replier.name": "Liblikas", + "annual_report.summary.archetype.reveal": "Näita mu põhitüüpi", + "annual_report.summary.archetype.reveal_description": "Täname, et oled Mastodoni liige! Aeg on välja selgitada, millist põhitüüpi sa {year}. kehastasid.", + "annual_report.summary.archetype.title_public": "Kasutaja {name} põhitüüp", + "annual_report.summary.archetype.title_self": "Sinu põhitüüp", + "annual_report.summary.close": "Sule", + "annual_report.summary.copy_link": "Kopeeri link", "annual_report.summary.highlighted_post.title": "Kõige populaarsemad postitused", "annual_report.summary.most_used_app.most_used_app": "enim kasutatud äpp", "annual_report.summary.most_used_hashtag.most_used_hashtag": "enim kasutatud teemaviide", diff --git a/app/javascript/mastodon/locales/he.json b/app/javascript/mastodon/locales/he.json index b2abc99d02..3cee7adc5e 100644 --- a/app/javascript/mastodon/locales/he.json +++ b/app/javascript/mastodon/locales/he.json @@ -121,6 +121,7 @@ "annual_report.nav_item.badge": "חדש", "annual_report.shared_page.donate": "לתרומה", "annual_report.shared_page.footer": "נוצר עם כל ה-{heart} על ידי צוות מסטודון", + "annual_report.shared_page.footer_server_info": "{username} משתמש.ת ב־{domain}, שרת אחד מבין קהילות רבות שבנויות על מסטודון.", "annual_report.summary.archetype.booster.desc_public": "{name} צדו הודעות מעניינות להדהד, והגבירו קולותיהם של יוצרים אחרים בדיוק של חתול המזנק על הטרף.", "annual_report.summary.archetype.booster.desc_self": "צדת הודעות מעניינות להדהד, והגברת קולותיהם של יוצרים אחרים בדיוק של חתול המזנק על הטרף.", "annual_report.summary.archetype.booster.name": "החתול הצייד", @@ -440,6 +441,8 @@ "follow_suggestions.who_to_follow": "אחרי מי לעקוב", "followed_tags": "התגיות שהחשבון שלך עוקב אחריהן", "footer.about": "אודות", + "footer.about_mastodon": "אודות מסטודון", + "footer.about_server": "‮אודות ‭{domain}", "footer.about_this_server": "אודות", "footer.directory": "ספריית פרופילים", "footer.get_app": "להתקנת היישומון", diff --git a/app/javascript/mastodon/locales/zh-CN.json b/app/javascript/mastodon/locales/zh-CN.json index cb63df5e99..232d8ec677 100644 --- a/app/javascript/mastodon/locales/zh-CN.json +++ b/app/javascript/mastodon/locales/zh-CN.json @@ -121,6 +121,7 @@ "annual_report.nav_item.badge": "新", "annual_report.shared_page.donate": "捐助", "annual_report.shared_page.footer": "由 Mastodon 团队用 {heart} 生成", + "annual_report.shared_page.footer_server_info": "{username} 使用 {domain},运行 Mastodon 的众多社区之一。", "annual_report.summary.archetype.booster.desc_public": "{name}持续寻找值得转嘟的嘟文,以精准眼光放大其他创作者的影响力。", "annual_report.summary.archetype.booster.desc_self": "你持续寻找值得转嘟的嘟文,以精准眼光放大其他创作者的影响力。", "annual_report.summary.archetype.booster.name": "转发游侠", @@ -440,6 +441,8 @@ "follow_suggestions.who_to_follow": "推荐关注", "followed_tags": "已关注话题", "footer.about": "关于", + "footer.about_mastodon": "关于 Mastodon", + "footer.about_server": "关于 {domain}", "footer.about_this_server": "关于本站", "footer.directory": "用户列表", "footer.get_app": "获取应用", diff --git a/config/locales/he.yml b/config/locales/he.yml index 98baeb601c..f22d5d9607 100644 --- a/config/locales/he.yml +++ b/config/locales/he.yml @@ -876,6 +876,7 @@ he: publish_statistics: פרסום הסטטיסטיקות בפומבי title: תגליות trends: נושאים חמים + wrapstodon: סיכומודון domain_blocks: all: לכולם disabled: לאף אחד diff --git a/config/locales/simple_form.he.yml b/config/locales/simple_form.he.yml index cdf1300a4f..5fcaeb4421 100644 --- a/config/locales/simple_form.he.yml +++ b/config/locales/simple_form.he.yml @@ -111,6 +111,7 @@ he: thumbnail: תמונה ביחס 2:1 בערך שתוצג ליד המידע על השרת שלך. trendable_by_default: לדלג על בדיקה ידנית של התכנים החמים. פריטים ספציפיים עדיין ניתנים להסרה לאחר מעשה. trends: נושאים חמים יציגו אילו הודעות, תגיות וידיעות חדשות צוברות חשיפה על השרת שלך. + wrapstodon: אפשר למשתמשיך המקומיים.ות ליצור סיכום חביב של פעילותם במסטודון בשנה האחרונה. התכונה מאופשרת בין 10 ועד 31 בדצמבר כל שנה, ומצעת למשתמשים שיצרו לפחות הודעה ציבורית אחת והשתמשו לפחות בתגית אחת במשך השנה. form_challenge: current_password: את.ה נכנס. ת לאזור מאובטח imports: @@ -314,6 +315,7 @@ he: thumbnail: תמונה ממוזערת מהשרת trendable_by_default: הרשאה לפריטים להופיע בנושאים החמים ללא אישור מוקדם trends: אפשר פריטים חמים (טרנדים) + wrapstodon: הפעלת סיכומודון interactions: must_be_follower: חסימת התראות משאינם עוקבים must_be_following: חסימת התראות משאינם נעקבים diff --git a/config/locales/simple_form.tr.yml b/config/locales/simple_form.tr.yml index 0e79c05766..35df26dc6e 100644 --- a/config/locales/simple_form.tr.yml +++ b/config/locales/simple_form.tr.yml @@ -111,6 +111,7 @@ tr: thumbnail: Sunucu bilginizin yanında gösterilen yaklaşık 2:1'lik görüntü. trendable_by_default: Öne çıkan içeriğin elle incelenmesini atla. Tekil öğeler sonrada öne çıkanlardan kaldırılabilir. trends: Öne çıkanlar, sunucunuzda ilgi toplayan gönderileri, etiketleri ve haber yazılarını gösterir. + wrapstodon: Yerel kullanıcılara, yıl boyunca Mastodon kullanımlarının eğlenceli bir özetini oluşturma imkanı sunun. Bu özellik, her yıl 10 Aralık ile 31 Aralık tarihleri arasında kullanılabilir ve yıl içinde en az bir adet Halka Açık veya Sessiz Halka Açık gönderi paylaşan ve en az bir hashtag kullanan kullanıcılara sunulur. form_challenge: current_password: Güvenli bir bölgeye giriyorsunuz imports: @@ -312,6 +313,7 @@ tr: thumbnail: Sunucu küçük resmi trendable_by_default: Ön incelemesiz öne çıkanlara izin ver trends: Öne çıkanları etkinleştir + wrapstodon: Wrapstodonu Etkinleştir interactions: must_be_follower: Takipçim olmayan kişilerden gelen bildirimleri engelle must_be_following: Takip etmediğim kişilerden gelen bildirimleri engelle diff --git a/config/locales/simple_form.zh-CN.yml b/config/locales/simple_form.zh-CN.yml index d5c4525233..0bc0ffa413 100644 --- a/config/locales/simple_form.zh-CN.yml +++ b/config/locales/simple_form.zh-CN.yml @@ -111,6 +111,7 @@ zh-CN: thumbnail: 与服务器信息一并展示的约 2:1 比例的图像。 trendable_by_default: 跳过对热门内容的手工审核。个别项目仍可在之后从趋势中删除。 trends: 热门页中会显示正在你服务器上受到关注的嘟文、标签和新闻故事。 + wrapstodon: 为本站用户提供生成他们过去一年使用 Mastodon 情况的趣味总结的功能。此功能在每年12月10日至12月31日提供给这一年发布过至少1条公开嘟文(无论是否设置为在时间线上显示)及至少使用过1个话题标签的用户。 form_challenge: current_password: 你正在进入安全区域 imports: @@ -311,6 +312,7 @@ zh-CN: thumbnail: 本站缩略图 trendable_by_default: 允许在未审核的情况下将话题置为热门 trends: 启用热门 + wrapstodon: 启用 Wrapstodon 年度回顾 interactions: must_be_follower: 屏蔽来自未关注我的用户的通知 must_be_following: 屏蔽来自我未关注的用户的通知 diff --git a/config/locales/tr.yml b/config/locales/tr.yml index 13f1e6a62f..0eb31119e8 100644 --- a/config/locales/tr.yml +++ b/config/locales/tr.yml @@ -848,6 +848,7 @@ tr: publish_statistics: İstatistikleri yayınla title: Keşfet trends: Öne çıkanlar + wrapstodon: Wrapstodon domain_blocks: all: Herkes için disabled: Hiç kimseye diff --git a/config/locales/zh-CN.yml b/config/locales/zh-CN.yml index 9dffdcd249..87356403d2 100644 --- a/config/locales/zh-CN.yml +++ b/config/locales/zh-CN.yml @@ -834,6 +834,7 @@ zh-CN: publish_statistics: 发布统计数据 title: 发现 trends: 热门 + wrapstodon: Wrapstodon 年度回顾 domain_blocks: all: 对每个人 disabled: 不对任何人 From 06a5199c44c31a920c1ab4d282480e55a4298446 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 19 Dec 2025 14:41:58 +0100 Subject: [PATCH 5/9] Update dependency storybook to v10.1.10 [SECURITY] (#37314) Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com> --- yarn.lock | 413 ++++++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 383 insertions(+), 30 deletions(-) diff --git a/yarn.lock b/yarn.lock index 5ff0db112b..3fe0024c21 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2079,6 +2079,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/aix-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/aix-ppc64@npm:0.27.2" + conditions: os=aix & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/android-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-arm64@npm:0.25.5" @@ -2086,6 +2093,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm64@npm:0.27.2" + conditions: os=android & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/android-arm@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-arm@npm:0.25.5" @@ -2093,6 +2107,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-arm@npm:0.27.2" + conditions: os=android & cpu=arm + languageName: node + linkType: hard + "@esbuild/android-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/android-x64@npm:0.25.5" @@ -2100,6 +2121,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/android-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/android-x64@npm:0.27.2" + conditions: os=android & cpu=x64 + languageName: node + linkType: hard + "@esbuild/darwin-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/darwin-arm64@npm:0.25.5" @@ -2107,6 +2135,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-arm64@npm:0.27.2" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/darwin-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/darwin-x64@npm:0.25.5" @@ -2114,6 +2149,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/darwin-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/darwin-x64@npm:0.27.2" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + "@esbuild/freebsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/freebsd-arm64@npm:0.25.5" @@ -2121,6 +2163,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-arm64@npm:0.27.2" + conditions: os=freebsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/freebsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/freebsd-x64@npm:0.25.5" @@ -2128,6 +2177,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/freebsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/freebsd-x64@npm:0.27.2" + conditions: os=freebsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/linux-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-arm64@npm:0.25.5" @@ -2135,6 +2191,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm64@npm:0.27.2" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/linux-arm@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-arm@npm:0.25.5" @@ -2142,6 +2205,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-arm@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-arm@npm:0.27.2" + conditions: os=linux & cpu=arm + languageName: node + linkType: hard + "@esbuild/linux-ia32@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-ia32@npm:0.25.5" @@ -2149,6 +2219,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ia32@npm:0.27.2" + conditions: os=linux & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/linux-loong64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-loong64@npm:0.25.5" @@ -2156,6 +2233,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-loong64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-loong64@npm:0.27.2" + conditions: os=linux & cpu=loong64 + languageName: node + linkType: hard + "@esbuild/linux-mips64el@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-mips64el@npm:0.25.5" @@ -2163,6 +2247,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-mips64el@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-mips64el@npm:0.27.2" + conditions: os=linux & cpu=mips64el + languageName: node + linkType: hard + "@esbuild/linux-ppc64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-ppc64@npm:0.25.5" @@ -2170,6 +2261,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-ppc64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-ppc64@npm:0.27.2" + conditions: os=linux & cpu=ppc64 + languageName: node + linkType: hard + "@esbuild/linux-riscv64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-riscv64@npm:0.25.5" @@ -2177,6 +2275,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-riscv64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-riscv64@npm:0.27.2" + conditions: os=linux & cpu=riscv64 + languageName: node + linkType: hard + "@esbuild/linux-s390x@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-s390x@npm:0.25.5" @@ -2184,6 +2289,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-s390x@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-s390x@npm:0.27.2" + conditions: os=linux & cpu=s390x + languageName: node + linkType: hard + "@esbuild/linux-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/linux-x64@npm:0.25.5" @@ -2191,6 +2303,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/linux-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/linux-x64@npm:0.27.2" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + "@esbuild/netbsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/netbsd-arm64@npm:0.25.5" @@ -2198,6 +2317,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-arm64@npm:0.27.2" + conditions: os=netbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/netbsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/netbsd-x64@npm:0.25.5" @@ -2205,6 +2331,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/netbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/netbsd-x64@npm:0.27.2" + conditions: os=netbsd & cpu=x64 + languageName: node + linkType: hard + "@esbuild/openbsd-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/openbsd-arm64@npm:0.25.5" @@ -2212,6 +2345,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-arm64@npm:0.27.2" + conditions: os=openbsd & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/openbsd-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/openbsd-x64@npm:0.25.5" @@ -2219,6 +2359,20 @@ __metadata: languageName: node linkType: hard +"@esbuild/openbsd-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openbsd-x64@npm:0.27.2" + conditions: os=openbsd & cpu=x64 + languageName: node + linkType: hard + +"@esbuild/openharmony-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/openharmony-arm64@npm:0.27.2" + conditions: os=openharmony & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/sunos-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/sunos-x64@npm:0.25.5" @@ -2226,6 +2380,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/sunos-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/sunos-x64@npm:0.27.2" + conditions: os=sunos & cpu=x64 + languageName: node + linkType: hard + "@esbuild/win32-arm64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-arm64@npm:0.25.5" @@ -2233,6 +2394,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-arm64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-arm64@npm:0.27.2" + conditions: os=win32 & cpu=arm64 + languageName: node + linkType: hard + "@esbuild/win32-ia32@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-ia32@npm:0.25.5" @@ -2240,6 +2408,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-ia32@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-ia32@npm:0.27.2" + conditions: os=win32 & cpu=ia32 + languageName: node + linkType: hard + "@esbuild/win32-x64@npm:0.25.5": version: 0.25.5 resolution: "@esbuild/win32-x64@npm:0.25.5" @@ -2247,6 +2422,13 @@ __metadata: languageName: node linkType: hard +"@esbuild/win32-x64@npm:0.27.2": + version: 0.27.2 + resolution: "@esbuild/win32-x64@npm:0.27.2" + conditions: os=win32 & cpu=x64 + languageName: node + linkType: hard + "@eslint-community/eslint-utils@npm:^4.4.0, @eslint-community/eslint-utils@npm:^4.7.0, @eslint-community/eslint-utils@npm:^4.8.0": version: 4.9.0 resolution: "@eslint-community/eslint-utils@npm:4.9.0" @@ -3723,6 +3905,16 @@ __metadata: languageName: node linkType: hard +"@storybook/icons@npm:^2.0.0": + version: 2.0.1 + resolution: "@storybook/icons@npm:2.0.1" + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + checksum: 10c0/df2bbf1a5b50f12ab1bf78cae6de4dbf7c49df0e3a5f845553b51b20adbe8386a09fd172ea60342379f9284bb528cba2d0e2659cae6eb8d015cf92c8b32f1222 + languageName: node + linkType: hard + "@storybook/react-dom-shim@npm:10.0.6": version: 10.0.6 resolution: "@storybook/react-dom-shim@npm:10.0.6" @@ -4937,25 +5129,6 @@ __metadata: languageName: node linkType: hard -"@vitest/mocker@npm:3.2.4": - version: 3.2.4 - resolution: "@vitest/mocker@npm:3.2.4" - dependencies: - "@vitest/spy": "npm:3.2.4" - estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.17" - peerDependencies: - msw: ^2.4.9 - vite: ^5.0.0 || ^6.0.0 || ^7.0.0-0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - checksum: 10c0/f7a4aea19bbbf8f15905847ee9143b6298b2c110f8b64789224cb0ffdc2e96f9802876aa2ca83f1ec1b6e1ff45e822abb34f0054c24d57b29ab18add06536ccd - languageName: node - linkType: hard - "@vitest/mocker@npm:4.0.15": version: 4.0.15 resolution: "@vitest/mocker@npm:4.0.15" @@ -5686,6 +5859,15 @@ __metadata: languageName: node linkType: hard +"bundle-name@npm:^4.1.0": + version: 4.1.0 + resolution: "bundle-name@npm:4.1.0" + dependencies: + run-applescript: "npm:^7.0.0" + checksum: 10c0/8e575981e79c2bcf14d8b1c027a3775c095d362d1382312f444a7c861b0e21513c0bd8db5bd2b16e50ba0709fa622d4eab6b53192d222120305e68359daece29 + languageName: node + linkType: hard + "bytes@npm:^3.1.2, bytes@npm:~3.1.2": version: 3.1.2 resolution: "bytes@npm:3.1.2" @@ -6375,6 +6557,23 @@ __metadata: languageName: node linkType: hard +"default-browser-id@npm:^5.0.0": + version: 5.0.1 + resolution: "default-browser-id@npm:5.0.1" + checksum: 10c0/5288b3094c740ef3a86df9b999b04ff5ba4dee6b64e7b355c0fff5217752c8c86908d67f32f6cba9bb4f9b7b61a1b640c0a4f9e34c57e0ff3493559a625245ee + languageName: node + linkType: hard + +"default-browser@npm:^5.2.1": + version: 5.4.0 + resolution: "default-browser@npm:5.4.0" + dependencies: + bundle-name: "npm:^4.1.0" + default-browser-id: "npm:^5.0.0" + checksum: 10c0/a49ddd0c7b1a319163f64a5fc68ebb45a98548ea23a3155e04518f026173d85cfa2f451b646366c36c8f70b01e4cb773e23d1d22d2c61d8b84e5fbf151b4b609 + languageName: node + linkType: hard + "define-data-property@npm:^1.0.1, define-data-property@npm:^1.1.4": version: 1.1.4 resolution: "define-data-property@npm:1.1.4" @@ -6393,6 +6592,13 @@ __metadata: languageName: node linkType: hard +"define-lazy-prop@npm:^3.0.0": + version: 3.0.0 + resolution: "define-lazy-prop@npm:3.0.0" + checksum: 10c0/5ab0b2bf3fa58b3a443140bbd4cd3db1f91b985cc8a246d330b9ac3fc0b6a325a6d82bddc0b055123d745b3f9931afeea74a5ec545439a1630b9c8512b0eeb49 + languageName: node + linkType: hard + "define-properties@npm:^1.1.3, define-properties@npm:^1.2.1": version: 1.2.1 resolution: "define-properties@npm:1.2.1" @@ -6878,7 +7084,96 @@ __metadata: languageName: node linkType: hard -"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0, esbuild@npm:^0.25.0": +"esbuild@npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0": + version: 0.27.2 + resolution: "esbuild@npm:0.27.2" + dependencies: + "@esbuild/aix-ppc64": "npm:0.27.2" + "@esbuild/android-arm": "npm:0.27.2" + "@esbuild/android-arm64": "npm:0.27.2" + "@esbuild/android-x64": "npm:0.27.2" + "@esbuild/darwin-arm64": "npm:0.27.2" + "@esbuild/darwin-x64": "npm:0.27.2" + "@esbuild/freebsd-arm64": "npm:0.27.2" + "@esbuild/freebsd-x64": "npm:0.27.2" + "@esbuild/linux-arm": "npm:0.27.2" + "@esbuild/linux-arm64": "npm:0.27.2" + "@esbuild/linux-ia32": "npm:0.27.2" + "@esbuild/linux-loong64": "npm:0.27.2" + "@esbuild/linux-mips64el": "npm:0.27.2" + "@esbuild/linux-ppc64": "npm:0.27.2" + "@esbuild/linux-riscv64": "npm:0.27.2" + "@esbuild/linux-s390x": "npm:0.27.2" + "@esbuild/linux-x64": "npm:0.27.2" + "@esbuild/netbsd-arm64": "npm:0.27.2" + "@esbuild/netbsd-x64": "npm:0.27.2" + "@esbuild/openbsd-arm64": "npm:0.27.2" + "@esbuild/openbsd-x64": "npm:0.27.2" + "@esbuild/openharmony-arm64": "npm:0.27.2" + "@esbuild/sunos-x64": "npm:0.27.2" + "@esbuild/win32-arm64": "npm:0.27.2" + "@esbuild/win32-ia32": "npm:0.27.2" + "@esbuild/win32-x64": "npm:0.27.2" + dependenciesMeta: + "@esbuild/aix-ppc64": + optional: true + "@esbuild/android-arm": + optional: true + "@esbuild/android-arm64": + optional: true + "@esbuild/android-x64": + optional: true + "@esbuild/darwin-arm64": + optional: true + "@esbuild/darwin-x64": + optional: true + "@esbuild/freebsd-arm64": + optional: true + "@esbuild/freebsd-x64": + optional: true + "@esbuild/linux-arm": + optional: true + "@esbuild/linux-arm64": + optional: true + "@esbuild/linux-ia32": + optional: true + "@esbuild/linux-loong64": + optional: true + "@esbuild/linux-mips64el": + optional: true + "@esbuild/linux-ppc64": + optional: true + "@esbuild/linux-riscv64": + optional: true + "@esbuild/linux-s390x": + optional: true + "@esbuild/linux-x64": + optional: true + "@esbuild/netbsd-arm64": + optional: true + "@esbuild/netbsd-x64": + optional: true + "@esbuild/openbsd-arm64": + optional: true + "@esbuild/openbsd-x64": + optional: true + "@esbuild/openharmony-arm64": + optional: true + "@esbuild/sunos-x64": + optional: true + "@esbuild/win32-arm64": + optional: true + "@esbuild/win32-ia32": + optional: true + "@esbuild/win32-x64": + optional: true + bin: + esbuild: bin/esbuild + checksum: 10c0/cf83f626f55500f521d5fe7f4bc5871bec240d3deb2a01fbd379edc43b3664d1167428738a5aad8794b35d1cca985c44c375b1cd38a2ca613c77ced2c83aafcd + languageName: node + linkType: hard + +"esbuild@npm:^0.25.0": version: 0.25.5 resolution: "esbuild@npm:0.25.5" dependencies: @@ -8549,6 +8844,15 @@ __metadata: languageName: node linkType: hard +"is-docker@npm:^3.0.0": + version: 3.0.0 + resolution: "is-docker@npm:3.0.0" + bin: + is-docker: cli.js + checksum: 10c0/d2c4f8e6d3e34df75a5defd44991b6068afad4835bb783b902fa12d13ebdb8f41b2a199dcb0b5ed2cb78bfee9e4c0bbdb69c2d9646f4106464674d3e697a5856 + languageName: node + linkType: hard + "is-extglob@npm:^2.1.1": version: 2.1.1 resolution: "is-extglob@npm:2.1.1" @@ -8599,6 +8903,17 @@ __metadata: languageName: node linkType: hard +"is-inside-container@npm:^1.0.0": + version: 1.0.0 + resolution: "is-inside-container@npm:1.0.0" + dependencies: + is-docker: "npm:^3.0.0" + bin: + is-inside-container: cli.js + checksum: 10c0/a8efb0e84f6197e6ff5c64c52890fa9acb49b7b74fed4da7c95383965da6f0fa592b4dbd5e38a79f87fc108196937acdbcd758fcefc9b140e479b39ce1fcd1cd + languageName: node + linkType: hard + "is-map@npm:^2.0.3": version: 2.0.3 resolution: "is-map@npm:2.0.3" @@ -8793,6 +9108,15 @@ __metadata: languageName: node linkType: hard +"is-wsl@npm:^3.1.0": + version: 3.1.0 + resolution: "is-wsl@npm:3.1.0" + dependencies: + is-inside-container: "npm:^1.0.0" + checksum: 10c0/d3317c11995690a32c362100225e22ba793678fe8732660c6de511ae71a0ff05b06980cf21f98a6bf40d7be0e9e9506f859abe00a1118287d63e53d0a3d06947 + languageName: node + linkType: hard + "isarray@npm:0.0.1": version: 0.0.1 resolution: "isarray@npm:0.0.1" @@ -10082,6 +10406,18 @@ __metadata: languageName: node linkType: hard +"open@npm:^10.2.0": + version: 10.2.0 + resolution: "open@npm:10.2.0" + dependencies: + default-browser: "npm:^5.2.1" + define-lazy-prop: "npm:^3.0.0" + is-inside-container: "npm:^1.0.0" + wsl-utils: "npm:^0.1.0" + checksum: 10c0/5a36d0c1fd2f74ce553beb427ca8b8494b623fc22c6132d0c1688f246a375e24584ea0b44c67133d9ab774fa69be8e12fbe1ff12504b1142bd960fb09671948f + languageName: node + linkType: hard + "open@npm:^8.0.0": version: 8.4.2 resolution: "open@npm:8.4.2" @@ -12128,6 +12464,13 @@ __metadata: languageName: node linkType: hard +"run-applescript@npm:^7.0.0": + version: 7.1.0 + resolution: "run-applescript@npm:7.1.0" + checksum: 10c0/ab826c57c20f244b2ee807704b1ef4ba7f566aa766481ae5922aac785e2570809e297c69afcccc3593095b538a8a77d26f2b2e9a1d9dffee24e0e039502d1a03 + languageName: node + linkType: hard + "run-parallel@npm:^1.1.9": version: 1.2.0 resolution: "run-parallel@npm:1.2.0" @@ -12746,19 +13089,20 @@ __metadata: linkType: hard "storybook@npm:^10.0.5": - version: 10.0.5 - resolution: "storybook@npm:10.0.5" + version: 10.1.10 + resolution: "storybook@npm:10.1.10" dependencies: "@storybook/global": "npm:^5.0.0" - "@storybook/icons": "npm:^1.6.0" + "@storybook/icons": "npm:^2.0.0" "@testing-library/jest-dom": "npm:^6.6.3" "@testing-library/user-event": "npm:^14.6.1" "@vitest/expect": "npm:3.2.4" - "@vitest/mocker": "npm:3.2.4" "@vitest/spy": "npm:3.2.4" - esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0" + esbuild: "npm:^0.18.0 || ^0.19.0 || ^0.20.0 || ^0.21.0 || ^0.22.0 || ^0.23.0 || ^0.24.0 || ^0.25.0 || ^0.26.0 || ^0.27.0" + open: "npm:^10.2.0" recast: "npm:^0.23.5" semver: "npm:^7.6.2" + use-sync-external-store: "npm:^1.5.0" ws: "npm:^8.18.0" peerDependencies: prettier: ^2 || ^3 @@ -12767,7 +13111,7 @@ __metadata: optional: true bin: storybook: ./dist/bin/dispatcher.js - checksum: 10c0/ea4bcdbc8d793f53970fe2e72de805bfd5b0872d3640f7526bdf42fbe0114f225c09f3683ab011ac08b5240450fd7726f17c5210d929c6f261dadc851ee09eec + checksum: 10c0/beff5472ee86a995cbde2789b2aabd941f823e31ca6957bb4434cb8ee3d3703cf1248e44f4b4d402416a52bfee94677e74f233cc906487901e831e8ab610defa languageName: node linkType: hard @@ -13978,12 +14322,12 @@ __metadata: languageName: node linkType: hard -"use-sync-external-store@npm:^1.4.0": - version: 1.4.0 - resolution: "use-sync-external-store@npm:1.4.0" +"use-sync-external-store@npm:^1.4.0, use-sync-external-store@npm:^1.5.0": + version: 1.6.0 + resolution: "use-sync-external-store@npm:1.6.0" peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 - checksum: 10c0/ec011a5055962c0f6b509d6e78c0b143f8cd069890ae370528753053c55e3b360d3648e76cfaa854faa7a59eb08d6c5fb1015e60ffde9046d32f5b2a295acea5 + checksum: 10c0/35e1179f872a53227bdf8a827f7911da4c37c0f4091c29b76b1e32473d1670ebe7bcd880b808b7549ba9a5605c233350f800ffab963ee4a4ee346ee983b6019b languageName: node linkType: hard @@ -14677,6 +15021,15 @@ __metadata: languageName: node linkType: hard +"wsl-utils@npm:^0.1.0": + version: 0.1.0 + resolution: "wsl-utils@npm:0.1.0" + dependencies: + is-wsl: "npm:^3.1.0" + checksum: 10c0/44318f3585eb97be994fc21a20ddab2649feaf1fbe893f1f866d936eea3d5f8c743bec6dc02e49fbdd3c0e69e9b36f449d90a0b165a4f47dd089747af4cf2377 + languageName: node + linkType: hard + "xml-name-validator@npm:^5.0.0": version: 5.0.0 resolution: "xml-name-validator@npm:5.0.0" From f254b4706731fd5921047c7c95f64f71d560d494 Mon Sep 17 00:00:00 2001 From: Echo Date: Fri, 19 Dec 2025 14:43:27 +0100 Subject: [PATCH 6/9] Remove trailing variation selector code for legacy emojis (#37320) --- app/javascript/mastodon/features/emoji/normalize.test.ts | 1 + app/javascript/mastodon/features/emoji/normalize.ts | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/app/javascript/mastodon/features/emoji/normalize.test.ts b/app/javascript/mastodon/features/emoji/normalize.test.ts index b4c7669961..8222ab81e5 100644 --- a/app/javascript/mastodon/features/emoji/normalize.test.ts +++ b/app/javascript/mastodon/features/emoji/normalize.test.ts @@ -33,6 +33,7 @@ describe('emojiToUnicodeHex', () => { ['⚫', '26AB'], ['🖤', '1F5A4'], ['💀', '1F480'], + ['❤️', '2764'], // Checks for trailing variation selector removal. ['💂‍♂️', '1F482-200D-2642-FE0F'], ] as const)( 'emojiToUnicodeHex converts %s to %s', diff --git a/app/javascript/mastodon/features/emoji/normalize.ts b/app/javascript/mastodon/features/emoji/normalize.ts index 38b01d6905..24df808ae1 100644 --- a/app/javascript/mastodon/features/emoji/normalize.ts +++ b/app/javascript/mastodon/features/emoji/normalize.ts @@ -32,6 +32,12 @@ export function emojiToUnicodeHex(emoji: string): string { codes.push(code); } } + + // Handles how Emojibase removes the variation selector for single code emojis. + // See: https://emojibase.dev/docs/spec/#merged-variation-selectors + if (codes.at(1) === VARIATION_SELECTOR_CODE && codes.length === 2) { + codes.pop(); + } return hexNumbersToString(codes); } From 4e6395891482f88de078d9c0766bd6736d07ca27 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 19 Dec 2025 14:44:27 +0100 Subject: [PATCH 7/9] Federated "featureable in collections" preference (#37298) --- .../parser/interaction_policy_parser.rb | 41 +++++++ app/models/account.rb | 79 +++++++------- .../activitypub/actor_serializer.rb | 14 +++ .../activitypub/process_account_service.rb | 5 + ...add_feature_approval_policy_to_accounts.rb | 7 ++ db/schema.rb | 3 +- .../parser/interaction_policy_parser_spec.rb | 102 ++++++++++++++++++ .../activitypub/actor_serializer_spec.rb | 37 ++++++- .../process_account_service_spec.rb | 43 ++++++++ 9 files changed, 290 insertions(+), 41 deletions(-) create mode 100644 app/lib/activitypub/parser/interaction_policy_parser.rb create mode 100644 db/migrate/20251217091936_add_feature_approval_policy_to_accounts.rb create mode 100644 spec/lib/activitypub/parser/interaction_policy_parser_spec.rb diff --git a/app/lib/activitypub/parser/interaction_policy_parser.rb b/app/lib/activitypub/parser/interaction_policy_parser.rb new file mode 100644 index 0000000000..6587b245ee --- /dev/null +++ b/app/lib/activitypub/parser/interaction_policy_parser.rb @@ -0,0 +1,41 @@ +# frozen_string_literal: true + +class ActivityPub::Parser::InteractionPolicyParser + def initialize(json, account) + @json = json + @account = account + end + + def bitmap + flags = 0 + return flags if @json.blank? + + flags |= subpolicy(@json['automaticApproval']) + flags <<= 16 + flags |= subpolicy(@json['manualApproval']) + + flags + end + + private + + def subpolicy(partial_json) + flags = 0 + + allowed_actors = Array(partial_json).dup + allowed_actors.uniq! + + flags |= InteractionPolicy::POLICY_FLAGS[:public] if allowed_actors.delete('as:Public') || allowed_actors.delete('Public') || allowed_actors.delete('https://www.w3.org/ns/activitystreams#Public') + flags |= InteractionPolicy::POLICY_FLAGS[:followers] if allowed_actors.delete(@account.followers_url) + flags |= InteractionPolicy::POLICY_FLAGS[:following] if allowed_actors.delete(@account.following_url) + + includes_target_actor = allowed_actors.delete(ActivityPub::TagManager.instance.uri_for(@account)).present? + + # Any unrecognized actor is marked as unsupported + flags |= InteractionPolicy::POLICY_FLAGS[:unsupported_policy] unless allowed_actors.empty? + + flags |= InteractionPolicy::POLICY_FLAGS[:disabled] if flags.zero? && includes_target_actor + + flags + end +end diff --git a/app/models/account.rb b/app/models/account.rb index 562d33508d..22c87557d2 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -5,54 +5,55 @@ # Table name: accounts # # id :bigint(8) not null, primary key -# username :string default(""), not null -# domain :string -# private_key :text -# public_key :text default(""), not null -# created_at :datetime not null -# updated_at :datetime not null -# note :text default(""), not null -# display_name :string default(""), not null -# uri :string default(""), not null -# url :string -# avatar_file_name :string +# actor_type :string +# also_known_as :string is an Array +# attribution_domains :string default([]), is an Array # avatar_content_type :string +# avatar_file_name :string # avatar_file_size :integer -# avatar_updated_at :datetime -# header_file_name :string -# header_content_type :string -# header_file_size :integer -# header_updated_at :datetime # avatar_remote_url :string -# locked :boolean default(FALSE), not null -# header_remote_url :string default(""), not null -# last_webfingered_at :datetime -# inbox_url :string default(""), not null -# outbox_url :string default(""), not null -# shared_inbox_url :string default(""), not null -# followers_url :string default(""), not null -# following_url :string default(""), not null -# protocol :integer default("ostatus"), not null -# memorial :boolean default(FALSE), not null -# moved_to_account_id :bigint(8) +# avatar_storage_schema_version :integer +# avatar_updated_at :datetime +# discoverable :boolean +# display_name :string default(""), not null +# domain :string +# feature_approval_policy :integer default(0), not null # featured_collection_url :string # fields :jsonb -# actor_type :string -# discoverable :boolean -# also_known_as :string is an Array +# followers_url :string default(""), not null +# following_url :string default(""), not null +# header_content_type :string +# header_file_name :string +# header_file_size :integer +# header_remote_url :string default(""), not null +# header_storage_schema_version :integer +# header_updated_at :datetime +# hide_collections :boolean +# id_scheme :integer default("numeric_ap_id") +# inbox_url :string default(""), not null +# indexable :boolean default(FALSE), not null +# last_webfingered_at :datetime +# locked :boolean default(FALSE), not null +# memorial :boolean default(FALSE), not null +# note :text default(""), not null +# outbox_url :string default(""), not null +# private_key :text +# protocol :integer default("ostatus"), not null +# public_key :text default(""), not null +# requested_review_at :datetime +# reviewed_at :datetime +# sensitized_at :datetime +# shared_inbox_url :string default(""), not null # silenced_at :datetime # suspended_at :datetime -# hide_collections :boolean -# avatar_storage_schema_version :integer -# header_storage_schema_version :integer # suspension_origin :integer -# sensitized_at :datetime # trendable :boolean -# reviewed_at :datetime -# requested_review_at :datetime -# indexable :boolean default(FALSE), not null -# attribution_domains :string default([]), is an Array -# id_scheme :integer default("numeric_ap_id") +# uri :string default(""), not null +# url :string +# username :string default(""), not null +# created_at :datetime not null +# updated_at :datetime not null +# moved_to_account_id :bigint(8) # class Account < ApplicationRecord diff --git a/app/serializers/activitypub/actor_serializer.rb b/app/serializers/activitypub/actor_serializer.rb index ed90fa428f..c19d42bfb4 100644 --- a/app/serializers/activitypub/actor_serializer.rb +++ b/app/serializers/activitypub/actor_serializer.rb @@ -10,12 +10,16 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer :moved_to, :property_value, :discoverable, :suspended, :memorial, :indexable, :attribution_domains + context_extensions :interaction_policies if Mastodon::Feature.collections_enabled? + attributes :id, :type, :following, :followers, :inbox, :outbox, :featured, :featured_tags, :preferred_username, :name, :summary, :url, :manually_approves_followers, :discoverable, :indexable, :published, :memorial + attribute :interaction_policy, if: -> { Mastodon::Feature.collections_enabled? } + has_one :public_key, serializer: ActivityPub::PublicKeySerializer has_many :virtual_tags, key: :tag @@ -163,6 +167,16 @@ class ActivityPub::ActorSerializer < ActivityPub::Serializer object.created_at.midnight.iso8601 end + def interaction_policy + uri = object.discoverable? ? ActivityPub::TagManager::COLLECTIONS[:public] : ActivityPub::TagManager.instance.uri_for(object) + + { + canFeature: { + automaticApproval: [uri], + }, + } + end + class CustomEmojiSerializer < ActivityPub::EmojiSerializer end diff --git a/app/services/activitypub/process_account_service.rb b/app/services/activitypub/process_account_service.rb index eb67daf7e8..f133fbc84a 100644 --- a/app/services/activitypub/process_account_service.rb +++ b/app/services/activitypub/process_account_service.rb @@ -107,6 +107,7 @@ class ActivityPub::ProcessAccountService < BaseService @account.uri = @uri @account.actor_type = actor_type @account.created_at = @json['published'] if @json['published'].present? + @account.feature_approval_policy = feature_approval_policy if Mastodon::Feature.collections_enabled? end def valid_collection_uri(uri) @@ -360,4 +361,8 @@ class ActivityPub::ProcessAccountService < BaseService emoji.image_remote_url = image_url emoji.save end + + def feature_approval_policy + ActivityPub::Parser::InteractionPolicyParser.new(@json.dig('interactionPolicy', 'canFeature'), @account).bitmap + end end diff --git a/db/migrate/20251217091936_add_feature_approval_policy_to_accounts.rb b/db/migrate/20251217091936_add_feature_approval_policy_to_accounts.rb new file mode 100644 index 0000000000..6d6e6e8205 --- /dev/null +++ b/db/migrate/20251217091936_add_feature_approval_policy_to_accounts.rb @@ -0,0 +1,7 @@ +# frozen_string_literal: true + +class AddFeatureApprovalPolicyToAccounts < ActiveRecord::Migration[8.0] + def change + add_column :accounts, :feature_approval_policy, :integer, null: false, default: 0 + end +end diff --git a/db/schema.rb b/db/schema.rb index 51595381f5..78d7ef6826 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -10,7 +10,7 @@ # # It's strongly recommended that you check this file into your version control system. -ActiveRecord::Schema[8.0].define(version: 2025_12_09_093813) do +ActiveRecord::Schema[8.0].define(version: 2025_12_17_091936) do # These are extensions that must be enabled in order to support this database enable_extension "pg_catalog.plpgsql" @@ -200,6 +200,7 @@ ActiveRecord::Schema[8.0].define(version: 2025_12_09_093813) do t.string "attribution_domains", default: [], array: true t.string "following_url", default: "", null: false t.integer "id_scheme", default: 1 + t.integer "feature_approval_policy", default: 0, null: false t.index "(((setweight(to_tsvector('simple'::regconfig, (display_name)::text), 'A'::\"char\") || setweight(to_tsvector('simple'::regconfig, (username)::text), 'B'::\"char\")) || setweight(to_tsvector('simple'::regconfig, (COALESCE(domain, ''::character varying))::text), 'C'::\"char\")))", name: "search_index", using: :gin t.index "lower((username)::text), COALESCE(lower((domain)::text), ''::text)", name: "index_accounts_on_username_and_domain_lower", unique: true t.index ["domain", "id"], name: "index_accounts_on_domain_and_id" diff --git a/spec/lib/activitypub/parser/interaction_policy_parser_spec.rb b/spec/lib/activitypub/parser/interaction_policy_parser_spec.rb new file mode 100644 index 0000000000..0af445eab2 --- /dev/null +++ b/spec/lib/activitypub/parser/interaction_policy_parser_spec.rb @@ -0,0 +1,102 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe ActivityPub::Parser::InteractionPolicyParser do + subject { described_class.new(json_policy, account) } + + let(:account) do + Fabricate(:account, + uri: 'https://foo.test', + domain: 'foo.test', + followers_url: 'https://foo.test/followers', + following_url: 'https://foo.test/following') + end + + describe '#bitmap' do + context 'when no policy is given' do + let(:json_policy) { nil } + + it 'returns zero' do + expect(subject.bitmap).to be_zero + end + end + + context 'with special public URI' do + let(:json_policy) do + { + 'manualApproval' => [public_uri], + } + end + + shared_examples 'setting the public bit' do + it 'sets the public bit' do + expect(subject.bitmap).to eq 0b10 + end + end + + context 'when public URI is given in full' do + let(:public_uri) { 'https://www.w3.org/ns/activitystreams#Public' } + + it_behaves_like 'setting the public bit' + end + + context 'when public URI is abbreviated using namespace' do + let(:public_uri) { 'as:Public' } + + it_behaves_like 'setting the public bit' + end + + context 'when public URI is abbreviated without namespace' do + let(:public_uri) { 'Public' } + + it_behaves_like 'setting the public bit' + end + end + + context 'when mixing array and scalar values' do + let(:json_policy) do + { + 'automaticApproval' => 'https://foo.test', + 'manualApproval' => [ + 'https://foo.test/followers', + 'https://foo.test/following', + ], + } + end + + it 'sets the correct flags' do + expect(subject.bitmap).to eq 0b100000000000000001100 + end + end + + context 'when including individual actor URIs' do + let(:json_policy) do + { + 'automaticApproval' => ['https://example.com/actor', 'https://masto.example.com/@user'], + 'manualApproval' => ['https://masto.example.com/@other'], + } + end + + it 'sets the unsupported bit' do + expect(subject.bitmap).to eq 0b10000000000000001 + end + end + + context "when giving the affected actor's URI in addition to other supported URIs" do + let(:json_policy) do + { + 'manualApproval' => [ + 'https://foo.test/followers', + 'https://foo.test/following', + 'https://foo.test', + ], + } + end + + it 'is being ignored' do + expect(subject.bitmap).to eq 0b1100 + end + end + end +end diff --git a/spec/serializers/activitypub/actor_serializer_spec.rb b/spec/serializers/activitypub/actor_serializer_spec.rb index ad24455953..734da6673c 100644 --- a/spec/serializers/activitypub/actor_serializer_spec.rb +++ b/spec/serializers/activitypub/actor_serializer_spec.rb @@ -3,7 +3,7 @@ require 'rails_helper' RSpec.describe ActivityPub::ActorSerializer do - subject { serialized_record_json(record, described_class) } + subject { serialized_record_json(record, described_class, adapter: ActivityPub::Adapter) } describe '#type' do context 'with the instance actor' do @@ -36,4 +36,39 @@ RSpec.describe ActivityPub::ActorSerializer do it { is_expected.to include('type' => 'Person') } end end + + describe '#interactionPolicy' do + let(:record) { Fabricate(:account) } + + # TODO: Remove when feature flag is removed + context 'when collections feature is disabled?' do + it 'is not present' do + expect(subject).to_not have_key('interactionPolicy') + end + end + + context 'when collections feature is enabled', feature: :collections do + context 'when actor is discoverable' do + it 'includes an automatic policy allowing everyone' do + expect(subject).to include('interactionPolicy' => { + 'canFeature' => { + 'automaticApproval' => ['https://www.w3.org/ns/activitystreams#Public'], + }, + }) + end + end + + context 'when actor is not discoverable' do + let(:record) { Fabricate(:account, discoverable: false) } + + it 'includes an automatic policy limited to the actor itself' do + expect(subject).to include('interactionPolicy' => { + 'canFeature' => { + 'automaticApproval' => [ActivityPub::TagManager.instance.uri_for(record)], + }, + }) + end + end + end + end end diff --git a/spec/services/activitypub/process_account_service_spec.rb b/spec/services/activitypub/process_account_service_spec.rb index eb0ba3524a..b2c6bc17b2 100644 --- a/spec/services/activitypub/process_account_service_spec.rb +++ b/spec/services/activitypub/process_account_service_spec.rb @@ -272,6 +272,49 @@ RSpec.describe ActivityPub::ProcessAccountService do end end + context 'with interaction policy' do + let(:payload) do + { + id: 'https://foo.test', + type: 'Actor', + inbox: 'https://foo.test/inbox', + followers: 'https://foo.test/followers', + following: 'https://foo.test/following', + interactionPolicy: { + canFeature: { + automaticApproval: 'https://foo.test', + manualApproval: [ + 'https://foo.test/followers', + 'https://foo.test/following', + ], + }, + }, + }.with_indifferent_access + end + + before do + stub_request(:get, %r{^https://foo\.test/follow}) + .to_return(status: 200, body: '', headers: {}) + end + + # TODO: Remove when feature flag is removed + context 'when collections feature is disabled' do + it 'does not set the interaction policy' do + account = subject.call('user1', 'foo.test', payload) + + expect(account.feature_approval_policy).to be_zero + end + end + + context 'when collections feature is enabled', feature: :collections do + it 'sets the interaction policy to the correct value' do + account = subject.call('user1', 'foo.test', payload) + + expect(account.feature_approval_policy).to eq 0b100000000000000001100 + end + end + end + private def create_some_remote_accounts From 8d9192835d6cac6ae51918143b20876258305fbe Mon Sep 17 00:00:00 2001 From: diondiondion Date: Fri, 19 Dec 2025 16:18:10 +0100 Subject: [PATCH 8/9] Add stub story for `StatusQuoteManager` / `Status` component (#37321) --- .../components/status_quoted.stories.tsx | 35 +++++++++++++++++++ .../mastodon/components/status_quoted.tsx | 13 ++++--- app/javascript/testing/factories.ts | 2 +- 3 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 app/javascript/mastodon/components/status_quoted.stories.tsx diff --git a/app/javascript/mastodon/components/status_quoted.stories.tsx b/app/javascript/mastodon/components/status_quoted.stories.tsx new file mode 100644 index 0000000000..aa17a5422c --- /dev/null +++ b/app/javascript/mastodon/components/status_quoted.stories.tsx @@ -0,0 +1,35 @@ +import type { Meta, StoryObj } from '@storybook/react-vite'; + +import { accountFactoryState, statusFactoryState } from '@/testing/factories'; + +import type { StatusQuoteManagerProps } from './status_quoted'; +import { StatusQuoteManager } from './status_quoted'; + +const meta = { + title: 'Components/Status/StatusQuoteManager', + render(args) { + return ; + }, + args: { + id: '1', + }, + parameters: { + state: { + accounts: { + '1': accountFactoryState({ id: '1', acct: 'hashtaguser' }), + }, + statuses: { + '1': statusFactoryState({ + id: '1', + text: 'Hello world!', + }), + }, + }, + }, +} satisfies Meta; + +export default meta; + +type Story = StoryObj; + +export const Default: Story = {}; diff --git a/app/javascript/mastodon/components/status_quoted.tsx b/app/javascript/mastodon/components/status_quoted.tsx index eac2167294..33e791a548 100644 --- a/app/javascript/mastodon/components/status_quoted.tsx +++ b/app/javascript/mastodon/components/status_quoted.tsx @@ -4,20 +4,19 @@ import { FormattedMessage } from 'react-intl'; import type { Map as ImmutableMap } from 'immutable'; +import { fetchRelationships } from 'mastodon/actions/accounts'; +import { revealAccount } from 'mastodon/actions/accounts_typed'; +import { fetchStatus } from 'mastodon/actions/statuses'; import { LearnMoreLink } from 'mastodon/components/learn_more_link'; import StatusContainer from 'mastodon/containers/status_container'; import { domain } from 'mastodon/initial_state'; import type { Account } from 'mastodon/models/account'; import type { Status } from 'mastodon/models/status'; +import { makeGetStatusWithExtraInfo } from 'mastodon/selectors'; +import { getAccountHidden } from 'mastodon/selectors/accounts'; import type { RootState } from 'mastodon/store'; import { useAppDispatch, useAppSelector } from 'mastodon/store'; -import { fetchRelationships } from '../actions/accounts'; -import { revealAccount } from '../actions/accounts_typed'; -import { fetchStatus } from '../actions/statuses'; -import { makeGetStatusWithExtraInfo } from '../selectors'; -import { getAccountHidden } from '../selectors/accounts'; - import { Button } from './button'; const MAX_QUOTE_POSTS_NESTING_LEVEL = 1; @@ -333,7 +332,7 @@ export const QuotedStatus: React.FC = ({ ); }; -interface StatusQuoteManagerProps { +export interface StatusQuoteManagerProps { id: string; contextType?: string; [key: string]: unknown; diff --git a/app/javascript/testing/factories.ts b/app/javascript/testing/factories.ts index 28c0eca809..26b020d8c2 100644 --- a/app/javascript/testing/factories.ts +++ b/app/javascript/testing/factories.ts @@ -76,7 +76,7 @@ export const statusFactory: FactoryFunction = ({ mentions: [], tags: [], emojis: [], - contentHtml: '

This is a test status.

', + contentHtml: data.text ?? '

This is a test status.

', ...data, }); From 0231b6d350d6027fa822f286732405b573d634e8 Mon Sep 17 00:00:00 2001 From: David Roetzel Date: Fri, 19 Dec 2025 16:20:30 +0100 Subject: [PATCH 9/9] Expose feature policy in API (#37322) --- app/models/account.rb | 1 + .../account/interaction_policy_concern.rb | 69 +++++++++++++++ app/serializers/rest/account_serializer.rb | 10 +++ .../interaction_policy_concern_spec.rb | 84 +++++++++++++++++++ .../rest/account_serializer_spec.rb | 55 +++++++++++- 5 files changed, 218 insertions(+), 1 deletion(-) create mode 100644 app/models/concerns/account/interaction_policy_concern.rb create mode 100644 spec/models/concerns/account/interaction_policy_concern_spec.rb diff --git a/app/models/account.rb b/app/models/account.rb index 22c87557d2..deb1589a09 100644 --- a/app/models/account.rb +++ b/app/models/account.rb @@ -91,6 +91,7 @@ class Account < ApplicationRecord include Account::FaspConcern include Account::FinderConcern include Account::Header + include Account::InteractionPolicyConcern include Account::Interactions include Account::Mappings include Account::Merging diff --git a/app/models/concerns/account/interaction_policy_concern.rb b/app/models/concerns/account/interaction_policy_concern.rb new file mode 100644 index 0000000000..8fe9eda1ba --- /dev/null +++ b/app/models/concerns/account/interaction_policy_concern.rb @@ -0,0 +1,69 @@ +# frozen_string_literal: true + +module Account::InteractionPolicyConcern + extend ActiveSupport::Concern + + included do + composed_of :feature_interaction_policy, class_name: 'InteractionPolicy', mapping: { feature_approval_policy: :bitmap } + end + + def feature_policy_as_keys(kind) + raise ArgumentError unless kind.in?(%i(automatic manual)) + return local_feature_policy(kind) if local? + + sub_policy = feature_interaction_policy.send(kind) + sub_policy.as_keys + end + + # Returns `:automatic`, `:manual`, `:unknown`, ':missing` or `:denied` + def feature_policy_for_account(other_account) + return :denied if other_account.nil? || (local? && !discoverable?) + return :automatic if local? + # Post author is always allowed to feature themselves + return :automatic if self == other_account + return :missing if feature_approval_policy.zero? + + automatic_policy = feature_interaction_policy.automatic + following_self = nil + followed_by_self = nil + + return :automatic if automatic_policy.public? + + if automatic_policy.followers? + following_self = followed_by?(other_account) + return :automatic if following_self + end + + if automatic_policy.following? + followed_by_self = following?(other_account) + return :automatic if followed_by_self + end + + # We don't know we are allowed by the automatic policy, considering the manual one + manual_policy = feature_interaction_policy.manual + + return :manual if manual_policy.public? + + if manual_policy.followers? + following_self = followed_by?(other_account) if following_self.nil? + return :manual if following_self + end + + if manual_policy.following? + followed_by_self = following?(other_account) if followed_by_self.nil? + return :manual if followed_by_self + end + + return :unknown if [automatic_policy, manual_policy].any?(&:unsupported_policy?) + + :denied + end + + private + + def local_feature_policy(kind) + return [] if kind == :manual || !discoverable? + + [:public] + end +end diff --git a/app/serializers/rest/account_serializer.rb b/app/serializers/rest/account_serializer.rb index b102f79fdb..26715dd103 100644 --- a/app/serializers/rest/account_serializer.rb +++ b/app/serializers/rest/account_serializer.rb @@ -20,6 +20,8 @@ class REST::AccountSerializer < ActiveModel::Serializer attribute :memorial, if: :memorial? + attribute :feature_approval, if: -> { Mastodon::Feature.collections_enabled? } + class AccountDecorator < SimpleDelegator def self.model_name Account.model_name @@ -157,4 +159,12 @@ class REST::AccountSerializer < ActiveModel::Serializer def moved_and_not_nested? object.moved? end + + def feature_approval + { + automatic: object.feature_policy_as_keys(:automatic), + manual: object.feature_policy_as_keys(:manual), + current_user: object.feature_policy_for_account(current_user&.account), + } + end end diff --git a/spec/models/concerns/account/interaction_policy_concern_spec.rb b/spec/models/concerns/account/interaction_policy_concern_spec.rb new file mode 100644 index 0000000000..586a33b77f --- /dev/null +++ b/spec/models/concerns/account/interaction_policy_concern_spec.rb @@ -0,0 +1,84 @@ +# frozen_string_literal: true + +require 'rails_helper' + +RSpec.describe Account::InteractionPolicyConcern do + describe '#feature_policy_as_keys' do + context 'when account is local' do + context 'when account is discoverable' do + let(:account) { Fabricate(:account) } + + it 'returns public for automtatic and nothing for manual' do + expect(account.feature_policy_as_keys(:automatic)).to eq [:public] + expect(account.feature_policy_as_keys(:manual)).to eq [] + end + end + + context 'when account is not discoverable' do + let(:account) { Fabricate(:account, discoverable: false) } + + it 'returns empty arrays for both inputs' do + expect(account.feature_policy_as_keys(:automatic)).to eq [] + expect(account.feature_policy_as_keys(:manual)).to eq [] + end + end + end + + context 'when account is remote' do + let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy: (0b0101 << 16) | 0b0010) } + + it 'returns the expected values' do + expect(account.feature_policy_as_keys(:automatic)).to eq ['unsupported_policy', 'followers'] + expect(account.feature_policy_as_keys(:manual)).to eq ['public'] + end + end + end + + describe '#feature_policy_for_account' do + context 'when account is remote' do + let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy:) } + let(:feature_approval_policy) { (0b0101 << 16) | 0b0010 } + let(:other_account) { Fabricate(:account) } + + context 'when no policy is available' do + let(:feature_approval_policy) { 0 } + + context 'when both accounts are the same' do + it 'returns :automatic' do + expect(account.feature_policy_for_account(account)).to eq :automatic + end + end + + context 'with two different accounts' do + it 'returns :missing' do + expect(account.feature_policy_for_account(other_account)).to eq :missing + end + end + end + + context 'when the other account is not following the account' do + it 'returns :manual because of the public entry in the manual policy' do + expect(account.feature_policy_for_account(other_account)).to eq :manual + end + end + + context 'when the other account is following the account' do + before do + other_account.follow!(account) + end + + it 'returns :automatic because of the followers entry in the automatic policy' do + expect(account.feature_policy_for_account(other_account)).to eq :automatic + end + end + + context 'when the account falls into the unknown bucket' do + let(:feature_approval_policy) { (0b0001 << 16) | 0b0100 } + + it 'returns :automatic because of the followers entry in the automatic policy' do + expect(account.feature_policy_for_account(other_account)).to eq :unknown + end + end + end + end +end diff --git a/spec/serializers/rest/account_serializer_spec.rb b/spec/serializers/rest/account_serializer_spec.rb index 5fd4f8d706..998de6b0fb 100644 --- a/spec/serializers/rest/account_serializer_spec.rb +++ b/spec/serializers/rest/account_serializer_spec.rb @@ -3,12 +3,18 @@ require 'rails_helper' RSpec.describe REST::AccountSerializer do - subject { serialized_record_json(account, described_class) } + subject do + serialized_record_json(account, described_class, options: { + scope: current_user, + scope_name: :current_user, + }) + end let(:default_datetime) { DateTime.new(2024, 11, 28, 16, 20, 0) } let(:role) { Fabricate(:user_role, name: 'Role', highlighted: true) } let(:user) { Fabricate(:user, role: role) } let(:account) { user.account } + let(:current_user) { Fabricate(:user) } context 'when the account is suspended' do before do @@ -68,4 +74,51 @@ RSpec.describe REST::AccountSerializer do expect(subject['last_status_at']).to eq('2024-11-28') end end + + describe '#feature_approval' do + # TODO: Remove when feature flag is removed + context 'when collections feature is disabled' do + it 'does not include the approval policy' do + expect(subject).to_not have_key('feature_approval') + end + end + + context 'when collections feature is enabled', feature: :collections do + context 'when account is local' do + context 'when account is discoverable' do + it 'includes a policy that allows featuring' do + expect(subject['feature_approval']).to include({ + 'automatic' => ['public'], + 'manual' => [], + 'current_user' => 'automatic', + }) + end + end + + context 'when account is not discoverable' do + let(:account) { Fabricate(:account, discoverable: false) } + + it 'includes a policy that disallows featuring' do + expect(subject['feature_approval']).to include({ + 'automatic' => [], + 'manual' => [], + 'current_user' => 'denied', + }) + end + end + end + + context 'when account is remote' do + let(:account) { Fabricate(:account, domain: 'example.com', feature_approval_policy: 0b11000000000000000010) } + + it 'includes the matching policy' do + expect(subject['feature_approval']).to include({ + 'automatic' => ['followers', 'following'], + 'manual' => ['public'], + 'current_user' => 'manual', + }) + end + end + end + end end